链表¶
link : 代码随想录链表部分
1. 链表介绍¶
1.1 链表的分类¶
单链表
链表:链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
图示:
双链表
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
图示:
循环链表
循环链表,就是链表首尾相连。
循环链表可以用来解决 约瑟夫环问题。
1.2 链表的存储方式¶
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
即 数组在内存中是连续分布的,链表在内存中不是连续分布的
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
1.3 链表的实现(代码实现)¶
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
一定要自己定义构造函数, 从而便于在初始化的时候为数据域赋值
1.4 链表的操作¶
删除节点
删除节点 D :
将C节点的next指针 指向E节点就可以了
添加节点
添加节点 F :
链表的增添和删除都是O(1)操作,也不会影响到其他节点。 但是如果删除最后一个节点,就需要从头开始查找,复杂度为 \(O(n)\)
1.5 性能分析¶
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
2. 移除链表元素¶
link : LeetCode 203
思考:
以链表 1 4 2 4 来举例,移除元素4:
特殊情况:移除头节点
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
移除头节点的两种方式:
- 直接使用原来的链表来进行删除操作。
- 设置一个虚拟头结点在进行删除操作
方法一:直接使用原来的链表来进行删除操作。(将头结点向后移动一位,这样就从链表中移除了一个头结点)
这种情况下,将移除头节点作为一种特殊的情况,单独写一段代码来处理
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 直接在原链表进行操作
// 判断头节点是否满足要求
// 必须使用while, 从而保证得到的 链表 头节点不满足要求
// 比如 1 1 1 1 2, val=1 的情况
while(head != null && head.val == val){
head = head.next;
}
// 判断非头节点
// 非头节点的删除是通过前一个节点来完成
ListNode newNode = head;
// 需要判断 newNode是否为空
// 比如 1 1 1 1 1, val = 1, 那么 newNode=null
while(newNode != null && newNode.next != null){
if (newNode.next.val == val){
newNode.next = newNode.next.next;
}else{
newNode = newNode.next;
}
}
return head;
}
}
方法二:设置一个虚拟头结点在进行删除操作 (这样原链表的所有节点就都可以按照统一的方式进行移除了)
最后在题目中,return 头结点的时候,别忘了 return virtualHead.next;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 设置一个虚拟节点, 从而使得删除头节点和删除非头节点都是通过前面的节点实现
ListNode virtualHead = new ListNode(-1, head);
ListNode newNode = virtualHead;
while(newNode.next != null){
if (newNode.next.val == val)
newNode.next = newNode.next.next;
else newNode = newNode.next;
}
return virtualHead.next;
}
}
3. 设计链表¶
link : LeetCode 707
思考:
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
链表操作的两种方式: - 直接使用原来的链表进行操作 - 设置一个虚拟头节点进行操作
采用 设置一个虚拟头节点的方法:
// 定义链表节点类
class ListNode{
int val;
ListNode next;
public ListNode(){}
public ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
// 链表的虚拟头节点
ListNode virtualHead;
// size: 链表的元素个数
int size;
// 初始化链表
// 此处是初始化一个虚拟的头节点
public MyLinkedList() {
this.virtualHead = new ListNode(-1);
this.size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
// 因为index是从0开始, 所以index的有效范围为 [0, size-1]
public int get(int index) {
if (index >= size || index < 0) return -1;
ListNode tempNode = virtualHead;
int res;
// 因为包含了虚拟节点,所以进行查找的时候需要查找到 第 index+1 个节点
for (int i=0; i <= index; i++){
tempNode = tempNode.next;
}
res = tempNode.val;
return res;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
public void addAtHead(int val) {
ListNode newHeadNode = new ListNode(val);
// 因为是在链表的最前面插入一个节点, 所以下一个节点是原链表的头节点
newHeadNode.next = this.virtualHead.next;
this.virtualHead.next = newHeadNode;
// 插入一个节点, 链表的长度加一
this.size++;
}
// 在链表最后面添加一个节点
public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
ListNode tempNode = this.virtualHead;
while(tempNode.next != null)
tempNode = tempNode.next;
tempNode.next = newNode;
// 链表节点加一
this.size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
public void addAtIndex(int index, int val) {
// 在头部插入节点
if (index <= 0){
this.addAtHead(val);
}
else if(index == size){
this.addAtTail(val);
}else if (index > size){
return;
}else{
ListNode newNode = new ListNode(val);
ListNode tempNode = this.virtualHead;
// 因为是插入到 链表中下标为index的节点之前, 所以需要找到该节点前面的 节点
// 而因为链表中有虚拟头节点, 所以相当于需要找到第 index - 1 + 1个节点
for (int i=0; i<index; i++)
tempNode = tempNode.next;
// 找到前面的节点
newNode.next = tempNode.next;
tempNode.next = newNode;
size++;
}
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
public void deleteAtIndex(int index) {
if (index >= 0 && index < size){
ListNode tempNode = this.virtualHead;
// 找到下标为 index 的前面的节点
for (int i=0; i<index; i++)
tempNode = tempNode.next;
// 进行删除
tempNode.next = tempNode.next.next;
size--;
}else return;
}
}
4. 反转链表¶
link: LeetCode 206
思考:
只需要改变链表的 next 指针的指向,就可以直接将链表反转,从而不用定义一个新的链表
双指针法:
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
之后 需要pre 和 cur 都向前走一位
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
// 使用双指针, 一个pre, 一个curNode, 一个指向当前节点之前的节点, 一个指向当前节点
ListNode pre = null;
// 当前的节点
// 需要当前节点遍历整个链表
ListNode curNode = head;
// 当前节点的下一个节点
ListNode tempNode;
while(curNode != null){
tempNode = curNode.next;
curNode.next = pre; // 改变当前节点的指向
// 更新 pre 和 curNode, 都向前走一位
pre = curNode;
curNode = tempNode;
}
// 遍历结束后, pre正好指向最后一个节点, curNode = null
// 因为改变了指针的指向, 所以最后的节点变成了头节点
return pre;
}
}
5. 两两交换链表中的节点¶
link: LeetCode 24
思考:
因为需要进行节点交换,而不是单纯更改节点内的值,因此需要改变节点的指针
图示:
交换过程:
- 初始的时候,cur指针指向虚拟的头节点(其实可以将cur看成虚拟头节点)
对节点的操作,需要前面的节点才能找到,因此为了保证对头节点的操作逻辑和与其他节点相同,涉及到链表的节点操作都用虚拟头节点
tmp1 = cur.next
tmp2 = cur.next.next.next
cur.next = cur.next.next // 指向 cur->2
cur.next.next = tmp // 2->1
cur.next.next.next = tmp2 // 1->3
// 更新cur, 向前移动两位
cur = cur.next.next // cur指向1号节点
- 操作之后的链表
即:
代码实现:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
// 使用虚拟头节点
ListNode virtualNode = new ListNode(-1, head);
// 定义当前节点, 一开始指向虚拟头节点, 从而保证处理头节点和其他节点采用相同的逻辑
ListNode curNode = virtualNode;
// 需要进行交换的是 curNode.next 和 curNode.next.next
// 因此需要保证两者都不为null
while (curNode.next != null && curNode.next.next!=null){
ListNode tmp1 = curNode.next;
// 要保证链表的连接
ListNode tmp2 = curNode.next.next.next;
curNode.next = curNode.next.next; // cur->2
curNode.next.next = tmp1; // 2->1
curNode.next.next.next = tmp2; // 1->3
// 交换之后, cur 向前移动两位, 移动到 下一组需要交换的两个节点的前面一个节点
curNode = curNode.next.next;
}
return virtualNode.next;
}
}
6. 删除链表的倒数第N个节点¶
link : LeetCode 19
思考: 要删除倒数第N个节点, 那么必须找到该节点前面的节点
使用双指针:双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
思路:
为了保证操作头节点的逻辑和操作其他节点相同,使用虚拟头节点
-
定义fast指针和slow指针,初始值为虚拟头结点,如图:
-
fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:(移动n步也可以, 代码实现是按照移动n步)
- fast和slow同时移动,直到fast指向末尾,如题:
- 删除slow指向的下一个节点,如图:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 定义一个虚拟头节点, 从而保证处理头节点的逻辑和处理其他节点相同
ListNode virtualNode = new ListNode(-1, head);
// 使用双指针, 先让fastNode向前移动n步, 然后两者一块移动
// 当 fast指针 指向链表最后一个元素, slow指针指向目标节点的前一个节点
ListNode slowNode = virtualNode;
ListNode fastNode = virtualNode;
for (int i=0; i<n; i++){
fastNode = fastNode.next;
}
// 两者同时移动
// 当fastNode指向最后的一个元素跳出循环
while(fastNode.next != null){
slowNode = slowNode.next;
fastNode = fastNode.next;
}
// 删除目标元素
slowNode.next = slowNode.next.next;
return virtualNode.next;
}
}
7. 链表相交¶
link: LeetCode 160
思考:
题目即为求 两个链表交点的节点的指针(即 c1),但是交点不是数值相等, 而是指针相等, 比如下面的图是:
很明显,相交的节点是 “8”, 而不是 “1”
思路:
首先求出两个链表的长度,然后求出两个链表的长度的差值,然后将指向长度比较长的链表的指针先移动
比如下面图示:
如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:
求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:
从而比较 curA和curB是否相等, 如果不相等的话,继续同时向后移动
/**
* 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) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
// 求出两个链表的长度
while(curA != null){
curA = curA.next;
lenA++;
}
while(curB != null){
curB = curB.next;
lenB++;
}
curA = headA;
curB = headB;
// 保证 A 链表是长度比较长的链表
// 如果不是则交换, 从而保证下面的逻辑通用
if (lenB > lenA){
int temp = lenA;
lenA = lenB;
lenB = temp;
ListNode tempNode = curA;
curA = curB;
curB = tempNode;
}
// 下面首先长度比较长的链表先移动两者的差值
int gap = lenA - lenB;
while(gap-- > 0){
curA = curA.next;
}
// 判断curA和curB是否相等
while(curA != null){
if (curA == curB) return curA;
curA = curA.next;
curB = curB.next;
}
return null;
}
}
8. 环形链表 (重点看)¶
link: LeetCode 142
题目描述:
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思考:
link : 代码随想录 环形链表
video:
考察两方面的内容: 1. 判断链表是否有环(快慢双指针) 2. 如果有环,如何寻找环的入口处
判断链表是否有环:
使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
fast 必须是移动两个节点,slow 必须是移动一个节点,这样快指针相当于以 每次多移动一个节点的速度追赶慢指针,如果有环,则必定能够追赶上。 如果fast每次移动三个节点,slow每次移动一个节点,这样快指针相当于以每次多移动两个节点的速度追赶慢指针,那么有可能会跳过慢指针
fast 和 slow的移动图示:
如果链表中有环,如何寻找环的入口:
假设从头节点到环形入口节点 的节点数是 \(x\) , 环形入口节点到 fast与slow相遇节点的节点书为 \(y\), 从相遇节点啊到环形入口节点 的节点数为 \(z\)
fast与slow 相遇的时候, slow 走过的节点数为 \(x+y\) , fast走过的节点数为 \(x + n(y + z) + y\)
slow 必定是在进入环中 第一圈的时候被fast遇到 fast 必定是在环中走了至少一圈,然后遇到了slow, \(n\) 表示fast在环中走的圈数
因为fast每一步走两个节点, slow每一步走一个节点,所以相遇的时候 根据走过的路径距离形成一个等式: $$ 2(x + y) = x + n(y + z) + y $$ 左右两边同时消去 \(x + y\), 并且把 \(x\) 单独放在左边: $$ x = n(y + z) - y $$
此时看不出什么等量关系,从 \(n(y+z)\) 中提取出来一个 \(y + z\) $$ x = (n-1)(y+z) + z $$
其中 \(y+z\) 是环形中的节点数目,如果 \(n=1\), 即fast 在环形中走一圈后, 与 slow 相遇, 那么 \(x = z\)
即:从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。(另一个等量关系)
当然,也有可以 fast 在环形中走了 若干圈(即 n!=1), 与 slow 相遇, 但是即使是这样, 从 头节点出发一个指针index1,从相遇节点也出发一个指针index2,这两个指针每次只走一个节点,即使 index2 在环中走了若干圈,但是最后仍然会和 index1 相遇
index1 = head;
while (index1 != index2){
index1 = index1.next
index2 = index2.next
size++;
}
// 循环必定会打破, 打破循环的时候 index1=index2 就是环的入口
代码实现:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
// 1. 使用快慢指针找到相遇的地方
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if (fast == slow){
// 2. 寻找环的入口
ListNode index1 = head;
ListNode index2 = slow;
while(index1 != index2){
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}