2025-12-27-算法刷题-关于链表操作

2540 个字
13 分钟
2025-12-27-算法刷题-关于链表操作

后悔数据结构当初没有好好学的第n天 现在恶补,知识学爆

基础操作:

  • 查找元素:根据值查找节点位置
  • 指定位置插入:在特定位置插入新节点
  • 指定位置获取:获取特定位置的节点值
  • 指定位置删除:删除特定位置的节点
  • 获取长度:统计链表中节点的数量
题目描述主要实现思路
BM1反转链表使用三指针(prev、curr、next)迭代反转:保存下一个节点,反转当前指向,移动指针。返回prev作为新头。
BM2链表内指定区间反转引入虚拟头结点定位第m-1个节点(pre)。然后在[m,n]区间执行(n-m)次头插法(逐个将下一个节点插入pre后)。
BM3链表中的节点每k个一组翻转每k个节点为一组,使用反转链表方法局部反转。若剩余不足k个,则保持原序。递归或迭代均可,推荐迭代分段处理。
BM4合并两个排序的链表双指针模拟归并排序:比较两个链表当前节点值,小者接入新链表,移动对应指针。处理剩余部分。
BM5合并k个已排序的链表使用小根堆(优先队列)维护k个链表头结点,每次弹出最小值并接入结果链表,同时推入其下一个节点。
BM6判断链表中是否有环快慢指针(Floyd判圈算法):fast每次走2步,slow走1步。若相遇则有环,否则无环。
BM7链表中环的入口结点先用快慢指针相遇于环内某点,再令一指针从头启动,与慢指针同步移动,相遇处即环入口。
BM8链表中倒数最后k个结点快慢指针:fast先走k步,然后slow与fast同步移动,至fast到尾时slow即为倒数第k个节点。
BM9删除链表的倒数第n个节点同BM8定位倒数第n+1个节点(pre),然后pre.next = pre.next.next删除目标节点。注意头节点特殊处理。
BM10两个链表的第一个公共结点双指针同步走:先计算长度差,长者先走差值步;或让指针走完一链表后换另一链表,总路程相等时相遇即公共节点。
BM11链表相加(二)模拟加法从低位到高位(需先反转链表或用栈),处理进位。结果可能需反转回原序。
BM12单链表的排序归并排序(自底向上):分段合并有序子链表,或快慢指针找中点递归归并。时间O(n log n)。
BM13判断一个链表是否为回文结构快慢指针找中点,反转后半部分,与前半部分逐节点比较值是否相等。恢复链表可选。
BM14链表的奇偶重排分离奇偶位节点成两个链表(odd、even),然后even接odd尾部。注意偶数长度处理。
BM15删除有序链表中重复的元素-I单指针遍历:若当前节点与下一节点值相同,跳过下一节点(保留首次出现)。
BM16删除有序链表中重复的元素-II引入虚拟头结点,双指针或单指针遍历:若连续重复,跳过整个重复段(一个不留)。


一般会给出一个最基础的链表

function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
  • 节点包含两个成员
    • val:存储节点的值,通常为整数(int),题目中 |val| ≤ 1000。
    • next:指向下一个节点的指针(引用),类型为同类 ListNode*(或 ListNode),初始可能为 NULL/null/None
  • 无哑头节点(dummy head):输入的 head 就是真实头结点(有有效值),除非题目特别说明
  • 单向链表:只能从头到尾遍历,无前向指针
  • 输入形式
    • 函数签名通常为 ListNode* head(或类似),可能额外传入其他参数(如 m、n、k 等)
    • 空链表:head = NULL / null / None
  • 输出形式
    • 大多数题目要求返回新的头结点(ListNode*)
    • 操作通常要求原地修改,以满足空间复杂度 O(1)

常见操作#

单链表常见操作的实现方法#

以下针对单链表(节点结构为 val 和 next)的几种常见操作,提供标准、高效的实现思路。所有操作均基于从头结点开始遍历,时间复杂度与空间复杂度分析清晰。假设节点定义如下(以 JavaScript 为例,其他语言类似):

function ListNode(val, next) {
this.val = val;
this.next = next || null;
}

1. 查找元素:根据值查找节点位置(返回位置或节点)#

思路:从头遍历,逐个比较节点值,直到找到匹配值或到达链表末尾。

实现要点

  • 返回第一个匹配节点的位置(从 1 开始)或节点本身。
  • 未找到返回 -1 或 null。
function findNode(head, target) {
let pos = 1;
let curr = head;
while (curr !== null) {
if (curr.val === target) {
return pos; // 或 return curr; 返回节点本身
}
curr = curr.next;
pos++;
}
return -1; // 未找到
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2. 指定位置插入:在第 i 个位置插入新节点(i 从 1 开始)#

思路:遍历到第 i-1 个节点,将新节点插入其后。特殊处理插入到头部(i=1)。

实现要点

  • 若 i=1,新节点成为新头。
  • 若 i > 长度,插入失败或插入尾部(视题目要求)。
function insertAtPosition(head, i, val) {
let newNode = new ListNode(val);
if (i === 1) {
newNode.next = head;
return newNode; // 新头结点
}
let curr = head;
for (let pos = 1; pos < i - 1 && curr !== null; pos++) {
curr = curr.next;
}
if (curr === null) return head; // i 超出范围,不插入
newNode.next = curr.next;
curr.next = newNode;
return head;
}
  • 时间复杂度:O(i) → 最坏 O(n)
  • 空间复杂度:O(1)

3. 指定位置获取:获取第 i 个节点的値(i 从 1 开始)#

思路:遍历 i-1 步,直接返回当前节点的值。

function getAtPosition(head, i) {
let curr = head;
for (let pos = 1; pos < i && curr !== null; pos++) {
curr = curr.next;
}
return curr ? curr.val : null; // 未找到返回 null
}
  • 时间复杂度:O(i) → 最坏 O(n)
  • 空间复杂度:O(1)

4. 指定位置删除:删除第 i 个节点(i 从 1 开始)#

思路:遍历到第 i-1 个节点,修改其 next 指向跳过第 i 个节点。特殊处理删除头结点。

function deleteAtPosition(head, i) {
if (head === null) return null;
if (i === 1) return head.next; // 删除头结点
let curr = head;
for (let pos = 1; pos < i - 1 && curr !== null; pos++) {
curr = curr.next;
}
if (curr === null || curr.next === null) return head; // i 超出范围
curr.next = curr.next.next; // 跳过第 i 个节点
return head;
}
  • 时间复杂度:O(i) → 最坏 O(n)
  • 空间复杂度:O(1)

5. 获取链表长度(节点数量)#

思路:遍历链表,累计计数。

function getLength(head) {
let len = 0;
let curr = head;
while (curr !== null) {
len++;
curr = curr.next;
}
return len;
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)


反转链表#

BM1 | 全量反转链表#

给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。

数据范围: 0≤n≤10000≤n≤1000

要求:空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n) 。

如当输入链表{1,2,3}时,

经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。

以上转换过程如下图所示:

解法:

  • 初始化 prev 为 null(新链表的尾部)。
  • current 从链表头节点开始。
  • 在循环中:
    • 暂存 next = current.next(避免指针丢失)。
    • 将 current.next 指向 prev(反转当前指针)。
    • 更新 prev = current(前移 prev)。
    • 更新 current = next(前移 current)。
  • 循环结束后,prev 指向原链表的尾节点(新头节点),更新 list.head = prev。

其实就是三指针原地反转

alt text
alt text

export function ReverseList(head: ListNode): ListNode {
    let prev = null;
    let curr = head;
    while (curr !== null) {
        // 保存下一个节点,防止断链
        let next = curr.next;
        // 反转当前节点的指向
        curr.next = prev;
        // 指针向前移动
        prev = curr;
        curr = next;
    }
    // prev 指向反转后的新头结点
    return prev;
};

BM2 | 反转链表部分区间#

给定一个单链表的头结点 head,长度为 n,反转该链表从位置 m 到 n 的部分,返回反转后的链表。

数据范围: 0≤m≤n≤n≤1000 ,链表中任意节点的值满足 |val|≤1000

例如:
给出的链表为 1→2→3→4→5→NULL1→2→3→4→5→NULL, m=2,n=4m=2,n=4,
返回 1→4→3→2→5→NULL1→4→3→2→5→NULL.

要实现原地反转指定区间,需要:

  1. 找到反转区间的前一个节点(pre),即第 m-1 个节点。
  2. 找到反转区间的最后一个节点(记为 end),即第 n 个节点。
  3. 将 [m, n] 区间使用经典链表反转方法进行原地反转。
  4. 正确连接反转后的区间与前后部分:
    • pre.next 指向反转后区间的新的头节点(原第 n 个节点)。
    • 反转后区间的尾节点(原第 m 个节点)指向 end.next。

关键操作:

  • 先遍历定位到 pre 和反转区间的起始节点 start(第 m 个节点)。
  • 然后在 [start, end] 区间内使用三指针迭代反转。
  • 最后调整指针连接。

alt text
alt text

export function reverseBetween(head: ListNode | null, m: number, n: number): ListNode | null {
if (head === null || m === n) return head;
const dummy = new ListNode(0);
dummy.next = head;
let pre = dummy;
// 移动到第 m-1 个节点
for (let i = 0; i < m - 1; i++) {
pre = pre.next!;
}
let start = pre.next!; // 第 m 个节点(反转区间的原头部)
let then = start.next; // 第 m+1 个节点(待头插的节点)
// 执行 (n - m) 次头插
for (let i = 0; i < n - m; i++) {
start.next = then.next; // 从原区间摘除 then
then.next = pre.next; // then 插入 pre 之后(成为新头部)
pre.next = then; // 更新 pre 的 next
then = start.next; // 更新 then 为下一个待移动节点
}
return dummy.next;
}


分享到社交平台

将本文分享给你的朋友们

2025-12-27-算法刷题-关于链表操作
https://firefly.cuteleaf.cn/posts/2025-12-27-算法刷题-关于链表操作/
作者
Zhongye
发布于
2025-12-27
版权声明
CC BY-NC-SA 4.0

评论

Profile Image of the Author
Zhongye
南漂中
公告
新的博客站!旧站点传送门 zhongye1.github.io/Arknight-notes
音乐
专辑封面

音乐

暂无播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章数
142
分类数
14
标签数
214
总字数
339,690
运行天数
0
最后更新
0 天前

目录