1. 程式人生 > >資料結構——30行程式碼實現棧和模擬遞迴

資料結構——30行程式碼實現棧和模擬遞迴

本文始發於個人公眾號:TechFlow,原創不易,求個關注


棧的定義


原本今天想給大家講講快速選擇演算法的,但是發現一連寫了好幾篇排序相關了,所以臨時改了題目,今天聊點資料結構,來看看經典並且簡單的資料結構——棧。

棧這個結構我想大家應該都耳熟能詳,尤其是在很多地方將和堆並列在一起,稱作“堆疊”就更廣為人知了。但其實堆和棧本質上是兩種不同的資料結構,我們不能簡單地混為一談。讓我們先從比較簡單的棧開始。

棧和佇列的本質其實都是陣列(嚴格地說是線性表)。只不過我們在陣列上增加了一些限制,使得它滿足一定的條件而已,所以很多對資料結構畏首畏尾的同學可以放寬心,棧沒什麼特別的花樣,就是一種特殊的陣列。

和其他廣義上的線性表資料結構比起來,棧的特殊性只有兩條,一條是先進後出,另一條是隻能從陣列的一側讀寫。但本質上來說這兩條是一樣的,由於我們只能從一側讀寫元素,所以進的越早出的越晚,當然是先進後出。從下面這張圖應該很容易能看明白。

棧規定了我們只能從一側進行讀寫,常規上我們將能夠讀寫的一側稱作是棧頂。不能讀寫的另一側稱為是棧底。從上面的圖可以看到,只有棧頂的元素出棧了之後,才能訪問到棧底的元素。

我們用Python的陣列來實現棧這個資料結構,去掉註釋真的只有30行不到,可以說是非常簡單,我們先來看程式碼。

class Stack(object):

    def __init__(self, size_limit=None):
        self._size_limit = size_limit
        self.elements = []
        self._size = 0

    # 進棧,判斷是否越界
    def push(self, value):
        if self._size_limit is not None and len(self.elements) > self._size_limit:
            raise IndexError("Stack is full")
        else:
            self.elements.append(value)
            self._size += 1

    # 判斷棧是否為空
    def is_empty(self):
        return self._size == 0

    # 棧清空
    def clear(self):
        self.elements = []
        self._size = 0

    # 訪問元素數量
    def size(self):
        return self._size

    # 查詢棧頂元素
    def top(self):
        return self.elements[-1]
    
    # 彈出棧頂元素
    def pop(self):
        val = self.elements.pop()
        self._size -= 1
        return val

本質上來說,一般的棧實現只有以上這麼幾個方法,可能會更少。因為有些語言當中的棧,top和彈出是合併的。意味著訪問必須要彈出,不支援非彈出訪問。所以棧的實現邏輯是非常簡單的,甚至可以說是毫無技術含量,非常適合入門資料結構。

當然,從另一個方面也可以說棧的實現原理並不太重要,相比之下更重要的是棧一般會用在什麼地方。


棧的應用


棧最廣泛的應用就是在作業系統當中,比如在程式執行呼叫方法的時候,在編譯器內部,其實是記錄了一個當前呼叫的方法棧。舉個例子,比如當前呼叫到的方法是A,如果在A方法中又去呼叫了方法B,那麼計算機就會在系統方法棧當中儲存一個指向B方法的指標,如果B方法又呼叫到了C方法,那麼又會新增一個C的指標。當C方法執行結束,那麼C就會彈出,計算機會將C的結果帶入B,繼續執行之前的B,以此類推,直到棧空為止。

那麼,問題來了,如果一個方法A自己呼叫自己會怎麼樣?

答案是計算機會建立一個新的A的指標填入棧中,如果A繼續遞迴,那麼系統再建立一個新的指標入棧……

從上面這個過程,我們可以確定兩個事情。第一,我們寫程式時候的遞迴,在編譯器內部其實是以棧的形式執行的。第二,如果我們用一個死迴圈去不停地遞迴,由於棧存在大小限制,所以當棧的深度超過限制的時候,就會出現SystemStackExceed的錯誤。也就是說遞歸併不是無限的,因為除了作業系統對於執行記憶體的限制之外,編譯器還會有最大遞迴深度的限制,防止遞迴中死迴圈導致系統崩潰。雖然各個語言實現機制不完全一樣,但是有一點是肯定的,遞迴深度是有限的,我們不能無限制遞迴。

那問題來了,如果我們系統就是會存在大規模的遞迴怎麼辦?難道還要手動給機器加記憶體嗎?

這是ACM玩家在賽場上經常遇到的問題之一,有經驗的選手在第一天的熱身賽時一定會做的事情除了配置vim或者其他IDE之外,就是會測試一下電腦的最大遞迴深度。在C++當中,是支援通過組合語言強行開啟遞迴深度限制的,但是即使如此也是有限的,並且據我所知只有C++可以這麼幹,對於其他語言,以及開大了遞迴深度還是不夠用的情況,就只有一種辦法,就是手動建棧模擬遞迴。


手動遞迴


許多同學可能覺得遞迴痛苦,但是如果他們試著手動建棧來模擬遞迴的話,會發現要更加痛苦。不僅要額外增加變數儲存中間狀態,並且對於程式設計也是一個巨大的挑戰。

我們來看一個例子:

class Node:
    def __init__(self, val):
        self.val = val
        # 左孩子
        self.lchild = None
        # 右孩子
        self.rchild = None


if __name__ == "__main__":
    # 建樹
    root = Node(0)
    node1 = Node(1)
    root.lchild = node1
    node2 = Node(2)
    root.rchild = node2
    node3 = Node(3)
    node1.lchild = node3
    node4 = Node(4)
    node1.rchild = node4
    node5 = Node(5)
    node2.rchild = node5

這是一棵簡單的二叉樹,畫出來是這個樣子:

          0
         / \
        1   2
       / \   \
      3   4   5

下面我們要通過棧在不使用遞迴的情況下來中序遍歷它,中序遍歷我們都知道,就是先遍歷左子樹,然後輸出當前節點,再遍歷右子樹。寫成遞迴非常方便,只有幾行:

def dfs(node):
    if node is None:
        return
    dfs(node.lchild)
    print(node.val)
    dfs(node.rchild)

大家想想,如果不使用遞迴應該怎麼辦?如果你真的試著去寫,就會發現看起來很簡單的問題好像變得非常複雜。我們很容易可以想到,我們把節點儲存在棧當中,但是儲存資料只是表象。本質問題是當我們從棧當中拿到了一個節點之後,我們怎麼判斷它究竟應該做什麼?應該遍歷左節點嗎,應該輸出嗎,還是應該遍歷右節點?

對這些問題仔細分析和思考,我們可以發現它們都和遞迴的回溯有關。

在遞迴當中,當我們遍歷完了當前節點的某棵子樹之後,隨著棧的彈出,還會回到這個節點。比如上面這棵樹當中,在遞迴過程當中,我們會兩次碰到1這個節點。第一次時它不會輸出1,而是先去遍歷了它的左子樹,也就是3,之後再次回到1,由於它的左子樹已經遍歷過,所以會輸出1。這個離開又回來的過程稱為回溯。如果你把樹結構想象成瀑布的話,這個過程有點像是順流而下,又逆流而上,翻譯成回溯還是蠻合理的。

我們回到之前的問題,所有的搞不清楚的本質都來源於我們無法判斷當前遇到的節點究竟是初次見面,還是回溯之後的久別重逢。而這關係到我們要對它做什麼。原本在遞迴當中,由於程式會記錄遞迴時的狀態和程式碼執行的位置,遞歸回溯之後會回到上次呼叫的位置,所以我們可以忽略這個問題。而現在我們由於不再使用遞迴,所以需要我們自己來判斷節點的狀態。

想通了其實很簡單,我們只需要在節點當中加一個狀態的欄位,表示這個節點是否會發生回溯。顯然在一開始的時候,所有的節點狀態都是True。

class Node:
    def __init__(self, val):
        self.val = val
        self.lchild = None
        self.rchild = None
        self.flag = True

我們在Node類中加一個flag作為記錄,初始化時我們預設它為True。接著就很簡單了,我們就按照左中右的順序遍歷節點,只要左子樹存在就往左邊遍歷,在一路往左的過程中遇到的這些節點的flag全部置為False,因為它們的回溯已經開始,以後不會再發生回溯了。由於往右遍歷不會存在回溯的問題,所以可以忽略,想明白了,程式碼也就順理成章。

# 使用我們自己剛剛建立的資料結構
stack = Stack()
# 插入根節點
stack.push(root)

while not stack.is_empty():
    # 獲取棧頂元素,也就是當前遍歷的節點
    tmp = stack.top()
    # 如果不曾回溯過,並且左子樹存在
    while tmp.flag and tmp.lchild is not None:
        # 回溯標記置為False
        tmp.flag = False
        # 棧頂push左孩子
        stack.push(tmp.lchild)
        # 往左遍歷
        tmp = tmp.lchild
    # 彈出棧頂
    tmp = stack.pop()
    # 此時說明左節點已經遍歷完了,輸出
    print(tmp.val)
    # 往右遍歷
    if tmp.rchild is not None:
        stack.push(tmp.rchild)

這段程式碼雖然短,但其實不簡單,想要完全看懂需要對遞迴和迴圈有深入的理解。屬於典型的看著簡單實際不容易的題,我個人比較喜歡這類問題,除了鍛鍊思維之外也很適合用來面試,候選人的思維能力、程式碼駕馭能力基本上都一清二楚了。沒有看懂的同學也不用擔心,因為在實際場景當中並不會遇到這樣的場景,以後還會推出其他關於遞迴和搜尋演算法的文章,只要你堅持閱讀,我相信一定會看懂的。

今天的文章就是這些,如果覺得有所收穫,請順手掃碼點個關注吧,你們的舉手之勞對我來說很重要。

相關推薦

資料結構——30程式碼實現模擬

本文始發於個人公眾號:TechFlow,原創不易,求個關注 棧的定義 原本今天想給大家講講快速選擇演算法的,但是發現一連寫了好幾篇排序相關了,所以臨時改了題目,今天聊點資料結構,來看看經典並且簡單的資料結構——棧。 棧這個結構我想大家應該都耳熟能詳,尤其是在很多地方將和堆並列在一起,稱作“堆疊”就更廣為人

資料結構 | 30程式碼,手把手帶你實現Trie樹

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注 今天是演算法和資料結構專題的第28篇文章,我們一起來聊聊一個經典的字串處理資料結構——Trie。 在之前的4篇文章當中我們介紹了關於博弈論的一些演算法,其中應用最廣也是最重要的就是最後的SG函式。瞭解到這些之後,足夠我們應付常見的博弈

資料結構C/C++程式碼實現 連結串列基本操作

實現棧連結串列基本操作: #include<stdio.h> #include<stdlib.h> typedef int ElemType; typedef struct linknode {     ElemType data;     stru

資料結構演算法程式碼實現——佇列(一)

棧和佇列 棧和佇列是一種特殊的線性表。 從資料結構角度看:棧和佇列也是線性表,其特點性在於棧和佇列的基本操作是線性表操作的子集。它們是操作受限的線性表。 從資料型別角度看:它們是和線性表不相同的兩類重要的抽象資料型別。 棧的定義 棧(Stack)是限

資料結構C/C++程式碼實現 順序表基本操作

順序表棧基本操作的實現  原始碼: #include<stdio.h> #include<stdlib.h> #include<iostream> using namespace std; #define MAXSIZE 100 //#d

30程式碼實現Javascript中的MVC

從09年左右開始,MVC逐漸在前端領域大放異彩,並終於在剛剛過去的2015年隨著React Native的推出而迎來大爆發:AngularJS、EmberJS、Backbone、ReactJS、RiotJS、VueJS…… 一連串的名字走馬觀花式的出現和更迭,它們中一些已經漸漸淡出了大家的視

資料結構週週練】014 利用演算法求鏈式儲存的二叉樹是否為完全二叉樹

一、前言 首先,明天是個很重要的節日,以後我也會過這個節日,在這裡,提前祝所有程式猿們,猿猴節快樂,哦不,是1024程式設計師節快樂。 今天要給大家分享的演算法是判斷二叉樹是否為完全二叉樹,相信大家對完全二叉樹的概念並不陌生,如果是順序儲存就會很方便,那鏈式儲存怎麼判斷呢,我的做法是:若

資料結構週週練】013 利用演算法求二叉樹的高

一、前言 二叉樹的高是樹比較重要的一個概念,指的是樹中結點的最大層數本次演算法通過非遞迴演算法來求得樹的高度,借用棧來實現樹中結點的儲存。 學英語真的很重要,所以文中的註釋還有輸出以後會盡量用英語寫,文中出現的英語語法或者單詞使用錯誤,還希望各位英語大神能不吝賜教。 二、題目 將

再談資料結構(一):佇列

1 - 前言 棧和佇列是兩種非常常用的兩種資料結構,它們的邏輯結構是線性的,儲存結構有順序儲存和鏈式儲存。在平時的學習中,感覺雖然棧和佇列的概念十分容易理解,但是對於這兩種資料結構的靈活運用及程式碼實現還是比較生疏。需要結合實際問題來熟練佇列和棧的操作。 2 - 例題分析 2.1

30程式碼實現微信自動回覆機器人

一、寫在前面 前段時間寫過一篇微信好友大揭祕,很多朋友對itchat非常感興趣,今天下午又學到了itchat另一種有趣的玩法---微信自動回覆機器人。程式很簡單僅僅三十行程式碼左右,實現了機器人自動與你的微信好友聊天。   二、程式介紹   本程式通過itch

如何用 30 程式碼實現微信自動回覆機器人?

作者 | Ahab 責編 | 胡巍巍 寫在前面 很多朋友對itchat非常感興趣,近日又學到了itchat另一種有趣的玩法——微信自動回覆機器人。 程式很簡單僅僅三十行程式碼左右,實現了機器人自動與你的微信好友聊天,下面是我的機器人小籠包跟自己微

threejs第一課 30程式碼實現旋轉的立方體

需要電子檔書籍可以Q群:828202939   希望可以和大家一起學習、一起進步!! 所有的課程原始碼在我上傳的資源裡面,本來想設定開源,好像不行!部落格和專欄同步! 如有錯別字或有理解不到位的地方,可以留言或者加微信15250969798,在下會及時修改!!!!!

資料結構》實驗三:佇列實驗報告

一..實驗目的      鞏固棧和佇列資料結構,學會運用棧和佇列。 1.回顧棧和佇列的邏輯結構和受限操作特點,棧和佇列的物理儲存結構和常見操作。 2.學習運用棧和佇列的知識來解決實際問題。 3.進一步鞏固程式除錯方法。 4.進一步鞏固模板程式設計。 二.實驗時間    準備

python演算法與資料結構002--利用列表實現的功能

class Statck(object): """ 棧:後進先出的資料結構 利用列表實現棧的基本功能。 """ def __init__(self): self.items=[] def

資料結構——————排序演算法程式碼實現(未完待續......)

排序演算法 插入排序 折半插入排序 希爾排序 氣泡排序 快速排序 簡單選擇排序 堆排序 歸併排序(未完成) 基數排序(未完成) #include<bits/stdc++.h> using namespace

資料結構之---C語言實現的表示式求值(表示式樹)

利用棧實現表示式樹這裡我一共有兩種思路: part one: 首先判斷輸入表示式的每個字元,如果遇到運算子,不壓棧, 接著彈出兩個棧頂的元素,進行元素,接著把結果壓棧。 程式碼: //棧實現表示式 //思路:此程式的思路是,讀取輸入的字串,然後判斷每個字元, //當遇到

資料結構java版之《佇列》

1、棧。(Android的Activity載入是基礎棧結構的)底層使用陣列實現package ch4; /** * 棧 * @author Howard * 特點: * 1、通常情況作為程式設計

讀書筆記之《資料結構》---第三章 佇列

本章目錄 棧 棧的應用舉例 棧的遞迴與實現 佇列 離散事件模型 棧 棧是限定僅在表尾進行插入或刪除操作的線性表。表尾稱為棧頂,表頭稱為棧底 棧的特點:後進先出 棧的應用舉例 6. 進行數制轉換 2.括號匹配檢測:進行括號的匹配過程 3.行編輯程式功能:例

資料結構》實驗三:佇列實驗 (實驗報告)

一.實驗目的      鞏固棧和佇列資料結構,學會運用棧和佇列。 1.回顧棧和佇列的邏輯結構和受限操作特點,棧和佇列的物理儲存結構和常見操作。 2.學習運用棧和佇列的知識來解決實際問題。 3.進一步鞏固程式除錯方法。 4.進一步鞏固模板程式設計。 二.實驗內

資料結構---用順序表實現的基本操作

順序表實現棧    順序棧:棧的順序儲存結構,是利用一組地址連續的儲存單元依次存放自棧底到棧頂的資料元素,同時附設指標top指示棧頂元素在順序 棧中的位置。    棧在資料結構中也是一個比較重要的結構,它有一個重要的特性是:先進後出。先入棧的元素最後出棧。具