1.链表基本知识
-
链表中的每个节点至少包含两个部分,数据域与指针域
-
链表中的每个节点,通过指针域的值,形成一个线性结构
-
查找节点
O(n),插入节点O(1),删除节点O(1) -
不适合快速的定位数据,适合动态的插入和删除数据的场景
2.几种经典的链表实现方式
1. 第一类
// 节点类
class Node {
constructor(data) {
this.data = data; // 数据域
this.next = null; // 指针域
}
}
function main() {
//设置链表头
let head = null;
// head先指向一个新节点
head = new Node(1);
// head指向下一个节点
head.next = new Node(2);
// 依次指向
head.next.next = new Node(3);
head.next.next.next = new Node(4);
// 创建了一条4个节点的链表,从头遍历
let p = head;
// 当p不为空时,因为链表最后一位是空
while (p !== null) {
console.log(p.data, "->");
// p依次指向链表中的每一个节点
p = p.next;
}
}
main();
2. 第二类
// 拆成两个数组
let data = new Array(10).fill(0); // 数据域
let next = new Array(10).fill(0); // 指针域
// 添加节点
// 在ind节点后面添加一个地址为p的节点,值为val
function add(ind, p, val) {
// 为了可以中间插入,让p节点指向原先ind节点指向的位置
next[p] = next[ind];
// ind节点指向p
next[ind] = p;
// 节点p值为val
data[p] = val;
return;
}
function main() {
// 首先定义头节点的地址是3
let head = 3;
// 地址为3的节点值为0
data[3] = 0;
// 依次添加
add(3, 5, 1);
add(5, 2, 2);
add(2, 7, 3);
add(7, 9, 100);
// 遍历
let p = head;
// 当p不为0时,因为数组初始填充0
while (p !== 0) {
console.log(data[p], "->");
p = next[p];
}
}
main();
3.链表的经典应用场景
- 场景一:操作系统内的动态内存分配
- 场景二:LRU 缓存淘汰算法
4.经典面试题
141. 环形链表
- 思路一:我们只需要依次遍历整个链表,并创建一个哈希表来存储遍历过的节点,遍历每一个节点,然后存入哈希表。在存入哈希表之前,先判断哈希表中是否存在该节点。如果不存在,则存入哈希表,一直遍历到某节点的
next节点为null,说明链表没有环,遍历结束。当要存入的节点,已经存在于哈希表中,说明链表有环,遍历结束。 - 思路二:快慢指针;如果链表有环,那么快慢指针一定会相遇,指向同一个节点,当指向同一个节点时,遍历结束
var hasCycle = function(head) {
// 如果头节点为空,肯定没有环
if(head === null) return false;
// 定义两个指针,p是慢指针,q是快指针
let p = q = head;
// 如果q.next为空,说明只有一个节点,肯定没有环
if(q.next === null) return false
do{
// p每次走一步,q每次走两步
p = p.next;
q = q.next.next;
}while(p !== q && q && q.next);
// q和q的next节点都不为null,说明有环
return q && q.next ;
};
142. 环形链表 II
证明:p、q相遇点距入环口的距离与头节点距入环口的距离相等
前提:p每次走一步,q每次走两步
假设入环口距离头节点为a,当慢指针p走到入环口时,q在2a处,再假设当前q已在环内,距离入环口为x,即距离p也是x,得出链表总长2a+x。此时q要追上p,要经过x次迭代,所以会在a+x处相遇。此时相遇点距入环口的距离也为a。
var detectCycle = function(head) {
// 先判断有没有环
if(head === null) return null;
let p = q = head;
if(q.next === null) return null
do{
p = p.next;
q = q.next.next;
}while(p !== q && q && q.next);
// q或者q的next节点为null,说明没环
if(q === null || q.next === null) return null;
// 有环,p重置到头节点,然后p、q同步走,相遇即是开始入环的第一个节点
p = head;
while(p !== q) p = p.next, q = q.next;
return q;
};
202. 快乐数
当输入值为19时
19 -> 82 -> 68 -> 100 -> 1
链表思维->唯一指向思维
题目就可以转化为,把每个数看做节点,转化规则看做指针,1看成空地址,本质上就是判断一个链表是否有环。
如果遍历到重复的节点值,说明有环,就不是快乐数,即141的解法
var getNext = function(x){
let z = 0;
while(x){
// 取个位的平方相加
z += (x % 10) * (x % 10);
// 去掉个位
x = Math.floor(x / 10);
}
return z;
}
var isHappy = function(n) {
let p = q = n;
do{
p = getNext(p);
q = getNext(getNext(q));
}while(p !== q && q !== 1);
// q为1说明是快乐数
return q === 1;
};
206. 反转链表
- 第一种解法
定义指针——
pre,pre指向空(反转头)
定义指针——cur,cur指向我们的头节点(未反转头)
定义指针——p,p指向cur所指向节点的下一个节点(未反转头的下一位),这样我们的指针就初始化完毕了
首先,我们将cur指针所指向的节点指向pre指针所指向的节点
然后移动指针pre到指针cur所在的位置,移动cur到p所在的位置,此时,我们已经反转了第一个节点
将我们的p指针指向cur指针所指向节点的下一个节点
然后重复上述操作,当cur指针指向 null 的时候,就完成了整个链表的反转
var reverseList = function(head) {
if(head === null) return null;
let pre = null, cur = head, p = head.next;
while(cur){
cur.next = pre;
pre = cur;
(cur = p) && (p = p.next);
}
return pre;
};
- 第二种解法
var reverseList = function(head) {
// 给递归加结束条件,返回原链表最后一个节点,即新链表第一个节点
if(head === null || head.next === null) return head;
// 拿到新链表第一个节点,并在最后返回
let p = reverseList(head.next);
// 保存当前节点的下一个节点
let tail = head.next;
// 当前节点指向下一个节点的指向
head.next = tail.next;
// 下一个节点指向当前节点
tail.next = head;
return p;
};
92. 反转链表 II
编程技巧:虚拟头节点
思路:找到待反转区域的前一位,然后把反转区域看成一个链表,调用反转头n个节点的函数,反转n-m+1个节点后,再把反转区域的前一位指向反转后的新链表的头节点
// 反转前n个节点,参照92题
var reverseN = function(head, n) {
if(n === 1) return head;
let p = reverseN(head.next, n - 1);
let tail = head.next;
head.next = tail.next;
tail.next = head;
return p;
};
var reverseBetween = function(head, left, right) {
// 先设置一个虚拟头节点,p指向虚拟头,用作迭代
let ret = new ListNode(0, head), p = ret;
// 反转的位数,后续改变了left的值,所以先存起来
let cnt = right - left + 1;
// 先--,因为要走left-1步,找到待反转区域的前一位
while(--left) p = p.next;
// 反转p后的节点
p.next = reverseN(p.next, cnt);
return ret.next;
};
25. K 个一组翻转链表
// 反转前n个节点,参照92题
var __reverseN = function(head, n) {
if(n === 1) return head;
let p = __reverseN(head.next, n - 1);
let tail = head.next;
head.next = tail.next;
tail.next = head;
return p;
};
// 判断剩余链表中够不够n个节点
var reverseN = function(head, n){
let p = head;
// 后续改变了n的值,所以先存起来
let cnt = n;
while(--n && p) p = p.next;
// p为空退出循环,说明不够n个节点返回原节点
if(p === null) return head;
return __reverseN(head, cnt);
}
var reverseKGroup = function(head, k) {
// 设置虚拟头,p指向待反转区域的前一位,q指向待反转区域的头一位,也是反转后的尾节点
let ret= new ListNode(0, head), p = ret, q = p.next;
// 反转后的头节点是原头节点时,证明没有反转,剩的节点不够k个,停止循环
while((p.next = reverseN(q, k)) !== q){
// 此时q是待反转区域的前一位
p = q;
q = p.next;
}
return ret.next;
};
61. 旋转链表
举例,我们命名一个指针指向链表的Head,K=2是让Head往右移动两位。第一步,通过遍历,得到链表的长度length和链表的尾节点,然后让尾节点指向链表的Head,这样就成了环,再让尾节点走length - k步,得到计算新链表head的前一位,断开和head的指向,形成新链表
var rotateRight = function(head, k) {
if(head === null) return null;
let n = 1, p = head;
// 走到最后一位,并算出链表长度
while(p.next) p = p.next, n++;
// 首尾相连
p.next = head;
// 取余,抛掉整圈数
k %= n;
// 走n-k步
k = n - k;
while(k--) p = p.next;
head = p.next;
// 断开首尾连接
p.next = null;
return head;
};
19. 删除链表的倒数第 N 个结点
需要找到待删除元素的前一个节点,该节点此时距离末尾空节点也为N。
方法:设p指向虚拟头节点、q节点指向头节点,并往后走N步,再p、q一起走,当q指向空节点时,q指向待删除元素的前一个节点
var removeNthFromEnd = function(head, n) {
let ret = new ListNode(0, head), p = ret, q = head;
// q先往后走n步
while(n--) q = q.next;
// p、q一起走
while(q) p = p.next, q = q.next;
// 此时p指向待删除元素的前一位,绕过下一个节点,完成删除
p.next = p.next.next;
return ret.next;
};
83. 删除排序链表中的重复元素
var deleteDuplicates = function(head) {
if(head === null) return null;
let p = head;
while(p.next){
// 判断当前节点和下一个节点值是否相同,相同则删除下一个节点,否则继续向后遍历
if(p.val === p.next.val){
p.next = p.next.next;
}else{
p = p.next;
}
}
return head;
};
82. 删除排序链表中的重复元素 II
var deleteDuplicates = function(head) {
if(head === null) return null;
let ret = new ListNode(0, head), p = ret, q;
while(p.next){
// 判断当前节点的下一个节点值是否产生重复,重复则找到下一个不重复的节点的地址,否则继续向后遍历
if(p.next.next && p.next.val === p.next.next.val){
q = p.next.next;
while(q && q.val === p.next.val) q = q.next;
p.next = q;
}else{
p = p.next;
}
}
return ret.next
};
86. 分隔链表
我们可以设置两个头指针,一个连接所有小于特定值x,另一个连接大于x,再把两条链表相连
var partition = function(head, x) {
// r1:放所有小于x的虚拟头节点
// r2:放所有大于x的虚拟头节点
// p1:r1链表的尾节点
// p2:r2链表的尾节点
// p:迭代原链表
// q:存p的下一个节点
let r1 = new ListNode(), r2 = new ListNode(), p1 = r1, p2 = r2, p = head, q;
while(p){
// 先将p的下一个节点存到q中
q = p.next;
// 小于x时往r1接入,否则往r2接入
if(p.val < x){
// p.next先存原先p.next中的值,p1接p,再重新让p1指向此时最后一个节点p
p.next = p1.next;
p1.next = p;
p1 = p;
}else{
// 同上
p.next = p2.next;
p2.next = p;
p2 = p;
}
p = q;
}
// 两链表相连
p1.next = r2.next;
return r1.next;
};
138. 复制带随机指针的链表
1->2->3->4
- 首先,在每个节点后面新插入一个当前节点的复制节点
1->1'->2->2'->3->3'->4->4' - 再让每个复制出来的节点的
random指针向后指一位,比如假设1的random指针指向3,那么复制出的1'的random也指向3,正确的应该时让1'的random指向3'。 - 最后将新链表两两分开,拆成两条独立的链表,得到
1'->2'->3'->4'
var copyRandomList = function(head) {
// 判空
if(head === null) return null;
// 第一步
let p = head, q, new_head;
while(p){
// 复制一个新节点,并接在当前q节点后
q = new Node(p.val);
q.random = p.random;
q.next = p.next;
p.next = q;
p = q.next;
}
// 第二步
p = head.next;
while(p){
if(p.random) p.random = p.random.next;
// 判空
(p = p.next) && (p = p.next)
}
// 第三步
new_head = head.next;
p = head;
while(p){
// p是旧链表节点,p是新链表节点
q = p.next;
p.next = q.next;
if(p.next) q.next = p.next.next;
p = p.next;
}
return new_head;
};