JavaScript is required

2025-12-27-关于链表操作

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

算法#力扣#算法#链表

后悔数据结构当初没有好好学的第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引入虚拟头结点,双指针或单指针遍历:若连续重复,跳过整个重复段(一个不留)。


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

JavaScript
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 为例,其他语言类似):

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

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

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

实现要点:

  • 返回第一个匹配节点的位置(从 1 开始)或节点本身。
  • 未找到返回 -1 或 null。
js
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 > 长度,插入失败或插入尾部(视题目要求)。
ts
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 步,直接返回当前节点的值。

ts
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 个节点。特殊处理删除头结点。

ts
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. 获取链表长度(节点数量)

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

ts
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

ts
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

ts
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
}