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.链表面试题
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;}
}
//要求:时间复杂度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代表需要翻转的节点
//如果是在面试的高度,并不仅仅是结果的问题,还要看具体的做法是否为优。//空间复杂度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;}
}
要求:只遍历列表一遍
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就是所要找的
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;}
}
//分成两个链表
//不能改变原来的数据顺序,使用尾插来解决
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;}
//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;}
}
//相交是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接口,具体如下:
5.2 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) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
//链表遍历很复杂