头图

链表类的算法题在面试中是最常出现的,题目虽然简单,但也十分考验面试者的逻辑思维和算法熟练度,双指针法是解单链表算法题的常用技巧,以下通过几道常见的链表题来看看它们的使用吧!

题目如下:

  • 判断链表是否有环
  • 链表的中间结点
  • 合并两个有序链表
  • 合并K个升序链表
  • 分割链表
  • 删除链表的倒数第 N 个结点
  • 判断回文链表

1、判断链表是否有环

思路

使用快慢指针遍历链表,快指针每次走2步,慢指针每次走1步,如果两者相遇,则有环,否则没有

package main

import "fmt"

type Node struct {
    Val  int
    Next *Node
}

func hasCycle(head *Node) bool {
    if head != nil {
        fast := head
        slow := head
        for fast != nil && fast.Next != nil {
            fast = fast.Next.Next
            slow = slow.Next
            if fast == slow {
                return true
            }
        }
    }
    return false
}

2、链表的中间结点

思路

使用快慢指针,快指针每次走2步,慢指针每次走1步,快指针走完了,慢指针所在的位置就是中间节点

func middleNode(head *Node) *Node {
    if head == nil || head.Next == nil {
        return nil
    }
    slow := head
    fast := head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        head.Printf()
    }
    return slow
}

3、合并两个有序链表

思路

双指针分别遍历两个链表,比较两个节点的大小,将小的放到新的链表里,遍历完一遍后,将未遍历的部分指向新链表后面即可

func MergeTwoLists(list1 *Node, list2 *Node) *Node {
    //初始化一个虚拟的头节点
    newList := &Node{}
    p := newList
    p1 := list1
    p2 := list2
    //遍历对比两个指针值的大小,有一个走完了就停止
    for p1 != nil && p2 != nil {
        //将值小的节点接到p指针后面
        if p1.Val > p2.Val {
            p.Next = p2
            p2 = p2.Next
        } else {
            p.Next = p1
            p1 = p1.Next
        }
        //p指针前进
        p = p.Next
    }
    //将未比较的剩余节点都放到p指针后面
    if p1 != nil {
        p.Next = p1
    }
    if p2 != nil {
        p.Next = p2
    }
    //返回虚拟头节点的下一个节点就是真正的头节点
    return newList.Next
}

4、合并K个升序链表

思路

在合并两个升序链表的基础上依次将这K个链表合并即可

func mergeKLists(lists []*ListNode) (res *ListNode) {
    for _, v := range lists {
        res = mergeTwoLists(res, v)
    }
    return res
}

//mergeTwoLists() 实现见上文

5、分割链表

思路

遍历链表,根据节点的值将节点分到两个链表里,再将链表连接到一起即可

func partition(head *Node, x int) *Node {
    curNode := head
    //存放值小于x的链表的虚拟头节点
    dummy1 := &Node{}
    //存放值大于等于x节点的链表的虚拟头节点
    dummy2 := &Node{}
    p1 := dummy1
    p2 := dummy2
    //遍历链表,将原链表分割为两个
    for curNode != nil {
        if curNode.Val < x {
            p1.Next = curNode
            p1 = p1.Next
        } else {
            p2.Next = curNode
            p2 = p2.Next
        }
        //断开原链表的指针
        temp := curNode.Next
        curNode.Next = nil
        curNode = temp
    }
    //连接两个链表
    p1.Next = dummy2.Next
    return dummy1.Next
}

6、删除链表的倒数第 N 个结点

思路

1、要删除链表的倒数第n个结点就要先找到倒数第n+1个结点
2、如何找:初始化两个双指针,先让p1走k个节点,然后让p1、p2一起走,p1走到nil了,p2所在的位置就是倒数第k个节点

func removeNthFromEnd(head *Node, n int) *Node {
    //虚拟头节点
    p1 := &Node{}
    p1.Next = head
    cur := getKthFromEnd(p1, n+1)
    cur.Next = cur.Next.Next
    return p1.Next
}

//获取链表中倒数第k个节点
func getKthFromEnd(head *Node, k int) *Node {
    p1 := head
    p2 := head
    //先让p1走k步
    for i := 0; i < k; i++ {
        p1 = p1.Next
    }
    for p1 != nil {
        p1 = p1.Next
        p2 = p2.Next
    }
    return p2
}

7、判断回文链表

思路

1、先通过快慢指针找到中间节点,如果是偶数节点,slow指针再向前一步
2、反转后半部分的链表
3、分别从head和slow开始遍历链表,对比值是否一致,如果是一致的则说明是回文链表

func isPalindrome(head *Node) bool {
    fast := head
    slow := head
    //同时遍历链表
    for fast != nil && fast.Next != nil {
        fast = fast.Next.Next
        slow = slow.Next
    }
    //如果fast为nil则说明是奇数链表,slow需要前进一步
    if fast != nil {
        slow = slow.Next
    }
    //反转后半部分的链表
    right := reverse(slow)
    //从中间和head同时遍历判断值是否一致
    left := head
    for right != nil {
        if right.Val != left.Val {
            return false
        }
        left = left.Next
        right = right.Next
    }
    return true
}

//反转链表
func reverse(head *Node) *Node {
    var pre, next *Node
    cur := head
    for cur != nil {
        next = cur.Next
        cur.Next = pre
        pre = cur
        cur = next
    }
    return pre
}
参考资料:
1、https://labuladong.github.io/...
2、《数据结构与算法之美》

爆裂Gopher
20 声望11 粉丝

一篇文章讲明白一个知识点,每月更新,欢迎关注与交流。