這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一、什麼是雙向鏈表
和單鏈表比較,雙向鏈表的元素不但知道自己的下線,還知道自己的上線(越來越像傳銷組織了)。小煤車開起來,圖裡面可以看出,每個車廂除了一個指向後面車廂的箭頭外,還有一個指向前面車廂的箭頭(車頭、車尾除外)。車頭只有指向後面車廂的箭頭,車尾只有指向前面車廂的箭頭。
二、雙向鏈表與Go的對應結構
1、節點
我們先把車廂分解開來。每節車廂都由煤炭、車體、拉前車廂繩索、拉後車廂繩索這4部分組成。車體是我們的運輸工具,在Go語言裡我們用結構提DNode表示;煤炭代表運的貨物,用data變數表示;拉前車廂繩索和拉後車廂繩索我們分別用指標prev和next表示。這樣一節車廂,用Go語言描述如下:
type DNode struct { data Object prev *DNode next *DNode}
2、雙向鏈表
一個運煤車隊就是一個雙向鏈表。車隊要有車頭、車廂、車尾,作為車隊的負責人還得知道車隊有多長。在Go語言裡,車隊用結構體DList表示,車頭用head變數表示,車位用tail變數表示,車隊長度就用size來表示,把這些表示合起來:
type DList struct { size uint64 head *DNode tail *DNode}
結構講完了,下面講講如何增加、減少車廂,也就是雙向鏈表的介面。
三、介面說明及實現
介面主要分為這幾類。一個是雙向鏈表本身的,還有一類是節點的。鏈表本身的還分為公開和私人兩種。下面我們就詳細聊聊這些介面。
1、初始化鏈表Init
雙向鏈表的初始化,可以理解成大衛哥準備買一個車隊準備運煤。第一步,得獲得國家有關部門的批准,有了批准大衛哥就可以買車廂運煤了。但是,批准下來的時候,大衛哥的車隊啥都沒有,沒有車頭、車尾,連一節車廂也沒有。Go語言代碼實現:
func (dList *DList) Init() { _dList := *(dList) _dList.size = 0 // 沒車廂 _dList.head = nil // 沒車頭 _dList.tail = nil // 沒車尾}
2、新增資料Append
大衛哥新買了車廂,買好的車廂要掛到車隊後面。第一節車廂就是車頭。
func (dList *DList) Append(data Object) { newNode := new(DNode) (*newNode).data = data if (*dList).GetSize() == 0 { // 買個車頭 (*dList).head = newNode (*dList).tail = newNode (*newNode).prev = nil (*newNode).next = nil } else { // 掛在車隊尾部 (*newNode).prev = (*dList).tail (*newNode).next = nil (*((*dList).tail)).next = newNode (*dList).tail = newNode } (*dList).size++;}
3、在節點後面插入資料InsertNext
有時候,車廂不是放在車隊尾巴,而是要放在中間,比如都是運蘋果的車廂最好放一起。
func (dList *DList) InsertNext(elmt *DNode, data Object) bool { if elmt == nil { // apend return false } if dList.isTail(elmt) { // 恰好在車隊尾巴 dList.Append(data) } else { newNode := new(DNode) (*newNode).data = data (*newNode).prev = elmt (*newNode).next = (*elmt).next (*elmt).next = newNode (*((*newNode).next)).prev = newNode (*dList).size++; } return true}
5、在節點前面插入資料InsertPrev
在節點前面插入資料,可以理解為在當前節點前一個節點的後面插入資料。
func (dList *DList) InsertPrev(elmt *DNode, data Object) bool { if elmt == nil { return false } if dList.isHead(elmt) { // 如果是新增一個車頭就特殊處理 newNode := new(DNode) (*newNode).data = data (*newNode).next = dList.GetHead() (*newNode).prev = nil (*(dList.head)).prev = newNode dList.head = newNode dList.size++ return true } else { prev := (*elmt).prev return dList.InsertNext(prev, data) }}
這裡的isHead就是判斷節點是否是車頭,後面大衛哥會介紹。
6、刪除一個節點Remove
有些車廂出現問題需要維修,就要把它從車隊裡卸下來。
func (dList *DList) Remove(elmt *DNode) Object { if elmt == nil { return false } prev := (*elmt).prev next := (*elmt).next if dList.isHead(elmt) { dList.head = next } else { (*prev).next = next } if dList.isTail(elmt) { dList.tail = prev } else { (*next).prev = prev } dList.size-- return (*elmt).GetData() }
卸下來後,車廂裡的資料還是要保留的。
7、尋找指定資料所在的節點Search
比如說,要找到蘋果在哪節車廂。就要用到尋找功能了。
func (dList *DList) Search(data Object, yourMatch ...MatchFun) *DNode { if dList.GetSize() == 0 { return nil } match := defaultMatch if len(yourMatch) > 0 { match = yourMatch[0] } node := dList.GetHead() for ; node != nil; node = node.GetNext() { if match(node.GetData(), data) == 0 { break } } return node}
match是匹配函數,定義如下:
type MatchFun func (data1 Object, data2 Object) int
如果data1和data2相等就返回0,data1大於data2就返回正數,小於就返回負數。
8、擷取鏈表長度GetSize
func (dList *DList) GetSize() uint64 { return (*dList).size}
9、擷取頭部節點GetHead
func (dList *DList) GetHead() *DNode { return (*dList).head}
10、擷取尾部節點GetTail
func (dList *DList) GetTail() *DNode { return (*dList).tail}
11、節點是否是頭部節點isHead
func (dList *DList) isHead(elmt *DNode) bool { return dList.GetHead() == elmt}
12、節點是否是列表尾部isTail
func (dList *DList) isTail(elmt *DNode) bool { return dList.GetTail() == elmt}
13、擷取節點內資料GetData
func (dNode *DNode) GetData() Object { return (*dNode).data}
這個是節點的方法,不是鏈表的。用來擷取車廂內裝的是什麼。
14、擷取下一個節點GetNext
func (dNode *DNode) GetNext() *DNode { return (*dNode).next}
這個也是節點的方法,協助車廂找到下一節車廂。
15、擷取前一個節點GetPrev
func (dNode *DNode) GetPrev() *DNode { return (*dNode).prev}
這裡同樣是節點的方法。用來找到上一節車廂。
代碼下載