[代码随想录] 链表总结
链表基础知识
链表定义
链表是一种通过指针串在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表类型
这里进行图片展示,通过图片理解
单链表
双链表
循环链表
链表的存储方式
数组在内存中是连续分布的,链表在内存中存储不是连续分布的,通过指针域的指针链接各个节点
java中链表节点的表示
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;
}
}
链表算法
在这之前先清楚一件事,leetcode中的题目传入的链表都没有头节点 ,传入的都是链表中的第一个节点,这个需要清楚
移除链表元素
203.移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
这道题比较简单,属于基础的对链表的操作,直接展示代码。需要注意的是,使用虚拟头节点可直接从虚拟头节点遍历链表,最后返回虚拟头节点的next;不使用虚拟头节点则需要保证head的值不等于val,返回head即可
/**
* 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) {
if(head == null) {
return head;
}
ListNode res = new ListNode(-1, head);
ListNode pre = res, cur = head;
while(cur != null) {
if(cur.val == val) {
pre.next = cur.next;
}else {
pre = cur;
}
cur = cur.next;
}
return res.next;
}
}
// 不使用虚拟头节点
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head != null && head.val == val) {
head = head.next;
}
if(head == null) {
return head;
}
ListNode pre = head;
ListNode cur = head.next;
while(cur != null) {
if(cur.val == val) {
pre.next = cur.next;
}else {
pre = cur;
}
cur = cur.next;
}
return head;
}
}
设计链表
707.设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index有效,则删除链表中的第 index 个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
该道题目主要是对链表的操作实现,包括插入,删除,查询。这里可以将addAtHead(val)、addAtTail(val)两个函数看作特殊情况的addAtIndex(index,val)。addAtHead(val)是在链表头部插入,即addAtIndex(0, val);addAtTail(val)是在链表的尾部插入,即addAtIndex(size, val)。定义size来维护链表的长度。所有的操作都是链表基础操作,耐心便不难写出。
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) {
this.val = val;
}
}
class MyLinkedList {
int size;
ListNode head;
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
public int get(int index) {
if(index < 0 || index >= size) {
return -1;
}
ListNode cur = head;
for(int i = 0;i <= index;i++) {
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
if(index > size) return;
if(index < 0) index = 0;
ListNode cur = head;
// 链表下标表示同数组下标
// i < index找到的是index前一个节点,i <= index找到的是index个节点
for(int i = 0;i < index;i++) {
cur = cur.next;
}
ListNode newNode = new ListNode(val);
newNode.next = cur.next;
cur.next = newNode;
size++;
}
public void deleteAtIndex(int index) {
if(index < 0 || index >= size) return;
size--;
if(index == 0) {
head = head.next;
return;
}
ListNode cur = head;
for(int i = 0;i < index;i++) {
cur = cur.next;
}
cur.next = cur.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
翻转链表
206.反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
双指针算法
这道题可以定义两个指针,一前一后指向两个节点,然后在定义一个temp存储后指针的下一个节点,两个指针指向的节点翻转,然后两个指针同时后移。
/**
* 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) {
// cur当前节点,pre前一个节点
ListNode temp = null, pre = null;
ListNode cur = head;
while(cur != null) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
递归算法
这道题也可以递归实现,通过递归步进指针
// 递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
return reverse(cur, temp);
}
}
两两交换链表中的节点
24. 两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
双指针实现
这道题可以通过定义两个指针,一个temp指针临时存放下一步的节点,通过三个指针之间的操作实现链表两两翻转,和上边的翻转链表题目有点类似。三个指针的操作如下图
/**
* 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 dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode prev = dummyNode;
while (prev.next != null && prev.next.next != null) {
ListNode temp = head.next.next; // 缓存 next
prev.next = head.next; // 将 prev 的 next 改为 head 的 next
head.next.next = head; // 将 head.next(prev.next) 的next,指向 head
head.next = temp; // 将head 的 next 接上缓存的temp
prev = head; // 步进1位
head = head.next; // 步进1位
}
return dummyNode.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 swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode next = head.next;
// 递归
ListNode newNode = swapPairs(next.next);
// 交换
next.next = head;
head.next = newNode;
return next;
}
}
删除链表中倒数第n个节点
19.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
这道题最快想到的暴力解法应该是先遍历整个链表,求到链表的长度,然后根据n值求的倒数指针到达倒数第n+1个节点需要多少步,然后再删除节点。但是这样比较太冗余,所有采用双指针解法。
快慢指针解决思路,快指针和慢指针之间相差n - 1个结点,当快指针到达最后一个节点时(fast.next == null),慢指针指向的便为倒数第n个节点的前一个节点。(先让快指针先走n步,实现相差n-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 removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
for(int i = 0;i < n;i++) {
fast = fast.next;
}
while(fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
链表相交
07. 链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
- 题目数据 保证 整个链式结构中不存在环。
- 注意,函数返回结果后,链表必须 保持其原始结构 。
这道题需要先求两个链表的长度,通过长度差值让长链表先走差值步,让两个链表对齐,然后两个链表同时前进,如果相交则某一时刻两个指针会相等,不相交则返回空
/**
* 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) {
lenA++;
curA = curA.next;
}
while(curB != null) {
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
// 让curA始终作为长链表
if(lenB > lenA) {
int lenTemp = lenA;
lenA = lenB;
lenB = lenTemp;
ListNode nodeTemp = curA;
curA = curB;
curB = nodeTemp;
}
for(int i = 0;i < lenA - lenB;i++) {
curA = curA.next;
}
while(curA != null) {
if(curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
}
环形链表
142.环形链表II
力扣题目链接
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
- 不允许修改链表
判断链表是否有环很简单,经典快慢指针,让快指针每次比慢指针多步进一步,如果存在环的话快慢两个指针一定会相遇。难点在于判断环的入口,当有环快慢指针相遇时,可通过从链表头部出发一个指针,相遇节点出发一个指针,两个指针同时一步一步前进,相遇时该节点就位环的入口
接下来是判断环的入口的数学验证:
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
那么相遇时: slow指针走过的节点数为:x + y,fast指针走过的节点数为:x + y + n(y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y ,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z,
这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到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) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if(fast == slow) {
ListNode index1 = head;
ListNode index2 = slow;
while(index2 != index1) {
index1 = index1.next;
index2 = index2.next;
}
return index1;
}
}
return null;
}
}
总结
这个图是 代码随想录知识星球 成员:海螺人,所画,总结的非常好,分享给大家