數據結構與算法的JavaScript實現及應用:Stack/遞歸/漢諾

2021-01-07 51CTO

摘要

在這篇文章裡,我將介紹數據結構Stack的基本操作和它的一些應用。

我們將看到Stack在括號匹配檢測,表達式求值,函數調用上的應用。

遞歸是一種特殊的函數調用,由於遞歸在程序設計中十分重要且不容易理解,所以我將闡述我對遞歸的理解。

最後我們將看到利用Stack和遞歸是怎麼優雅的解決一個經典遊戲:漢諾塔。

本文還將給出表達式求值和漢諾塔的HTML5演示。

Stack簡介

Stack即棧,以下是維基百科的定義:

在計算機科學中,是一種特殊的串行形式的數據結構,它的特殊之處在於只能允許在鏈結串行或陣列的一端(稱為堆棧頂端指標,英語:top)進行加入資料(英語:push)和輸出資料(英語:pop)的運算。另外堆棧也可以用一維陣列或連結串行的形式來完成。

根據定義我們知道棧只有三種操作:入棧(push),出棧(pop),獲取棧頂元素(top)。而且棧只能夠操縱棧頂元素,即只能在一端進行操作。

由於棧具有後進入的元素率先彈出的性質,棧又被稱為後進先出(LIFO, Last In First Out)的結構。

棧的操作十分簡單,我們可以用單鍊表(LinkedList)和數組來實現棧。

然而在JavaScript中,Array自帶pop(), push()的操作,而且我們可以利用Array[Array.length-1]來實現top()操作。所以沒有必要去另外實現一個Stack類型,用Array表達即可。

應用

Stack的LIFO的特性使得其適於解決許多實際問題,以下我們選取它的三個應用來加以闡述。

括號匹配檢測

我們平時在編輯器中寫代碼時,有些編輯器會自動檢測括號是否前後匹配,不匹配的話則會報錯提示。

利用Stack的LIFO的特性,我們可以輕鬆實現這個功能。

算法的偽代碼如下:

 s = new stack()   while read to c != EOF:           if c is opening:          s.push( c )           else if c is closing:                   if s is empty or f s.pop() is not correspond to c:              return error!      if s is not empty:      return error!      return ok 

算法的原理為,遇到一個結束的括號時,我們總是要查找最後一個開放的括號是否與之匹配,若找不到開放的括號,或最後一個開放的括號不匹配,則報錯。

由於總是而且僅需要尋找最後一個元素,所以我們將開放的括號入棧,匹配時則出棧。

由於Stack的特性,這個算法簡單明了,且消耗的時間複雜度為線性級O(n)。

表達式求值

Stack的強大特性,也使得其能夠運用在表達式求值上。

設想一個表達式:2+4/(3-1)

這個表達式具備了三種類型的符號:

運算數:2 4 2 2 運算符:+ / - 圓括號:()

計算它的算法如下:

 ops = new Stack, nums = new Stack   while read c in expression != EOF:           if c is '(':          ops.push(c)           else if c is a number:          nums.push(c)           else if c is an operator:                   while ops.top() is equal or precedence over c:              op = ops.pop()              opn2 = nums.pop()              opn1 = nums.pop()                           nums.push( cal(op,opn1,opn2) )           else if c is ')':                   op = ops.pop()          while op != '(':              opn2 = nums.pop()              opn1 = nums.pop()              nums.push( cal(op,opn1,opn2) )              op = ops.pop()   return nums.top() 

以下是表達式求值的DEMO:

函數調用

我們在調試代碼的時候,碰到函數報錯時經常會發現如下類似的報錯提示:

/Users/tim/Codes/JavaScript/dsaginjs/DSinJS/Stack/InfixExpression.js:59     return prioty[a] ) prioty[b];                       ^  SyntaxError: Unexpected token )      at Module._compile (module.js:439:25)      at Object.Module._extensions..js (module.js:474:10)      at Module.load (module.js:356:32)      at Function.Module._load (module.js:312:12)      at Function.Module.runMain (module.js:497:10)      at startup (node.js:119:16)      at node.js:902:3 

其實我們只是在一處出錯了,為什麼會列印出這麼多報錯信息呢?

原因就在於解釋器把報錯的函數經過的所有調用函數的棧列印了出來。

在函數調用的時候,我們需要切換到被調用的函數上,但是一旦函數調用結束,我們還得回到原來的位置。

利用棧,我們可以有條不紊的實現這點,即在函數調用的時候,我們把當前函數的變量和上下文入棧,等函數調用結束,我們再出棧,獲取之前的上下文和變量。

遞歸

函數調用的一個特殊情況是自己調用自己,這種情況稱之為遞歸。

最簡單的示例,求解階乘:

function factorial(n) {      return n == 0 ? 1 : n * factorial(n-1);  } 

遞歸是個很有用的利器,但是新手往往很難理解,以下來談一談我對遞歸的理解。

遞歸需要一個終止條件,否則會無限遞歸 每次遞歸需要將問題縮小,否則達不到終止條件,也會無限遞歸

很顯然,遞歸是自己調用自己,這很像我們在理髮店裡面照鏡子一樣,如果沒有終止條件,我們永遠也不知道還有多少個自己。

其實用遞歸來求解問題的本質是找到比原問題更小的問題,而且能用同樣的方法來處理。另外我們需要確定遞歸在什麼時候結束,也就是確定遞歸的終止條件。

比如求階乘:

我們怎麼求3的階乘? 3的階乘等於3乘與2的階乘 我們怎麼求2的階乘? 2的階乘等於2乘與1的階乘 我們怎麼求1的階乘? 1的階乘等於1乘與0的階乘 我們怎麼求0的階乘? 我們不需要求0的階乘,因為我們已知0的階乘為1

通過這麼分析問題,提出n的階乘怎麼求?

回答是:如果n為0,則返回1,否則返回n乘與n-1的階乘。

於是我們便得到了階乘的遞歸表達:

function factorial(n) {      return n == 0 ? 1 : n * factorial(n-1);  } 

階乘的遞歸的終止條件是n==0;縮小問題的方法則是發現了n-1的問題和n的問題是一致的。

顯然,遞歸的基礎是函數調用,而函數調用的基礎是Stack。所以每次遞歸的開銷,則是需要不停的進行Stack的push和pop操作,這樣會耗費大量的空間,也可能會造成冗餘運算。

解決的辦法可能有:尾遞歸,動態規劃。

漢諾塔

漢諾塔是一個經典的遊戲。

其維基百科的描述為:

有三根杆子A,B,C。A杆上有N個(N>1)穿孔圓盤,盤的尺寸由下到上依次變小。要求按下列規則將所有圓盤移至C杆:

每次只能移動一個圓盤; 大盤不能疊在小盤上面。

漢諾塔有很多解法,我認為最優雅的解法是遞歸法,如果你不了解漢諾塔的解法的話,在繼續閱讀前,請嘗試用遞歸法來解決它。

好吧,我承認一開始我沒想出來如何用遞歸法來解決它,但是看完了它的解法後,我覺得優雅而精妙。

漢諾塔的遞歸法偽代碼如下:

 hanoi( n, a, b, c )  {      if( n > 0 )      {          hanoi(n-1,a,c,b);                   c.push( a.pop() );          hanoi(n-1,b,a,c);      }  } 

整個算法的思路是:

將a柱子上的n-1個盤子暫時移到b柱子上 a柱子只剩下最大的盤子,把它移到目標柱子c上 最後再將b柱子上的n-1個盤子移到目標柱子c上

問題的縮小策略是我們怎麼把n個盤子從a移到c,同樣適用於n-1個盤子從a移到c。

當n一直縮小到3時,我們先把最上面的兩個盤子移動到b,然後把最大的盤子移動到c,然後再把b上的兩個盤子移動到c。 
當n一直縮小到2時,問題終於變得直觀了,我們將最上面的盤子移到b,然後把最下面的盤子移到c,最後再把b的盤子移到c。 
當n縮小到1時,我們直接將a最上面的盤子移到c就OK了。

這就是問題的終止條件。

該算法的時間複雜度為指數級O(2^n),推導詳見Complexity for towers of Hanoi?。最少求解步驟為2^n-1。

漢諾塔的傳說:

傳說印度某間寺院有三根柱子,上串64個金盤。寺院裡的僧侶依照一個古老的預言,以上述規則移動這些盤子;預言說當這些盤子移動完畢,世界就會滅亡。這個傳說叫做梵天寺之塔問題(Tower of Brahma puzzle)。

若傳說屬實,僧侶們需要2^64 − 1步才能完成這個任務;若他們每秒可完成一個盤子的移動,就需要5846億年才能完成。整個宇宙現在也不過137億年 -.-!

以下是漢諾塔的演示demo:

原文連結:http://wuzhiwei.net/ds_app_stack/

【編輯推薦】

【責任編輯:

林師授

TEL:(010)68476606】

相關焦點

  • java 數據結構與算法 之遞歸算法
    我們也只有一點一點的積累,趁現在有時間,今天討論一下java 的數據結構與算法:遞歸算法,希望能達到溫故而知新的效果。一。定義:遞歸(recursion):是指定義自身的同時又出現了對自身的引用。遞歸算法:同理一個算法直接或間接調用自己就叫遞歸算法。一個有意義的遞歸算法總是包含兩部分:遞歸的調用與遞歸的終止條件。二。
  • 有關樹遍歷的javascript實現【前端-總結-leetcode算法題】
    在這裡插入圖片描述前言二月的第一天,總結一下近段時間刷的有關樹遍歷的leetcode算法題,希望寫完這篇文章的我和看完文章的你都有些收穫吧。全篇主要講的是有關樹的遍歷,使用前端javascript語言實現。當然有關樹的操作還有很多需要的深入理解和總結的。今天就暫時先把樹遍歷理解全吧。
  • 使用JavaScript 遞歸算法,匯總累加題目的分數值
    所謂遞歸算法,是將問題轉化為規模縮小的同類問題的子問題,每一個子問題都用一個同樣的算法去解決。一般來說,一個遞歸算法就是函數調用自身去解決它的子問題。使用遞歸算法實現大題的分數值匯總。為了實現樹型結構數據的遍歷與處理,免不了要想到使用遞歸函數來實現。我們先來看一下定義,所謂遞歸算法,是將問題轉化為規模縮小的同類問題的子問題,每一個子問題都用一個同樣的算法去解決。一般來說,一個遞歸算法就是函數調用自身去解決它的子問題。遞歸算法的特點:在函數過程中調用自身。
  • 算法與數據結構入門:棧與遞歸
    在此之前,我們介紹了動態規劃、深度優先搜索等基礎算法,但是,有部分好友評論說,難度太難了,我們知道動態規劃的自頂向下跟深度優先搜索一般都用遞歸實現,今天我們就先來講講算法與數據結構中,基礎中的基礎遞歸。講遞歸之前,我們先來了解下棧。
  • 「算法與數據結構」二叉樹之美
    腦圖👇樹的基本概念樹是用來模擬具有樹狀結構性質的數據集合。或者你可以把它認為是一種「抽象數據結構」或是實現這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合。既然樹的分類有這麼多的話,那麼我們是不是都需要一一掌握呢,我個人覺得,掌握二叉樹這種結構就足夠了,它也是樹最簡單、應用最廣泛的種類。那麼接下來,我們就來介紹一下二叉樹吧。二叉樹的概念二叉樹是一種典型的樹樹狀結構。
  • 快速排序的JavaScript實現詳解
    從算法上講,這可以用遞歸或循環實現。但是對於這個問題,用遞歸法更為自然。了解快速排序背後的邏輯先看一下快速排序的工作原理:在數組中選擇一個元素,這個元素被稱為基準(Pivot)。通常把數組中的第一個或最後一個元素作為基準。
  • 手敲一遍數據結構和排序算法
    排序算法 性質記憶 冒泡排序 選擇排序 插入排序 快速排序 歸併排序 希爾排序 堆排序數據結構 數組 堆 棧 隊列 鍊表 二叉樹 二叉搜索樹 圖 哈希表搜索 廣度優先搜索 深度優先搜索附件 排序算法代碼點擊
  • 算法圖解 | 遞歸算法和棧的應用
    遞歸算法:什麼是遞歸呢?同理為了避免遞歸算法一直遞歸成無限循環,它也需要設置一定的停止條件。像找鑰匙這個例子,如果沒找到鑰匙,但打開了所有的盒子,沒有未打開的盒子,就是停止條件。遞歸條件指的是函數調用自己,而基線條件則指的是函數不再調用自己,從而避免形成無限循環。
  • 程式設計師必會算法系列之——「遞歸算法」
    講遞歸算法時會涉及到「棧」數據結構的一些知識,如果有不太了解的可以看這裡一文讓你實現手寫「棧」數據結構。遞歸的缺點使用遞歸算法時每次方法的調用都需要在棧中開闢出一個空間保存相關數據,頻繁的壓棧、彈棧會導致效率變低。遞歸使用注意事項1、使用遞歸算法解決問題必須要有出口,不然就形成死循環了。
  • python遞歸算法(上)
    什麼是遞歸在函數內部,是可以調用其他函數的。如果一個函數在內部調用自身,就稱這個函數就是遞歸函數。舉個例子:實現一個可以自定義重複列印你好的函數。要實現重複列印,可能我們立馬就會想到使用循環。如果要求不能使用循環呢,那我們就可以通過下面的方法來實現。原理很好理解,就是不斷的調用自身,如果前面不加上if條件判斷,理論上是會陷入死循環的,但是實際上遞歸到一定次數(最大遞歸次數)就會報錯停止。
  • 【原創教程】數據結構與算法(1)——二叉樹
    4、層序遍歷層序遍歷就是逐層遍歷樹結構,下圖展示了它的層次結構:三、程序實現二叉樹的前序、中序、後序和層序遍歷,分別為leetcode的第144、94、145、102題,都可以用遞歸和迭代兩種方法做。1、遞歸實現對於前序、中序和後序來說,遞歸的現實非常簡單,他們的實現區別是根節點訪問的順序不一樣。
  • 快速入門數據結構和算法
    本文簡要分享算法基礎、常見的數據結構以及排序算法,給同學們帶來一堂數據結構和算法的基礎課。文末福利:阿里雲開發者訓練營來了。一  前言1  為什麼要學習算法和數據結構?2  業務開發要掌握到程度? 二  數據結構基礎1  什麼是數據結構?數據結構是數據的組織、管理和存儲格式,其使用目的是為了高效的訪問和修改數據。數據結構是算法的基石。如果把算法比喻成美麗靈動的舞者,那麼數據結構就是舞者腳下廣闊而堅實的舞臺。2  物理結構和邏輯結構的區別?
  • 你必須要知道的JavaScript數據結構與面試題解答
    數據結構決定了如何收集數據,我們可以用來訪問數據的功能以及數據之間的關係。數據結構幾乎用於計算機科學和編程的所有領域,從作業系統到基本的編碼再到人工智慧。管理和利用大型數據集從資料庫中搜索特定數據針對特定程序量身定製的設計算法一次處理來自用戶的多個請求簡化並加速數據處理數據結構對於有效,現實地解決問題至關重要。
  • 二叉樹的所有遍歷非遞歸實現
    四:後序遍歷五:層序遍歷六:總結一:二叉樹簡介1.1:二叉樹結構維基百科中對二叉樹是這樣定義的:(英文名:Binary tree)是每個節點最多只有兩個分支(即不存在分支度大於2的節點)的樹結構。來看一下2.1 前序遍歷非遞歸實現思路:①:首先將根節點放入到stack中存儲②:遍歷棧,如果stack不為空,直接彈出根節點③:如果右節點不為空,將右節點放入到棧中④:如果左節點不為空,將左節點放入到棧中
  • JS 數據結構與算法——棧 & 隊列
    寫在前面原計劃是把《你不知道的Javascript》三部全部看完的,偶然間朋友推薦了數據結構與算法的一套入門視頻,學之。發現數據結構並沒有想像中那麼遙不可及,反而發覺挺有意思的。手頭上恰好有《學習Javascript數據結構與算法》的書籍,便轉而先把數據結構與算法學習。一、認識數據結構什麼是數據結構?
  • JavaScript實現的9大排序算法
    最佳情況:T(n) = O(nlogn)最差情況:T(n) = O(n2)平均情況:T(n) = O(nlogn)1)算法簡介堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
  • 【學習筆記】300分鐘搞定數據結構與算法
    對於棧中的數據來說,所有操作都是在棧的頂部完成的,只可以查看棧頂部的元素,只能夠向棧的頂部壓⼊數據,也只能從棧的頂部彈出數據。實現:利用一個單鍊表來實現棧的數據結構。而且,因為我們都只針對棧頂元素進行操作,所以借用單鍊表的頭就能讓所有棧的操作在 O(1) 的時間內完成。
  • 數據結構和算法學習指南
    首先,這裡講的都是普通的數據結構和算法,咱不是搞競賽的,野路子出生,只解決常規的問題,以面試為最終目標。另外,以下是我個人的經驗的總結,沒有哪本算法書會寫這些東西,所以請讀者試著理解我的角度,別糾結於細節問題,因為這篇文章就是對數據結構和算法建立一個框架性的認識。
  • 數據結構快速盤點 - 非線性結構
    從另一個角度看,樹是一種遞歸的數據結構。而且樹的不同表示方法,比如不常用的長子 + 兄弟法,對於 你理解樹這種數據結構有著很大用處, 說是一種對樹的本質的更深刻的理解也不為過。我剛才提到了樹是一種遞歸的數據結構,因此樹的遍歷算法使用遞歸去完成非常簡單,幸運的是樹的算法基本上都要依賴於樹的遍歷。 但是遞歸在計算機中的性能一直都有問題,因此掌握不那麼容易理解的"命令式地迭代"遍歷算法在某些情況下是有用的。如果你使用迭代式方式去遍歷的話,可以藉助上面提到的棧來進行,可以極大減少代碼量。
  • 如何優雅地使用javascript遞歸畫一棵結構樹
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫遞歸和尾遞歸簡單的說,遞歸就是函數自己調用自己,它作為一種算法在程序設計語言中廣泛應用。