<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          棧和隊列

          共 7541字,需瀏覽 16分鐘

           ·

          2021-04-13 16:40

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

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

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


          本篇文章的主要內(nè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ù)組范圍內(nèi),如下:

          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
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野吉衣中文字幕在线 | 91精品人妻少妇无码毛片91麻豆 | heyzo超碰 | 狠狠综合久久 | 操学生妹视频 |