數據結構和算法(Golang實現)(14)常見數據結構-棧和隊列

棧和隊列

1、棧 Stack 和隊列 Queue

咱們平常生活中,都須要將物品排列,或者安排事情的前後順序。更通俗地講,咱們買東西時,人太多的狀況下,咱們要排隊,排隊也有前後順序,有些人早了點來,排完隊就離開了,有些人晚一點,纔剛剛進去人羣排隊。算法

數據是有順序的,從數據1到數據2,再到數據3,和平常生活同樣,咱們須要放數據,也須要排列數據。segmentfault

在計算機的世界裏,會常常聽見兩種結構,棧(stack)隊列 (queue)。它們是一種收集數據的有序集合(Collection),只不過刪除和訪問數據的順序不一樣。數組

  1. 棧:先進後出,先進隊的數據最後纔出來。在英文的意思裏,stack能夠做爲一疊的意思,這個排列是垂直的,你將一張紙放在另一張紙上面,先放的紙確定是最後纔會被拿走,由於上面有一張紙擋住了它。
  2. 隊列:先進先出,先進隊的數據先出來。在英文的意思裏,queue和現實世界的排隊意思同樣,這個排列是水平的,先排先得。

咱們能夠用數據結構:鏈表(可連續或不連續的將數據與數據關聯起來的結構),或數組(連續的內存空間,按索引取值) 來實現棧(stack)隊列 (queue)安全

數組實現:能快速隨機訪問存儲的元素,經過下標index訪問,支持隨機訪問,查詢速度快,但存在元素在數組空間中大量移動的操做,增刪效率低。數據結構

鏈表實現:只支持順序訪問,在某些遍歷操做中查詢速度慢,但增刪元素快。併發

2、實現數組棧 ArrayStack

數組形式的下壓棧,後進先出:app

主要使用可變長數組來實現。數據結構和算法

// 數組棧,後進先出
type ArrayStack struct {
    array []string   // 底層切片
    size  int        // 棧的元素數量
    lock  sync.Mutex // 爲了併發安全使用的鎖
}

咱們來分析它的各操做。函數

2.1.入棧

// 入棧
func (stack *ArrayStack) Push(v string) {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 放入切片中,後進的元素放在數組最後面
    stack.array = append(stack.array, v)

    // 棧中元素數量+1
    stack.size = stack.size + 1
}

將元素入棧,會先加鎖實現併發安全。性能

入棧時直接把元素放在數組的最後面,而後元素數量加 1。性能損耗主要花在切片追加元素上,切片若是容量不夠會自動擴容,底層損耗的複雜度咱們這裏不計,因此時間複雜度爲O(1)

2.2.出棧

func (stack *ArrayStack) Pop() string {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 棧中元素已空
    if stack.size == 0 {
        panic("empty")
    }

    // 棧頂元素
    v := stack.array[stack.size-1]

    // 切片收縮,但可能佔用空間愈來愈大
    //stack.array = stack.array[0 : stack.size-1]

    // 建立新的數組,空間佔用不會愈來愈大,但可能移動元素次數過多
    newArray := make([]string, stack.size-1, stack.size-1)
    for i := 0; i < stack.size-1; i++ {
        newArray[i] = stack.array[i]
    }
    stack.array = newArray

    // 棧中元素數量-1
    stack.size = stack.size - 1
    return v
}

元素出棧,會先加鎖實現併發安全。

若是棧大小爲0,那麼不容許出棧,不然從數組的最後面拿出元素。

元素取出後:

  1. 若是切片偏移量向前移動stack.array[0 : stack.size-1],代表最後的元素已經不屬於該數組了,數組變相的縮容了。此時,切片被縮容的部分並不會被回收,仍然佔用着空間,因此空間複雜度較高,但操做的時間複雜度爲:O(1)
  2. 若是咱們建立新的數組newArray,而後把老數組的元素複製到新數組,就不會佔用多餘的空間,但移動次數過多,時間複雜度爲:O(n)

最後元素數量減一,並返回值。

2.3.獲取棧頂元素

// 獲取棧頂元素
func (stack *ArrayStack) Peek() string {
    // 棧中元素已空
    if stack.size == 0 {
        panic("empty")
    }

    // 棧頂元素值
    v := stack.array[stack.size-1]
    return v
}

獲取棧頂元素,但不出棧。和出棧同樣,時間複雜度爲:O(1)

2.4.獲取棧大小和斷定是否爲空

// 棧大小
func (stack *ArrayStack) Size() int {
    return stack.size
}

// 棧是否爲空
func (stack *ArrayStack) IsEmpty() bool {
    return stack.size == 0
}

一目瞭然,時間複雜度都是:O(1)

2.5.示例

func main() {
    arrayStack := new(ArrayStack)
    arrayStack.Push("cat")
    arrayStack.Push("dog")
    arrayStack.Push("hen")
    fmt.Println("size:", arrayStack.Size())
    fmt.Println("pop:", arrayStack.Pop())
    fmt.Println("pop:", arrayStack.Pop())
    fmt.Println("size:", arrayStack.Size())
    arrayStack.Push("drag")
    fmt.Println("pop:", arrayStack.Pop())
}

輸出:

size: 3
pop: hen
pop: dog
size: 1
pop: drag

3、實現鏈表棧 LinkStack

鏈表形式的下壓棧,後進先出:

// 鏈表棧,後進先出
type LinkStack struct {
    root *LinkNode  // 鏈表起點
    size int        // 棧的元素數量
    lock sync.Mutex // 爲了併發安全使用的鎖
}

// 鏈表節點
type LinkNode struct {
    Next  *LinkNode
    Value string
}

咱們來分析它的各操做。

3.1.入棧

// 入棧
func (stack *LinkStack) Push(v string) {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 若是棧頂爲空,那麼增長節點
    if stack.root == nil {
        stack.root = new(LinkNode)
        stack.root.Value = v
    } else {
        // 不然新元素插入鏈表的頭部
        // 原來的鏈表
        preNode := stack.root

        // 新節點
        newNode := new(LinkNode)
        newNode.Value = v

        // 原來的鏈表連接到新元素後面
        newNode.Next = preNode

        // 將新節點放在頭部
        stack.root = newNode
    }

    // 棧中元素數量+1
    stack.size = stack.size + 1
}

將元素入棧,會先加鎖實現併發安全。

若是棧裏面的底層鏈表爲空,代表沒有元素,那麼新建節點並設置爲鏈表起點:stack.root = new(LinkNode)

不然取出老的節點:preNode := stack.root,新建節點:newNode := new(LinkNode),而後將原來的老節點連接在新節點後面:newNode.Next = preNode,最後將新節點設置爲鏈表起點stack.root = newNode

時間複雜度爲:O(1)

3.2.出棧

// 出棧
func (stack *LinkStack) Pop() string {
    stack.lock.Lock()
    defer stack.lock.Unlock()

    // 棧中元素已空
    if stack.size == 0 {
        panic("empty")
    }

    // 頂部元素要出棧
    topNode := stack.root
    v := topNode.Value

    // 將頂部元素的後繼連接鏈上
    stack.root = topNode.Next

    // 棧中元素數量-1
    stack.size = stack.size - 1

    return v
}

元素出棧。若是棧大小爲0,那麼不容許出棧。

直接將鏈表的第一個節點topNode := stack.root的值取出,而後將表頭設置爲鏈表的下一個節點:stack.root = topNode.Next,至關於移除了鏈表的第一個節點。

時間複雜度爲:O(1)

3.3.獲取棧頂元素

// 獲取棧頂元素
func (stack *LinkStack) Peek() string {
    // 棧中元素已空
    if stack.size == 0 {
        panic("empty")
    }

    // 頂部元素值
    v := stack.root.Value
    return v
}

獲取棧頂元素,但不出棧。和出棧同樣,時間複雜度爲:O(1)

3.4.獲取棧大小和斷定是否爲空

// 棧大小
func (stack *LinkStack) Size() int {
    return stack.size
}

// 棧是否爲空
func (stack *LinkStack) IsEmpty() bool {
    return stack.size == 0
}

3.5.示例

func main() {
    linkStack := new(LinkStack)
    linkStack.Push("cat")
    linkStack.Push("dog")
    linkStack.Push("hen")
    fmt.Println("size:", linkStack.Size())
    fmt.Println("pop:", linkStack.Pop())
    fmt.Println("pop:", linkStack.Pop())
    fmt.Println("size:", linkStack.Size())
    linkStack.Push("drag")
    fmt.Println("pop:", linkStack.Pop())
}

輸出:

size: 3
pop: hen
pop: dog
size: 1
pop: drag

4、實現數組隊列 ArrayQueue

隊列先進先出,和棧操做順序相反,咱們這裏只實現入隊,和出隊操做,其餘操做和棧同樣。

// 數組隊列,先進先出
type ArrayQueue struct {
    array []string   // 底層切片
    size  int        // 隊列的元素數量
    lock  sync.Mutex // 爲了併發安全使用的鎖
}

4.1.入隊

// 入隊
func (queue *ArrayQueue) Add(v string) {
    queue.lock.Lock()
    defer queue.lock.Unlock()

    // 放入切片中,後進的元素放在數組最後面
    queue.array = append(queue.array, v)

    // 隊中元素數量+1
    queue.size = queue.size + 1
}

直接將元素放在數組最後面便可,和棧同樣,時間複雜度爲:O(n)

4.2.出隊

// 出隊
func (queue *ArrayQueue) Remove() string {
    queue.lock.Lock()
    defer queue.lock.Unlock()

    // 隊中元素已空
    if queue.size == 0 {
        panic("empty")
    }

    // 隊列最前面元素
    v := queue.array[0]

    /*    直接原位移動,但縮容後繼的空間不會被釋放
        for i := 1; i < queue.size; i++ {
            // 從第一位開始進行數據移動
            queue.array[i-1] = queue.array[i]
        }
        // 原數組縮容
        queue.array = queue.array[0 : queue.size-1]
    */

    // 建立新的數組,移動次數過多
    newArray := make([]string, queue.size-1, queue.size-1)
    for i := 1; i < queue.size; i++ {
        // 從老數組的第一位開始進行數據移動
        newArray[i-1] = queue.array[i]
    }
    queue.array = newArray

    // 隊中元素數量-1
    queue.size = queue.size - 1
    return v
}

出隊,把數組的第一個元素的值返回,並對數據進行空間挪位,挪位有兩種:

  1. 原地挪位,依次補位queue.array[i-1] = queue.array[i],而後數組縮容:queue.array = queue.array[0 : queue.size-1],可是這樣切片縮容的那部份內存空間不會釋放。
  2. 建立新的數組,將老數組中除第一個元素之外的元素移動到新數組。

時間複雜度是:O(n)

5、實現鏈表隊列 LinkQueue

隊列先進先出,和棧操做順序相反,咱們這裏只實現入隊,和出隊操做,其餘操做和棧同樣。

// 鏈表隊列,先進先出
type LinkQueue struct {
    root *LinkNode  // 鏈表起點
    size int        // 隊列的元素數量
    lock sync.Mutex // 爲了併發安全使用的鎖
}

// 鏈表節點
type LinkNode struct {
    Next  *LinkNode
    Value string
}

5.1.入隊

// 入隊
func (queue *LinkQueue) Add(v string) {
    queue.lock.Lock()
    defer queue.lock.Unlock()

    // 若是棧頂爲空,那麼增長節點
    if queue.root == nil {
        queue.root = new(LinkNode)
        queue.root.Value = v
    } else {
        // 不然新元素插入鏈表的末尾
        // 新節點
        newNode := new(LinkNode)
        newNode.Value = v

        // 一直遍歷到鏈表尾部
        nowNode := queue.root
        for nowNode.Next != nil {
            nowNode = nowNode.Next
        }

        // 新節點放在鏈表尾部
        nowNode.Next = newNode
    }

    // 隊中元素數量+1
    queue.size = queue.size + 1
}

將元素放在鏈表的末尾,因此須要遍歷鏈表,時間複雜度爲:O(n)

5.2.出隊

// 出隊
func (queue *LinkQueue) Remove() string {
    queue.lock.Lock()
    defer queue.lock.Unlock()

    // 隊中元素已空
    if queue.size == 0 {
        panic("empty")
    }

    // 頂部元素要出隊
    topNode := queue.root
    v := topNode.Value

    // 將頂部元素的後繼連接鏈上
    queue.root = topNode.Next

    // 隊中元素數量-1
    queue.size = queue.size - 1

    return v
}

鏈表第一個節點出隊便可,時間複雜度爲:O(1)

系列文章入口

我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook

相關文章
相關標籤/搜索