数据结构之链表
1. 簡介
鏈表(Linked List)是一種基本的數據結構,用于表示一組元素,這些元素按順序排列,每個元素都與下一個元素連接。與數組不同,鏈表的元素不是在內存中連續(xù)存儲的,而是通過指針來連接的。鏈表由節(jié)點(Node)組成,每個節(jié)點包含兩個主要部分:數據和指向下一個節(jié)點(或上一個節(jié)點,如果是雙向鏈表)的引用(指針)。鏈表可以分為單向鏈表、雙向鏈表和循環(huán)鏈表等不同類型。
以下是鏈表的主要特點和屬性:
特點和屬性:
- 有序集合: 鏈表中的元素是按順序排列的,每個元素都有一個位置。
- 節(jié)點包含數據: 每個節(jié)點包含數據(元素的值)。
- 節(jié)點之間通過引用連接: 鏈表中的節(jié)點通過指針或引用相互連接。單向鏈表只有一個指向下一個節(jié)點的引用,雙向鏈表有兩個引用,分別指向下一個節(jié)點和上一個節(jié)點。
- 靈活的大小: 鏈表的大小可以動態(tài)增長或縮小,而不需要提前指定大小。
- 插入和刪除元素高效: 插入和刪除元素通常是鏈表的強項,因為只需要更新指針,而不需要移動大量元素。
鏈表的常見操作包括:
- 插入(Insertion): 在鏈表中插入一個新節(jié)點。
- 刪除(Deletion): 從鏈表中刪除一個節(jié)點。
- 搜索(Search): 查找鏈表中特定元素。
- 遍歷(Traversal): 遍歷鏈表中的所有節(jié)點。
鏈表在許多編程場景中都有用,特別是在需要頻繁插入和刪除操作的情況下。它們通常比數組更靈活。然而,鏈表也有一些缺點,例如訪問元素需要從頭節(jié)點開始遍歷,因此在訪問元素方面效率較低。
2. 鏈表分類
常見的鏈表分類有:單向鏈表、雙向鏈表、循環(huán)鏈表、帶頭鏈表和跳表等,每種鏈表類型都適合不同的使用場景和問題。根據具體需求和性能要求,可以選擇適當類型的鏈表來解決問題。鏈表是計算機科學中常見的數據結構,對于處理動態(tài)數據集非常有用。
2.1 單向鏈表
單向鏈表(Singly Linked List)是一種鏈表數據結構,其中每個節(jié)點包含數據元素和一個指向下一個節(jié)點的引用。鏈表的頭節(jié)點用來表示鏈表的起始點,而尾節(jié)點的下一個節(jié)點通常為空(nil)。
以下是單向鏈表的主要特點和屬性:
特點和屬性:
- 每個節(jié)點包含兩個部分:數據元素和指向下一個節(jié)點的引用。
- 節(jié)點之間的連接是單向的,只能從頭節(jié)點開始遍歷鏈表。
- 插入和刪除節(jié)點操作在單向鏈表中非常高效,因為只需更新指針,而不需要移動大量元素。
- 鏈表的大小可以動態(tài)增長或縮小,不需要提前指定大小。
單向鏈表通常用于需要頻繁插入和刪除操作的情況,因為這些操作相對容易實現。然而,訪問鏈表中的特定元素需要從頭節(jié)點開始遍歷,效率較低。
下面是一個簡單的示例,展示了如何在Go語言中實現單向鏈表:
package main
import "fmt"
// 定義鏈表節(jié)點結構
type Node struct {
data int
next *Node
}
func main() {
// 創(chuàng)建鏈表頭節(jié)點
head := &Node{data: 1}
// 插入新節(jié)點到鏈表
newNode := &Node{data: 2}
head.next = newNode
// 遍歷鏈表并打印節(jié)點的數據
current := head
for current != nil {
fmt.Printf("%d -> ", current.data)
current = current.next
}
fmt.Println("nil")
}
在這個示例中,我們定義了一個Node結構來表示鏈表的節(jié)點,每個節(jié)點包含一個整數數據元素和一個指向下一個節(jié)點的引用。然后,我們創(chuàng)建一個鏈表頭節(jié)點,插入一個新節(jié)點,并遍歷鏈表并打印節(jié)點的數據。
這個示例只展示了鏈表的基本操作,包括創(chuàng)建、插入和遍歷。單向鏈表還支持其他操作,如刪除節(jié)點、查找節(jié)點等,具體操作可以根據需要自行擴展。
2.2 雙向鏈表
雙向鏈表(Doubly Linked List)是一種鏈表數據結構,其中每個節(jié)點包含數據元素、一個指向下一個節(jié)點的引用和一個指向前一個節(jié)點的引用。相對于單向鏈表,雙向鏈表提供了更多的靈活性,因為它可以在前向和后向兩個方向上遍歷鏈表。
以下是雙向鏈表的主要特點和屬性:
特點和屬性:
- 每個節(jié)點包含三個部分:數據元素、指向下一個節(jié)點的引用(通常稱為
next),和指向前一個節(jié)點的引用(通常稱為prev)。 - 節(jié)點之間的連接是雙向的,可以從頭節(jié)點向后遍歷,也可以從尾節(jié)點向前遍歷。
- 插入和刪除節(jié)點操作在雙向鏈表中仍然高效,因為只需更新相鄰節(jié)點的引用。
- 鏈表的大小可以動態(tài)增長或縮小,不需要提前指定大小。
雙向鏈表通常用于需要前向和后向遍歷的情況,或者在需要頻繁插入和刪除節(jié)點的情況下。相對于單向鏈表,雙向鏈表提供了更多的靈活性,但也需要額外的空間來存儲前向引用。
下面是一個簡單的示例,展示了如何在Go語言中實現雙向鏈表:
package main
import "fmt"
// 定義雙向鏈表節(jié)點結構
type Node struct {
data int
next *Node
prev *Node
}
func main() {
// 創(chuàng)建鏈表頭節(jié)點
head := &Node{data: 1}
tail := head
// 插入新節(jié)點到鏈表
newNode := &Node{data: 2, prev: tail}
tail.next = newNode
tail = newNode
// 遍歷鏈表并打印節(jié)點的數據(前向遍歷)
current := head
for current != nil {
fmt.Printf("%d -> ", current.data)
current = current.next
}
fmt.Println("nil")
// 遍歷鏈表并打印節(jié)點的數據(后向遍歷)
current = tail
for current != nil {
fmt.Printf("%d -> ", current.data)
current = current.prev
}
fmt.Println("nil")
}
在這個示例中,我們定義了一個Node結構來表示雙向鏈表的節(jié)點,每個節(jié)點包含一個整數數據元素、一個指向下一個節(jié)點的引用和一個指向前一個節(jié)點的引用。我們創(chuàng)建了鏈表的頭節(jié)點和尾節(jié)點,并插入一個新節(jié)點。然后,我們展示了如何在前向和后向兩個方向上遍歷鏈表并打印節(jié)點的數據。
雙向鏈表的實現可以根據需要進行擴展,包括插入、刪除、查找節(jié)點等操作。雙向鏈表的前向和后向遍歷功能增加了訪問靈活性,但也需要額外的內存來存儲前向引用。
2.3 循環(huán)鏈表
循環(huán)鏈表(Circular Linked List)是一種鏈表數據結構,與常規(guī)鏈表不同的是,循環(huán)鏈表的最后一個節(jié)點指向第一個節(jié)點,形成一個環(huán)狀結構。這意味著你可以無限地遍歷鏈表,因為在鏈表的末尾沒有終止標志,可以一直繞著環(huán)遍歷下去。
以下是循環(huán)鏈表的主要特點和屬性:
特點和屬性:
- 每個節(jié)點包含兩個部分:數據元素和指向下一個節(jié)點的引用。
- 節(jié)點之間的連接是循環(huán)的,最后一個節(jié)點的引用指向第一個節(jié)點。
- 循環(huán)鏈表可以無限遍歷下去,因為沒有明確的終止點。
- 插入和刪除節(jié)點操作在循環(huán)鏈表中非常高效,因為只需更新相鄰節(jié)點的引用。
- 鏈表的大小可以動態(tài)增長或縮小,不需要提前指定大小。
循環(huán)鏈表通常用于環(huán)狀問題的建模,例如循環(huán)隊列、約瑟夫問題(Josephus problem)等。它還可以用于實現循環(huán)訪問的數據結構,例如輪播圖或周期性任務列表。
以下是一個簡單的示例,展示了如何在Go語言中實現循環(huán)鏈表:
package main
import "fmt"
// 定義循環(huán)鏈表節(jié)點結構
type Node struct {
data int
next *Node
}
func main() {
// 創(chuàng)建鏈表頭節(jié)點
head := &Node{data: 1}
tail := head
// 插入新節(jié)點到鏈表
newNode := &Node{data: 2}
tail.next = newNode
tail = newNode
tail.next = head // 使鏈表成為循環(huán)
// 遍歷循環(huán)鏈表并打印節(jié)點的數據
current := head
for i := 0; i < 10; i++ { // 遍歷前10個節(jié)點
fmt.Printf("%d -> ", current.data)
current = current.next
}
fmt.Println("...")
}
在這個示例中,我們創(chuàng)建了一個循環(huán)鏈表,包含兩個節(jié)點,然后將鏈表變?yōu)檠h(huán),使最后一個節(jié)點指向第一個節(jié)點。然后,我們遍歷前10個節(jié)點并打印它們的數據。由于鏈表是循環(huán)的,遍歷可以無限繼續(xù),我們在示例中只遍歷了前10個節(jié)點。
循環(huán)鏈表的實現可以根據需要進行擴展,包括插入、刪除、查找節(jié)點等操作。循環(huán)鏈表是一種非常有趣的數據結構,可以應用于各種特定的問題和場景。
2.4 帶頭鏈表
帶頭鏈表(Head Linked List),也稱為帶頭節(jié)點鏈表或哨兵節(jié)點鏈表,是一種鏈表數據結構,其中鏈表的頭部包含一個額外的節(jié)點,通常稱為頭節(jié)點(Head Node)或哨兵節(jié)點(Sentinel Node)。這個額外的節(jié)點不包含實際數據,它的主要目的是簡化鏈表操作,確保鏈表不為空,并在插入和刪除節(jié)點時提供一致性。
以下是帶頭鏈表的主要特點和屬性:
特點和屬性:
- 鏈表的頭節(jié)點包含兩個部分:指向鏈表的第一個實際節(jié)點的引用和通常為空的數據元素。
- 鏈表的頭節(jié)點使鏈表操作更簡單,因為不需要特殊處理空鏈表的情況。
- 帶頭鏈表可以用于各種鏈表問題,包括單向鏈表、雙向鏈表、循環(huán)鏈表等不同類型的鏈表。
帶頭鏈表通常用于簡化鏈表操作,因為它確保鏈表不為空,即使鏈表沒有實際數據節(jié)點時,頭節(jié)點也存在。這減少了對特殊情況的處理。
以下是一個示例,展示了如何在Go語言中實現帶頭鏈表:
package main
import "fmt"
// 定義鏈表節(jié)點結構
type Node struct {
data int
next *Node
}
func main() {
// 創(chuàng)建鏈表頭節(jié)點(帶頭鏈表)
head := &Node{}
current := head
// 插入新節(jié)點到鏈表
newNode := &Node{data: 1}
current.next = newNode
current = newNode
// 遍歷鏈表并打印節(jié)點的數據
current = head.next // 跳過頭節(jié)點
for current != nil {
fmt.Printf("%d -> ", current.data)
current = current.next
}
fmt.Println("nil")
}
在這個示例中,我們創(chuàng)建了一個帶頭鏈表,其中鏈表的頭節(jié)點不包含實際數據,然后插入一個新節(jié)點到鏈表中。在遍歷鏈表時,我們跳過頭節(jié)點并打印數據。帶頭鏈表的頭節(jié)點不包含實際數據,但確保了鏈表操作的一致性。帶頭鏈表通常用于實現各種鏈表類型,包括單向鏈表和雙向鏈表等。
2.5 跳表
跳表(Skip List)是一種高級數據結構,用于加速元素的查找操作,類似于平衡樹,但實現更加簡單。跳表通過層級結構在鏈表中添加索引層,從而在查找元素時可以跳過部分元素,提高查找效率。跳表通常用于需要快速查找和插入的數據結構,尤其在有序數據集上表現出色。
以下是跳表的主要特點和屬性:
特點和屬性:
- 層級結構: 跳表包含多個層級,每個層級是一個有序鏈表,其中底層鏈表包含所有元素。
- 索引節(jié)點: 在每個層級,跳表添加了一些額外的節(jié)點,稱為索引節(jié)點,以加速查找。
- 快速查找: 查找元素時,跳表可以從頂層開始,根據元素值向右移動,然后下降到下一個層級繼續(xù)查找。
- 高效插入和刪除: 插入和刪除元素時,跳表可以利用索引節(jié)點快速定位插入或刪除位置。
- 平均查找時間: 在平均情況下,跳表的查找時間復雜度為O(log n),其中n是元素數量。
- 可變高度: 跳表的高度可以根據需要調整,以適應元素的動態(tài)插入和刪除。
跳表是一種強大的數據結構,適用于需要高效查找和插入操作的場景,例如數據庫索引、緩存實現等。
下面是一個簡單的示例,展示了如何在Go語言中實現跳表:
package main
import (
"fmt"
"math"
"math/rand"
)
// 定義跳表節(jié)點結構
type Node struct {
data int
next []*Node // 每個節(jié)點的下一層節(jié)點
}
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)
}
}
在這個示例中,我們實現了一個簡單的跳表,用于插入和查找整數數據。跳表包含多個層級,每個節(jié)點都包含一個數據元素和一個指向下一個層級的節(jié)點數組。我們可以插入數據并搜索數據,以檢查數據是否存在于跳表中。跳表的高度可以根據需要調整,以適應動態(tài)插入操作。這個示例展示了跳表的基本工作原理,實際應用中可以根據需求進行更復雜的擴展。
聲明:本作品采用署名-非商業(yè)性使用-相同方式共享 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請注明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意
總結
- 上一篇: 喜闻乐见最新的ORM查询BUG,看看有没
- 下一篇: Redis宕机恢复