Go資料結構與力扣—連結串列

yinhui發表於2021-04-13
[TOC]

​ 因為想幹純後端,所以開始刷力扣鞏固演算法與資料結構,結果在寫幾天前的每日一題的時候發現自己對於連結串列的操作已經有些遺忘,以及對於連結串列的實現等也開始忘記。於是重新複習連結串列,並且寫下這篇文章。

本文的主要內容為:連結串列的理論知識、程式碼實現與力扣部分連結串列相關題目及題解。(其實本來想寫全部題目的,但是共有五十多道,一是寫出來篇幅太長,而是要寫完這五十多道,這篇文章就不知道要拖到多久之後才能完成了)

首先給出連結串列的定義:連結串列是資料元素的線性集合。它對物件例項的每一個元素用一個單元或節點描述。節點不鄙視陣列成員,節點之間的地址也不是連續的。它們的位置是通過每一個節點中明確包含了一個節點地址資訊的指標來確定的。

基礎知識

定義

單連結串列就是單向連結串列。每一個節點中有兩個域。一個是鏈域負責儲存指向下一連結串列地址的指標,一個是資料域,儲存資料。

型別定義:

type chainNode struct {
    value int
    next *chainNode
}

頭節點與頭指標

我們定義了連結串列節點後,便可以實現一個單連結串列了。不過為了索引單連結串列,我們一般會提出一個東西:頭節點。

連結串列第一個節點不一定是頭節點,但是頭節點一定是連結串列第一個節點。頭節點代表的是放在連結串列第一個元素(存有資料)前的節點。它的值域一般是忽略的,它的存在意義是為了方便對連結串列的一些操作,並不是必須定義的。

而頭指標則是指向連結串列第一個節點的指標,如果連結串列存在頭節點,則指向頭節點,若不存在頭節點,就是指向連結串列第一個節點。

一般情況下頭節點可以不存在,但頭指標必須存在,因為我們需要依靠它來找到連結串列。

下面我們實現一個存在頭節點的連結串列:

type chainNode struct {
    value int
    next *chainNode
}

func main(){

    chain := new(chainNode)


    node1 := &chainNode{5,nil}
    chain.next =node1


}

因為new關鍵字的作用是接受型別作為引數然後返回指向該型別的地址,所以這裡chain便是頭指標,它指向的chainNode便是頭節點,二之後的node1才是真正的連結串列第一個元素。

單連結串列操作

宣告:以下對節點的排序,我們預設頭節點後的第一個節點為1.

連結串列的建立

無論是對連結串列進行什麼操作,首先我們要建立一個連結串列,有了連結串列後才能進行操作。

而建立連結串列一般有兩種方法,分別是頭插法和尾插法

尾插法

尾插法,就是生成的每一個新節點都插入到當前連結串列的尾部。即將最後一個節點的鏈域指向它。

程式碼:

func createListTail(head *chainNode,n int) *chainNode{
    for node := head ; node != nil ; node = node.next {
        if node.next == nil {
            node.next = &chainNode{n,nil}
            break
        }
    }
    return head
}

這是我們只知道頭指標的情況下的尾插法,當然如果我們知道尾指標的話,就可以快速很多了,所以一般情況下會設立尾指標來方便進行尾插法。

頭插法

生成的每一個節點不再是插入到連結串列尾部,而是插入到頭節點(我們預設存在頭節點)之後。

這時的操作就是新節點的鏈域指向頭節點的下一個節點,之後頭節點的鏈域指向新節點。

程式碼:

func createListHead(head *chainNode,n int) *chainNode{
    node := &chainNode{n,head.next}
    head.next = node
    return head
}

因為不需要尋找尾指標,所以看上去更簡單些

查詢

其實寫這個的時候挺糾結的,因為會發現各種資料中的查詢有兩個意思。

一個是知道序列取值,一個是知道值取地址(序列)。

不過它們的思路都是一樣的,從頭節點開始遍歷,找出目標節點。

我以尋找第某個節點的值為例。

程式碼:

func get(head *chainNode,n int) (int,error) {
    i := 0
    for node := head ; node != nil ; node = node.next{
        if i==n {
            return node.value,nil
        }
    i++
    }

    return 0,errors.New("Can't Find")
}

在此基礎上遍歷表的所有元素,那麼只需要將條件去除,每一個值都輸出就可以了。

插入

若需要將某個節點插入到連結串列的第i位,那我們只需要將其的鏈域指向本來的第i位節點,然後將第i-1位的鏈域指向它即可。

程式碼:

func insert(head *chainNode,elem int,add int) (*chainNode,error) {
    i := 0
    for node:= head ; node != nil ; node = node.next {
        if i == add-1 {
            newNode := &chainNode{elem,node.next}
            node.next = newNode
            return head,nil
        }
        i++
    }

    return head,errors.New("overflow")
}

刪除

若我們要刪除第i個節點,只需將第i-1節點的鏈域指向第i+1,就可將第i節點從連結串列中刪除。如果是c語言中我們還需要手動將被刪除的節點的記憶體釋放,不過考慮到go的記憶體釋放機制,我們應該可以忽略這一步。

程式碼:

func del(head *chainNode,n int)  {
    i:=0
    for node:=head;node != nil ; node = node.next {
        if i == n-1 {
            node.next = node.next.next
            return
        }
        i++
    }
}

力扣題型

基礎題

我對基礎題的定義是隻需要用到上面提到的基礎操作,而不需要進行額外行為。

231290.二進位制連結串列轉正數 難度:簡單

連結串列從頭到位代表一個二進位制整數,現在要求將其轉為十進位制並返回。

比較簡單,從頭開始取,每拿到一個就x2+後面的。

func getDecimalValue(head *ListNode) int {
    num := 0
    for node := head ; node != nil ; node=node.Next {
        num = num*2 +node.Val
    }
    return num
}

結果: 0ms 記憶體:2mb

劍指offer 22 連結串列中倒數第K個節點 難度:簡單

題目的意思就是給一個數,比如3。則將從倒數第三個節點開始的連結串列返回。

那返回倒數第三個連結串列的指標不就好了。

那我們先寫個簡單的。首先遍歷出連結串列長度n,那麼倒數第k個節點就是正數第n-k+1的節點

func getKthFromEnd(head *ListNode, k int) *ListNode {
  n:=0
    cur := head
    for head.Next != nil {
        head = head.Next
        n++
    }

    for i:=1;i<=n-k+1;i++ {
        cur = cur.Next
    }
    return cur
}

結果: 4ms 記憶體:2.2mb

這一題還有一個有趣的解法,執行時間0ms,記憶體消耗只比上一種解法高几k,即是雙指標。

雙指標的解法想法就是,我們使用兩個指標,快指標fast先走k步,然後兩個指標一同運動。當fast走到尾的時候,slow正好走到了倒數第k個。

程式碼:

func getKthFromEnd(head *ListNode, k int) *ListNode {
    slow, fast := head, head
    for ;k > 0; k -- {
        fast = fast.Next
    }
    for fast != nil {
        slow, fast = slow.Next, fast.Next
    }
    return slow
}

876.連結串列的中間節點 難度:簡單

題目要求簡單,給出一個連結串列,需要返回中間節點。如果有兩個中間節點就返回第二個.算是上一題的小變化

這種題目的第一個反應雙指標

快指標一次走兩步,慢指標一步,這樣快指標走到底的時候慢指標走一半。

程式碼

func middleNode(head *ListNode) *ListNode {
    fast := head
    low := head
    for fast.Next != nil && fast.Next.Next != nil {
        fast=fast.Next.Next
        low = low.Next
    }
    if fast.Next != nil {
        return low.Next
    }else {
        return low
    }
}

21.合併兩個有序連結串列 難度:簡單

思路比較簡單,生成一個假頭後與兩個連結串列中的值比較,誰小就連誰,之後與該連結串列下一個值及另一連結串列當前值比較。

func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
    dummy := &ListNode{0,nil}
    node := dummy
    for l1 !=nil && l2!=nil {
        if l1.Val < l2.Val {
            node.Next = l1
            node = node.Next
            l1 = l1.Next
        }else {
            node.Next = l2
            node = node.Next
            l2 = l2.Next
        }
    }
    switch {
    case l1 != nil:
            node.Next = l1
    case l2 != nil:
            node.Next = l2  
    }

    return dummy.Next
}

時間複雜度:O(n^2) 記憶體消耗:2.5mb

雙100%

進階題

這類題型的定義是除了上述基礎操作外,需要進行一些對連結串列鏈域變動的行為。

7.刪除連結串列中的節點 難度:簡單

這一題比較簡單,它的要求就是隻傳入一個節點,然後我們要刪除這個節點。

這個就比較簡單了,我們只需要這個節點成為下一個節點就好了。

func deleteNode(node *ListNode) {
   *node = *node.Next 
}

這裡有個點,就是我們寫的是*node 而不是 node

因為我們這裡並沒有return,直接寫node的話作用域只是函式內,因此需要寫*node來對真實的指標進行更改。

82.刪除連結串列中的排序元素 難度:中等

題目比較簡單,因為是排序連結串列,一次遍歷,將node與node.next對比就好了。

不過力扣不講武德,頭節點的值居然是需要考慮的有效值

所以要考慮頭節點情況,就新加個節點當頭節點。

然後一個指標cur開始迴圈,它是安全的,它要比的是next與next.next的值,如果相等就刪除。

同時為了比較方便(直接比較跳過容易把奇數次重複給留個尾巴),我們寫一個標誌位儲存val,用來對比。

程式碼:

func deleteDuplicates(head *ListNode) *ListNode {
    newHead := &ListNode{0,head}
    cur := newHead
    for cur.Next != nil &&  cur.Next.Next != nil {
        if cur.Next.Val == cur.Next.Next.Val{
            v := cur.Next.Val
            for cur.Next != nil && cur.Next.Val == v {
                cur.Next = cur.Next.Next
            }
        }else {
            cur = cur.Next
        }
    }

    return newHead.Next
}

19.刪除連結串列的倒數第N個節點 難度:中等

這就是倒數第k個節點與刪除節點的組合

快慢指標解決

func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummpy := &ListNode{0,head}
    fast,slow := dummpy,dummpy
    for n>=0 {
        fast = fast.Next
        n-- 
    }
    for fast != nil {
        slow,fast = slow.Next,fast.Next
    }
    if slow.Next.Next!=nil{
        slow.Next = slow.Next.Next
    }else{
        slow.Next = nil
    }

    return dummpy.Next
}

206.反轉連結串列 && 劍指Offer 24 難度:簡單

加上頭指標一共用了三個指標的遍歷法

一個pre,記錄.next 一個cur記錄上一節點。

ctQsJg.png

如圖,這是一開始的樣子

然後,將head.next指向cur,cur=head,head=pre,pre=head.next

ctQ6zj.png

就變成了這樣,由此往復,便可將連結串列完全倒轉過來

程式碼:

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

面試題02.07 連結串列相交 難度:簡單

檢查兩個連結串列中是否有節點相交

一開始想的是遍歷來對比,後來發現還是可以雙指標解法

如果兩個連結串列相交的話,那麼當兩個指標在走完自己連結串列後走對面連結串列的話,必定會相交。

a
o - c

b /

這樣看,假設兩指標分別從a、b到c,之後再從b、a到c

那麼它們的移動距離分別是 ao+ob+bo=bo+oc+ao,再加上兩指標速度相同,所以他們必定相遇

那麼當他們相遇,且有值當時候,就說明兩連結串列相交

func getIntersectionNode(headA, headB *ListNode) *ListNode {
    p1, p2 := headA, headB
    for p1 != p2 {
        if p1 == nil {
            p1 = headB
        } else {
            p1 = p1.Next
        }
        if p2 == nil {
            p2 = headA
        } else {
            p2 = p2.Next
        }
    }
    return p1
}

234.迴文連結串列 難度:簡單

​ 快慢指標找重點斷開後做連結串列反轉然後對比

func isPalindrome(head *ListNode) bool {
    if head == nil || head.Next == nil{
        return true
    }   
    slow,fast := head,head
    var pre *ListNode

    for fast!=nil && fast.Next != nil {
        pre = slow
        slow = slow.Next
        fast = fast.Next.Next
    }
    pre.Next = nil

    var dummy *ListNode
    for slow != nil {
        fast = slow.Next
        slow.Next = dummy
        dummy = slow
        slow = fast
    }

    for dummy!=nil && head != nil {
        if dummy.Val != head.Val{
            return false
        }
        head = head.Next
        dummy = dummy.Next
    }

    return true
}

劍指offer 35.複雜連結串列的複製 難度:中等

這一題的解法可以這樣。

我們先將之當成單連結串列,複製節點的next與val,然後再複製指標。

但是直接複製指標的話,指向地址不是新節點的地址。 我們可以將新節點直接插入每一箇舊節點之後,然後指標=舊指標指向節點的next

之後我們再將新節點都分離出來,新成新連結串列輸出。(記得將原連結串列拼回去,不然會報錯Next pointer of node with label 13 from the original list was modified.)

程式碼:

func copyRandomList(head *Node) *Node {
    if head == nil {
        return nil 
    }
    node := head
    for node!=nil {
        newNode := &Node{
            node.Val,
            node.Next,
            nil,
        }
        node.Next = newNode
        node = newNode.Next
    }

    node = head 
    for node!=nil {
        if node.Random != nil {
            node.Next.Random = node.Random.Next
        }
        node = node.Next.Next
    }

    newHead := head.Next
    oldNode := head
    node = newHead

    for node.Next != nil {
        oldNode.Next = oldNode.Next.Next
        node.Next = node.Next.Next
        oldNode = oldNode.Next
        node = node.Next
    }
    oldNode.Next = nil
    return newHead
}

142.環形連結串列2 難度:中等

快慢指標解法。不過需要注意一個問題,那就是我們快慢指標會相遇,但不一定會在第一個節點相遇。

func detectCycle(head *ListNode) *ListNode {
    slow,fast := head,head
    for fast != nil&&fast.Next != nil {   //考慮有指向nil這種陰間行為,記得.next!=nil
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast{
            node := head
            for node != slow {
                node = node.Next
                slow = slow.Next
            }
            return node
        }
    }

    return nil

148.排序連結串列 難度:中等

想法1 直接取值入陣列排序然後重寫

func sortList(head *ListNode) *ListNode {
    l1 := []int{}
    for node:=head;node != nil ;node = node.Next{
        l1 = append(l1,node.Val)
    }
    sort.Ints(l1)
    i:=0
    for node:=head;node !=nil;node=node.Next{
        node.Val = l1[i]
        i++
    }
    return head
}

我本來以為超時的,沒想到通過了

常規解法是歸併排序

我們先將連結串列多次等分,得出單個連結串列。

然後結合之前的合併有序連結串列的想法進行兩兩合併,得出結果

func sortList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil { // 遞迴的出口,不用排序 直接返回
        return head
    }
    slow,fast := head,head
    var pre *ListNode
    for fast != nil && fast.Next != nil {
        pre = slow
        slow = slow.Next
        fast = fast.Next.Next
    }
    pre.Next = nil

    l1 := sortList(head)
    l2 := sortList(slow)

    return mergeList(l1,l2)
}


func mergeList(l1,l2 *ListNode) *ListNode {
    dummy := &ListNode{0,nil}
    node := dummy
    for l1!=nil && l2!=nil {
        if l1.Val < l2.Val {
            node.Next = l1
            l1 = l1.Next
        }else {
            node.Next = l2
            l2 = l2.Next
        }
        node = node.Next
    }
    switch  {
    case l1 != nil:
        node.Next = l1
    case l2 != nil:
        node.Next = l2
    }

    return dummy.Next
}

23.合併K個升序連結串列 難度:困難

最後以一個困難結尾,聽說這個題也是位元組面試喜歡問的
合併連結串列的升級版,簡單解法就是不斷迴圈

func mergeKLists(lists []*ListNode) *ListNode {
 if len(lists) == 0 {
 return nil
 }
 end := lists[0]
 for i:=1;i<len(lists);i++{
 end = merge(end,lists[i])
 }
 return end
}func merge(l1,l2 *ListNode) *ListNode {
 dummy := &ListNode{0,nil}
  node := dummy
  for l1!=nil && l2!=nil {
  if l1.Val < l2.Val {
    node.Next = l1
    l1 = l1.Next
  }else {
    node.Next = l2
    l2 = l2.Next
  }
  node = node.Next
  }
  switch  {
  case l1 != nil:
  node.Next = l1
  case l2 != nil:
  node.Next = l2
  }return dummy.Next
}

時間複雜度: O(k^2*n)

那麼有沒有優美解法呢

有一個最小堆法和一個分治法,實話實說最小堆法我沒看出優化的地方,所以我先寫分治法

分治法的思路比較簡單,歸併排序,我們先將連結串列陣列無限細分至每組只有兩個連結串列,然後連結串列合併後與其他連結串列合併直到最後結果

func mergeKLists(lists []*ListNode) *ListNode {
  if len(lists) == 0 || lists == nil {
  return nil
  }return mergeControl(lists,0,len(lists)-1)
}func mergeControl(lists []*ListNode,start,end int) *ListNode{
  if start == end {  // 僅剩一個連結串列,返回上一步與隔壁連結串列進行合併
  return lists[start]
  }
  if start>end {
  return nil
  }
​
  mid := (start+end)/2
  l1 := mergeControl(lists,start,mid)
  l2 := mergeControl(lists,mid+1,end)

  return merge(l1,l2)
}func merge(l1,l2 *ListNode) *ListNode {
  dummy := &ListNode{0,nil}
  node := dummy
  for l1!=nil && l2!=nil {
  if l1.Val < l2.Val {
    node.Next = l1
    l1 = l1.Next
  }else {
    node.Next = l2
    l2 = l2.Next
  }
  node = node.Next
  }
  switch  {
  case l1 != nil:
  node.Next = l1
  case l2 != nil:
  node.Next = l2
  }return dummy.Next
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結
y1nhui

相關文章