Preface

单链表的反转,按K反转等各种考点变体。

处理单链表时, 尽量采用递归实现:

  • 1 因为递归代码简洁+优美,节省时间,并且不容易出错,是面试优选策略。
  • 2 在面试时间比较宝贵的情况下,尽量预留时间展示自己的才能。

1 反转单链表(基础)

单链表结构定义如下:

1
2
3
4
5
// Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

方法一: 递归实现(尾插法)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 尾插法
func reverseList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    tail := reverseList(head.Next)
    head.Next.Next = head
    head.Next = nil
    return tail // became new head
}

方法二: 循环实现(头插法)

双指针,头插法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func reverseListIter(head *ListNode) *ListNode {
    var newHead *ListNode
    for head != nil {
        pop := head
        head = head.Next
        // 插入新链表头部
        pop.Next = newHead
        newHead = pop
    }
    return newHead // 原链表的尾元素
}

2 变体一: 递归实现-从 m 到 n 中间节点的反转

上面我们使用递归,实现了反转整个链表的算法; 稍微改造下,只反转前k个元素(即递归深度到第k个元素,就可以返回)。

递归实现(从head 开始, 只反转前k个元素): 输入: 1->2->3->4->5->6->7->8 反转前5个 输出:[5->4->3->2->1]->6->7->8

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// reverseTopK 递归反转链表前k个节点
// 返回:反转后的头结点
func reverseTopK(head *ListNode, k int) *ListNode {
	// 边界
	if head == nil || k <= 1 {
		return head
	}
	newHead := reverseTopK(head.Next, k-1)
    // 调整当前节点指向
	adjust(head)
	return newHead
}

func adjust(cur *ListNode) {
    // 保存下下个节点(反转完成后要接上尾部剩余链表)
	nextNext := cur.Next.Next
	// 指针反转
	cur.Next.Next = cur
	// 切断
	cur.Next = nextNext
}

反转中间一部分[m,n], 再分析一下:
一种特殊情况是,m=1, 也就是反转前 n 个元素。
当 m = 2 ,我们把第一个节点去掉,就可以转化为上面的特殊情况了;
总结一下: 推广到一般的情况(m>1),都可以转换为 m=1 的特殊情况处理。

1
2
3
4
5
6
7
8
// 反转中间一部分[m,n] 递归实现
func reverseBetween(head *ListNode, m, n int) *ListNode {
    if m == 1 {
        return reverseTopK(head, n)
    }
    head.Next = reverseBetween(head.Next, m-1, n-1)
    return head
}

3 变体二: 按K个元素一组反转 (最后一组不足K个不反转)

我们先考虑下特殊情况:
1 链表元素总个数n 小于 k, 不做反转直接返回即可。
2 如果等于k个, 那么把当前链表按k反转,并返回新的head节点。
3 如果多余k个,我们先按照情形2处理前k个元素,剩下的(n-k)的链表当成一个子问题,执行步骤1,2,3.

最后这个子问题解决后,返回它的头指针 tailHead,
我们再把当前块的尾指针 head.Next = tailHead 即可。

 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
func reverseKGroup(head *ListNode, k int) *ListNode {
    p := head
    for i := 0; i < k; i++ {
        // 不足k个直接返回
        if p == nil {
            return head
        }
        p = p.Next
    }
    newHead := reverseKCutTail(head, k)
    // 超过K个节点部分, 又是一个按K反转的问题
    head.Next = reverseKGroup(p, k)
    return newHead
}

// 反转链表前 K 个元素, 并截断尾部(注意与前文 reverseKKeepTail 的区别)
func reverseKCutTail(head *ListNode, k int) *ListNode {
    if k <= 1 {
        return head
    }
    rear := reverseKCutTail(head.Next, k-1)
    head.Next.Next = head
    head.Next = nil
    return rear
}

这个问题还有一个变体(从后往前分组), 这个题是 Shopee(2019年)的面试题:

解决办法:
1 先把整个链表全部反转,然后从新的head开始,按照k个一组,执行前插法;

注意:最后一个分组不足k个,逐个元素进行前插法,即可保证不足k个不反转。

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* [逆序] 从后往前, 每k个反转一次(不足k个不反转)(循环实现)
输入:1->2->3->4->5->6->7  k = 3
输出: 1->4->3->2->7->6->5

输入:1->2->3->4->5->6->7->8  k = 3
输出: 1->2->5->4->3->8->7->6
*/

func reverse(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    last := reverse(head.Next)
    head.Next.Next = head
    head.Next = nil
    return last // became header
}

func reverseKGroup(head *ListNode, k int) *ListNode {
    rhead := reverse(head)
    count := 0
    cur := rhead
    remainList := rhead
    var newHead *ListNode
    for cur != nil {
        count++
        pre := cur
        cur = cur.Next
        if count == k {
            // 头插法
            pre.Next = newHead
            newHead = remainList
            // move next
            remainList = cur
            count = 0
        }
    }
    // 不足k个的部分,采用头插法
    for remainList != nil {
        pop := remainList
        remainList = remainList.Next
        // 头插法
        pop.Next = newHead
        newHead = pop
    }
    return newHead
}

总结

正所谓万变不离其宗,我们先分析问题的特殊场景,然后推广到一般;
把一般情况转化为一个个特殊场景的子问题,最后使用递归就可以完美解决。