數據結構和算法(Golang實現)(12)常見數據結構-鏈表

鏈表

講數據結構就離不開講鏈表。由於數據結構是用來組織數據的,如何將一個數據關聯到另一個數據呢?鏈表能夠將數據和數據之間關聯起來,從一個數據指向另一個數據。node

1、鏈表

定義:程序員

鏈表由一個個數據節點組成的,它是一個遞歸結構,要麼它是空的,要麼它存在一個指向另一個數據節點的引用。

鏈表,能夠說是最基礎的數據結構。算法

最簡單的鏈表以下:編程

package main

import (
    "fmt"
)

type LinkNode struct {
    Data     int64
    NextNode *LinkNode
}

func main() {
    // 新的節點
    node := new(LinkNode)
    node.Data = 2

    // 新的節點
    node1 := new(LinkNode)
    node1.Data = 3
    node.NextNode = node1 // node1 連接到 node 節點上

    // 新的節點
    node2 := new(LinkNode)
    node2.Data = 4
    node1.NextNode = node2 // node2 連接到 node1 節點上

    // 按順序打印數據
    nowNode := node
    for {
        if nowNode != nil {
            // 打印節點值
            fmt.Println(nowNode.Data)
            // 獲取下一個節點
            nowNode = nowNode.NextNode
        }

        // 若是下一個節點爲空,表示鏈表結束了
        break
    }
}

打印出:segmentfault

2
3
4

結構體LinkNode有兩個字段,一個字段存放數據Data,另外一個字典指向下一個節點NextNode。這種從一個數據節點指向下一個數據節點的結構,均可以叫作鏈表。數組

有些書籍,把鏈表作了很細的劃分,好比單鏈表,雙鏈表,循環單鏈表,循環雙鏈表,其實沒有必要強行分類,鏈表就是從一個數據指向另一個數據,一種將數據和數據關聯起來的結構而已。數據結構

好吧,咱們仍是要知道是什麼。併發

  1. 單鏈表,就是鏈表是單向的,像咱們上面這個結構同樣,能夠一直往下找到下一個數據節點,它只有一個方向,它不能往回找。
  2. 雙鏈表,每一個節點既能夠找到它以前的節點,也能夠找到以後的節點,是雙向的。
  3. 循環鏈表,就是它一直往下找數據節點,最後回到了本身那個節點,造成了一個迴路。循環單鏈表和循環雙鏈表的區別就是,一個只能一個方向走,一個兩個方向均可以走。

咱們來實現一個循環鏈表Ring(集鏈表大成者),參考Golang標準庫container/ring::數據結構和算法

// 循環鏈表
type Ring struct {
    next, prev *Ring       // 前驅和後驅節點
    Value      interface{} // 數據
}

該循環鏈表有一個三個字段,next表示後驅節點,prev表示前驅節點,Value表示值。編程語言

咱們來分析該結構各操做的時間複雜度。

1.1.初始化循環鏈表

初始化一個空的循環鏈表:

package main

import (
    "fmt"
)

// 初始化空的循環鏈表,前驅和後驅都指向本身,由於是循環的
func (r *Ring) init() *Ring {
    r.next = r
    r.prev = r
    return r
}


func main() {
    r := new(Ring)
    r.init()
}

由於綁定前驅和後驅節點爲本身,沒有循環,時間複雜度爲:O(1)

建立一個指定大小N的循環鏈表,值全爲空:

// 建立N個節點的循環鏈表
func New(n int) *Ring {
    if n <= 0 {
        return nil
    }
    r := new(Ring)
    p := r
    for i := 1; i < n; i++ {
        p.next = &Ring{prev: p}
        p = p.next
    }
    p.next = r
    r.prev = p
    return r
}

會連續綁定前驅和後驅節點,時間複雜度爲:O(n)

1.2.獲取上一個或下一個節點

// 獲取下一個節點
func (r *Ring) Next() *Ring {
    if r.next == nil {
        return r.init()
    }
    return r.next
}

// 獲取上一個節點
func (r *Ring) Prev() *Ring {
    if r.next == nil {
        return r.init()
    }
    return r.prev
}

獲取前驅或後驅節點,時間複雜度爲:O(1)

1.2.獲取第 n 個節點

由於鏈表是循環的,當n爲負數,表示從前面往前遍歷,不然日後面遍歷:

func (r *Ring) Move(n int) *Ring {
    if r.next == nil {
        return r.init()
    }
    switch {
    case n < 0:
        for ; n < 0; n++ {
            r = r.prev
        }
    case n > 0:
        for ; n > 0; n-- {
            r = r.next
        }
    }
    return r
}

由於須要遍歷n次,因此時間複雜度爲:O(n)

1.3.添加節點

// 往節點A,連接一個節點,而且返回以前節點A的後驅節點
func (r *Ring) Link(s *Ring) *Ring {
    n := r.Next()
    if s != nil {
        p := s.Prev()
        r.next = s
        s.prev = r
        n.prev = p
        p.next = n
    }
    return n
}

添加節點的操做比較複雜,若是節點s是一個新的節點。

那麼也就是在r節點後插入一個新節點s,而r節點以前的後驅節點,將會連接到新節點後面,並返回r節點以前的第一個後驅節點n,圖以下:

能夠看到插入新節點,會從新造成一個環,新節點s被插入了中間。

執行如下程序:

package main

import (
    "fmt"
)

ffunc linkNewTest() {
     // 第一個節點
     r := &Ring{Value: -1}

     // 連接新的五個節點
     r.Link(&Ring{Value: 1})
     r.Link(&Ring{Value: 2})
     r.Link(&Ring{Value: 3})
     r.Link(&Ring{Value: 4})

     node := r
     for {
         // 打印節點值
         fmt.Println(node.Value)

         // 移到下一個節點
         node = node.Next()

         //  若是節點回到了起點,結束
         if node == r {
             return
         }
     }
 }

func main() {
    linkNewTest()
}

輸出:

-1
4
3
2
1

每次連接的是一個新節點,那麼鏈會愈來愈長,仍然是一個環。由於只是更改連接位置,時間複雜度爲:O(1)

1.4.刪除節點

// 刪除節點後面的 n 個節點
func (r *Ring) Unlink(n int) *Ring {
    if n < 0 {
        return nil
    }
    return r.Link(r.Move(n + 1))
}

將循環鏈表的後面幾個節點刪除。

執行:

package main

import (
    "fmt"
)

func deleteTest() {
    // 第一個節點
    r := &Ring{Value: -1}

    // 連接新的五個節點
    r.Link(&Ring{Value: 1})
    r.Link(&Ring{Value: 2})
    r.Link(&Ring{Value: 3})
    r.Link(&Ring{Value: 4})

    temp := r.Unlink(3) // 解除了後面兩個節點

    // 打印原來的節點
    node := r
    for {
        // 打印節點值
        fmt.Println(node.Value)
        // 移到下一個節點
        node = node.Next()

        //  若是節點回到了起點,結束
        if node == r {
            break
        }
    }

    fmt.Println("------")

    // 打印被切斷的節點
    node = temp
    for {
        // 打印節點值
        fmt.Println(node.Value)
        // 移到下一個節點
        node = node.Next()

        //  若是節點回到了起點,結束
        if node == temp {
            break
        }
    }
}

func main() {
    deleteTest()
}

輸出:

-1
1
------
4
3
2

刪除循環鏈表後面的三個節點:r.Unlink(3)

能夠看到節點r後面的兩個節點被切斷了,而後分紅了兩個循環鏈表,r所在的鏈表變成了-1,1

而切除的那部分造成一個新循環鏈表是4 3 2,而且返回給了用戶。

由於只要定位要刪除的節點位置,而後進行連接:r.Link(r.Move(n + 1)),因此時間複雜度爲:O(n)+O(1)=O(n)

1.5.獲取鏈表長度

// 查看循環鏈表長度
func (r *Ring) Len() int {
    n := 0
    if r != nil {
        n = 1
        for p := r.Next(); p != r; p = p.next {
            n++
        }
    }
    return n
}

經過循環,當引用回到本身,那麼計數完畢,時間複雜度:O(n)

由於循環鏈表還不夠強壯,不知道起始節點是哪一個,計數鏈表長度還要遍歷,因此用循環鏈表實現的雙端隊列就出現了,通常具體編程都使用更高層次的數據結構。

詳細可查看棧和隊列章節。

2、數組和鏈表

數組是編程語言做爲一種基本類型提供出來的,相同數據類型的元素按必定順序排列的集合。

它的做用只有一種:存放數據,讓你很快能找到存的數據。若是你不去額外改進它,它就只是存放數據而已,它不會將一個數據節點和另一個數據節點關聯起來。好比創建一個大小爲5的數組array:

package main

import "fmt"


//  打印出:
//  [0 0 0 0 0]
//  [8 9 7 0 0]
//  7
func main() {
    array := [5]int64{}
    fmt.Println(array)
    array[0] = 8
    array[1] = 9
    array[2] = 7
    fmt.Println(array)
    fmt.Println(array[2])
}

咱們能夠經過下標0,1,2來獲取到數組中的數據,下標0,1,2就表示數據的位置,排第一位,排第二位,咱們也能夠把指定位置的數據替換成另一個數據。

數組這一數據類型,是被編程語言高度抽象封裝的結構,下標會轉換成虛擬內存地址,而後操做系統會自動幫咱們進行尋址,這個尋址過程是特別快的,因此往數組的某個下標取一個值和放一個值,時間複雜度都爲O(1)

它是一種將虛擬內存地址數據元素映射起來的內置語法結構,數據和數據之間是挨着,存放在一個連續的內存區域,每個固定大小(8字節)的內存片斷都有一個虛擬的地址編號。固然這個虛擬內存不是真正的內存,每一個程序啓動都會有一個虛擬內存空間來映射真正的內存,這是計算機組成的內容,和數據結構也有點關係,咱們會在另外的高級專題講,這裏就不展開了。

用數組也能夠實現鏈表,好比定義一個數組[5]Value,值類型爲一個結構體Value

package main

import "fmt"

func ArrayLink() {
    type Value struct {
        Data      string
        NextIndex int64
    }

    var array [5]Value          // 五個節點的數組
    array[0] = Value{"I", 3}    // 下一個節點的下標爲3
    array[1] = Value{"Army", 4} // 下一個節點的下標爲4
    array[2] = Value{"You", 1}  // 下一個節點的下標爲1
    array[3] = Value{"Love", 2} // 下一個節點的下標爲2
    array[4] = Value{"!", -1}   // -1表示沒有下一個節點
    node := array[0]
    for {
        fmt.Println(node.Data)
        if node.NextIndex == -1 {
            break
        }
        node = array[node.NextIndex]
    }

}

func main() {
    ArrayLink()
}

打印出:

I
Love
You
Army
!

獲取某個下標的數據,經過該數據能夠知道下一個數據的下標是什麼,而後拿出該下標的數據,繼續往下作。問題是,有時候須要作刪除,移動等各類操做,而數組的大小是固定的,須要大量空間移動,因此某些狀況下,數組的效率很低。

數組和鏈表是兩個不一樣的概念。一個是編程語言提供的基本數據類型,表示一個連續的內存空間,可經過一個索引訪問數據。另外一個是咱們定義的數據結構,經過一個數據節點,能夠定位到另外一個數據節點,不要求連續的內存空間。

數組的優勢是佔用空間小,查詢快,直接使用索引就能夠獲取數據元素,缺點是移動和刪除數據元素要大量移動空間。

鏈表的優勢是移動和刪除數據元素速度快,只要把相關的數據元素從新連接起來,但缺點是佔用空間大,查找須要遍歷。

不少其餘的數據結構都由數組和鏈表配合實現的。

3、總結

鏈表數組能夠用來輔助構建各類基本數據結構。

數據結構名字特別多,在之後的計算機生涯中,有些本身造的數據結構,或者不常見的別人造的數據結構,不知道叫什麼名字是很正常的。咱們只需知道常見的數據結構便可,方便與其餘程序員交流。

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索