目录
一、ArrayList的缺陷
二、链表(主要介绍不带头的非循环的 单链表 / 双链表)
注:
三、模拟链表的实现:
MySingleList(单链表)
MyLinkedList(双链表)
四、LinkedList的使用
1. LinkedLis的构造
2. LinkedList的常用方法
3. LinkedList的遍历
五、ArrayList和LinkedList的区别
六、链表的oj练习
oj链接:
解析:
前言
上篇文章总结了ArrayList的底层实现和方法的基本使用,可以了解ArrayList底层是使用数组来实现的,ArrayList的优点就是可以支持数据的随机访问,但是ArrayList也有缺点,所以每个集合都有自己特定的使用场景。
一、ArrayList的缺陷
1. 不适合做任意位置插入和删除操作(插入或删除元素时,需要将后序的元素往前或者往后挪动,时间复杂度O(N)) |
2. 开辟的空间是连续的,开辟的空间不够时需要动态扩容 |
所以此时也就引出了链表这种数据结构;
二、链表(主要介绍不带头的非循环的 单链表 / 双链表)
如上图所示链表的结构:是一种物理存储结构上不连续的存储结构,数据元素的逻辑顺序是通过链表节点中的引用连接下一个数据元素的。
注:
1. 如上图:链表结构在逻辑上是连续的,但是在物理上不一定连续 |
2. 节点一般都是从堆上申请出来的 |
3. 从堆上申请的空间,是按照一定的策略分配的,申请的空间可能是连续的,也可以不是连续的 |
三、模拟链表的实现:
1. MySingleList --- 单链表的实现 (只有一个next域,有定义好的head) |
2. MyLinkedList --- 双链表的实现 (有next和prev 域,有定义好的head 和 last)(head引用头节点,last引用最后一个节点) |
MySingleList(单链表)
//单链表的实现
/*Ctrl + R 选中类中相同的变量名称然后,改名*/
public class MySingleList {
public int val;
//内部类如果是static的,生成对象的时候是不依赖于外部类对象的
//不加static,生成对象的时候依赖于外部类对象
static class ListNode {
public int val;//存储值
public ListNode next;//存储next域
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//head引用的是当前列表的头节点
public void createLink() {
ListNode listNode1 = new ListNode(1);
ListNode listNode2 = new ListNode(2);
ListNode listNode3 = new ListNode(3);
ListNode listNode4 = new ListNode(4);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
head = listNode1;
//当这个方法走完,node1,2,3,4....都被回收了,
//他们都是局部变量
}
//遍历打印链表
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//从指定位置开始打印链表
public void display(ListNode newHead) {
ListNode cur = newHead;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key,是否在单链表中
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) return true;
cur = cur.next;
}
return false;
}
//得到单链表的长度
public int size() {
ListNode cur = head;
int size = 0;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
//头插法
public void addFirst(int data){
ListNode listNode = new ListNode(data);
listNode.next = head;
head = listNode;
}
//尾插法(考虑如果当前链表没有节点)
public void addLast(int data) {
ListNode listNode = new ListNode(data);
if (head == null) {
head = listNode;
return;//不要忘了return
}
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = listNode;
}
//指定下标位置插入节点
public void addIndex(int index, int data) {
checkIndex(index);
if (index == 0) {
addFirst(data);
return;
}
if (index == size()) {
addLast(data);
return;
}
ListNode listNode = new ListNode(data);
ListNode cur = findIndexSubOne(index);//cur走到下标的前一个位置
listNode.next = cur.next;
cur.next = listNode;
}
//找到index-1位置的节点的地址
private ListNode findIndexSubOne(int index) {
ListNode cur = head;
int count = 0;
while (count != index - 1) {
cur = cur.next;
count++;
}
return cur;
}
private void checkIndex(int index) {
if (index < 0 || index > size()) {
throw new ListIndexOutOfException("下标位置不合法!");
}
}
MyLinkedList(双链表)
package Review;
/*和单链表不一样,双链表引入了一个last引用来指向最后一个节点(一直是指向最后一个节点)
* 也就是说在开始没有节点时,head和last节点都是指向头节点的*/
/*所有的插入都是先绑后边*/
public class MyLinkedList {
static class ListNode {
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode last;
//打印遍历链表
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//头插法O(1)
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (head == null) {
head = node;
last = node;
}else {
node.next = head;
head.prev = node;
head = node;
}
}
//尾插法O(1):因为省去了找尾巴的过程
public void addLast(int data) {
//如果链表中没有元素
ListNode node = new ListNode(data);
if (head == null) {
head = node;
last = node;
}else {
last.next = node;
node.prev = last;
last = node;
}
}
//任意位置插入,第一个数据节点为0号的下标
public void addIndex(int index, int data) {
if (index < 0 || index >size()) {
throw new ListIndexOutOfException();
}
if (index == 0) {
addFirst(data);
return;
}
if (index == size()) {
addLast(data);
return;
}
//不是头插不是尾插,在中间插入一个元素
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
ListNode node = new ListNode(data);
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
//查找是否包含关键字key在链表中
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
// 删除关键字为key的节点
public void remove(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//1.删除的是头节点
if (cur == head) {
head = head.next;
//如果head.next是空的(只有一个节点),此时空指针异常
if (head != null) {
head.prev = null;
}
} else {
//中间 尾巴
cur.prev.next = cur.next;
//不是尾巴节点
if (cur.next != null) {
cur.next.prev = cur.prev;
} else {
//是尾巴节点
last = last.prev;
}
}
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeKeyAll(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//1.删除的是头节点
if (cur == head) {
head = head.next;
//如果head.next是空的(只有一个节点),此时空指针异常
if (head != null) {
head.prev = null;
}
} else {
//中间 尾巴
cur.prev.next = cur.next;
//不是尾巴节点
if (cur.next != null) {
cur.next.prev = cur.prev;
} else {
//是尾巴节点
last = last.prev;
}
}
}
cur = cur.next;
}
}
//求长度
public int size() {
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
//将双向链表置空
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
cur = curNext;
}
//最后让这俩引用也要置空
head = null;
last = null;
}
}
四、LinkedList的使用
1. LinkedList实现了List接口(ArrayList也实现了List接口) |
2. LinkedList的底层就是一个双向链表的结构 |
3. LinkedList没有实现RandomAccess接口,所以不支持随机访问 |
4. LinkedList适合插入和删除的场景,时间复杂度都为O(1) |
1. LinkedLis的构造
LinkedList() | 无参构造 |
public LinkedList(Collection<? extends E> c) | 传一个实现了Collection接口类的具体对象的参数(<>中是泛型参数的上界,这个类型必须继承了E或者是E本身) |
构造器的演示:
//源码中的LinkedList在new对象时的参数只要是实现了Collection接口的
//具体类都可以作为参数
/*Collection<? extends E> c 代表的要么是String,要么是String的子类
* c代表的是泛型参数的上界*/
ArrayList<String> arrayList = new ArrayList<>();
LinkedList<String> list = new LinkedList<>(arrayList);
list.add("hello");
list.add("ok");
list.add("world");
System.out.println(list);
2. LinkedList的常用方法
add(E e) | 添加e数据元素(尾插法) |
add(int index, E element) | 将e插入到index位置 |
addAll(Collection<? extends E> c) | 插入集合c中的元素(c类必须实现了Collection接口) |
remove(int index) | 删除index位置元素 |
get(int index) set(int index,E elment) | 获取和设置index下标位置的元素 |
contains(int key) | 判断是否包含key元素 |
List<E> subList(int fromIndex, int toIndex) | 截取部分list,注:返回值是List<E> |
3. LinkedList的遍历
1. 直接遍历(因为重写了toString方法) |
2. for循环或者for each循环遍历 |
3. 迭代器遍历 |
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
LinkedList<String> list = new LinkedList<>(arrayList);
list.add("hello");
list.add("ok");
list.add("world");
for (String str : list) {
System.out.print(str + " ");
}
System.out.println();
ListIterator<String> it = list.listIterator(list.size());
//listIterator()括号中可以给参数或者没有参数,如果有参数代表的是
//list中的下标的位置,要从哪一个下标位置开始遍历
while (it.hasPrevious()) {
//此时就是反向遍历list中的元素
System.out.print(it.previous() + " ");
}
五、ArrayList和LinkedList的区别
- ArrayList的物理存储空间是连续的,LinkedList物理上不一定连续。
- ArrayList支持随机访问,LinkedList不支持随机访问。
- ArrayList在插入和删除的时候时间复杂度O(N),LinkedList时间复杂度O(1);所以LinkedList适合频繁插入和删除的场景。
- ArrayList空间不够需要动态扩容,LinkedList不需要。
六、链表的oj练习
oj链接:
1. 删除链表中等于给定值 val 的所有节点。力扣 |
2. 反转一个单链表。 力扣 |
3. 返回链表的中间节点。力扣 |
4. 输入一个链表,输出该链表中倒数第k个结点。链表中倒数第k个结点_牛客题霸_牛客网 |
5. 合并两个有序的链表。力扣 |
6. 以给定值x为基准将链表分成两部分,所有小于x的结点排在大于或等于x的结点之前 。链表分割_牛客题霸_牛客网 |
7. 判断链表是否是回文结构。链表的回文结构_牛客题霸_牛客网 |
8. 输出链表的公共节点。力扣 |
9. 判断链表是否有环。力扣 |
解析:
//链表的反转
public ListNode ReverseList() {
if (head == null) return null;
if (head.next == null) return head;
ListNode cur = head.next;
head.next = null;
while (cur != null) {
ListNode curNext = cur.next;
//头插法
cur.next = head;
head = cur;
cur = curNext;
}
return head;
}
//返回链表的中间节点
public ListNode middleNode(ListNode head) {
if (head == null) return null;
ListNode s = head;
ListNode f = head;
//此时要注意,f走一步就需要判断是否为空,否则可能走一步就
//空指针异常了
while (f != null && f.next != null) {
f = f.next.next;
s = s.next;
}
//结束的情况: fast = null && fast.next = null
return s;
}
//返回链表的倒数第k个节点
public ListNode FindKthToTail (ListNode pHead, int k) {
// write code here
//快指针先走k-1步,然后和慢指针一起走,此时返回的慢指针就是倒数第k个节点
if (k < 0 || pHead == null) return null;
ListNode cur = pHead;
int len = 0;
while (cur != null) {
cur = cur.next;
len++;
}
ListNode fast = pHead;
ListNode slow = pHead;
while (k != 1) {
fast = fast.next;
if (fast == null) return null;
k--;
}
//fast和slow一起走
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
//合并两个有序的链表
public ListNode Merge(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
tmp.next = list1;
list1 = list1.next;
tmp = tmp.next;
}else {
tmp.next = list2;
list2 = list2.next;
tmp = tmp.next;
}
}
//如果list1为空了
if (list2 != null) {
tmp.next = list2;
}
if (list1 != null) {
tmp.next = list1;
}
return newHead.next;
}
//判断链表是否是回文结构
public boolean isPail (ListNode head) {
if (head ==null) return false;
if (head.next == null) return true;
//1.找中间节点
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2.反转
ListNode cur = slow.next;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//3.一个从头往后走,一个从后往前走
//这里不要和fast指针的值进行比较,fast偶数情况下走到最后已经空指针了
//也不能和cur进行比较,因为最后cur在什么情况下都会空指针
while (slow != head) {
if (head.val != slow.val) {
return false;
}
//偶数的情况
if (head.next == slow) {
return true;
}
slow = slow.next;
head = head.next;
}
return true;
}
//链表分割
/*思路:分成两个段,第一个段放小于x的节点,第二个段放大于x的节点,然后把两个段
* 连起来,此时要考虑是否同时有小于x的和大于x的数据,之后要考虑串起来之后的最后
* 一个节点next是否需要置空*/
public ListNode partition(int x) {
ListNode bs = null;
ListNode be = null;
ListNode as = null;
ListNode ae = null;
ListNode cur = head;
while (cur != null) {
if (cur.val < x) {
if (bs == null) {
bs = cur;
be = cur;
}else {
be.next = cur;
be = be.next;
}
}else {
if (as == null) {
as = cur;
ae = cur;
} else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
//有可能不会同时存在小于x和大于等于x的数据
//如果第一个段中没有数据,就返回第二个段,如果第二个段还没有数据
//此时直接返回null,
if (bs == null) return as;
//第一个段不为空
be.next = as;
//如果第二个段不为空,也就是有>x的数据,此时需要把最后一个节点next域置空
if (as != null) {
ae.next = null;
}
return bs;
}
//相交链表
public ListNode getIntersectionNode1(ListNode headA, ListNode headB) {
//1.先求长度
ListNode cur1 = headA, cur2 = headB;
int len1 = 0, len2 = 0;
while (cur1 != null) {
len1++;
cur1 = cur1.next;
}
while (cur2 != null) {
len2++;
cur2 = cur2.next;
}
//2.让比较长的链表的cur 走差值的步数
cur1 = headA;//这个地方一定要注意:让cur1和cur2指回来
cur2 = headB;//因为求完长度之后,cur1 和 cur2 都已经是空的了
int len = Math.abs(len1 - len2);
while (len != 0) {
if (len1 < len2) {
cur2 = cur2.next;
}else {
cur1 = cur1.next;
}
}
//2.然后再让cur1 和 cur2 一起走
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pl = headA,ps = headB;
int len1 = 0,len2 = 0;
while (pl != null) {
len1++;
pl = pl.next;
}
while (ps != null) {
len2++;
ps = ps.next;
}
//2.让pl和ps 指回来
pl = headA;
ps = headB;
int len = len1 - len2;
//3.根据len的值修改pl 和 ps 的指向
if (len < 0) {
pl = headB;
ps = headA;
len = len2 - len1;
}//此时len一定是一个正数 pl一定指向的是最长的 ps一定指向的是最短的
while (len != 0) {
pl = pl.next;
len--;
}
while (pl != ps) {//这里可以不用判断pl和ps是否是空的,如果是空的
//此时返回的是ps或者pl,返回的也是null
pl = pl.next;
ps = ps.next;
}
return ps;
}
//判断链表是否有环
//快慢指针为啥不能走三步,走一步:如果有链表有环且只有两个节点,此时会错过,
//永远也不会相遇
//但是如果是走两步和走一步的情况,此时fast和slow最多差一个环的长度,此时
//fast和slow是一次追击一步,如果存在环一定会相遇
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
public boolean hasCycle2(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
//或者走到这里肯定有一个是空的了
if (fast == null || fast.next == null) {
return false;
}
return true;
}
//创建一个环
public void createLoop() {
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = head.next.next;
}
//环形链表:返回链表开始入环的第一个节点
//结论:让一个指针从链表的起始位置开始遍历链表,同时让一个指针从相遇点开始绕环遍历
//两个指针每次都是走一步,最终一定会在入口点相遇
public ListNode detectCycle(ListNode head) {
//先判断是否有环
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
//或者走到这里肯定有一个是空的了
if (fast == null || fast.next == null) {
return null;
}
//走到这里一定是有环
slow = head;
while (slow != fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}