鏈表的定義
鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。----摘自百度百科
鏈表的底層
可見它並不是一組連續的內存,他是靠指針將零散的內存串聯起來,形成的一個完整的數據結構。
鏈表的一些重要概念
- 結點
- 鏈表中的某一個元素我們稱之爲結點
- 後繼指針
- 如上圖所示,鏈表中的元素不光包含數據,還應該包含一個指向下一個元素的指針,這個指針就叫做後繼指針。
- 頭結點
- 頭結點爲鏈表的第一個結點
- 只有通過頭結點我們纔可以遍歷完整的鏈表(單向鏈表的情況下)
- 尾結點
- 最後一個結點稱爲尾結點
- 尾結點的後繼指針指向一個空地址NULL
- 單向鏈表
- 整個鏈表只有一個遍歷方向
- 每一個結點包含數據本身(指針),和一個後繼指針
- 雙向鏈表
- 可以通過兩個方向遍歷
- 每一個結點包含數據本身(指針),一個後繼指針和前置指針
- 循環鏈表
- 尾結點指向頭結點的鏈表
鏈表的特點
- 無法直接隨機訪問(依據下標訪問)其中的任意數據
- 隨機訪問數據非常低效
- 高效的插入和刪除
關於第一點和第二點,因爲鏈表結構如上圖所示,並不是一組連續的內存空間,因此不能通過直接的內存尋址公式計算出來,只能通過頭結點依次遍歷來實現訪問特定元素,因此它的隨機訪問的時間複雜度爲O(n)。
但是鏈表的插入和刪除,因爲鏈表中的結點都是通過指針來指示下一個元素或者上一個元素,因此插入和刪除只需要簡單的將插入目標的前一個結點的指針指向新的元素,新元素後繼指針指向之前的下一元素即可。因此時間複雜度爲O(1)。
鏈表的實現—僞代碼篇
依據之前所說,梳理一下實現一個雙向鏈表所需的關鍵內容。
-
鏈表這個數據結構本身,應該包含結點
-
而每一個結點,應該包含:
- 數據本身(無論是指向數據的指針或者是一個實際的值類型)
- 後繼指針(指向下一個元素)
- 前置指針(指向上一個元素)
class List {
Node root //根結點,起始結點
}
class Node {
*Node next //後繼指針
*Node prev //前置指針
Value Value //這裏的value代表任意類型
}
以上爲一段僞代碼,接下來爲List擴充插入和刪除操作:
class List {
Node root //根結點,起始結點
//將new插入到at之後
function insert(Node new,Node at) {
//at是最後一個結點
if(at.next == null) {
at.next = &new //當前元素的下一個元素變爲new,當前元素前一個元素不變
new.next = null //new元素下一個元素變爲原下一個元素
new.prev = &at //new元素前一個元素變爲當前元素
}else{
*Node oldNext = at.next //原下一個元素
at.next = &new //當前元素的下一個元素變爲new,當前元素前一個元素不變
new.next = oldNext //new元素下一個元素變爲原下一個元素
new.prev = &at //new元素前一個元素變爲當前元素
oldNext.prev = &new //原下一個元素的前一個元素變爲new,原下一個元素下一個元素不變
}
}
//刪除指定結點
function delete(Node del) {
//del爲最後一個結點
if(del.next == null) {
*Node oldPrev = del.prev //原前一個元素
oldPrev.next = null //原前一個元素的下一個元素變爲原下一個元素
}elseif(del.prev == null){ //del爲第一個結點
*Node oldNext = del.next //原下一個元素
oldNext.prev = null //原下一個元素的前一個元素變爲原前一個元素
}else{
*Node oldNext = del.next //原下一個元素
*Node oldPrev = del.prev //原前一個元素
oldPrev.next = oldNext //原前一個元素的下一個元素變爲原下一個元素
oldNext.prev = oldPrev //原下一個元素的前一個元素變爲原前一個元素
}
del.next = null //置空指針,防止內存泄露
del.prev = null
}
}
上面就是鏈表的最基本操作。
這裏需要注意的是**需要對於邊界值進行特殊處理,否則如果你在null上操作指針肯定會引起FATAL錯誤。**至於其他比如插入xxx之前等等額外方法實現和這些沒有什麼太大的區別。
下面我們來看看GO,關於鏈表的實現。
鏈表的實現—GO源碼篇
go語言的List在list包中。它的基本實現是我們之前的僞代碼一致。
// Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value interface{}
}
// Next returns the next list element or nil.
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
以上是它的結點結構與方法,可以看到它一樣包含了前置、後繼指針,任意類型的Value,和屬於哪一個list的List指針。Next和Prev只是將原本直接訪問做了簡單的封裝,加入了一些判斷邏輯。
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New() *List { return new(List).Init() }
// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
n := at.next
at.next = e
e.prev = at
e.next = n
n.prev = e
e.list = l
l.len++
return e
}
// remove removes e from its list, decrements l.len, and returns e.
func (l *List) remove(e *Element) *Element {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
return e
}
以上是它的List結構以及對應的新增和刪除方法,其餘方法大家可以自行查看源碼。和我們的僞代碼差不多,他的List結構也是包含了根結點與一個鏈表總長度。因爲鏈表只能通過遍歷獲取長度,因此預設一個長度值將會節省很多求長度的操作。然後New()方法是go語言特有的構造函數方式,可以看到初始化的時候,會初始化一個根結點指針都指向自己的節點與長度爲0的鏈表。
這裏發現go語言並沒有進行邊界的特殊處理,這是因爲go初始化鏈表的時候,將頭結點的指針均指向了自己,並且不包含任何數據,也就是說鏈表的第一個結點只是用來做邊界定義。這種鏈表叫做帶頭鏈表。這種定義方式可以簡化我們的處理邏輯。
歡迎大家關注我的個人博客:http://blog.geek-scorpion.com/