链表✅
链表设计的核心知识点包括
- 基本结构
- 哨兵节点使用
- 快慢指针
- 多指针操作
数组转换为链表?
// 实现 arrayToList 函数,将数组转换为链表
// 输入:[1, 2, 3, 4, 5]
// 输出:{ value: 1, next: { value: 2, next: { value: 3, next: { value: 4, next: { value: 5, next: null } } } } }
function arrayToList (arr) {
}
答案
单向链表结构实现?
// 实现链表类,支持如下核心方法
// 节点
interface Node {
value: number
next: Node | null
}
// 链表
abstract class LinkedList {
constructor(values: number[]): LinkedList // 构造函数,接收一个数组作为参数,将数组转换为链表
add(value: any):void // 往链表末尾添加节点
delete(value: any):void // 删除值为 value 的节点
deleteValues(values: any[]):void // 批量删除,删除值在 values 数组中的节点
find(value: any):Node | null // 查找值为 value 的节点
head:Node | null // 获取链表头节点
tail:Node | null // 获取链表尾节点
}
答案
合并两个有序链表
/**
* 合并两个有序链表
* 输入:l1 = [1,2,4], l2 = [1,3,4]
* 输出:[1,1,2,3,4,4]
*/
function mergeLinkedList (l1, l2) {
}
答案
删除存在重复值的节点
/**
* 删除链表中重复的元素
* 输入:[1, 1, 2, 3, 3]
* 输出:[2, 3]
*/
function removeDuplicate (head) {
}
答案
删除倒数第 n 个节点
/**
* 删除链表的倒数第 n 个节点
* 输入:[1, 2, 3, 4, 5], n = 2
* 输出:[1, 2, 3, 5]
*/
function removeNthFromEnd (head, n) {
}
答案
翻转链表
// 反转链表 时间复杂度 O(n),空间复杂度 O(1)
// 输入:[1, 2, 3, 4]
// 输出:[4, 3, 2, 1]
function reverseList (head) {
}
答案
k 个一组翻转链表
答案
核心概念
LeetCode 第 25 题(Hard 经典):给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。如果节点总数不是 k 的整数倍,那么最后剩余的节点保持原有顺序:
- 哨兵节点(Dummy):设立虚拟头节点
dummy.next = head,统一头区间的翻转边界。 - 分组定位:使用指针向前探测 $k$ 步,确认当前组是否满足 $k$ 个节点。若不足直接返回。
- 子区间翻转:对当前 $k$ 个节点执行局部迭代反转,并与前驱节点(
pre)和后继节点(nextGroup)重新穿针引线。 - 推进指针:将
pre指针移动至翻转后的子链表尾部,继续循环下一组。
代码实现:
function reverseKGroup (head, k) {
if (!head || k <= 1) return head
const dummy = { next: head }
let pre = dummy
while (true) {
// 1. 检查剩余节点是否满足 k 个
let tail = pre
for (let i = 0; i < k; i++) {
tail = tail.next
if (!tail) {
return dummy.next // 不足 k 个,保持原样结束
}
}
const nextGroup = tail.next
let cur = pre.next
let prev = nextGroup
// 2. 局部翻转 k 个节点
while (cur !== nextGroup) {
const temp = cur.next
cur.next = prev
prev = cur
cur = temp
}
// 3. 重新拼接:pre 指向翻转后的新头,更新 pre 到新尾
const newTail = pre.next
pre.next = tail
pre = newTail
}
}
// 复杂度分析
// 时间复杂度:O(n),每个节点最多被访问和修改指针常数次。
// 空间复杂度:O(1),严格原地操作。
面试官视角
- 考查复杂指针操作的边界鲁棒性(不丢指针、断链、成环),使用哨兵节点是代码简洁易读的关键加分项。
判断单向链表是否是循环链表
答案
核心概念 LeetCode 141(环形链表):判断一个链表中是否有环。
- 快慢指针法(Floyd 判圈算法):定义
slow指针每次走 1 步,fast指针每次走 2 步。 - 若链表无环,
fast指针必先到达末尾null。 - 若链表有环,
fast指针将在环内不断循环,最终必然与slow指针相遇(slow === fast),时间复杂度 $O(n)$,空间复杂度 $O(1)$。
代码实现:
function hasCycle (head) {
if (!head || !head.next) return false
let slow = head
let fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
return true
}
}
return false
}
定位环起点
答案
核心概念
LeetCode 142(环形链表 II):给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 null:
- 数学证明(相遇与同速相碰):
- 设头节点到入环点的距离为 $a$,入环点到首次快慢指针相遇点的距离为 $b$,环的剩余长度为 $c$(环总长 $L = b + c$)。
- 快指针路程是慢指针的 2 倍:$2(a + b) = a + n(b + c) + b \implies a = (n - 1)(b + c) + c = (n - 1)L + c$。
- 推论结论:从相遇点出发一个指针,同时从链表头出发另一个指针,两者均每次走 1 步,最终必定在入环点首次相遇!
代码实现:
function detectCycle (head) {
if (!head || !head.next) return null
let slow = head
let fast = head
// 1. 寻找相遇点
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
if (slow === fast) {
// 2. 找到环,将 slow 重置到起点,两者同速推进找环入口
let ptr = head
while (ptr !== slow) {
ptr = ptr.next
slow = slow.next
}
return ptr // 环入口节点
}
}
return null // 无环
}
复杂度分析
- 时间复杂度:$O(n)$
- 空间复杂度:$O(1)$,无额外哈希表存储。
面试官视角
- 能否给出快慢指针相遇点与环起点的推导公式是顶级区分度,务必结合数轴距离关系拆解($a = c$)。
跳表的原理和应用
-
原理:
- 多层链表结构
- 每层是下层的子集
- 类似二分查找的思想
-
应用:
- Redis的有序集合
- 替代平衡树的场景
-
时间复杂度:平均O(logN)
ugly-num 变体
应该算是一道easy-medium,给定一个数组,只有一个初始数字1,对这个数组的每个数字K,做k*2+1和k*3+1,然后加入数组,要求这个数组是sorted并且没有重复元素,返回第N个这个数组应该是[1,3,4,7,9,10,13,..]
算法
3(1*2+1),4(1*3+1)
7(3*2+1),10(3*3+1)
9(4*2+1), 13(4*3+1)
因为出现了3算出来的比4还大,所以单纯用queue不行,要用heap,然后用set去重分析了时间