1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        棧和隊列

        共 7541字,需瀏覽 16分鐘

         ·

        2021-04-13 16:40

        本篇文章是視頻課程老湯 83 小時精講:數(shù)據(jù)結構與算法【動畫+手寫代碼】》中的《基礎篇二:線性結構及其算法》棧和隊列相關的文字總結

        注意:文字講解少了很多動態(tài)演示,也少了不少的代碼演示,如果文章看得不是很明白的話,請看視頻講解

        視頻課程可以通過長按識別下面的二維碼找到:


        本篇文章的主要內容包括:
        1. 棧(Stack)
          1. 棧的應用一:方法的系統(tǒng)調用棧
          2. 棧的應用二:前進后退功能
          3. 棧的代碼實現(xiàn):分別使用數(shù)組和鏈表實現(xiàn)
        2. 隊列(Queue)
          1. 使用數(shù)組實現(xiàn)隊列
          2. 使用單向鏈表實現(xiàn)隊列
          3. 優(yōu)化單向鏈表實現(xiàn)的隊列
          4. 優(yōu)化數(shù)組實現(xiàn)的隊列:循環(huán)隊列

        前面我們講解了兩種最基本的線性表結構:數(shù)組和鏈表,從這節(jié)課開始,我們再講解兩個使用比較廣泛的線性表結構,那就是棧和隊列。

        棧(Stack)

        我們知道不管是數(shù)組還是鏈表都允許從兩端操作數(shù)據(jù),比如都支持 addFirst、getFirst、addLast 以及 getLast 操作,如下圖:


        但是,我們今天要講的棧就是操作受限的數(shù)據(jù)結構,對于一種線性表結構,如果我們只能對它的一端進行操作的話,那么這種數(shù)據(jù)結構就是棧了,如下圖:


        由于棧只能操作一端的數(shù)據(jù),這就使得棧有一個最基本的特點:后進先出 (Last In First Out,簡稱 LIFO)。如下圖:


        可以看出:
        • 元素進棧的順序是:34、21、66
        • 元素出棧的順序是:66、21、34
        也就是說,后面進來的元素,優(yōu)先出棧。這就是棧的特點:后進先出

        對于棧,還有兩個最基本的概念名詞:
        1. 棧頂:可以操作數(shù)據(jù)的一端稱為棧頂
        2. 棧底:不可以操作數(shù)據(jù)的一端稱為棧底

        你可能會問,這種操作受限的線性表數(shù)據(jù)結構到底有什么用呢?我們接下來舉兩個棧的使用場景的例子。

        棧的應用一:方法的系統(tǒng)調用棧

        在計算機的編程語言中,方法的調用是通過系統(tǒng)調用棧來實現(xiàn)的,我們看如下的代碼:
        public static void main(String[] args) {     a(10);}
        public static int a(int x) { int y = b(x, 1); return y;}
        public static int b(int x, int y) { int sum = x + y; c(sum); return sum;}
        public static void c(int x) { System.out.println(x);}
        以上 4 個方法的調用順序是:main 方法 -> a 方法 -> b 方法 -> c 方法

        在這里,被調用的方法執(zhí)行結束的時候需要保證能正確回到調用方,比如:c 方法執(zhí)行結束需要回到 b 方法,b 方法執(zhí)行結束需要回到 a 方法,a 方法執(zhí)行結束需要回到 main 方法,這個方法剛好和調用順序相反,即 c 方法 -> b 方法 -> a 方法 -> main 方法

        上面的方法調用實際上和棧的進棧出棧的操作是非常吻合的,符合后進先出的特點,所以我們可以使用棧來完成方法的調用

        實際上,在方法調用前,JVM 會分配一個系統(tǒng)調用棧,方法的調用的順序就是方法進棧的順序,方法調用結束回到調用方的順序就是出棧的順序了。


        棧的應用二:前進后退功能

        對于編寫代碼的工具上的前進后退功能你肯定用過,比如 IDEA 上的前進后退功能:


        我們在寫代碼的時候,會在 IDEA 中留下很多的操作痕跡,比如你在 IDEA 中打開了一個文件,然后依次做了一系列的操作:操作1 -> 操作2 -> 操作3 -> 操作4

        這個時候,如果你想回到 操作2,那么返回的順序就是:操作4 -> 操作3 -> 操作2 。這個返回的順序和之前操作的順序是相反的,也符合后進先出的特點,所以我們可以使用棧來實現(xiàn):

        1. 首先使用棧來記錄每一個操作,用戶每操作過一次就將操作壓入棧中
        2. 等用戶想返回的時候,只需要將操作出棧即可

        到現(xiàn)在為止,我們實現(xiàn)了后退功能,還有一個前進功能呢?你會發(fā)現(xiàn),前進功能要做的事情和后退功能一樣,只是順序剛好相反而已。我們可以使用第二個棧來實現(xiàn)前進功能。

        用戶的操作都壓入到第一個棧中:

        • 當用戶需要后退的時候,將第一個棧中的棧頂操作出棧,然后將出棧的操作再次壓入到第二個棧中
        • 當用戶需要前進的時候,將第二個棧中的棧頂操作出棧,然后將出棧的操作再次壓入到第一個棧中

        這樣,我們就能完成后退前進功能。

        棧的實現(xiàn)

        棧最主要的兩個操作是:
        1. push 操作:向棧中壓入元素

        2. pop 操作:從棧中彈出棧頂元素


        棧是一個操作受限的線性結構,也就是我們只能對一端進行操作,所以我們可以基于數(shù)組和鏈表分別來實現(xiàn)棧

        使用數(shù)組來實現(xiàn)棧的時候,我們只對數(shù)組的末端進行操作:

        1. push 操作:向數(shù)組的末尾追加元素,這個操作的時間復雜度是 O(1)

        2. pop 操作:將數(shù)組末尾的元素刪除掉,注意,這里并不要真的刪除末端元素,而是直接將 size-- 即可,這樣可以實現(xiàn)常量級別的時間復雜度


        使用單向鏈表來實現(xiàn)棧的時候,我們只對鏈表的表頭進行操作:


        1. push 操作:向單向鏈表表頭插入元素,這個操作是常量級別的時間復雜度

        2. pop 操作:將單向鏈表的表頭刪除,這個操作也是常量級別的時間復雜度


        可以看出,不管是使用數(shù)組還是單向鏈表實現(xiàn)棧,push 和 pop 的時間復雜度都是 O(1)


        注意:代碼實現(xiàn)請見視頻講解


        隊列 (Queue)


        和棧一樣,隊列是操作受限的線性數(shù)據(jù)結構,但是和棧不一樣的是,隊列是一種先進先出的數(shù)據(jù)結構。

        我們只能:
        • 從一端將數(shù)據(jù)添加到隊列中,這個過程我們稱為入隊 (enqueue)
        • 從另一端將數(shù)據(jù)從隊列中刪除,這個過程我們稱為出隊 (dequeue)


        一般而言,入隊的一端我們稱為隊尾,出隊的一端我們稱為隊首


        和棧一樣,隊列可以使用數(shù)組、也可以使用鏈表來實現(xiàn)。


        一、使用數(shù)組實現(xiàn)隊列

        使用數(shù)組來實現(xiàn)隊列,我們需要先確定兩個問題:

        1. 到底使用靜態(tài)數(shù)組還是動態(tài)數(shù)組,這要看實現(xiàn)的隊列是固定容量的還是動態(tài)容量的,我們這里先實現(xiàn)一個動態(tài)容量的隊列,所以使用動態(tài)數(shù)組來實現(xiàn)
        2. 到底使用數(shù)組的哪一端作為隊首,哪一端作為隊尾,我們看下面的分析

        如果選數(shù)組的尾作為隊首的話,那么:

        • 入隊的操作就是 addFirst,它的時間復雜度是 O(n)
        • 出隊的操作就是 removeLast,它的時間復雜度是 O(1)


        如果選數(shù)組的頭作為隊首的話,那么:
        • 入隊的操作就是 addLast,它的時間復雜度是 O(1)
        • 出隊的操作就是 removeFirst,它的時間復雜度是 O(n)


        所以說。不管選哪個端作為隊首,時間復雜度都是一樣的,都會有一個操作是 O(n),一個是 O(1),所以選哪一端作為隊首都一樣


        注意:代碼實現(xiàn)請見視頻講解


        二、使用單向鏈表實現(xiàn)隊列


        使用單向鏈表實現(xiàn)隊列的話需要確定使用鏈表的哪一頭作為隊首,我們分別來討論下

        如果選單向鏈表的尾作為隊首的話,那么:
        • 入隊的操作就是 addFirst,它的時間復雜度是 O(1)
        • 出隊的操作就是 removeLast,它的時間復雜度是 O(n)


        如果選鏈表的頭作為隊首的話,那么:
        • 入隊的操作就是 addLast,它的時間復雜度是 O(n)
        • 出隊的操作就是 removeFirst,它的時間復雜度是 O(1)


        所以說。不管選哪個端作為隊首,時間復雜度都是一樣的,都會有一個操作是 O(n),一個是 O(1),所以選哪一端作為隊首都一樣

        注意:代碼實現(xiàn)請見視頻講解


        三、優(yōu)化單向鏈表實現(xiàn)的隊列


        我們使用單向鏈表實現(xiàn)了隊列,不管是以鏈表的哪一端作為隊首,隊列中的出隊和入隊兩個操作肯定會有一個操作的時間復雜度是 O(n),我們能不能將這個時間復雜度降為 O(1) 呢?


        我們要優(yōu)化時間復雜度,先要找到導致時間復雜度高的原因,我們先看看下面的兩張圖:




        從圖中就可以看出,導致時間復雜度為 O(n) 的原因就是我們對單向鏈表的最后一個節(jié)點的操作的時間復雜度都是 O(n)

        之所以對最后一個節(jié)點的操作的時間復雜度是 O(n),是因為每次操作最后一個節(jié)點,都需要從頭節(jié)點往后遍歷一遍單向鏈表

        所以,我們要降低單向鏈表實現(xiàn)的隊列出隊入隊的時間復雜度,那么就要降低對單向鏈表最后一個節(jié)點操作的時間復雜度。


        我們現(xiàn)在回想下,為什么對單向鏈表的頭不管是刪除還是新增操作的時間復雜度是 O(1) 呢?答案就是我們通過一個變量 head 記住了鏈表頭節(jié)點的位置,所以對頭的操作的時間復雜度可以為常量級別


        借助這個方法,我們也可以使用一個變量 tail 來記住單向鏈表的尾節(jié)點,看看能不能降低對尾節(jié)點的操作時間復雜度呢?


        當我們使用一個變量 tail 來記住尾節(jié)點的位置的時候,對尾節(jié)點操作的時間復雜度確實發(fā)生了變化:

        1. removeLast 操作的時間復雜度還是 O(n),之所以時間復雜度沒有變化,是因為要刪除最后一個節(jié)點,就需要找到最后一個節(jié)點的前一個節(jié)點,然而對于單向鏈表,要找到最后一個節(jié)點的前一個節(jié)點需要從頭節(jié)點開始遍歷找到,所以時間復雜度還是 O(n)
        2. addLast 操作的時間復雜度變成了 O(1),這個是因為在最后一個節(jié)點后面新增一個節(jié)點,只需要知道最后一個節(jié)點的位置即可,然而我們已經(jīng)使用 tail 變量記錄了最后一個節(jié)點的位置了。


        從上圖,我們也可以得出一個結論:要使得單向鏈表實現(xiàn)的隊列的出隊和入隊操作的時間復雜度都是 O(1) 的話,那么必須使用單向鏈表表頭做隊首


        以上,我們是使用單向鏈表 + 表頭表尾兩個指針來實現(xiàn)隊列,而且實現(xiàn)的出隊和入隊的時間復雜度都是 O(1)


        當然,你完全可以使用雙向鏈表來實現(xiàn)隊列,這樣出隊和入隊的時間復雜度也是 O(1)


        但是直接使用雙向鏈表實現(xiàn)的隊列,比使用單向鏈表 + 表頭表尾兩個指針實現(xiàn)的隊列更加的耗費空間,因為雙向鏈表的每個節(jié)點都多了一個前置指針


        注意:代碼實現(xiàn)請見視頻講解


        四、循環(huán)隊列


        前面我們對使用單向鏈表實現(xiàn)的隊列進行了優(yōu)化,使得出隊和入隊的時間復雜度都是 O(1)


        那么,我們能不能對數(shù)組實現(xiàn)的隊列也進行優(yōu)化呢?答案是可以的,那就是接下來要講解的循環(huán)隊列了


        前面使用數(shù)組實現(xiàn)的隊列,不管是以數(shù)組的哪一端作為隊首,實現(xiàn)的隊列的出隊和入隊兩個操作總會有一個操作的時間復雜度是 O(n),我們能不能將這個時間復雜度降為 O(1) 呢?

        現(xiàn)在我們就來對前面的數(shù)組實現(xiàn)的隊列進行優(yōu)化。

        我們要優(yōu)化時間復雜度,先要找到導致時間復雜度高的原因,我們先看看下面的兩張圖:



        從上圖可以看出,用數(shù)組實現(xiàn)的隊列之所以有時間復雜度為 O(n) 的操作,是因為我們對數(shù)組的第一個元素進行操作的時間復雜度都是 O(n)。

        不管是刪除第一個元素還是在第一個元素之前新增元素,都需要移動第一個元素后面所有的元素。

        我們要是能在第一個元素出隊的時候不移動后面的元素的話嗎,那么就可以降低出隊的時間復雜度。

        要解決這個問題,我們可以引入兩個變量,一個變量 head 來表示隊列的頭所在的數(shù)組的位置,另一個變量 tail 表示隊尾下一個可以插入元素的位置,如下:


        當?shù)谝粋€元素出隊了,我們不需要移動剩下的元素,而是直接將 head 往后移動一位,如下:



        當一個元素入隊后,將 tail 往后移動一位即可,如下:


        通過引用兩個變量分別表示隊首和隊尾,從而消除了因為出隊而引起的數(shù)據(jù)移動,所以這個時候的出隊入隊的時間復雜度就是 O(1) 了。

        細心的同學可能會發(fā)現(xiàn)一個問題,就是當 tail 等于數(shù)組的長度,也就是 tail 到了數(shù)組的最末尾的話,那么這個時候如果有元素再想入隊的話,該怎么辦呢?

        這個時候,如果數(shù)組前面還有空余的位置,那么元素就可以入隊到數(shù)組的前面。這樣我們實際上變成了一個循環(huán)數(shù)組了,我們通常叫這種隊列為循環(huán)隊列。

        接下來,我們再解決兩個問題:
        • 什么時候表示隊列為空
        • 什么時候表示隊列已經(jīng)滿了

        當隊列初始化的時候,隊列肯定是空的,這個時候 head 和 tail 兩個變量都指向數(shù)組索引為 0 的位置上,如下圖:


        所以說,我們可以確定的是,當 head == tail 的時候,就表示隊列為空了。

        當隊列中的元素排列成如下的狀態(tài):


        請問,這個時候,我們還能將新元素入隊嗎?

        假設我們現(xiàn)在將一個新元素入隊的話,那么新元素入隊后,head 就等于 tail 了,然而 head == tail 是隊列為空的條件,實際上這個時候隊列是滿的

        也就是說 head == tail 的時候既要表示隊列為空,又要表示隊列滿了這兩種狀態(tài),這個是不可能的事情。

        為了解決這個問題,我們舍棄 head 前面的一個位置不存儲元素,這個時候 tail 指向的就是這個空的元素,那么當隊列達到這種狀態(tài)的時候,表明隊列已經(jīng)滿了。也就是說隊列滿的條件是 tail + 1 == head。


        在實現(xiàn)循環(huán)隊列之前,我們還有一個問題需要解決:head 和 tail 是怎么移動的?每次移動都 +1 嗎?這樣的話,head 和 tail 肯定會超出數(shù)據(jù)界限的,我們可以使用取模的手段,將 head 和 tail 控制在數(shù)組范圍內,如下:

        head = (head + 1) % data.length tail = (tail + 1) % data.length

        注意:代碼實現(xiàn)講解請見視頻講解


        循環(huán)隊列的實現(xiàn)代碼如下:
        package com.douma.line.queue;
        /** * @微信公眾號 : 抖碼課堂 * @官方微信號 : bigdatatang01 * @作者 : 老湯 */public class LoopQueue<E> implements Queue<E> { private E[] data; private int head; private int tail;
        private int size;
        public LoopQueue() { this(20); }
        public LoopQueue(int capacity) { data = (E[])new Object[capacity]; head = tail = 0; size = 0; }
        // 當前隊列的容量 public int getCapacity() { return data.length - 1; }
        @Override public int getSize() { return size; }
        @Override public void enqueue(E e) { if ((tail + 1) % data.length == head) { // 隊列滿了 resize(getCapacity() * 2); } data[tail] = e; size++; tail = (tail + 1) % data.length; }
        @Override public E dequeue() { // O(1) if (isEmpty()) { throw new RuntimeException("dequeue error, No Element for dequeue"); } E res = data[head]; data[head] = null; size--; head = (head + 1) % data.length; if (size == getCapacity() / 4) { resize(getCapacity() / 2); } return res; }
        private void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity + 1]; for (int i = 0; i < size; i++) { newData[i] = data[(i + head) % data.length]; } data = newData; head = 0; tail = size; }
        @Override public E getFront() { return data[head]; }
        @Override public boolean isEmpty() { return head == tail; }
        @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Queue:size = %d, capacity = %d\n", size, getCapacity())); sb.append(" 隊首 ["); for (int i = head; i != tail ; i = (i + 1) % data.length) { sb.append(data[i]); if ((i + 1) % data.length != tail) { sb.append(","); } } sb.append("]"); return sb.toString(); }}

        碼字不易,點贊、看一看兩連即可。

        瀏覽 43
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            久久国产福利国产秒拍 | 口述二个男人躁我一个鲁大师 | 国产一级片网站 | x8x8女性性爽免费视频 | 色就是欧美 | 亚洲欧美综合 | 黄色中文字幕电影 | 肥婆操逼 | 亚洲精品无码国产片不卡 | 97精品伊人久久久大香线蕉97 |