变量名称定义

在开始反转链表套路学习前,先定义一组变量,这组变量会贯穿全部的题型:

  • cur:指向当前正在进行反转的节点和反转完成后的下一个待反转链表的头部;
  • pre:指向已完成反转链表的头部(头插法的缘故),初始值为nil(仅在 206 中是这样,其他情况下都是pre := &ListNode{}进行定义)。;
  • next:指向cur的下一个节点。

后面还会根据不同的题型有不同的变量引入。

206. 反转链表 - 力扣(LeetCode)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func reverseList(head *ListNode) *ListNode {
  var pre *ListNode  // 需要注意,不要写成 pre := *ListNode{},这样就完成了初始化、值不是 nil 了
  cur := head
  
  for cur != nil {
    next := cur.Next // 保存 next,防止丢失
    cur.Next = pre   // 断 cur 尾,使其指向反转链表头部
    pre = cur        // 更新 pre,让其移动到 cur 的位置,此时 cur 成为新头部,pre 符合其定义仍然指向反转链表的头部
    cur = next       // cur 移动,指向下一个待反转节点
  }
  
  return pre
}

循环中的这 4 步:

1
2
3
4
next := cur.Next
cur.Next = pre
pre = cur
cur = next

在之后的题型中会反复出现,意思也相同。

92. 反转链表 II - 力扣(LeetCode)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func reverseBetween(head *ListNode, left int, right int) *ListNode {
  // corner case
  if left == right || head.Next == nil {
    return head
  }
  
  dummy := &ListNode{Next: head} // 用 dummy 处理 left 为 1 的情况
  p0 := dummy
  for range left - 1 { 					 // 遍历到反转链表的前一个节点,只需遍历这一次 
    p0 = p0.Next
  }
  
  pre := &ListNode{}
  cur := p0.Next
  for range right - left + 1 {
    // 这 4 句同之前
    next := cur.Next
    cur.Next = pre
    pre = cur
    cur = next
  }
  p0.Next.Next = cur // p0.Next 指向的是原先的第一个节点,而这第一个节点此时为反转链表的尾节点(头插法🐎)。所以此时需要让其与后半段正常链表连接,而此时的 cur 指向的就是后半段的头部,所以令其指向 cur
  p0.Next = pre      // p0.Next 为反转链表的尾部,此时也需要将前半段与反转链表连接。p0 是前半段的尾部、反转链表的前一个节点,所以令其 next 指向反转链表的头部,以完成前半段的连接
  
  return dummy.Next
}

该题就出现了几个新变量,定义如下:

  • p0:指向反转链表的前一个节点;
  • dummy:哨兵节点,方便处理边界条件。

25. K 个一组翻转链表 - 力扣(LeetCode)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func reverseKGroup(head *ListNode, k int) *ListNode {
	cntHead := head
	n := 0
	for cntHead != nil { // 这里计数是方便后续 K 个、K 个的遍历需求
		cntHead = cntHead.Next
		n++
	}
	
  // 这 4 步定义与 LC92 完全一致
	dummy := &ListNode{Next: head}
	p0 := dummy
	cur := p0.Next
	pre := &ListNode{}

  // 保证每次遍历够 K 个
	for ; n >= k; n -= k {
    // 对区间内链表进行 K 次反转操作
		for range k {
      // 这 4 步同前面的题,意思完全一致
			next := cur.Next
			cur.Next = pre
			pre = cur
			cur = next
		}
		
		reverseTail := p0.Next // 新增操作,保存本次循环的反转链表的尾节点
		p0.Next.Next = cur     
		p0.Next = pre          
		p0 = reverseTail       // 将 p0 更新为本次循环的反转链表的尾节点,为下一个循环作准备。对于下一个循环而言,p0 符合定义
	}

	return dummy.Next
}

新增变量定义:

  • reverseTail:反转链表的尾节点。

总结

困扰了许久的反转链表问题总算攻克了,这部分在大厂面试题中属于热门内容,出题次数均大于 150 次。还是有很强的套路可循的,从 LC206 简单题、LC92 中等题再到 LC25 困难题的套路感逐渐加深。

其实反转链表这个套路还会出现在其他的热门链表考题中客串下,不过大多是 LC206 的核心 4 步。