自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

數(shù)據(jù)結(jié)構(gòu)與算法的JavaScript實(shí)現(xiàn)及應(yīng)用:Stack/遞歸/漢諾

開發(fā) 前端 算法
在這篇文章里,我將介紹數(shù)據(jù)結(jié)構(gòu)Stack的基本操作和它的一些應(yīng)用。我們將看到Stack在括號(hào)匹配檢測,表達(dá)式求值,函數(shù)調(diào)用上的應(yīng)用。

摘要

在這篇文章里,我將介紹數(shù)據(jù)結(jié)構(gòu)Stack的基本操作和它的一些應(yīng)用。

我們將看到Stack在括號(hào)匹配檢測,表達(dá)式求值,函數(shù)調(diào)用上的應(yīng)用。

遞歸是一種特殊的函數(shù)調(diào)用,由于遞歸在程序設(shè)計(jì)中十分重要且不容易理解,所以我將闡述我對(duì)遞歸的理解。

***我們將看到利用Stack和遞歸是怎么優(yōu)雅的解決一個(gè)經(jīng)典游戲:漢諾塔。

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

Stack

簡介

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

在計(jì)算機(jī)科學(xué)中,是一種特殊的串行形式的數(shù)據(jù)結(jié)構(gòu),它的特殊之處在于只能允許在鏈結(jié)串行或陣列的一端(稱為堆棧頂端指標(biāo),英語:top)進(jìn)行加入資料(英語:push)和輸出資料(英語:pop)的運(yùn)算。另外堆棧也可以用一維陣列或連結(jié)串行的形式來完成。

400px-Data_stack.svg

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

由于棧具有后進(jìn)入的元素率先彈出的性質(zhì),棧又被稱為后進(jìn)先出(LIFO, Last In First Out)的結(jié)構(gòu)。

棧的操作十分簡單,我們可以用單鏈表(LinkedList)和數(shù)組來實(shí)現(xiàn)棧。

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

應(yīng)用

Stack的LIFO的特性使得其適于解決許多實(shí)際問題,以下我們選取它的三個(gè)應(yīng)用來加以闡述。

括號(hào)匹配檢測

我們平時(shí)在編輯器中寫代碼時(shí),有些編輯器會(huì)自動(dòng)檢測括號(hào)是否前后匹配,不匹配的話則會(huì)報(bào)錯(cuò)提示。

利用Stack的LIFO的特性,我們可以輕松實(shí)現(xiàn)這個(gè)功能。

算法的偽代碼如下:

  1. //新建一個(gè)Stack s  
  2. s = new stack()  
  3. //讀取字符直至讀完  
  4. while read to c != EOF:  
  5.     //如果字符是開放括號(hào) 如 '(' '[' '{'等, 入棧  
  6.     if c is opening:  
  7.         s.push( c )  
  8.     //如果字符是結(jié)束括號(hào) 如 ')' ']' '}'  
  9.     else if c is closing:  
  10.         //若棧為空或者棧頂元素與開放括號(hào)不匹配 則報(bào)錯(cuò)  
  11.         if s is empty or f s.pop() is not correspond to c:  
  12.             return error!     
  13. //若***棧不為空,報(bào)錯(cuò)  
  14. if s is not empty:  
  15.     return error!     
  16. //如果沒有返回報(bào)錯(cuò),則返回正常  
  17. return ok 

算法的原理為,遇到一個(gè)結(jié)束的括號(hào)時(shí),我們總是要查找***一個(gè)開放的括號(hào)是否與之匹配,若找不到開放的括號(hào),或***一個(gè)開放的括號(hào)不匹配,則報(bào)錯(cuò)。

由于總是而且僅需要尋找***一個(gè)元素,所以我們將開放的括號(hào)入棧,匹配時(shí)則出棧。

由于Stack的特性,這個(gè)算法簡單明了,且消耗的時(shí)間復(fù)雜度為線性級(jí)O(n)。

表達(dá)式求值

Stack的強(qiáng)大特性,也使得其能夠運(yùn)用在表達(dá)式求值上。

設(shè)想一個(gè)表達(dá)式:2+4/(3-1)

這個(gè)表達(dá)式具備了三種類型的符號(hào):

  1. 運(yùn)算數(shù):2 4 2 2
  2. 運(yùn)算符:+ / -
  3. 圓括號(hào):()

計(jì)算它的算法如下:

  1. //分配兩個(gè)棧,ops為運(yùn)算符棧,nums為數(shù)字棧  
  2. ops = new Stack, nums = new Stack  
  3. //從表達(dá)式中讀取字符 直至結(jié)束  
  4. while read c in expression != EOF:  
  5.     //若為左括號(hào),入運(yùn)算符棧  
  6.     if c is '(':  
  7.         ops.push(c)  
  8.     //若為數(shù)字,入數(shù)字棧  
  9.     else if c is a number:  
  10.         nums.push(c)  
  11.     //若為操作符  
  12.     else if c is an operator:  
  13.         //若運(yùn)算符棧的棧頂元素比c的優(yōu)先級(jí)高或一樣高,則進(jìn)行單次運(yùn)算  
  14.         while ops.top() is equal or precedence over c:  
  15.             op = ops.pop()  
  16.             opn2 = nums.pop()  
  17.             opn1 = nums.pop()  
  18.             //進(jìn)行單次運(yùn)算,并把運(yùn)算數(shù)入數(shù)字棧  
  19.             nums.push( cal(op,opn1,opn2) )  
  20.     //若為右括號(hào)  
  21.     else if c is ')':  
  22.         //除非棧頂元素為左括號(hào),否則運(yùn)算符棧出棧并將計(jì)算結(jié)果入數(shù)字棧  
  23.         op = ops.pop()  
  24.         while op != '(':  
  25.             opn2 = nums.pop()  
  26.             opn1 = nums.pop()  
  27.             nums.push( cal(op,opn1,opn2) )  
  28.             op = ops.pop()  
  29. //返回?cái)?shù)字棧的棧頂元素  
  30. return nums.top() 

以下是表達(dá)式求值的DEMO:

函數(shù)調(diào)用

我們?cè)谡{(diào)試代碼的時(shí)候,碰到函數(shù)報(bào)錯(cuò)時(shí)經(jīng)常會(huì)發(fā)現(xiàn)如下類似的報(bào)錯(cuò)提示:

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

其實(shí)我們只是在一處出錯(cuò)了,為什么會(huì)打印出這么多報(bào)錯(cuò)信息呢?

原因就在于解釋器把報(bào)錯(cuò)的函數(shù)經(jīng)過的所有調(diào)用函數(shù)的棧打印了出來。

在函數(shù)調(diào)用的時(shí)候,我們需要切換到被調(diào)用的函數(shù)上,但是一旦函數(shù)調(diào)用結(jié)束,我們還得回到原來的位置。

利用棧,我們可以有條不紊的實(shí)現(xiàn)這點(diǎn),即在函數(shù)調(diào)用的時(shí)候,我們把當(dāng)前函數(shù)的變量和上下文入棧,等函數(shù)調(diào)用結(jié)束,我們?cè)俪鰲?,獲取之前的上下文和變量。

#p#

遞歸

函數(shù)調(diào)用的一個(gè)特殊情況是自己調(diào)用自己,這種情況稱之為遞歸。

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

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

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

  1. 遞歸需要一個(gè)終止條件,否則會(huì)無限遞歸
  2. 每次遞歸需要將問題縮小,否則達(dá)不到終止條件,也會(huì)無限遞歸

很顯然,遞歸是自己調(diào)用自己,這很像我們?cè)诶戆l(fā)店里面照鏡子一樣,如果沒有終止條件,我們永遠(yuǎn)也不知道還有多少個(gè)自己。

[[111099]]

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

比如求階乘:

  • 我們?cè)趺辞?的階乘? 3的階乘等于3乘與2的階乘
  • 我們?cè)趺辞?的階乘? 2的階乘等于2乘與1的階乘
  • 我們?cè)趺辞?的階乘? 1的階乘等于1乘與0的階乘
  • 我們?cè)趺辞?的階乘? 我們不需要求0的階乘,因?yàn)槲覀円阎?的階乘為1

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

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

于是我們便得到了階乘的遞歸表達(dá):

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

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

顯然,遞歸的基礎(chǔ)是函數(shù)調(diào)用,而函數(shù)調(diào)用的基礎(chǔ)是Stack。所以每次遞歸的開銷,則是需要不停的進(jìn)行Stack的push和pop操作,這樣會(huì)耗費(fèi)大量的空間,也可能會(huì)造成冗余運(yùn)算。

解決的辦法可能有:尾遞歸動(dòng)態(tài)規(guī)劃。

漢諾塔

漢諾塔是一個(gè)經(jīng)典的游戲。

維基百科的描述為

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

  1. 每次只能移動(dòng)一個(gè)圓盤;
  2. 大盤不能疊在小盤上面。

[[111100]]

漢諾塔有很多解法,我認(rèn)為***雅的解法是遞歸法,如果你不了解漢諾塔的解法的話,在繼續(xù)閱讀前,請(qǐng)嘗試用遞歸法來解決它。

好吧,我承認(rèn)一開始我沒想出來如何用遞歸法來解決它,但是看完了它的解法后,我覺得優(yōu)雅而精妙。

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

  1. //將n個(gè)盤子按規(guī)則從a柱子,移動(dòng)到c柱子  
  2. hanoi( n, a, b, c )  
  3. {  
  4.     if( n > 0 )  
  5.     {  
  6.         hanoi(n-1,a,c,b);  
  7.         //將a柱子的最上面的盤子移到c柱子  
  8.         c.push( a.pop() );  
  9.         hanoi(n-1,b,a,c);  
  10.     }  

整個(gè)算法的思路是:

  1. 將a柱子上的n-1個(gè)盤子暫時(shí)移到b柱子上
  2. a柱子只剩下***的盤子,把它移到目標(biāo)柱子c上
  3. ***再將b柱子上的n-1個(gè)盤子移到目標(biāo)柱子c上

問題的縮小策略是我們?cè)趺窗裯個(gè)盤子從a移到c,同樣適用于n-1個(gè)盤子從a移到c。

當(dāng)n一直縮小到3時(shí),我們先把最上面的兩個(gè)盤子移動(dòng)到b,然后把***的盤子移動(dòng)到c,然后再把b上的兩個(gè)盤子移動(dòng)到c。 
當(dāng)n一直縮小到2時(shí),問題終于變得直觀了,我們將最上面的盤子移到b,然后把最下面的盤子移到c,***再把b的盤子移到c。 
當(dāng)n縮小到1時(shí),我們直接將a最上面的盤子移到c就OK了。

這就是問題的終止條件。

該算法的時(shí)間復(fù)雜度為指數(shù)級(jí)O(2^n),推導(dǎo)詳見Complexity for towers of Hanoi?。最少求解步驟為2^n-1。

漢諾塔的傳說:

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

若傳說屬實(shí),僧侶們需要2^64 − 1步才能完成這個(gè)任務(wù);若他們每秒可完成一個(gè)盤子的移動(dòng),就需要5846億年才能完成。整個(gè)宇宙現(xiàn)在也不過137億年 -.-!

以下是漢諾塔的演示demo:

原文鏈接:http://wuzhiwei.net/ds_app_stack/

責(zé)任編輯:林師授 來源: Tim's Blog
相關(guān)推薦

2021-03-12 09:13:47

Java數(shù)據(jù)結(jié)構(gòu)算法

2021-04-13 09:37:41

Java數(shù)據(jù)結(jié)構(gòu)算法

2021-01-28 07:33:34

JavaScript鏈表數(shù)據(jù)

2023-09-15 10:33:41

算法數(shù)據(jù)結(jié)構(gòu)

2023-09-25 12:23:18

Python

2020-08-10 14:46:30

JavaScriptStack

2020-10-21 14:57:04

數(shù)據(jù)結(jié)構(gòu)算法圖形

2023-03-08 08:03:09

數(shù)據(jù)結(jié)構(gòu)算法歸并排序

2017-05-11 11:59:12

MySQL數(shù)據(jù)結(jié)構(gòu)算法原理

2023-10-27 07:04:20

2023-04-27 09:13:20

排序算法數(shù)據(jù)結(jié)構(gòu)

2011-07-11 15:03:36

MySQL索引數(shù)據(jù)結(jié)構(gòu)

2022-04-18 10:01:07

Go 語言漢諾塔游戲

2023-03-02 08:15:13

2023-03-10 08:07:39

數(shù)據(jù)結(jié)構(gòu)算法計(jì)數(shù)排序

2023-03-07 08:02:07

數(shù)據(jù)結(jié)構(gòu)算法數(shù)列

2021-04-27 06:21:29

Java數(shù)據(jù)結(jié)構(gòu)算法

2011-07-11 16:05:42

MySQL索引

2022-03-31 11:17:58

JavaScript數(shù)組方法

2023-03-13 10:08:31

數(shù)據(jù)結(jié)構(gòu)算法
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)