[Golang修仙之路] 算法专题:链表

76 阅读7分钟

这张图是自己总结的,目前觉得链表相关基础的有章可循的知识还是覆盖到了的。

image.png

反转链表

这个真是值得好好说道说道,四个题目层层递进,很有代表性。

1. 基础模版

这个不会=没刷过leetcode

理论:这个图很多人都画过,反转链表就是这个原理,但是这张图是哥们儿自己画的。

image.png

代码:

func reverseList(head *ListNode) *ListNode {
    var dummy *ListNode
    pre, cur := dummy, head
    for cur != nil {
        nxt := cur.Next
        cur.Next = pre
        pre = cur
        cur = nxt
    }
    return pre
}

2. 反转指定范围的链表

稍微进阶版本其实就是,给出指定下标left,right,要求反转[left,right]的节点,其他节点顺序不变。

理论:这个题无论题解画的多清楚,永远不如自己画一遍图,于是我就画了一遍图。

image.png

代码:

func reverseBetween(head *ListNode, left int, right int) *ListNode {
    dummy := &ListNode{Next:head}
    p0 := dummy
    for i := 0; i < left - 1; i++ {
        p0 = p0.Next
    }

    var pre, cur *ListNode = nil, p0.Next // pre 为nil, 而不是p0
    for i := 0; i <= right - left; i++ {
        nxt := cur.Next
        cur.Next = pre
        pre = cur
        cur = nxt
    }

    p0.Next.Next = cur
    p0.Next = pre
    return dummy.Next // 必须返回dummy.Next 而不是 head
}

3. k个一组反转链表

你可能会疑惑为什么我跳过了第三个题目(两两交换链表中的节点),因为我认为第三个题目就是k个一组反转链表中,k=2的情况。

而k个一组反转链表,只需要在「反转链表2」的基础上,知道:

  • 一共有几组要反转,即 lenght / k,lenght 为链表的总长度。
  • 每组要反转的次数,即k。

值得一提的是「哨兵节点p0」的更新。

image.png

直接看代码:

func reverseKGroup(head *ListNode, k int) *ListNode {
    p := head
    length := 0
    for p != nil {
        p = p.Next
        length++
    }
    
    dummy := &ListNode{Next:head}
    p0 := dummy
    var pre, cur *ListNode = nil, p0.Next
    for i := 0; i < length/k; i++ {
        for j := 0; j < k; j++ {
            nxt := cur.Next
            cur.Next = pre
            pre = cur
            cur = nxt
        }
        // tmp 是刚刚被反转过的这一小坨的尾巴
        tmp := p0.Next
        p0.Next.Next = cur
        p0.Next = pre
        p0 = tmp
    }
    return dummy.Next
}

前后指针

1. 删除链表倒数第k个节点

前后指针可以干啥?

答:可以找倒数第N个节点。

画图:

I2v1a7W5a3V4B0p7p5A492_Mpa.jpg

题目: 19. 删除链表的倒数第 N 个结点

代码:

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    // 1. 创建dummy
    d := &ListNode{Next: head}
    l, r := d, d
    
    // 2. 拉开距离
    for i := 0; i < n; i++ {
        r = r.Next
    }
    
    // 3. 一起走
    for r.Next != nil {
        r = r.Next
        l = l.Next
    }
    
    // 4. 找到节点,干事儿
    l.Next = l.Next.Next
    return d.Next
}

快慢指针

我理解快慢指针是个工具,这个工具可以干2个事儿。

image.png

快慢指针相关的题目:

我来说几个有代表性的:

1. 删除中间节点

跟找中点稍有不同,删除链表的中间节点,需要找到中间节点的前一个节点,记录pre即可。

func deleteMiddle(head *ListNode) *ListNode {
    dummy := &ListNode{Next:head}
    pre, f, s := dummy, head, head
    for f != nil && f.Next != nil {
        f = f.Next.Next
        s = s.Next
        pre = pre.Next
    }
    pre.Next = pre.Next.Next
    return dummy.Next
}

说句题外话,其实我刚开始写的比这个还sao一点儿,不需要pre,s直接指向dummy效果也是一样的。但是,看了灵神的代码之后,还是觉得这样清晰一些。

2. 回文链表

思路是这样的:

IMG_4776A60ED227-1 2.jpeg

IMG_4776A60ED227-2 2.jpeg

代码:(这代码是直接复制灵神的,我自己第一次写也过了,但是思路太烂,多了一步计算链表长度)

// 876. 链表的中间结点
func middleNode(head *ListNode) *ListNode {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }
    return slow
}

// 206. 反转链表
func reverseList(head *ListNode) *ListNode {
    var pre, cur *ListNode = nil, head
    for cur != nil {
        nxt := cur.Next
        cur.Next = pre
        pre = cur
        cur = nxt
    }
    return pre
}

func isPalindrome(head *ListNode) bool {
    mid := middleNode(head)
    head2 := reverseList(mid)
    for head2 != nil {
        if head.Val != head2.Val { // 不是回文链表
            return false
        }
        head = head.Next
        head2 = head2.Next
    }
    return true
}

3. 重排链表

重拍链表题目描述:

image.png

思路:

IMG_CE65B2333C6B-1.jpeg

代码:

func reorderList(head *ListNode) {
	mid := getMid(head)
	l, r := head, reverse(mid.Next)
    mid.Next = nil
    for r != nil {
        rNext := r.Next
        lNext := l.Next
        r.Next = l.Next
        l.Next = r
        r = rNext
        l = lNext
    }
}

func getMid(h *ListNode) *ListNode {
	s, f := h, h
	for f != nil && f.Next != nil {
		f = f.Next.Next
		s = s.Next
	}
	return s
}

func reverse(h *ListNode) *ListNode {
	var dummy *ListNode
	pre, cur := dummy, h
	for cur != nil {
		nxt := cur.Next
		cur.Next = pre
		pre = cur
		cur = nxt
	}
	return pre
}

4. 环形链表2

找到环形链表入环的那个节点并返回。

思路:自己25年1月份写的,用了leetcode官方题解的图,我觉得还是很清晰的。

image.png

快慢指针走,快指针为nil,证明无环,快慢指针相遇,快指针回到链表头,一起以1的速度前进,相遇点即为目标点。

func detectCycle(head *ListNode) *ListNode {
    s, f := head, head
    for {
        if f == nil || f.Next == nil {
            return nil
        }
        f = f.Next.Next
        s = s.Next
        if f == s {
            f = head
            break
        }
    }
    for {
        if f == s {
            break
        }
        f = f.Next
        s = s.Next
    }
    return s
}

合并链表

合并链表只是一种题目类型,其实并没有什么对应的算法,把过程模拟好即可。其实本质上非常像归并排序中的merge。

题目:

1. 合并2个有序链表

双指针遍历。

代码:

func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
    dummy := &ListNode{}
    p := dummy
    for list1 != nil && list2 != nil {
        if list1.Val < list2.Val {
                p.Next = list1
                list1 = list1.Next
        } else {
                p.Next = list2
                list2 = list2.Next
        }
        p = p.Next
    }
    for list1 != nil {
        p.Next = list1
        list1 = list1.Next
        p = p.Next
    }
    for list2 != nil {
        p.Next = list2
        list2 = list2.Next
        p = p.Next
    }
    return dummy.Next
}

2. 合并k个有序链表

思路:

  • 搓一个merge。
  • 分治:用分解问题的思路,两个整数l,r,表示当前要处理的区间。

递归树:(自己画的,有不合理的地方请指正)

IMG_5E38107AF85E-1.jpeg

代码:

func mergeKLists(lists []*ListNode) *ListNode {
    if len(lists) == 0 {
        return nil
    }
    return proc(lists, 0, len(lists)-1)
}

// 闭区间[l, r]
func proc(lists []*ListNode, l, r int) *ListNode {
    if l == r {
        return lists[l]
    }
    mid := (l + r) / 2
    left, right := proc(lists, l, mid), proc(lists, mid+1, r)
    return merge(left, right)
}

func merge(a, b *ListNode) *ListNode {
    dummy := new(ListNode)
    p := dummy
    for a != nil && b != nil {
        if a.Val < b.Val {
            p.Next = a
            a = a.Next
        } else {
            p.Next = b
            b = b.Next
        }
        p = p.Next
    }
    for a != nil {
        p.Next = a
        a = a.Next
        p = p.Next
    } 
    for b != nil {
        p.Next = b
        b = b.Next
        p = p.Next
    }
    return dummy.Next
}

双指针

其实快慢指针,前后指针,合并链表等类型,都用到了双指针的思想。只不过,有些题目就是单纯的双指针,比如这个相交链表。

1. 相交链表

题目:

思路:

两个指针p,q 分别指向head1,head2,p走到头,就指向head2(最初指向head1),q走到头,就指向head1(最初指向head2)。p,q继续往前走,如果相遇,相遇点就是交点(为什么看下图一目了然),如果不相遇(一方再次为nil),说明2条链表不相交。

image.png

代码:

func getIntersectionNode(headA, headB *ListNode) *ListNode {
    a, b := headA, headB
    flagA, flagB := true, true
    for {
        // 首次为nil,换头
        if a == nil && flagA {
            a = headB
            flagA = false
        }
        if b == nil && flagB {
            b = headA
            flagB = false
        }
        // 二次为nil,不相交
        if a == nil || b == nil {
            return nil
        }
        // 相遇,找到交点
        if a == b {
            break
        }
        a = a.Next
        b = b.Next
    }
    return a
}

删点

1. 删除链表中的重复元素2

2和1的区别是,2是所有重复元素都删除,1是重复元素只保留一个。

思路:对每一个值,都记录前一个节点pre,如果值重复,则删除,否则继续。说白了就是模拟。

代码:

func deleteDuplicates(head *ListNode) *ListNode {
    dummy := &ListNode{Next: head}
    pre, cur := dummy, head
    for cur != nil {
        // 一定会往前走1格
        p := cur
        for p != nil && p.Val == cur.Val {
            p = p.Next
        }
        if cur.Next == p {
            // 更新pre
            pre = cur
        } else {
            // 删除所有重复值
            pre.Next = p
        }
        cur = p
    }
    return dummy.Next
}

排序链表

用分治算法,我比较容易理解。

  • [head, tail)代表我要排序的区间,如果区间长度为1或0,则直接返回,否则merge两个更小级别的子问题。
  • 上来先手撕一个标准merge,然后写proc递归函数。
func sortList(head *ListNode) *ListNode {
    return proc(head, nil)
}

func proc(head, tail *ListNode) *ListNode {
    if head == nil {
        return head
    }
    if head.Next == tail {
        head.Next = nil
        return head
    }
    f, s := head, head
    for f != tail && f.Next != tail {
        f = f.Next.Next
        s = s.Next
    }
    left, right := proc(head, s), proc(s, tail)
    return merge(left, right)
}

func merge(a, b *ListNode) *ListNode {
    dummy := &ListNode{}
    p := dummy
    for a != nil && b != nil {
        if a.Val < b.Val {
            p.Next = a
            a = a.Next
        } else {
            p.Next = b
            b = b.Next
        }
        p = p.Next
    }
    for a != nil {
        p.Next = a
        a = a.Next
        p = p.Next
    }
    for b != nil {
        p.Next = b
        b = b.Next
        p = p.Next
    }
    return dummy.Next
}