> 文章列表 > 07.LinkedList与链表

07.LinkedList与链表

07.LinkedList与链表

1. ArrayList的缺陷

通过源码知道,ArrayList底层使用数组来存储元素

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// ...
// 默认容量是10private static final int DEFAULT_CAPACITY = 10;//...
// 数组:用来存储元素transient Object[] elementData; // non-private to simplify nested class access// 有效元素个数private int size;public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: " +initialCapacity);}}
// ...
}

由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后 搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java 集合中又引入了LinkedList,即链表结构。

2. 链表

2.1 链表的概念及结构

逻辑上是连续的,物理上不一定是连续的。

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。

 实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向

//双向有三个域        

2. 带头或者不带头

 3. 循环或者非循环

虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如 哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

重点

单向不带头的非循环

双向不带头的非循环

带头的不会改变头,不带头头会变

图示:

//一个节点至少有两个域

//val存放地址,next存放值

//链表相当于一个内部类 

 

 2.2 链表的实现 

// 1、无头单向非循环链表实现
public class SingleLinkedList {//头插法public void addFirst(int data){}//尾插法public void addLast(int data){}//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data){}//查找是否包含关键字key是否在单链表当中public boolean contains(int key){return false;}//删除第一次出现关键字为key的节点public void remove(int key){}//删除所有值为key的节点public void removeAllKey(int key){}//得到单链表的长度public int size(){return -1;}public void clear() {}public void display() {}
}

3.链表面试题

1. 删除链表中等于给定值 val 的所有节点。 203. 移除链表元素 - 力扣(Leetcode)  
class Solution {public static ListNode removeElements(ListNode head, int val) {if(head == null) {return head;}ListNode cur = head.next;ListNode prev = head;while (cur != null) {if(cur.val == val) {prev.next = cur.next;cur = cur.next;}else {prev = cur;cur = cur.next;}}if(head.val ==  val) {head = head.next;}return head;}
}
2. 反转一个单链表。 206. 反转链表 - 力扣(Leetcode)
//要求:时间复杂度O(n),空间复杂度O(1)
//利用头插法
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;}

//cur代表需要翻转的节点

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。876. 链表的中间结点 - 力扣(Leetcode)
//如果是在面试的高度,并不仅仅是结果的问题,还要看具体的做法是否为优。
//空间复杂度O(1)
//使用快慢指针
//路程一样,fast一次走两步,slow一次走一步,fast走完全程,slow刚好为空。
//fast==fast.next.next;
//slow=slow.next;
class Solution {public ListNode middleNode(ListNode head) {if(head==null){return null;}if(head.next==null){return head;}ListNode slow=head;ListNode fast=head;while(fast !=null&&fast.next!=null){//注意二者不可以互换fast=fast.next.next;slow=slow.next;}return slow;}
}
4. 输入一个链表,输出该链表中倒数第k个结点。 链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

 要求:只遍历列表一遍

public class Solution {public ListNode FindKthToTail(ListNode head,int k) {if(k<=0||head==null){return null;}ListNode fast=head;ListNode slow=head;//1.首先让fast走k-1部int count=k-1;while(count--!=0){fast=fast.next;if(fast==null){return null;}}while(fast.next!=null){fast=fast.next;slow=slow.next;}return slow;}
}

//快慢指针

//首先让fast走k-1步

//fast走完了

//slow就是所要找的

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。21. 合并两个有序链表 - 力扣(Leetcode)
class Solution {public ListNode mergeTwoLists(ListNode list1, ListNode list2) {ListNode newHead=new ListNode();ListNode tmH=newHead;while(list1 !=null&&list2!=null){if(list1.val<list2.val){tmH.next=list1;tmH=tmH.next;list1=list1.next;}else{tmH.next=list2;tmH=tmH.next;list2=list2.next;}}if(list1!=null){tmH.next=list1;}if(list2!=null){tmH.next=list2;}return newHead.next;}
}
6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。链表分割_牛客题霸_牛客网 (nowcoder.com)

 //分成两个链表

//不能改变原来的数据顺序,使用尾插来解决

public ListNode partition( int x) {// write code hereListNode 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 = cur;//cur = cur.next;省略1}//cur = cur.next; 省略2}else {//第一次插入的时候if(as == null) {as = cur;ae = cur;}else {ae.next = cur;ae = cur;}}cur = cur.next;}if(bs == null) {return as;}be.next = as;if(as != null) {ae.next = null;}return bs;}
7. 链表的回文结构。链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

//1.先找到中间节点

// 2.将列表的后半部分进行翻转

 public boolean chkPalindrome() {// write code hereif(head == null) return false;//1、找中间节点ListNode fast = head;ListNode slow = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;}//slow所指的位置就是中间节点//2、开始翻转ListNode cur = slow.next;while (cur != null) {ListNode curNext = cur.next;cur.next = slow;slow = cur;cur = curNext;}//此时翻转完成//3、开始判断是否为回文while (head != slow) {if(head.val != slow.val) {return false;}//偶数节点if(head.next == slow) {return true;}head = head.next;slow = slow.next;}return true;}
}
8. 输入两个链表,找出它们的第一个公共结点。160. 相交链表 - 力扣(Leetcode)

//相交是Y类型不可能是X类型

//val值一样不能说明是相交

//next值相交才能判断有公共节点

//方法:

//1.分别求两个链表的长度 

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {if(headA==null&&headB!=null){return null;}if(headB==null&&headA!=null){return null;}ListNode plong = headA;//假设A长ListNode pshort = headB;//1、分别求两个链表的长度int len1 = 0;int len2 = 0;while (plong != null) {len1++;plong = plong.next;}//O(N)while (pshort != null) {len2++;pshort = pshort.next;}plong = headA;pshort = headB;//2、求差值步的lenint len = len1 - len2;if(len < 0) {plong = headB;pshort = headA;len = len2 - len1;}//plong 一定指向最长的链表  pshort一定指向最短的链表  len一定是一个正数//3、链表长的走len步while (len != 0) {plong = plong.next;len--;}//4、一起走,根据next值判断相遇!while (plong != pshort) {plong = plong.next;pshort = pshort.next;}return plong;}
}

 

9. 给定一个链表,判断链表中是否有环。(贝壳面试题) 

141. 环形链表 - 力扣(Leetcode)

要求:空间复杂度是O(1)

思路:快慢指针

如果这个链表有环,当两个引用速度不一致的时候,一定会在环里相遇,所以现在问题转化为--->速度应当是多少

方法:让fast走两步,slow走一步--->提问:可不可以让fast走n(n≠2),slow走m(m≠1),如果步数不按要求,可能会一直错过。如果一次走太多步,会导致很晚相遇。

【思路】 快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表 带环则一定会在环中相遇,否则快指针率先走到链表的末尾。

【扩展问题】

为什么快指针每次走两步,慢指针走一步可以? 假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快 指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离 就缩小一步,不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指 针的,即相遇。

快指针一次走3步,走4步,...n步行吗?no

/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public boolean hasCycle(ListNode head) {if(head==null || head.next==null){return false;}ListNode slow=head;ListNode fast=head.next;while(slow!=fast){if(fast==null || fast.next==null){return false;}slow=slow.next;fast=fast.next.next;}return true;}
}

10. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL142. 环形链表 II - 力扣(Leetcode) 

slow走的路程X+(C-Y)

fast走的路程X+C+(C-Y)

2*(X+(C-Y)=X+C+(C-Y)

X==Y

结论:起始点到入口点的距离和相遇点到入口的距离相等

上述为特殊情况(此时fast只走了一圈)

现在延拓此结论,fast此时走了N圈(此时圈很小)

2*(X+(C-Y)=X+N*C+(C-Y)

推得X=(N-1)C+Y

结论

让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针 都是每次均走一步,最终肯定会在入口点的位置相遇。

证明

4.LinkedList的模拟实现 

// 2、无头双向链表实现
public class MyLinkedList {//头插法public void addFirst(int data){ }//尾插法public void addLast(int data){}//任意位置插入,第一个数据节点为0号下标public void addIndex(int index,int data){}//查找是否包含关键字key是否在单链表当中public boolean contains(int key){}//删除第一次出现关键字为key的节点public void remove(int key){}//删除所有值为key的节点public void removeAllKey(int key){}//得到单链表的长度public int size(){}public void display(){}public void clear(){}//让每一个节点都被回收
}

5.LinkedList的使用

5.1 什么是LinkedList

LinkedList (Java Platform SE 8 ) (oracle.com)

LinkedList的底层是双向链表结构(链表后面介绍),由于链表没有将元素存储在连续的空间中,元素存储在单独的节 点中,然后通过引用将节点连接起来了,因此在在任意位置插入或者删除元素时,不需要搬移元素,效率比较高。

在集合框架中,LinkedList也实现了List接口,具体如下:

【说明】
1. LinkedList实现了List接口
2. LinkedList的底层使用了双向链表
3. LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
4. LinkedList的任意位置插入和删除元素时效率比较高,时间复杂度为O(1)
5. LinkedList比较适合任意位置插入的场景

5.2 LinkedList的使用

1. LinkedList的构造
方法 解释
LinkedList() 无参构造
public LinkedList(Collection<? extends E> c) 使用其他集合容器中元素构造List

 

public static void main(String[] args) {
// 构造一个空的LinkedListList<Integer> list1 = new LinkedList<>();List<String> list2 = new java.util.ArrayList<>();list2.add("JavaSE");list2.add("JavaWeb");list2.add("JavaEE");
// 使用ArrayList构造LinkedListList<String> list3 = new LinkedList<>(list2);}

2. LinkedList的其他常用方法介绍

方法 解释
boolean add(E e) 尾插 e
void add(int index, E element) 将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) 尾插 c 中的元素
E remove(int index) 删除 index 位置元素
boolean remove(Object o) 删除遇到的第一个 o
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
void clear() 清空
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
int lastIndexOf(Object o) 返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) 截取部分 list

 

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1); // add(elem): 表示尾插list.add(2);list.add(3);list.add(4);list.add(5);list.add(6);list.add(7);System.out.println(list.size());System.out.println(list);
// 在起始位置插入0list.add(0, 0); // add(index, elem): 在index位置插入元素elemSystem.out.println(list);list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()list.removeFirst(); // removeFirst(): 删除第一个元素list.removeLast(); // removeLast(): 删除最后元素list.remove(1); // remove(index): 删除index位置的元素System.out.println(list);
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回falseif(!list.contains(1)){list.add(0, 1);}list.add(1);System.out.println(list);System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置int elem = list.get(0); // get(index): 获取指定位置元素list.set(0, 100); // set(index, elem): 将index位置的元素设置为elemSystem.out.println(list);
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回List<Integer> copy = list.subList(0, 3);System.out.println(list);System.out.println(copy);list.clear(); // 将list中元素清空System.out.println(list.size());}

3. LinkedList的遍历
 

public static void main(String[] args) {LinkedList<Integer> list = new LinkedList<>();list.add(1); // add(elem): 表示尾插list.add(2);list.add(3);list.add(4);list.add(5);list.add(6);list.add(7);System.out.println(list.size());
// foreach遍历for (int e:list) {System.out.print(e + " ");} System.out.println();
// 使用迭代器遍历---正向遍历ListIterator<Integer> it = list.listIterator();while(it.hasNext()){System.out.print(it.next()+ " ");} System.out.println();
// 使用反向迭代器---反向遍历ListIterator<Integer> rit = list.listIterator(list.size());while (rit.hasPrevious()){System.out.print(rit.previous() +" ");} System.out.println();}

6. ArrayList和LinkedList的区别

不同点 ArrayList LinkedList
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持O(1) 不支持:O(N)
头插 需要搬移元素,效率低O(N) 只需修改引用的指向,时间复杂度为O(1)
插入 空间不够时需要扩容 没有容量的概念
应用场景 元素高效存储+频繁访问 任意位置插入和删除频繁

//链表遍历很复杂