講數據結構就離不開講鏈表。由於數據結構是用來組織數據的,如何將一個數據關聯到另一個數據呢?鏈表能夠將數據和數據之間關聯起來,從一個數據指向另一個數據。node
定義:程序員
鏈表由一個個數據節點組成的,它是一個遞歸結構,要麼它是空的,要麼它存在一個指向另一個數據節點的引用。
鏈表,能夠說是最基礎的數據結構。算法
最簡單的鏈表以下:編程
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
。這種從一個數據節點指向下一個數據節點的結構,均可以叫作鏈表。數組
有些書籍,把鏈表作了很細的劃分,好比單鏈表,雙鏈表,循環單鏈表,循環雙鏈表,其實沒有必要強行分類,鏈表就是從一個數據指向另一個數據,一種將數據和數據關聯起來的結構而已。數據結構
好吧,咱們仍是要知道是什麼。併發
咱們來實現一個循環鏈表Ring
(集鏈表大成者),參考Golang
標準庫container/ring
::數據結構和算法
// 循環鏈表 type Ring struct { next, prev *Ring // 前驅和後驅節點 Value interface{} // 數據 }
該循環鏈表有一個三個字段,next
表示後驅節點,prev
表示前驅節點,Value
表示值。編程語言
咱們來分析該結構各操做的時間複雜度。
初始化一個空的循環鏈表:
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)
。
// 獲取下一個節點 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)
。
由於鏈表是循環的,當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)
。
// 往節點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)
。
// 刪除節點後面的 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)
// 查看循環鏈表長度 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)
。
由於循環鏈表還不夠強壯,不知道起始節點是哪一個,計數鏈表長度還要遍歷,因此用循環鏈表實現的雙端隊列就出現了,通常具體編程都使用更高層次的數據結構。
詳細可查看棧和隊列章節。
數組是編程語言做爲一種基本類型提供出來的,相同數據類型的元素按必定順序排列的集合。
它的做用只有一種:存放數據,讓你很快能找到存的數據。若是你不去額外改進它,它就只是存放數據而已,它不會將一個數據節點和另一個數據節點關聯起來。好比創建一個大小爲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 !
獲取某個下標
的數據,經過該數據能夠知道下一個數據的下標
是什麼,而後拿出該下標的數據,繼續往下作。問題是,有時候須要作刪除,移動等各類操做,而數組的大小是固定的,須要大量空間移動,因此某些狀況下,數組的效率很低。
數組和鏈表是兩個不一樣的概念。一個是編程語言提供的基本數據類型,表示一個連續的內存空間,可經過一個索引訪問數據。另外一個是咱們定義的數據結構,經過一個數據節點,能夠定位到另外一個數據節點,不要求連續的內存空間。
數組的優勢是佔用空間小,查詢快,直接使用索引就能夠獲取數據元素,缺點是移動和刪除數據元素要大量移動空間。
鏈表的優勢是移動和刪除數據元素速度快,只要把相關的數據元素從新連接起來,但缺點是佔用空間大,查找須要遍歷。
不少其餘的數據結構都由數組和鏈表配合實現的。
鏈表
和數組
能夠用來輔助構建各類基本數據結構。
數據結構名字特別多,在之後的計算機生涯中,有些本身造的數據結構,或者不常見的別人造的數據結構,不知道叫什麼名字是很正常的。咱們只需知道常見的數據結構便可,方便與其餘程序員交流。
我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook。