Go實現雙向連結串列
本文介紹什麼是連結串列,常見的連結串列有哪些,然後介紹連結串列這種資料結構會在哪些地方可以用到,以及 Redis 佇列是底層的實現,通過一個小例項來演示 Redis 佇列有哪些功能,最後通過 Go 實現一個雙向連結串列。
目錄
- 1、連結串列
- 1.1 說明
- 1.2 單向連結串列
- 1.3 迴圈連結串列
- 1.4 雙向連結串列
- 2、redis佇列
- 2.1 說明
- 2.2 應用場景
- 2.3 演示
- 3、Go雙向連結串列
- 3.1 說明
- 3.2 實現
- 4、總結
- 5、參考文獻
1、連結串列
1.1 說明
連結串列(Linked list)是一種常見的基礎資料結構,是一種線性表,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer)。由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而順序表相應的時間複雜度分別是O(logn)和O(1)。
連結串列有很多種不同的型別:單向連結串列,雙向連結串列以及迴圈連結串列。
- 優勢:
可以克服陣列連結串列需要預先知道資料大小的缺點,連結串列結構可以充分利用計算機記憶體空間,實現靈活的記憶體動態管理。連結串列允許插入和移除表上任意位置上的節點。
- 劣勢:
由於連結串列增加了節點指標,空間開銷比較大。連結串列一般查詢資料的時候需要從第一個節點開始每次訪問下一個節點,直到訪問到需要的位置,查詢資料比較慢。
- 用途:
常用於組織檢索較少,而刪除、新增、遍歷較多的資料。
如:檔案系統、LRU cache、Redis 列表、記憶體管理等。
1.2 單向連結串列
連結串列中最簡單的一種是單向連結串列,
一個單向連結串列的節點被分成兩個部分。它包含兩個域,一個資訊域和一個指標域。第一個部分儲存或者顯示關於節點的資訊,第二個部分儲存下一個節點的地址,而最後一個節點則指向一個空值。單向連結串列只可向一個方向遍歷。
單連結串列有一個頭節點head,指向連結串列在記憶體的首地址。連結串列中的每一個節點的資料型別為結構體型別,節點有兩個成員:整型成員(實際需要儲存的資料)和指向下一個結構體型別節點的指標即下一個節點的地址(事實上,此單連結串列是用於存放整型資料的動態陣列)。連結串列按此結構對各節點的訪問需從連結串列的頭找起,後續節點的地址由當前節點給出。無論在表中訪問哪個節點,都需要從連結串列的頭開始,順序向後查詢。連結串列的尾節點由於無後續節點,其指標域為空,寫作為NULL。
1.3 迴圈連結串列
迴圈連結串列是與單向連結串列一樣,是一種鏈式的儲存結構,所不同的是,迴圈連結串列的最後一個結點的指標是指向該迴圈連結串列的第一個結點或者表頭結點,從而構成一個環形的鏈。
迴圈連結串列的運算與單連結串列的運算基本一致。所不同的有以下幾點:
1、在建立一個迴圈連結串列時,必須使其最後一個結點的指標指向表頭結點,而不是像單連結串列那樣置為NULL。
2、在判斷是否到表尾時,是判斷該結點鏈域的值是否是表頭結點,當鏈域的值等於表頭指標時,說明已到表尾。而非象單連結串列那樣判斷鏈域的值是否為NULL。
1.4 雙向連結串列
雙向連結串列其實是單連結串列的改進,當我們對單連結串列進行操作時,有時你要對某個結點的直接前驅進行操作時,又必須從表頭開始查詢。這是由單連結串列結點的結構所限制的。因為單連結串列每個結點只有一個儲存直接後繼結點地址的鏈域,那麼能不能定義一個既有儲存直接後繼結點地址的鏈域,又有儲存直接前驅結點地址的鏈域的這樣一個雙鏈域結點結構呢?這就是雙向連結串列。
在雙向連結串列中,結點除含有資料域外,還有兩個鏈域,一個儲存直接後繼結點地址,一般稱之為右鏈域(當此“連線”為最後一個“連線”時,指向空值或者空列表);一個儲存直接前驅結點地址,一般稱之為左鏈域(當此“連線”為第一個“連線”時,指向空值或者空列表)。
2、redis佇列
2.1 說明
Redis 列表是簡單的字串列表,按照插入順序排序。你可以新增一個元素到列表的頭部(左邊)或者尾部(右邊)
Redis 列表使用兩種資料結構作為底層實現:雙端列表(linkedlist)、壓縮列表(ziplist)
通過配置檔案中(list-max-ziplist-entries、list-max-ziplist-value)來選擇是哪種實現方式
在資料量比較少的時候,使用雙端連結串列和壓縮列表效能差異不大,但是使用壓縮列表更能節約記憶體空間
redis 連結串列的實現原始碼 redis src/adlist.h
2.2 應用場景
訊息佇列,秒殺專案
秒殺專案:
提前將需要的商品碼資訊存入 Redis 佇列,在搶購的時候每個使用者都從 Redis 佇列中取商品碼,由於 Redis 是單執行緒的,同時只能有一個商品碼被取出,取到商品碼的使用者為購買成功,而且 Redis 效能比較高,能抗住較大的使用者壓力。
2.3 演示
如何通過 Redis 佇列中防止併發情況下商品超賣的情況。
假設:
網站有三件商品需要賣,我們將資料存入 Redis 佇列中
1、 將三個商品碼(10001、10002、10003)存入 Redis 佇列中
# 存入商品
RPUSH commodity:queue 10001 10002 10003
複製程式碼
2、 存入以後,查詢資料是否符合預期
# 檢視全部元素
LRANGE commodity:queue 0 -1
# 檢視佇列的長度
LLEN commodity:queue
複製程式碼
3、 搶購開始,獲取商品碼,搶到商品碼的使用者則可以購買(由於 Redis 是單執行緒的,同一個商品碼只能被取一次 )
# 出隊
LPOP commodity:queue
複製程式碼
這裡瞭解到 Redis 列表是怎麼使用的,下面就用 Go 語言實現一個雙向連結串列來實現這些功能。
3、Go雙向連結串列
3.1 說明
這裡只是用 Go 語言實現一個雙向連結串列,實現:查詢連結串列的長度、連結串列右端插入資料、左端取資料、取指定區間的節點等功能( 類似於 Redis 列表的中的 RPUSH、LRANGE、LPOP、LLEN功能 )。
3.2 實現
- 節點定義
雙向連結串列有兩個指標,分別指向前一個節點和後一個節點
連結串列表頭 prev 的指標為空,連結串列表尾 next 的指標為空
// 連結串列的一個節點
type ListNode struct {
prev *ListNode // 前一個節點
next *ListNode // 後一個節點
value string // 資料
}
// 建立一個節點
func NewListNode(value string) (listNode *ListNode) {
listNode = &ListNode{
value: value,}
return
}
// 當前節點的前一個節點
func (n *ListNode) Prev() (prev *ListNode) {
prev = n.prev
return
}
// 當前節點的前一個節點
func (n *ListNode) Next() (next *ListNode) {
next = n.next
return
}
// 獲取節點的值
func (n *ListNode) GetValue() (value string) {
if n == nil {
return
}
value = n.value
return
}
複製程式碼
- 定義一個連結串列
連結串列為了方便操作,定義一個結構體,可以直接從表頭、表尾進行訪問,定義了一個屬性 len ,直接可以返回連結串列的長度,直接查詢連結串列的長度就不用遍歷時間複雜度從 O(n) 到 O(1)。
// 連結串列
type List struct {
head *ListNode // 表頭節點
tail *ListNode // 表尾節點
len int // 連結串列的長度
}
// 建立一個空連結串列
func NewList() (list *List) {
list = &List{
}
return
}
// 返回連結串列頭節點
func (l *List) Head() (head *ListNode) {
head = l.head
return
}
// 返回連結串列尾節點
func (l *List) Tail() (tail *ListNode) {
tail = l.tail
return
}
// 返回連結串列長度
func (l *List) Len() (len int) {
len = l.len
return
}
複製程式碼
- 在連結串列的右邊插入一個元素
// 在連結串列的右邊插入一個元素
func (l *List) RPush(value string) {
node := NewListNode(value)
// 連結串列未空的時候
if l.Len() == 0 {
l.head = node
l.tail = node
} else {
tail := l.tail
tail.next = node
node.prev = tail
l.tail = node
}
l.len = l.len + 1
return
}
複製程式碼
- 從連結串列左邊取出一個節點
// 從連結串列左邊取出一個節點
func (l *List) LPop() (node *ListNode) {
// 資料為空
if l.len == 0 {
return
}
node = l.head
if node.next == nil {
// 連結串列未空
l.head = nil
l.tail = nil
} else {
l.head = node.next
}
l.len = l.len - 1
return
}
複製程式碼
- 通過索引查詢節點
通過索引查詢節點,如果索引是負數則從表尾開始查詢。
自然數和負數索引分別通過兩種方式查詢節點,找到指定索引或者是連結串列全部查詢完則查詢完成。
// 通過索引查詢節點
// 查不到節點則返回空
func (l *List) Index(index int) (node *ListNode) {
// 索引為負數則表尾開始查詢
if index < 0 {
index = (-index) - 1
node = l.tail
for true {
// 未找到
if node == nil {
return
}
// 查到資料
if index == 0 {
return
}
node = node.prev
index--
}
} else {
node = l.head
for ; index > 0 && node != nil; index-- {
node = node.next
}
}
return
}
複製程式碼
- 返回指定區間的元素
// 返回指定區間的元素
func (l *List) Range(start,stop int) (nodes []*ListNode) {
nodes = make([]*ListNode,0)
// 轉為自然數
if start < 0 {
start = l.len + start
if start < 0 {
start = 0
}
}
if stop < 0 {
stop = l.len + stop
if stop < 0 {
stop = 0
}
}
// 區間個數
rangeLen := stop - start + 1
if rangeLen < 0 {
return
}
startNode := l.Index(start)
for i := 0; i < rangeLen; i++ {
if startNode == nil {
break
}
nodes = append(nodes,startNode)
startNode = startNode.next
}
return
}
複製程式碼
4、總結
- 到這裡關於連結串列的使用已經結束,介紹連結串列是有哪些(單向連結串列,雙向連結串列以及迴圈連結串列),也介紹了連結串列的應用場景(Redis 列表使用的是連結串列作為底層實現),最後用 Go 實現了雙向連結串列,演示了連結串列在 Go 語言中是怎麼使用的,大家可以在專案中更具實際的情況去使用。
5、參考文獻
專案地址:go 實現佇列