Skip to content

算法:删除链表的倒数第 N 个结点

轩辕十四
Published date:

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

img

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

**进阶:**你能尝试使用一趟扫描实现吗?

Table of contents

Open Table of contents

解法

func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
  if head == nil { return head }
  
  var first: ListNode? = head
  var last: ListNode? = head
  var m = 0
  while first?.next != nil {
    first = first?.next
    if m == n {
      last = last?.next
    } else {
      m += 1
    }
  }
  
  if m < n {
    return head?.next
  }
  
  last?.next = last?.next?.next
  
  return head
}

算法使用了“双指针”技术,通过设置一个“快指针”和一个“慢指针”来高效地完成这个任务。

1. 空链表检查

if head == nil { return head }

与上一个版本一样,首先我们检查链表是否为空。如果链表为空,直接返回原链表。

2. 初始化两个指针

var first: ListNode? = head
var last: ListNode? = head
var m = 0

3. 移动快指针

while first?.next != nil {
  first = first?.next
  if m == n {
    last = last?.next
  } else {
    m += 1
  }
}

4. 删除倒数第 N 个节点的特殊情况

if m < n {
  return head?.next
}

这一部分是处理特殊情况。如果链表的长度小于 n(例如 n 比链表长度还大),我们直接返回 head?.next,相当于删除头节点。

5. 删除倒数第 N 个节点

last?.next = last?.next?.next

一旦 first 指针到达链表的最后一个节点时,last 指针就会指向倒数第 n+1 个节点。此时,last?.next 就是倒数第 N 个节点。我们通过将 last?.next 指向 last?.next?.next,成功删除了倒数第 N 个节点。

6. 返回修改后的链表头

return head

最后,我们返回链表的头节点。即使删除了倒数第 N 个节点,链表的头节点仍然是原始头节点。

总结:

这段代码的关键思想与前一种解法类似:通过快慢指针的技巧来找出倒数第 N 个节点。主要不同点在于:

这种方式具有 O(L) 的时间复杂度和 O(1) 的空间复杂度,其中 L 是链表的长度。

优化后

func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
  if head == nil { return nil }
  
  var fast: ListNode? = head
  for _ in 0..<n {
    guard let node = fast?.next else { return head?.next }
    fast = fast?.next
  }
  
  var low: ListNode? = head
  while let node = fast?.next {
    fast = node
    low = low?.next
  }
  
  low?.next = low?.next?.next
  
  return head
}

与上一种解法相比,这种写法实现方式有所不同,依然采用了类似的“双指针”技巧,但在处理快慢指针时的细节和逻辑略有不同。让我们逐步讲解这个算法的每一部分:

1. 初始化和空链表检查:

if head == nil { return nil }

首先,我们检查链表是否为空。如果是空链表,直接返回 nil

2. 设置快指针:

var fast: ListNode? = head
for _ in 0..<n {
    guard let node = fast?.next else { return head?.next }
    fast = fast?.next
}

我们定义了一个 fast 指针,并将其初始化为链表的头节点。接下来,我们让 fast 指针先走 n 步。这样,fast 指针在执行完这个循环后,会离链表的尾部有 n 个节点的距离。

需要特别注意的是,如果链表长度小于 n,我们直接返回 head?.next,这是因为倒数第 N 个节点不存在,我们只需要删除链表中的第一个节点。

3. 设置慢指针:

var low: ListNode? = head
while let node = fast?.next {
    fast = node
    low = low?.next
}

现在,fast 指针已经向前走了 n 步,因此接下来,我们定义一个 low 指针,初始时指向头节点。然后,我们同时让 fastlow 一起向前移动,直到 fast 指针到达链表的最后一个节点。由于 fastlow 之间的距离保持为 n,当 fast 到达链表的尾部时,low 就会指向倒数第 n+1 个节点。

4. 删除倒数第 N 个节点:

low?.next = low?.next?.next

此时,low 指针指向的是倒数第 n+1 个节点,而 low?.next 就是我们要删除的倒数第 N 个节点。我们通过将 low?.next 的指针指向 low?.next?.next 来删除这个节点。

5. 返回结果:

return head

最后,我们返回修改后的链表头节点 head,这样链表中的节点已经删除了倒数第 N 个节点。

总结:

这个算法的关键点在于利用快慢指针的技巧。通过先让快指针走 n 步,然后快慢指针一起前进,直到快指针到达链表末尾,这样慢指针就能精确地定位到倒数第 n+1 个节点的位置。然后通过调整指针来删除倒数第 N 个节点,完成删除操作。这个算法的时间复杂度是 O(L),其中 L 是链表的长度,空间复杂度是 O(1),因为我们只使用了常数空间。

Previous
ReactNative 新架构中 iOS 通过 JSI 调用 RN 函数
Next
算法:回文数