咱們又常常聽到列表 List
數據結構,其實這只是更宏觀的統稱,表示存放數據的隊列。node
列表List
:存放數據,數據按順序排列,能夠依次入隊和出隊,有序號關係,能夠取出某序號的數據。先進先出的隊列 (queue)
和先進後出的棧(stack)
都是列表。你們也常常據說一種叫線性表
的數據結構,表示具備相同特性的數據元素的有限序列,實際上就是列表
的同義詞。
咱們通常寫算法進行數據計算,數據處理,都須要有個地方來存數據,咱們可使用封裝好的數據結構List
:算法
列表的實現有順序表示
或鏈式表示
。數據庫
順序表示:指的是用一組地址連續的存儲單元
依次存儲線性表的數據元素,稱爲線性表的順序存儲結構
。它以物理位置相鄰
來表示線性表中數據元素間的邏輯關係,可隨機存取表中任一元素。順序表示的又叫順序表
,也就是用數組來實現的列表。segmentfault
鏈式表示:指的是用一組任意的存儲單元
存儲線性表中的數據元素,稱爲線性表的鏈式存儲結構
。它的存儲單元能夠是連續的,也能夠是不連續的。在表示數據元素之間的邏輯關係時,除了存儲其自己的信息以外,還需存儲一個指示其直接後繼的信息,也就是用鏈表來實現的列表。數組
咱們在前面已經實現過這兩種表示的數據結構:先進先出的隊列 (queue)
和先進後出的棧(stack)
。接下來咱們會來實現鏈表形式的雙端列表,也叫雙端隊列,這個數據結構應用場景更普遍一點。在實際工程應用上,緩存數據庫Redis
的列表List
基本類型就是用它來實現的。緩存
雙端列表,也能夠叫雙端隊列安全
咱們會用雙向鏈表來實現這個數據結構:數據結構
// 雙端列表,雙端隊列 type DoubleList struct { head *ListNode // 指向鏈表頭部 tail *ListNode // 指向鏈表尾部 len int // 列表長度 lock sync.Mutex // 爲了進行併發安全pop操做 } // 列表節點 type ListNode struct { pre *ListNode // 前驅節點 next *ListNode // 後驅節點 value string // 值 }
設計結構體DoubleList
指向隊列頭部head
和尾部tail
的指針字段,方便找到鏈表最前和最後的節點,而且鏈表節點之間是雙向連接的,鏈表的第一個元素的前驅節點爲nil
,最後一個元素的後驅節點也爲nil
。如圖:併發
咱們實現的雙端列表和Golang
標準庫container/list
中實現的不同,感興趣的能夠閱讀標準庫的實現。app
// 獲取節點值 func (node *ListNode) GetValue() string { return node.value } // 獲取節點前驅節點 func (node *ListNode) GetPre() *ListNode { return node.pre } // 獲取節點後驅節點 func (node *ListNode) GetNext() *ListNode { return node.next } // 是否存在後驅節點 func (node *ListNode) HashNext() bool { return node.pre != nil } // 是否存在前驅節點 func (node *ListNode) HashPre() bool { return node.next != nil } // 是否爲空節點 func (node *ListNode) IsNil() bool { return node == nil }
以上是對節點結構體ListNode
的操做,主要判斷節點是否爲空,有沒有後驅和前驅節點,返回值等,時間複雜度都是O(1)
。
// 添加節點到鏈表頭部的第N個元素以前,N=0表示新節點成爲新的頭部 func (list *DoubleList) AddNodeFromHead(n int, v string) { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過列表長度,必定找不到,panic if n > list.len { panic("index out") } // 先找出頭部 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } // 新節點 newNode := new(ListNode) newNode.value = v // 若是定位到的節點爲空,表示列表爲空,將新節點設置爲新頭部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的節點,它的前驅 pre := node.pre // 若是定位到的節點前驅爲nil,那麼定位到的節點爲鏈表頭部,須要換頭部 if pre.IsNil() { // 將新節點連接在老頭部以前 newNode.next = node node.pre = newNode // 新節點成爲頭部 list.head = newNode } else { // 將新節點插入到定位到的節點以前 // 定位到的節點的前驅節點 pre 如今連接到新節點上 pre.next = newNode newNode.pre = pre // 定位到的節點的後驅節點 node.next 如今連接到新節點上 node.next.pre = newNode newNode.next = node.next } } // 列表長度+1 list.len = list.len + 1 }
首先加鎖實現併發安全。而後判斷索引是否超出列表長度:
// 索引超過列表長度,必定找不到,panic if n > list.len { panic("index out") }
若是n=0
表示新節點想成爲新的鏈表頭部,n=1
表示插入到鏈表頭部數起第二個節點以前,新節點成爲第二個節點,以此類推。
首先,找出頭部:node := list.head
,而後日後面遍歷,定位到索引指定的節點node
:
// 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next }
接着初始化新節點:newNode := new(ListNode)
。
定位到的節點有三種狀況,咱們須要在該節點以前插入新節點:
判判定位到的節點node
是否爲空,若是爲空,代表列表沒有元素,將新節點設置爲新頭部和新尾部。
不然找到定位到的節點的前驅節點:pre := node.pre
。
若是前驅節點爲空:pre.IsNil()
,代表定位到的節點node
爲頭部,那麼新節點要取代它,成爲新的頭部:
if pre.IsNil() { // 將新節點連接在老頭部以前 newNode.next = node node.pre = newNode // 新節點成爲頭部 list.head = newNode }
新節點成爲新的頭部,須要將新節點的後驅設置爲老頭部:newNode.next = node
,老頭部的前驅爲新頭部:node.pre = newNode
,而且新頭部變化:list.head = newNode
。
若是定位到的節點的前驅節點不爲空,代表定位到的節點node
不是頭部節點,那麼咱們只需將新節點連接到節點node
以前便可:
// 定位到的節點的前驅節點 pre 如今連接到新節點前 pre.next = newNode newNode.pre = pre // 定位到的節點連接到新節點以後 newNode.next = node node.pre = newNode
先將定位到的節點的前驅節點和新節點綁定,由於如今新節點插在前面了,把定位節點的前驅節點的後驅設置爲新節點:pre.next = newNode
,新節點的前驅設置爲定位節點的前驅節點:newNode.pre = pre
。
同時,定位到的節點如今要連接到新節點以後,因此新節點的後驅設置爲:newNode.next = node
,定位到的節點的前驅設置爲:node.pre = newNode
。
最後,鏈表長度加一。
大部分時間花在遍歷位置上,若是n=0
,那麼時間複雜度爲O(1)
,不然爲O(n)
。
// 添加節點到鏈表尾部的第N個元素以後,N=0表示新節點成爲新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過列表長度,必定找不到,panic if n > list.len { panic("index out") } // 先找出尾部 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 新節點 newNode := new(ListNode) newNode.value = v // 若是定位到的節點爲空,表示列表爲空,將新節點設置爲新頭部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的節點,它的後驅 next := node.next // 若是定位到的節點後驅爲nil,那麼定位到的節點爲鏈表尾部,須要換尾部 if next.IsNil() { // 將新節點連接在老尾部以後 node.next = newNode newNode.pre = node // 新節點成爲尾部 list.tail = newNode } else { // 將新節點插入到定位到的節點以後 // 新節點連接到定位到的節點以後 newNode.pre = node node.next = newNode // 定位到的節點的後驅節點連接在新節點以後 newNode.next = next next.pre = newNode } } // 列表長度+1 list.len = list.len + 1 }
操做和頭部插入節點類似,自行分析。
// 從頭部開始日後找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromHead(n int) *ListNode { // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取頭部節點 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } return node }
若是索引超出或等於列表長度,那麼找不到節點,返回空。
不然從頭部開始遍歷,拿到節點。
時間複雜度爲:O(n)
。
// 從尾部開始往前找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromTail(n int) *ListNode { // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取尾部節點 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } return node }
操做和從頭部獲取節點同樣,請自行分析。
// 從頭部開始日後找,獲取第N+1個位置的節點,並移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取頭部 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } // 移除的節點的前驅和後驅 pre := node.pre next := node.next // 若是前驅和後驅都爲nil,那麼移除的節點爲鏈表惟一節點 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是頭部節點,那麼下一個節點成爲頭節點 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部節點,那麼上一個節點成爲尾節點 list.tail = pre pre.next = nil } else { // 移除的是中間節點 pre.next = next next.pre = pre } // 節點減一 list.len = list.len - 1 return node }
首先加併發鎖實現併發安全。先判斷索引是否超出列表長度:n >= list.len
,若是超出直接返回空指針。
獲取頭部,而後遍歷定位到第N+1
個位置的元素:node = node.next
。
定位到的並要移除的節點有三種狀況發生:
查看要移除的節點的前驅和後驅:
// 移除的節點的前驅和後驅 pre := node.pre next := node.next
若是前驅和後驅都爲空:pre.IsNil() && next.IsNil()
,那麼要移除的節點是鏈表中惟一的節點,直接將列表頭部和尾部置空便可。
若是前驅節點爲空:pre.IsNil()
,表示移除的是頭部節點,那麼頭部節點的下一個節點要成爲新的頭部:list.head = next
,而且這時新的頭部前驅要設置爲空:next.pre = nil
。
同理,若是後驅節點爲空:next.IsNil()
,表示移除的是尾部節點,須要將尾部節點的前一個節點設置爲新的尾部:list.tail = pre
,而且這時新的尾部後驅要設置爲空:pre.next = nil
。
若是移除的節點處於兩個節點之間,那麼將這兩個節點連接起來便可:
// 移除的是中間節點 pre.next = next next.pre = pre
最後,列表長度減一。
主要的耗時用在定位節點上,其餘的操做都是鏈表連接,能夠知道時間複雜度爲:O(n)
。
// 從尾部開始往前找,獲取第N+1個位置的節點,並移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取尾部 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 移除的節點的前驅和後驅 pre := node.pre next := node.next // 若是前驅和後驅都爲nil,那麼移除的節點爲鏈表惟一節點 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是頭部節點,那麼下一個節點成爲頭節點 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部節點,那麼上一個節點成爲尾節點 list.tail = pre pre.next = nil } else { // 移除的是中間節點 pre.next = next next.pre = pre } // 節點減一 list.len = list.len - 1 return node }
操做和從頭部移除節點類似,請自行分析。
package main import ( "fmt" "sync" ) // 雙端列表,雙端隊列 type DoubleList struct { head *ListNode // 指向鏈表頭部 tail *ListNode // 指向鏈表尾部 len int // 列表長度 lock sync.Mutex // 爲了進行併發安全pop操做 } // 列表節點 type ListNode struct { pre *ListNode // 前驅節點 next *ListNode // 後驅節點 value string // 值 } // 獲取節點值 func (node *ListNode) GetValue() string { return node.value } // 獲取節點前驅節點 func (node *ListNode) GetPre() *ListNode { return node.pre } // 獲取節點後驅節點 func (node *ListNode) GetNext() *ListNode { return node.next } // 是否存在後驅節點 func (node *ListNode) HashNext() bool { return node.pre != nil } // 是否存在前驅節點 func (node *ListNode) HashPre() bool { return node.next != nil } // 是否爲空節點 func (node *ListNode) IsNil() bool { return node == nil } // 返回列表長度 func (list *DoubleList) Len() int { return list.len } // 添加節點到鏈表頭部的第N個元素以前,N=0表示新節點成爲新的頭部 func (list *DoubleList) AddNodeFromHead(n int, v string) { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過列表長度,必定找不到,panic if n > list.len { panic("index out") } // 先找出頭部 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } // 新節點 newNode := new(ListNode) newNode.value = v // 若是定位到的節點爲空,表示列表爲空,將新節點設置爲新頭部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的節點,它的前驅 pre := node.pre // 若是定位到的節點前驅爲nil,那麼定位到的節點爲鏈表頭部,須要換頭部 if pre.IsNil() { // 將新節點連接在老頭部以前 newNode.next = node node.pre = newNode // 新節點成爲頭部 list.head = newNode } else { // 將新節點插入到定位到的節點以前 // 定位到的節點的前驅節點 pre 如今連接到新節點前 pre.next = newNode newNode.pre = pre // 定位到的節點連接到新節點以後 newNode.next = node node.pre = newNode } } // 列表長度+1 list.len = list.len + 1 } // 添加節點到鏈表尾部的第N個元素以後,N=0表示新節點成爲新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過列表長度,必定找不到,panic if n > list.len { panic("index out") } // 先找出尾部 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 新節點 newNode := new(ListNode) newNode.value = v // 若是定位到的節點爲空,表示列表爲空,將新節點設置爲新頭部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的節點,它的後驅 next := node.next // 若是定位到的節點後驅爲nil,那麼定位到的節點爲鏈表尾部,須要換尾部 if next.IsNil() { // 將新節點連接在老尾部以後 node.next = newNode newNode.pre = node // 新節點成爲尾部 list.tail = newNode } else { // 將新節點插入到定位到的節點以後 // 新節點連接到定位到的節點以後 newNode.pre = node node.next = newNode // 定位到的節點的後驅節點連接在新節點以後 newNode.next = next next.pre = newNode } } // 列表長度+1 list.len = list.len + 1 } // 返回列表鏈表頭結點 func (list *DoubleList) First() *ListNode { return list.head } // 返回列表鏈表尾結點 func (list *DoubleList) Last() *ListNode { return list.tail } // 從頭部開始日後找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromHead(n int) *ListNode { // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取頭部節點 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } return node } // 從尾部開始往前找,獲取第N+1個位置的節點,索引從0開始。 func (list *DoubleList) IndexFromTail(n int) *ListNode { // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取尾部節點 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } return node } // 從頭部開始日後找,獲取第N+1個位置的節點,並移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取頭部 node := list.head // 日後遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.next } // 移除的節點的前驅和後驅 pre := node.pre next := node.next // 若是前驅和後驅都爲nil,那麼移除的節點爲鏈表惟一節點 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是頭部節點,那麼下一個節點成爲頭節點 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部節點,那麼上一個節點成爲尾節點 list.tail = pre pre.next = nil } else { // 移除的是中間節點 pre.next = next next.pre = pre } // 節點減一 list.len = list.len - 1 return node } // 從尾部開始往前找,獲取第N+1個位置的節點,並移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode { // 加併發鎖 list.lock.Lock() defer list.lock.Unlock() // 索引超過或等於列表長度,必定找不到,返回空指針 if n >= list.len { return nil } // 獲取尾部 node := list.tail // 往前遍歷拿到第 N+1 個位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 移除的節點的前驅和後驅 pre := node.pre next := node.next // 若是前驅和後驅都爲nil,那麼移除的節點爲鏈表惟一節點 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是頭部節點,那麼下一個節點成爲頭節點 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部節點,那麼上一個節點成爲尾節點 list.tail = pre pre.next = nil } else { // 移除的是中間節點 pre.next = next next.pre = pre } // 節點減一 list.len = list.len - 1 return node } func main() { list := new(DoubleList) // 在列表頭部插入新元素 list.AddNodeFromHead(0, "I") list.AddNodeFromHead(0, "love") list.AddNodeFromHead(0, "you") // 在列表尾部插入新元素 list.AddNodeFromTail(0, "may") list.AddNodeFromTail(0, "happy") // 正常遍歷,比較慢 for i := 0; i < list.Len(); i++ { // 從頭部開始索引 node := list.IndexFromHead(i) // 節點爲空不可能,由於list.Len()使得索引不會越界 if !node.IsNil() { fmt.Println(node.GetValue()) } } fmt.Println("----------") // 正常遍歷,特別快 // 先取出第一個元素 first := list.First() for !first.IsNil() { // 若是非空就一直遍歷 fmt.Println(first.GetValue()) // 接着下一個節點 first = first.GetNext() } fmt.Println("----------") // 元素一個個 POP 出來 for { node := list.PopFromHead(0) if node.IsNil() { // 沒有元素了,直接返回 break } fmt.Println(node.GetValue()) } fmt.Println("----------") fmt.Println("len", list.Len()) }
輸出:
you love I may happy ---------- you love I may happy ---------- you love I may happy ---------- len 0
首先,先從列表頭部插入三個新元素,而後從尾部插入兩個新元素,而後用三種方式進行遍歷,兩種只是查看元素,一種是遍歷移除元素。
我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook。