链表及相关题目 | Go 语言实现

129 阅读5分钟

链表是一种用指针连在一起的线性结构,从逻辑上看是连续的,但在物理存储上看离散的点,通常只需要一个 Head 指针指向指向头节点便可以找到所有的节点。

链表的每一个节点通常由数据部分和指针部分组成,单链表的指针部分指向下一节点,双链表的指针部分指向下一节点和上一个节点,环形链表与单链表相似,但是其尾节点的指针指向头节点。

单链表

链表1

新增节点

根据新增节点的位置不同分为以下三种情况

  • 头部新增:将新节点的 next 指针指向原本的头节点,Head 指针指向新节点。
  • 尾部新增:将原来的尾节点的 next 指针指向新节点
  • 中间新增:确定新增的位置 position,将新节点的 next 指针指向 position 节点,将 position 的上一节点的 next 指针指向新节点
 func AddNode(header **Node, data, position int) {
    cur := *header
 ​
    newNode := new(Node)
    newNode.Data = data
 ​
    if cur == nil {
       cur = newNode
       return
    }
 ​
    for i := 0; i < position; i++ {
       if cur.Next == nil {
          cur.Next = newNode
          return
       }
       cur = cur.Next
    }
 ​
    newNode.Next = cur.Next
    cur.Next = newNode
 }

删除节点

将 position 的上一个节点指向 position 的下一个节点,如果 position == 1,就是将 head 指向原来的第二个节点,如果 position 为尾节点,就是将倒数第二个节点的 next 设置为 nil

 func DeleteNode(header **Node, position int) {
    cur := *header
    for i := 0; i < position-1; i++ {
       if cur.Next == nil {
          return
       }
       cur = cur.Next
    }
    cur.Next = cur.Next.Next
 }

双链表

链表2

循环链表

链表4

相关题目

使用额外数据结构如栈,队列,哈希表可以很轻松的解决很多链表相关的问题,但这会带来额外的空间复杂度,如果是笔试,就越快做出越好,如果是面试,尽量思考低时间复杂度和空间复杂度的代码

反转链表

分别实现反转单向链表和反转双向链表的函数

单链表

思路:每次使用 next 和 pre 分别指向 cur 的下一个节点和上一个节点( cur 为 head 时它的上一个节点就是 nil),然后执行 cur 的 next 指针指向 pre,pre 向下移动( pre = cur),cur 向下移动( cur = next),重复此操作直到 cur 为 nil 代表结束,此时 pre 就是新的 head

 // 双指针法,时间复杂度 O(N) 空间复杂度 O(1)
 func ReverseList(head *Node) *Node {
     var pre *Node
     cur := head
     for cur != nil {
         next := cur.Next
         cur.Next = pre
         pre = cur
         cur = next
     }
     return pre
 }
 ​
 // 递归,时间复杂度 O(N) 空间复杂度 O(N)
 func Reverse(pre, cur *Node) *Node {
     if cur == nil {
         return pre
     }
     next := cur.Next
     cur.Next = pre
     return Reverse(pre, next)
 }
 ​
 // 栈(以下虚拟出一个栈,并没有实现栈)
 func Reverse(head *Node)*Node{
     stack := new(Stack)
     
     cur := head
     for cur != nil {
         stack.Add(cur)
         cur = cur.Next
     }
     
     newHead := stack.Pop()
     cur = newHead
     
     for !stack.IsEmpty() {
         cur.Next = stack.Pop()
         cur = cur.Next
     }
     
     // 注意到底末尾时 cur 的 next 指针指向他的上一个元素,此时要把 next 赋为 nil
     cur.Next = nil
     
     return newHead
 }

双链表

思路:直接将每个节点指向下一个元素和指向上一个元素的指针交换值即可,但要注意 cur 向下移动时应该是 cur = cur.Last

 func ReverseDoubleList(head *DoubleListNode) *DoubleListNode {
     cur := head
 ​
     for cur != nil {
         cur.Next, cur.Last = cur.Last, cur.Next
         head = cur
         cur = cur.Last
     }
 ​
     return head
 }

找出两个有序链表公共部分

思路1:分别使用 p1,p2 指向 list1 和 list2,比较 p1.Data 和 p2.Data,如果相等一起往下移动,如果不等就 谁大,移动另一个 ,直到某一个为 nil

 func FindEqualNode(head1, head2 *Node) []int {
    result := make([]int, 0)
 ​
    p1, p2 := head1, head2
 ​
    for p1 != nil && p2 != nil {
       if p1.Data > p2.Data {
          p2 = p2.Next
       } else if p1.Data < p2.Data {
          p1 = p1.Next
       } else {
          result = append(result, p1.Data)
          p1 = p1.Next
          p2 = p2.Next
       }
    }
 ​
    return result
 }

判断一个链表是否是回文链表

诸如 1->2->1, 1->2->3->2->1这样的链表就是回文链表

思路1:先使用快慢指针找到链表的中点,将中点以后的节点入栈,然后一个一个出栈与 list 从头开始比较,如果全部相等就是回文链表

 // PalindromeList 判断一个链表是否为回文链表(使用栈,额外空间复杂度为O(N))
 func PalindromeList(head *Node) bool {
    if head == nil {
       return false
    }
 ​
    slow, fast := head, head
    for fast.Next != nil && fast.Next.Next != nil {
       slow = slow.Next
       fast = fast.Next.Next
    }
    slow = slow.Next
 ​
    stack := new(Stack)
    for slow != nil {
       stack.Add(slow)
       slow = slow.Next
    }
 ​
    slow = head
    for !stack.IsEmpty() {
       if slow.Data != stack.Pop().Data {
          return false
       }
    }
 ​
    return true
 }

思路2:先找到中点,将中点后面的节点逆序,将中点指向 nil,使用 left 和 right 分别表示左边的头节点和右边的头节点,然后比较并一起向下移动,直到 left == nil,如果出现 left.Data != right.Data 的情况就代表不是回文链表

 // PalindromeList2 判断一个链表是否为回文链表(空间复杂度为O(1))
 func PalindromeList2(head *Node) bool {
    if head == nil {
       return false
    }
 ​
    slow, fast := head, head
    for fast.Next != nil && fast.Next.Next != nil {
       slow = slow.Next
       fast = fast.Next.Next
    }
 ​
    var pre *Node
    cur := slow
    for cur != nil {
       next := cur.Next
       cur.Next = pre
       pre = cur
       cur = next
    }
 ​
    for head != nil {
       if head.Data != pre.Data {
          return false
       }
       head = head.Next
       pre = pre.Next
    }
 ​
    return true
 }

复制含有随机指针的链表

一种链表的定义如下

 type RandNode struct {
    Data       int
    Next, Rand *RandNode
 }

rand 指针是单链表节点结构中新增的指针,rand 可能指向链表中的任意一个节点,也可能指向 nil。给定一个由Node 节点类型组成的无环单链表的头节点 head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

思路1:使用 map,遍历 list,每次遍历时,将当前节点作为 key,拷贝当前节点作为 value 放入 map 中,再次遍历 list,每次取出 map 中的 value,将新节点的 next 和 rand 指针指向原节点指向节点作为 key 的 value。

 // CopyRandList 复制一个含有随机指针的链表(使用 map,额外空间复杂度为O(N))
 func CopyRandList(head *RandNode) *RandNode {
    if head == nil {
       return nil
    }
 ​
    kv := make(map[*RandNode]*RandNode)
 ​
    for cur := head; cur != nil; cur = cur.Next {
       newNode := new(RandNode)
       newNode.Data = cur.Data
       kv[cur] = newNode
    }
 ​
    for cur := head; cur != nil; cur = cur.Next {
       kv[cur].Next = kv[cur.Next]
       kv[cur].Rand = kv[cur.Rand]
    }
 ​
    return kv[head]
 }

思路2:遍历 list,将每个节点复制后放在当前节点的下一个,复制完成后再次从头开始遍历,每次前进两个,每次遍历时将当前节点的下一节点的 rand 指针指向当前节点 rand 指针指向节点的下一节点,最后分离原节点和新节点

 // CopyRandList 复制一个含有随机指针的链表(不适用额外数据结构,额外空间复杂度O(1))
 func CopyRandList(head *RandNode) *RandNode {
    if head == nil {
       return nil
    }
 ​
    // copy old nodes and put them in the back of old nodes
    for cur := head; cur != nil; cur = cur.Next {
       newNode := new(RandNode)
       newNode.Data = cur.Data
       newNode.Next = cur.Next
       cur.Next = newNode
    }
 ​
    // set the rand
    for cur := head; cur != nil; cur = cur.Next.Next {
       cur.Next.Rand = cur.Rand.Next
    }
 ​
    // district old nodes and new nodes
    for cur := head; cur != nil; {
       next := cur.Next.Next
       cur.Next.Next = cur.Next.Next.Next
       cur = next
    }
 ​
    return head.Next
 }

判断链表是否有环

判断一个给定的链表是否有环,如果有环返回入环节点

思路1:使用 map,遍历链表,每次先将节点作为 key 从 map 中取值,如果取到值则代表有环,未取到值则放入 map 中

 // CheckLoop 使用 map 法,时间复杂度和额外空间复杂度都是 O(N)
 func CheckLoop(head *Node) (*Node, bool) {
     if head == nil || head.Next == nil || head.Next.Next == nil {
         return nil, false
     }
 ​
     kv := make(map[*Node]struct{})
     for cur := head; cur != nil; cur = cur.Next {
         if _, ok := kv[cur]; ok {
             return cur, true
         }
     }
     return nil, false
 }

思路2:快慢指针法,每次快指针移动两步,慢指针移动一部,如果快指针和慢指针相遇则代表有环,此时将快指针放回其起点,慢指针不变,然后各自每次移动一步,相遇的点就是环的入口

 // CheckLoop 快慢指针法,额外空间复杂度为O(1)
 func CheckLoop2(head *Node) (*Node, bool) {
     if head == nil || head.Next == nil || head.Next.Next == nil {
         return nil, false
     }
 ​
     slow, fast := head, head
 ​
     for fast.Next != nil && fast.Next.Next != nil {
         if fast == slow {
             fast = head
             for fast != slow {
                 fast = fast.Next
                 slow = slow.Next
             }
             return fast, true
         }
     }
 ​
     return nil, false
 }

两链表相交问题

给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返回 nil

思路:先分别判断两个链表是否有环,不同的情况处理方式不同

  1. A 没环,B 没环,因为公共部分是同长度的,先分别遍历 A, B 得到各自的长度 lenA, lenB,长的链表先移动两链表长度的差值,然后一起开始移动,每次比较,返回第一个相等的节点
  2. A 有环,B 有环,两个环的入口都可以看作交点,返回任意一个环的入口即可
  3. A 有环,B 没环,此时必不可能相交,直接返回 nil
 // FindFirstIntersectNode 判断两个链表是否有相交部分并返回相交点
 func FindFirstIntersectNode(head1, head2 *Node) *Node {
    loop1, ok1 := CheckLoop2(head1)
    _, ok2 := CheckLoop2(head2)
 ​
    if !ok1 && !ok2 {
       // get their length
       len1, len2 := 0, 0
       for cur := head1; cur != nil; cur = cur.Next {
          len1++
       }
       for cur := head2; cur != nil; cur = cur.Next {
          len2++
       }
 ​
       // the longer list move |n| times first
       n := len1 - len2
       p1, p2 := head1, head2
       if n > 0 {
          for i := 0; i < n; i++ {
             p1 = p1.Next
          }
       } else if n < 0 {
          for i := 0; i < -n; i++ {
             p2 = p2.Next
          }
       }
 ​
       // p1,p2 move together
       for p1 != p2 {
          p1 = p1.Next
          p2 = p2.Next
       }
 ​
       return p1
    } else if ok1 && ok2 {
       return loop1
    }
 ​
    return nil
 }