數據結構和算法(Golang實現)(15)常見數據結構-列表

列表

1、列表 List

咱們又常常聽到列表 List數據結構,其實這只是更宏觀的統稱,表示存放數據的隊列。node

列表 List:存放數據,數據按順序排列,能夠依次入隊和出隊,有序號關係,能夠取出某序號的數據。先進先出的 隊列 (queue)和先進後出的 棧(stack)都是列表。你們也常常據說一種叫 線性表的數據結構,表示具備相同特性的數據元素的有限序列,實際上就是 列表的同義詞。

咱們通常寫算法進行數據計算,數據處理,都須要有個地方來存數據,咱們可使用封裝好的數據結構List算法

列表的實現有順序表示鏈式表示數據庫

順序表示:指的是用一組地址連續的存儲單元依次存儲線性表的數據元素,稱爲線性表的順序存儲結構。它以物理位置相鄰來表示線性表中數據元素間的邏輯關係,可隨機存取表中任一元素。順序表示的又叫順序表,也就是用數組來實現的列表。segmentfault

鏈式表示:指的是用一組任意的存儲單元存儲線性表中的數據元素,稱爲線性表的鏈式存儲結構。它的存儲單元能夠是連續的,也能夠是不連續的。在表示數據元素之間的邏輯關係時,除了存儲其自己的信息以外,還需存儲一個指示其直接後繼的信息,也就是用鏈表來實現的列表。數組

咱們在前面已經實現過這兩種表示的數據結構:先進先出的隊列 (queue)和先進後出的棧(stack)。接下來咱們會來實現鏈表形式的雙端列表,也叫雙端隊列,這個數據結構應用場景更普遍一點。在實際工程應用上,緩存數據庫Redis列表List基本類型就是用它來實現的。緩存

2、實現雙端列表

雙端列表,也能夠叫雙端隊列安全

咱們會用雙向鏈表來實現這個數據結構:數據結構

// 雙端列表,雙端隊列
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

2.1.列表節點普通操做

// 獲取節點值
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)

2.2.從頭部開始某個位置前插入新節點

// 添加節點到鏈表頭部的第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)

2.3.從尾部開始某個位置後插入新節點

// 添加節點到鏈表尾部的第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
}

操做和頭部插入節點類似,自行分析。

2.4.從頭部開始某個位置獲取列表節點

// 從頭部開始日後找,獲取第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)

2.5.從尾部開始某個位置獲取列表節點

// 從尾部開始往前找,獲取第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
}

操做和從頭部獲取節點同樣,請自行分析。

2.6.從頭部開始移除並返回某個位置的節點

// 從頭部開始日後找,獲取第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)

2.7.從尾部開始移除並返回某個位置的節點

// 從尾部開始往前找,獲取第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
}

操做和從頭部移除節點類似,請自行分析。

2.8.完整例子

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

相關文章
相關標籤/搜索