链表
前言
本文(链表)涉及算法:迭代、递归、双指针、哈希集合
上一篇文章 js算法数组篇 感兴趣的同学可以去学习下
链表概念
通过指针串联在一起的线性结构 链表类型:
- 单链表:一个节点由两部分组成,一个是数据,一个是存放指向下一节点的指针,最后一个指针指向 null
- 双链表:双链表有两个指针,一个指向下一个节点,一个指向上一个节点
- 环形链表:顾名思义,就是链表首位相连
链表存储方式:
数组在内存中是连续分布的,链表则不是,链表通过指针域的指针链接在内存中的各个节点,散乱的分布在内存的某个地址上,取决于操作系统的内存管理
移除链表元素
题目:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
- 递归:时间复杂度 O(n)
const removeElements = (cur, val) => {
if (cur === null) return null; // 如果为 null 直接返回
cur.next = removeElements(cur.next, val);
return cur.val === val ? cur.next : cur; // 如果当前值等于 目标值,则跳过
};
- 迭代:设置虚拟头节点(防止第一个节点即需要删除),如果下一个节点不为 null 并且等于 给定值,则删除下一个节点;时间复杂度 O(n)
const removeElements = (cur, val) => {
let dummyHead = new ListNode(0); // 创建虚拟头节点
dummyHead.next = cur;
let temp = dummyHead;
while (temp.next !== null) {
if (temp.next.val === val) {
temp.next = temp.next.next; // 如果等于给定值,跳到下下个
} else {
temp = temp.next; // 否则继续向下走
}
}
return dummyHead.next; // 注意设置了虚拟头节点,需要去除(设置了这个,就不需要考虑头节点等于给定值的情况)
};
反转链表
题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 leetcode 题目
- 迭代:时间复杂度 O(n)
const reverseList = (head) => {
let pre = null;
let cur = head;
while (cur) {
let next = cur.next; // 保存操作
cur.next = pre; // 翻转操作
pre = cur;
cur = next;
}
return pre;
};
链表相交
题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。 leetcode 链接
- 哈希集合:时间复杂度 O(m+n)(m、n 分别为 headA、headB 链表的长度);空间复杂度 O(m)(m 为 headA 的长度)
const getIntersectionNode = (headA, headB) => {
let map = new Map();
let temp = headA;
while (temp) {
// 遍历,将值加入 map 中
map.set(temp, temp);
temp = temp.next;
}
temp = headB; // 重新赋值,遍历
while (temp) {
if (map.has(temp)) {
// 如果找到相同节点,则返回
return temp;
}
// 没找到,继续遍历
temp = temp.next;
}
return null;
};
- 双指针:时间复杂度 O(m+n);空间复杂度为 O(1)
情况一:两链表相交
- 链表 headA 和 headB 长度为 m 和 n;headA 不相交部分长度为 a,headB 不相交部分长度为 b,它们相交的部分长度为 c
- 所以 a + c = m;b + c = n
- 如果 a = b,那么它们会同时到达相交的地方
- 如果 a 和 b 不相等,他们会各自遍历一遍链表,不会同时遍历完
重点来了
,遍历完之后,将 pA 指向 headB 的头节点,pB 指向 headA 的头节点,继续遍历;指针 pA 移动 a + c + b 次后;pB 移动 b + c + a 次后,此时它们会同时到达相交的节点
情况二:链表不相交
- 如果 m = n,同时到达尾部,为 null
- 如果 m 和 n 不相等,各自走完 m + n 次节点,到达尾部,指向 null
const getIntersectionNode = (headA, headB) => {
if (headA === null || headB === null) {
return null;
}
let pA = headA,
pB = headB;
while (pA !== pB) {
pA = pA === null ? headB : pA.next; // 重点,要进行重新指向,进行另一条链表的遍历
pB = pB === null ? headA : pB.next;
}
return pA;
};
环形链表
题目:给你一个链表的头节点 head ,判断链表中是否有环。 leetcode 链接
- 哈希集合:时间复杂度 O(n);空间复杂度 O(n)
遍历所有节点,并且用 哈希存起来,判断是否已经访问过,思路很简单
const hasCycle = (head) => {
let map = new Map();
while (head) {
if (map.has(head)) {
// 判断是否已经访问过
return true;
}
map.set(head, head); // 访问过的存入哈希表
head = head.next;
}
return false;
};
- 双指针(快慢指针):时间复杂度 O(n);空间复杂度 O(1)
快指针一次走两步,慢指针一次一步,如果有环就会相遇
const hasCycle = (head) => {
if (head === null || head.next === null) {
return false;
}
let slow = head,
fast = head.next; // 设置快慢指针
while (slow !== fast) {
if (fast === null || fast.next === null) {
// 快指针走在前面,只需要考虑快指针是否存在
return false;
}
slow = slow.next; // 一次一步
fast = fast.next.next; // 一次两步
}
return true;
};
两两交换链表中的节点
题目:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
- 递归:时间复杂度 O(n)
- head 表示
头节点
,新的链表的第二节点
- newHead 表示新的链表的
头节点
,原始链表的第二节点
- 所以原始链表中,剩下节点的头节点即为
newHead.next
- 其余节点进行两两交换
head.next = swapPairs(newHead.next)
const swapPairs = (head) => {
if (head === null || head.next === null) {
// 递归结束条件
return head;
}
const newHead = head.next;
head.next = swapPairs(newHead.next);
newHead.next = head;
return newHead;
};
删除链表的倒数第 N 个结点
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
- 双指针:时间复杂度 O(n)
如果要删除倒数第 n 个节点,让 fast 移动 n 步,然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的下一个节点就可以了,不难写出以下代码
var removeNthFromEnd = function (head, n) {
let dummyHead = new ListNode(0); // 创建虚拟头节点(在移除链表元素中提到过作用)
dummyHead.next = head;
let slow = dummyHead,
fast = dummyHead;
while (n--) fast = fast.next; // fast 先走 n 步
while (fast.next !== null) {
// 让 fast 走到结尾,slow也跟着向下走
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除 slow 的下一个节点即可
return dummyHead.next; // 注意设置的虚拟头节点
};