資料結構之連結串列

落雷發表於2023-11-03

1. 簡介

連結串列(Linked List)是一種基本的資料結構,用於表示一組元素,這些元素按順序排列,每個元素都與下一個元素連線。與陣列不同,連結串列的元素不是在記憶體中連續儲存的,而是透過指標來連線的。連結串列由節點(Node)組成,每個節點包含兩個主要部分:資料和指向下一個節點(或上一個節點,如果是雙向連結串列)的引用(指標)。連結串列可以分為單向連結串列、雙向連結串列和迴圈連結串列等不同型別。

以下是連結串列的主要特點和屬性:

特點和屬性:

  1. 有序集合: 連結串列中的元素是按順序排列的,每個元素都有一個位置。
  2. 節點包含資料: 每個節點包含資料(元素的值)。
  3. 節點之間透過引用連線: 連結串列中的節點透過指標或引用相互連線。單向連結串列只有一個指向下一個節點的引用,雙向連結串列有兩個引用,分別指向下一個節點和上一個節點。
  4. 靈活的大小: 連結串列的大小可以動態增長或縮小,而不需要提前指定大小。
  5. 插入和刪除元素高效: 插入和刪除元素通常是連結串列的強項,因為只需要更新指標,而不需要移動大量元素。

連結串列的常見操作包括:

  • 插入(Insertion): 在連結串列中插入一個新節點。
  • 刪除(Deletion): 從連結串列中刪除一個節點。
  • 搜尋(Search): 查詢連結串列中特定元素。
  • 遍歷(Traversal): 遍歷連結串列中的所有節點。

連結串列在許多程式設計場景中都有用,特別是在需要頻繁插入和刪除操作的情況下。它們通常比陣列更靈活。然而,連結串列也有一些缺點,例如訪問元素需要從頭節點開始遍歷,因此在訪問元素方面效率較低。

2. 連結串列分類

常見的連結串列分類有:單向連結串列雙向連結串列迴圈連結串列帶頭連結串列跳錶等,每種連結串列型別都適合不同的使用場景和問題。根據具體需求和效能要求,可以選擇適當型別的連結串列來解決問題。連結串列是電腦科學中常見的資料結構,對於處理動態資料集非常有用。

2.1 單向連結串列

單向連結串列(Singly Linked List)是一種連結串列資料結構,其中每個節點包含資料元素和一個指向下一個節點的引用。連結串列的頭節點用來表示連結串列的起始點,而尾節點的下一個節點通常為空(nil)。

以下是單向連結串列的主要特點和屬性:

特點和屬性:

  1. 每個節點包含兩個部分:資料元素和指向下一個節點的引用。
  2. 節點之間的連線是單向的,只能從頭節點開始遍歷連結串列。
  3. 插入和刪除節點操作在單向連結串列中非常高效,因為只需更新指標,而不需要移動大量元素。
  4. 連結串列的大小可以動態增長或縮小,不需要提前指定大小。

單向連結串列通常用於需要頻繁插入和刪除操作的情況,因為這些操作相對容易實現。然而,訪問連結串列中的特定元素需要從頭節點開始遍歷,效率較低。

下面是一個簡單的示例,展示瞭如何在Go語言中實現單向連結串列:

package main

import "fmt"

// 定義連結串列節點結構
type Node struct {
    data int
    next *Node
}

func main() {
    // 建立連結串列頭節點
    head := &Node{data: 1}
    
    // 插入新節點到連結串列
    newNode := &Node{data: 2}
    head.next = newNode
    
    // 遍歷連結串列並列印節點的資料
    current := head
    for current != nil {
        fmt.Printf("%d -> ", current.data)
        current = current.next
    }
    fmt.Println("nil")
}

在這個示例中,我們定義了一個Node結構來表示連結串列的節點,每個節點包含一個整數資料元素和一個指向下一個節點的引用。然後,我們建立一個連結串列頭節點,插入一個新節點,並遍歷連結串列並列印節點的資料。

這個示例只展示了連結串列的基本操作,包括建立、插入和遍歷。單向連結串列還支援其他操作,如刪除節點、查詢節點等,具體操作可以根據需要自行擴充套件。

2.2 雙向連結串列

雙向連結串列(Doubly Linked List)是一種連結串列資料結構,其中每個節點包含資料元素、一個指向下一個節點的引用和一個指向前一個節點的引用。相對於單向連結串列,雙向連結串列提供了更多的靈活性,因為它可以在前向和後向兩個方向上遍歷連結串列。

以下是雙向連結串列的主要特點和屬性:

特點和屬性:

  1. 每個節點包含三個部分:資料元素、指向下一個節點的引用(通常稱為next),和指向前一個節點的引用(通常稱為prev)。
  2. 節點之間的連線是雙向的,可以從頭節點向後遍歷,也可以從尾節點向前遍歷。
  3. 插入和刪除節點操作在雙向連結串列中仍然高效,因為只需更新相鄰節點的引用。
  4. 連結串列的大小可以動態增長或縮小,不需要提前指定大小。

雙向連結串列通常用於需要前向和後向遍歷的情況,或者在需要頻繁插入和刪除節點的情況下。相對於單向連結串列,雙向連結串列提供了更多的靈活性,但也需要額外的空間來儲存前向引用。

下面是一個簡單的示例,展示瞭如何在Go語言中實現雙向連結串列:

package main

import "fmt"

// 定義雙向連結串列節點結構
type Node struct {
    data int
    next *Node
    prev *Node
}

func main() {
    // 建立連結串列頭節點
    head := &Node{data: 1}
    tail := head
    
    // 插入新節點到連結串列
    newNode := &Node{data: 2, prev: tail}
    tail.next = newNode
    tail = newNode
    
    // 遍歷連結串列並列印節點的資料(前向遍歷)
    current := head
    for current != nil {
        fmt.Printf("%d -> ", current.data)
        current = current.next
    }
    fmt.Println("nil")
    
    // 遍歷連結串列並列印節點的資料(後向遍歷)
    current = tail
    for current != nil {
        fmt.Printf("%d -> ", current.data)
        current = current.prev
    }
    fmt.Println("nil")
}

在這個示例中,我們定義了一個Node結構來表示雙向連結串列的節點,每個節點包含一個整數資料元素、一個指向下一個節點的引用和一個指向前一個節點的引用。我們建立了連結串列的頭節點和尾節點,並插入一個新節點。然後,我們展示瞭如何在前向和後向兩個方向上遍歷連結串列並列印節點的資料。

雙向連結串列的實現可以根據需要進行擴充套件,包括插入、刪除、查詢節點等操作。雙向連結串列的前向和後向遍歷功能增加了訪問靈活性,但也需要額外的記憶體來儲存前向引用。

2.3 迴圈連結串列

迴圈連結串列(Circular Linked List)是一種連結串列資料結構,與常規連結串列不同的是,迴圈連結串列的最後一個節點指向第一個節點,形成一個環狀結構。這意味著你可以無限地遍歷連結串列,因為在連結串列的末尾沒有終止標誌,可以一直繞著環遍歷下去。

以下是迴圈連結串列的主要特點和屬性:

特點和屬性:

  1. 每個節點包含兩個部分:資料元素和指向下一個節點的引用。
  2. 節點之間的連線是迴圈的,最後一個節點的引用指向第一個節點。
  3. 迴圈連結串列可以無限遍歷下去,因為沒有明確的終止點。
  4. 插入和刪除節點操作在迴圈連結串列中非常高效,因為只需更新相鄰節點的引用。
  5. 連結串列的大小可以動態增長或縮小,不需要提前指定大小。

迴圈連結串列通常用於環狀問題的建模,例如迴圈佇列、約瑟夫問題(Josephus problem)等。它還可以用於實現迴圈訪問的資料結構,例如輪播圖或週期性任務列表。

以下是一個簡單的示例,展示瞭如何在Go語言中實現迴圈連結串列:

package main

import "fmt"

// 定義迴圈連結串列節點結構
type Node struct {
    data int
    next *Node
}

func main() {
    // 建立連結串列頭節點
    head := &Node{data: 1}
    tail := head
    
    // 插入新節點到連結串列
    newNode := &Node{data: 2}
    tail.next = newNode
    tail = newNode
    tail.next = head  // 使連結串列成為迴圈
    
    // 遍歷迴圈連結串列並列印節點的資料
    current := head
    for i := 0; i < 10; i++ { // 遍歷前10個節點
        fmt.Printf("%d -> ", current.data)
        current = current.next
    }
    fmt.Println("...")
}

在這個示例中,我們建立了一個迴圈連結串列,包含兩個節點,然後將連結串列變為迴圈,使最後一個節點指向第一個節點。然後,我們遍歷前10個節點並列印它們的資料。由於連結串列是迴圈的,遍歷可以無限繼續,我們在示例中只遍歷了前10個節點。

迴圈連結串列的實現可以根據需要進行擴充套件,包括插入、刪除、查詢節點等操作。迴圈連結串列是一種非常有趣的資料結構,可以應用於各種特定的問題和場景。

2.4 帶頭連結串列

帶頭連結串列(Head Linked List),也稱為帶頭節點連結串列或哨兵節點連結串列,是一種連結串列資料結構,其中連結串列的頭部包含一個額外的節點,通常稱為頭節點(Head Node)或哨兵節點(Sentinel Node)。這個額外的節點不包含實際資料,它的主要目的是簡化連結串列操作,確保連結串列不為空,並在插入和刪除節點時提供一致性。

以下是帶頭連結串列的主要特點和屬性:

特點和屬性:

  1. 連結串列的頭節點包含兩個部分:指向連結串列的第一個實際節點的引用和通常為空的資料元素。
  2. 連結串列的頭節點使連結串列操作更簡單,因為不需要特殊處理空連結串列的情況。
  3. 帶頭連結串列可以用於各種連結串列問題,包括單向連結串列、雙向連結串列、迴圈連結串列等不同型別的連結串列。

帶頭連結串列通常用於簡化連結串列操作,因為它確保連結串列不為空,即使連結串列沒有實際資料節點時,頭節點也存在。這減少了對特殊情況的處理。

以下是一個示例,展示瞭如何在Go語言中實現帶頭連結串列:

package main

import "fmt"

// 定義連結串列節點結構
type Node struct {
    data int
    next *Node
}

func main() {
    // 建立連結串列頭節點(帶頭連結串列)
    head := &Node{}
    current := head
    
    // 插入新節點到連結串列
    newNode := &Node{data: 1}
    current.next = newNode
    current = newNode
    
    // 遍歷連結串列並列印節點的資料
    current = head.next // 跳過頭節點
    for current != nil {
        fmt.Printf("%d -> ", current.data)
        current = current.next
    }
    fmt.Println("nil")
}

在這個示例中,我們建立了一個帶頭連結串列,其中連結串列的頭節點不包含實際資料,然後插入一個新節點到連結串列中。在遍歷連結串列時,我們跳過頭節點並列印資料。帶頭連結串列的頭節點不包含實際資料,但確保了連結串列操作的一致性。帶頭連結串列通常用於實現各種連結串列型別,包括單向連結串列和雙向連結串列等。

2.5 跳錶

跳錶(Skip List)是一種高階資料結構,用於加速元素的查詢操作,類似於平衡樹,但實現更加簡單。跳錶透過層級結構在連結串列中新增索引層,從而在查詢元素時可以跳過部分元素,提高查詢效率。跳錶通常用於需要快速查詢和插入的資料結構,尤其在有序資料集上表現出色。

以下是跳錶的主要特點和屬性:

特點和屬性:

  1. 層級結構: 跳錶包含多個層級,每個層級是一個有序連結串列,其中底層連結串列包含所有元素。
  2. 索引節點: 在每個層級,跳錶新增了一些額外的節點,稱為索引節點,以加速查詢。
  3. 快速查詢: 查詢元素時,跳錶可以從頂層開始,根據元素值向右移動,然後下降到下一個層級繼續查詢。
  4. 高效插入和刪除: 插入和刪除元素時,跳錶可以利用索引節點快速定位插入或刪除位置。
  5. 平均查詢時間: 在平均情況下,跳錶的查詢時間複雜度為O(log n),其中n是元素數量。
  6. 可變高度: 跳錶的高度可以根據需要調整,以適應元素的動態插入和刪除。

跳錶是一種強大的資料結構,適用於需要高效查詢和插入操作的場景,例如資料庫索引、快取實現等。

下面是一個簡單的示例,展示瞭如何在Go語言中實現跳錶:

package main

import (
    "fmt"
    "math"
    "math/rand"
)

// 定義跳錶節點結構
type Node struct {
    data int
    next []*Node // 每個節點的下一層節點
}

type SkipList struct {
    header  *Node
    level   int
    maxNode *Node
}

func NewSkipList() *SkipList {
    header := &Node{data: math.MinInt32, next: make([]*Node, 1)}
    return &SkipList{header, 1, nil}
}

func (sl *SkipList) Insert(data int) {
    update := make([]*Node, sl.level)
    x := sl.header
    for i := sl.level - 1; i >= 0; i-- {
        for x.next[i] != nil && x.next[i].data < data {
            x = x.next[i]
        }
        update[i] = x
    }

    level := sl.randomLevel()
    if level > sl.level {
        for i := sl.level; i < level; i++ {
            update = append(update, sl.header)
        }
        sl.level = level
    }

    x = &Node{data: data, next: make([]*Node, level)}
    for i := 0; i < level; i++ {
        x.next[i] = update[i].next[i]
        update[i].next[i] = x
    }
}

func (sl *SkipList) Search(data int) bool {
    x := sl.header
    for i := sl.level - 1; i >= 0; i-- {
        for x.next[i] != nil && x.next[i].data < data {
            x = x.next[i]
        }
    }
    x = x.next[0]
    return x != nil && x.data == data
}

func (sl *SkipList) randomLevel() int {
    level := 1
    for rand.Float64() < 0.5 && level < 32 {
        level++
    }
    return level
}

func main() {
    sl := NewSkipList()
    data := []int{1, 4, 2, 8, 6, 9, 5, 3, 7}

    for _, value := range data {
        sl.Insert(value)
    }

    for _, value := range data {
        found := sl.Search(value)
        fmt.Printf("Searching for %d: %v\n", value, found)
    }
}

在這個示例中,我們實現了一個簡單的跳錶,用於插入和查詢整數資料。跳錶包含多個層級,每個節點都包含一個資料元素和一個指向下一個層級的節點陣列。我們可以插入資料並搜尋資料,以檢查資料是否存在於跳錶中。跳錶的高度可以根據需要調整,以適應動態插入操作。這個示例展示了跳錶的基本工作原理,實際應用中可以根據需求進行更復雜的擴充套件。


孟斯特

宣告:本作品採用署名-非商業性使用-相同方式共享 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意


相關文章