一 、链表理论基础
二、203.移除链表元素
三、707.设计链表
四、206.反转链表
五、 24. 两两交换链表中的节点
六、19.删除链表的倒数第N个节点
七、 面试题 02.07. 链表相交
八、 142.环形链表II
九、链表总结
一 、链表理论基础
建议:了解一下链接基础,以及链表和数组的区别
文章链接
1.定义
通过指针串联在一起的线性结构;
每个节点包括:数据域、指针域;
(比如单链表的指针域是存放指向下一个节点的指针,单链表最后一个节点的指针域比较特殊,是指向null空指针)
入口节点:链表的头结点(head);
2.类型
(1)单链表
指针域只能指向节点的下一个节点或null(最后一个节点的指针域比较特殊,是指向null空指针)
(2)双链表
每个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
(3)循环链表
链表收尾相连。可以解决“约瑟夫环”问题。
3.存储方式
非连续;
是通过指针域的指针链接在内存中的各个节点;
分配机制取决于操作系统的内存管理。
4.js代码定义
// JS
class ListNode {
val;
next = null;
constructor (value) {
this.val = value;
this.next - null;
}
}
// TS
class ListNode {
public val: number;
public next: ListNode | null = null;
constructor(value: number) {
this.val = value;
this.next = null;
}
}
5.链表的操作
(1)删除 O(1)
将要删除节点的前一个节点的next指针指向要删除节点的后一个节点即可。
(这其中涉及:查找要删除节点的前一个节点的操作,此查找操作时间复杂度O(n) )
(2)添加 O(1)
将要添加处的节点的next指针指向要添加的节点,再将添加的节点的next指针指向后一个节点即可。
(这其中涉及:查找要添加节点的前一个节点的操作,此查找操作时间复杂度O(n) )
6.数据vs链表
插入/删除 | 查询 | 适用 | |
---|---|---|---|
数组 | O(n) | O(1) | 数据量固定、频繁查询、少增删 |
链表 | O(1) | O(n) | 数据量不固定、频繁增删、少查询 |
二、LeetCode203.移除链表元素
输入 | 输出 |
---|---|
[1, 2, 3, 6, 4, 5, 6] 6 | [1, 2, 3, 4, 5] |
[] 1 | [] |
[7, 7, 7 ,7] 7 | [] |
两种思路:
(1)常规法
如果是头结点: 让head = head.next(因为头结点无前一个节点)
如果非头结点:让要删除的这个节点的前一个节点指向这个节点的下一个节点
(2)虚拟头结点法:(重点)
在链表的最前面加一个虚拟头结点,这样就一律当做非头结点来处理
建议: 本题最关键是要理解虚拟头结点的使用技巧,这个对链表题目很重要。
文章讲解
手把手带你学会操作链表 | LeetCode:203.移除链表元素
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
/*(1)常规法
如果是头结点: 让head = head.next(因为头结点无前一个节点)
如果非头结点:让要删除的这个节点的前一个节点指向这个节点的下一个节点
*/
while (head !== null && head.val === val) {
head = head.next;
}
if (head == null) return head;
let cur = head;
while (cur !== null && cur.next !== null) {
if (cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
};
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
/* (2)虚拟头结点法
在链表的最前面加一个虚拟头结点,这样就一律当做非头结点来处理
*/
let dummyHead = new ListNode(); // 虚拟头结点
dummyHead.next = head; // 虚拟头结点与链表连接起来
let cur = dummyHead; // 临时设定个cur,为了不断遍历链表改变链表,又不影响最后的返回
while (cur !== null && cur.next !== null) {
if (cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dummyHead.next; // head可能已经被删除
};
三、LeetCode 707.设计链表
get(index)
获取链接第index个节点的值,若无返回-1
addAtHead(val)
在链表头元素之前添加val节点,插入后的val为第一个节点
addAtTail(val)
将值为val的节点添加到链表最后
addAtTail(val)
将值为val的节点添加到链表最后
addAtIndex(index, val)
index=链表长度,加链表末尾;index>链表长度,不加;index<0,加头部;其它时候正常添加到链表中
deleteAtIndex(index, val)
若index有效,删除第index个节点
建议: 这是一道考察 链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点
文章讲解
帮你把链表操作学个通透!LeetCode:707.设计链表
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
var MyLinkedList = function() {
this._size = 0;
this._tail = null;
this._head = null;
};
MyLinkedList.prototype.getNode = function(index) {
if(index < 0 || index >= this._size) return null;
let dummyHead = new LinkNode(0, this._head);
let cur = dummyHead.next;
while(index) {
cur = cur.next;
index--;
}
return cur;
};
/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function(index) {
if(index < 0 || index >= this._size) return -1;
// 获取当前节点
return this.getNode(index).val;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function(val) {
let newHead = new LinkNode(val, this._head);
this._head = newHead;
this._size++;
if(!this._tail) {
this._tail = newHead;
}
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function(val) {
let newTail = new LinkNode(val, null);
this._size++;
if(this._tail) {
this._tail.next = newTail;
this._tail = newTail;
return;
}
this._tail = newTail;
this._head = newTail;
};
/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function(index, val) {
if(index > this._size) return;
if(index <= 0) {
this.addAtHead(val);
return;
}
if(index === this._size) {
this.addAtTail(val);
return;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
};
/**
* @param {number} index
* @return {void}
*/
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index < 0 || index >= this._size) return;
if(index === 0) {
this._head = this._head.next;
// 如果删除的这个节点同时是尾节点,要处理尾节点
if(index === this._size - 1){
this._tail = this._head
}
this._size--;
return;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = node.next.next;
// 处理尾节点
if(index === this._size - 1) {
this._tail = node;
}
this._size--;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* var obj = new MyLinkedList()
* var param_1 = obj.get(index)
* obj.addAtHead(val)
* obj.addAtTail(val)
* obj.addAtIndex(index,val)
* obj.deleteAtIndex(index)
*/
四、LeetCode 206.反转链表
要注意的点: 学会双指针法,就明白了递归法(因为递归其实实质还是双指针法)
帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法
双指针法:cur、pre分别指向head和null,改变cur的指向,2个指针后移。只要cur有值就不断循环
// 双指针法
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let cur = head;
let pre = null;
while (cur) { // 当cur===null,自然循环完了
let temp = cur.next; // 在改变cur方向之前,使用临时指针保存cur与下一节点之间的连接
cur.next = pre; // 改变方向
pre = cur; // 两个指针后移,先移动pre
cur = temp; // 再移动cur
}
return pre; // 最后cur指向null,pre指向新的链表head
};
递归法:其实拆解来看就是双指针法
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
// 自己定义reverse函数用于翻转及递归调用
var reverse = function(pre, cur) {
if(!cur) return pre;
const temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
return reverse(pre, temp);
}
return reverse(null, head);
};
五、 LeetCode 24. 两两交换链表中的节点
输入 | 输出 |
---|---|
[1, 2, 3, 4] | [2, 1, 4, 3] |
[] | [] |
[1] | [1] |
用虚拟头结点,这样会方便很多。
不是单纯改变节点内部val,是实际的进行节点交换。
为什么需要temp保存临时节点?因为如果不保存,那找1或者3就没有对应的关系了。
文章讲解
帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var swapPairs = function(head) {
let dummyHead = new ListNode(0, head);
let cur = dummyHead;
while(cur.next && cur.next.next) {
let temp = cur.next; // 保存1
let temp1 = cur.next.next.next; // 保存3
cur.next = cur.next.next; // 虚拟头结点的next指向2
cur.next.next = temp; // 2指向1
temp.next = temp1; // 1指向3
cur = cur.next.next; // 当前指针往后移动2位
}
return dummyHead.next;
};
六、LeetCode 19.删除链表的倒数第N个节点
双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点。
建议先看视频。
文章讲解
链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function(head, n) {
let dummyHead = new ListNode(0, head);
let slow = dummyHead;
let fast = dummyHead;
n++; // 虚拟头节点多增了1个节点
while(n-- && fast !== null) {
fast = fast.next; // 快指针先移动n位
}
while (fast !== null) {
// 再快慢指针同时移动,直到fast指针指向null
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummyHead.next;
};
七、 LeetCode 面试题 02.07. 链表相交
输入 | 输出 |
---|---|
2个单链表的头结点headA和headB | 两个单链表相交的起始节点(如果没有返回null) |
不存在环。
交点不是数值相等,而是指针相等。
本题没有视频讲解。
注意 数值相同,不代表指针相同。
文章讲解
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
var getListLen = function(head) {
let len = 0;
let cur = head;
while (cur) {
len++;
cur = cur.next;
}
return len;
}
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function(headA, headB) {
let curA = headA;
let curB = headB;
let lenA = getListLen(headA);
let lenB = getListLen(headB);
if (lenA < lenB) {
[curA, curB] = [curB, curA];
[lenA, lenB] = [lenB, lenA];
}
let i = lenA - lenB;
while (i-- > 0) {
curA = curA.next;
}
while (curA && curB !== curA) {
curA = curA.next;
curB = curB.next;
}
return curA;
};
八、 LeetCode 142.环形链表II
输入 | 输出 |
---|---|
一个环形链表,pos并不是输入,只是好表示环形链表 | 链表开始入环的第一个节点 |
head=[3, 2, 0, -4] pos = 1 | 1 |
head=[1, 2] pos = 0 | 0 |
算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口,建议先看视频。
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
if(!head || !head.next) return null;
let fast = head.next.next; // 初始快指针
let slow = head.next; // 初始慢指针
while( fast && fast.next) { // 因为快指针每次移动2步
fast = fast.next.next;
slow = slow.next;
if (fast == slow) { // 快慢指针相遇
let slow = head; // 相遇点
while (fast !== slow) {
slow = slow.next; // 一起往后走
fast = fast.next; // 一起往后走
}
return slow;
}
}
return null;
};
九、链表总结
1.种类(略)
2.存储方式(略)
3.如何进行增删查改(略)
4.vs数组(略)
5.经典题目:
(1)移除链表元素
①常规法要判断是不是头结点
②所以虚拟头结点,虚拟后记得使用临时指针cur指向dummyHead,就可以正常cur.next = cur.next.next;
最后return 的是dummyHead.next(因为dummyHead可能被删)
(2)设计链表
①get方法,需要遍历,就需要虚拟头结点
②addAtHead 需要先把新的节点连到旧链表上,再用dummyHead连接到新节点上
③addAtTail 遍历到最后尾部,再cur.next = newNode
④addAtIndex 遍历到n,cur.next = newNode
⑤deleteAtIndex 遍历到n, cur.next = cur.next.next
(3)反转链表
①双指针是递归法的基础,学会了双指针,递归法就会了。
cur指针指向head、pre指向null。temp = cur.next保存关系不丢失。
cur改变方向cur.next = pre后。
两个指针后移:先以pre = cur,cur = temp。
②抽一个函数出来专门做上面的操作。通过递归调用来给cur、pre指针循环赋值。
(4)两两交换链表中的节点
①有循环遍历就有dummyHead。
②while循环的终止条件想清楚:cur.next !== null && cu.next.next ! == null
(因为cur一定指向需要翻转的2个节点的前一个节点,自己可以画下)
③dummyHead指向2,2指向1,1指向3
在dummyHead指向2之前,先保存1、3
④交换完毕后,每次循环cur = cur.next.next 走两位
⑤最后return的是dummyHead.next
两两交换前需要保存节点之间的关系:
(5)删除链表中倒数第N个节点
①虚拟头结点,因为要循环遍历。快慢指针均从dummyHead开始,多了个节点,所以n++
②fast指针先走n步骤,直到n = 0;
③再fast和slow同时走,直到fast走到null
④slow.next = slow.next.next(因为slow指向要删除的节点的前一个节点)
⑤最后return的是dummyHead.next
(6)链表相交
找到相交规律。
①遍历获得A、B长度
②保证A是长的那个,省去分支判断
③求长度差,用于遍历A到与B同一起点的位置
④遍历A、B,遇到相同的直接返回
(7)环形链表
主要找到形成环的条件。快慢指针之间的关系。
6.常用技巧:
(1)虚拟头结点
(2)双指针、递归
(3)while循环