5 栈和队列
第五章? 棧和隊列
在常用的數據結構中,有一批結構被稱為容器。一個容器結構里總包含一組其他類型的數據對象,稱為元素,容器支持對這些元素的存儲、管理和使用。一組容器具有相同性質,支持同一組操作,可以被定義為一個抽象數據類型。
線性表就是一類容器,該類數據對象除了可以保存元素,支持元素訪問和刪除外,還記錄了元素之間的一種順序關系。
另外兩類最常用的容器是棧(stack)和隊列(queue),它們都是使用最廣泛的基本數據結構。
?
5.1 概述
棧和隊列主要用于在計算過程中保存臨時數據,這些數據是計算中發(fā)現或產生的,在后續(xù)的計算中可能需要使用它們。如果可能生成的數據項數在編程時就可確定,問題比較簡單,可以設置幾個變量作為臨時存儲。但如果需要存儲的數據項數不能事先確定,就必須采用更復雜的機制存儲和管理,這種機制被稱為緩沖存儲或緩存。棧和隊列就是使用最多的緩沖存儲結構。
5.1.1 棧、隊列和數據使用順序
在計算中,中間數據對象的生成有早有晚,存在時間上的先后順序。在后續(xù)使用這些元素時,也可能需要考慮它們生成的時間順序。最典型的兩種順序是:先進先出和后進先出。
從實現的角度看,應該考慮最簡單而且自然的技術。由于計算機存儲器的特點,要實現棧或隊列,最自然的技術就是用元素存儲的順序表示它們的時間順序。也就是說,應該使用線性表作為棧和隊列的實現結構。
5.1.2 應用環(huán)境
可直接使用list實現棧的功能。python標準庫提供了一種支持隊列用途的結構deque。
?
5.2 棧:概念和實現
存入棧中的元素之間沒有任何具體關系,只有到來的時間先后順序。棧的基本性質保證了,在任何時刻可以訪問、刪除的元素都是在此之前最后存入的那個元素,因此,棧確定了一種默認元素訪問順序。
5.2.1 棧抽象數據類型
棧的基本操作是一個封閉的集合(與線性表的情況不同)。下面是一個棧的抽象數據類型描述,其中定義的操作包括:棧的創(chuàng)建、判斷棧是否為空、入棧、出棧、訪問最后入棧元素(不刪除)。最后兩個操作都遵循LIFO原則。另外,對這兩個操作,棧為空時操作無定義。
棧的線性表實現
棧可以實現為在一端插入/刪除的線性表。在表實現中,執(zhí)行插入/刪除操作的一端被稱為棧頂,另一端被稱為棧底。訪問和彈出的都是棧頂元素。
用線性表實現棧時,操作只在表的一端進行,不涉及另一端,更不涉及表的中間部分。由于這種情況,自然應該選擇實現最方便并且保證兩個主要操作效率最高的那一端作為棧頂。
1)對于順序表,尾端插入和刪除是O(1)操作,應該用這一端作為棧頂。
2)對于鏈接表,首端插入和刪除是O(1)操作,應該用這一端作為棧頂。
在實際中,棧都采用這兩種技術實現。
5.2.2 棧的順序表實現
實現棧之前,先考慮為操作失敗的處理定義一個異常類。由于操作時棧不滿足需要(如空棧彈出)可以看作參數值錯誤,因此自定義異常類可以繼承ValueError。
1 class StackUnderflow(ValueError): 2 pass # 不提供ValueError之外的新功能,只是與其他ValueError異常有所區(qū)分。必要時可以定義專門的異常處理操作。采用順序表技術實現棧,首先會遇到實現順序表時提出的各類問題,如:是采用簡單順序表,還是動態(tài)順序表?如果采用簡單順序表就可能出現棧滿的情況,繼續(xù)壓入元素就會溢出,應該檢查和處理。而如果采用動態(tài)順序表,棧的存儲區(qū)滿時可以替換一個更大的存儲區(qū),這種情況又會出現存儲區(qū)的置換策略問題,以及分期付款式的O(1)時間復雜度問題。
list及其操作實際上提供了與棧的使用方式有關的功能,可以直接作為棧來使用。
由于list類型采用的是動態(tài)順序表技術,入棧操作具有分期付款式的O(1)時間復雜度,其他操作都是O(1)時間復雜度。
把list當作棧使用,完全可以滿足應用的需要。但是,這樣建立的對象還是list,提供了list類型的所有操作,特別是提供了一大批棧結構原本不應該支持的操作,威脅到棧的使用安全性(例如棧要求未經彈出的元素應該存在,但表允許任意刪除)。另外,這樣的“棧”不是一個獨立類型,因此沒有獨立類型的所有重要性質。
可以自定義一個棧類,把list隱藏在這個類的內部,作為其實現基礎。
基于list實現的棧,(沿用list的擴容方式)
1 class SStack(): 2 def __init__(self): 3 self._elems = [] 4 5 def is_empty(self): 6 return self._elems == [] 7 8 def top(self): 9 if self.is_empty(): 10 raise StackUnderflow("in SStack.top()") 11 return self._elems[-1] 12 13 def push(self, elem): 14 self._elems.append(elem) 15 16 def pop(self): 17 # if self._elems == []: 18 if self.is_empty(): 19 raise StackUnderflow("in SStack.top()") 20 return self._elems.pop()初始創(chuàng)建對象時建立一個空棧;top和pop操作都需要先檢查棧的情況,在棧空時引發(fā)異常。
屬性_elems是只在內部使用的屬性,這里采用下劃線開頭。
1 st1 = SStack() 2 st1.push(3) 3 st1.push(5) 4 while not st1.is_empty(): 5 print(st1.pop())5.2.3 棧的鏈表實現
基于順序表實現的棧可以滿足絕大部分的實際需要。考慮基于鏈表的棧,主要是因為順序表的兩個特點所帶來的問題:擴大存儲需要做一次高代價操作(替換存儲區(qū));順序表需要完整的大塊存儲區(qū)。采用鏈表技術,在這兩個問題上都有優(yōu)勢,鏈表的缺點是更多依賴于解釋器的存儲管理,每個結點的鏈接開銷,以及鏈接結點在實際計算機內存中任意散布可能帶來的操作開銷。
由于棧操作都在線性表一端進行,采用鏈表技術,自然應該把表頭一端作為棧頂,表尾作為棧底。
1 class LStack(): 2 def __init__(self): 3 self._top = None 4 5 def is_empty(self): 6 return self._top == None 7 8 def top(self): 9 if self.is_empty(): 10 raise LinkedListUnderflow('in pop') 11 return self._top.elem 12 13 def push(self, elem): 14 # 鏈表首端添加 15 self._top = LNode(elem, self._top) 16 17 def pop(self): 18 if self.is_empty(): 19 raise LinkedListUnderflow('in pop') 20 e = self._top.elem 21 self._top = self._top.next 22 return e?
5.3 棧的應用
基本用途基于兩個方面:
順序反轉,只需把所有元素按原來的順序全部入棧,再順序出棧,就能得到反序后的序列。整個操作需要O(n)時間。
1 l1 = [1, 3, 5, 7, 9] 2 l2 = [] 3 ls1 = LStack() 4 for i in l1: 5 ls1.push(i) 6 7 while not ls1.is_empty(): 8 l2.append(ls1.pop()) 9 print(l2) # [9, 7, 5, 3, 1]如果入棧和出棧操作任意交錯,可以得到不同的元素序列。
5.3.1 簡單應用:括號匹配問題
括號配對原則:在掃描正文過程中,遇到的閉括號應該與此前最近遇到且尚未獲得匹配的開括號配對。如果最近的未匹配開括號與當前閉括號不配對,或者找不到這樣的開括號,就是匹配失敗,說明這段正文里的括號不配對。
由于存在多種不同的括號對,每種括號都可能出現任意多次,而且還可能嵌套。為了檢查是否匹配,掃描中必須保存遇到的開括號。由于編程時無法預知要處理的正文里會有多少括號需要保存,因此不能用固定數目的變量保存,必須使用緩存結構。
由于括號的出現可能嵌套,需要逐對匹配:當前閉括號應該與前面最近的尚未配對的開括號匹配,下一個閉括號應該與前面次近的開括號匹配。這說明,需要存儲的開括號的使用原則是后進先出的。
進而,如果一個開括號已配對,就應該刪除這個括號,為隨后的匹配做好準備。
上面這些情況說明,用棧保存遇到的開括號可以正確支持匹配工作。
思路:
1)順序掃描被檢查正文(一個字符串)里的一個個字符。
2)檢查中跳過無關字符(非括號字符都是無關字符)。
3)遇到開括號將其入棧。
4)遇到閉括號時彈出棧頂元素與之匹配,如果匹配成功則繼續(xù),否則以失敗結束。
書中的示例是錯的:
1 def check_parens(text): 2 parens = '()[]{}' 3 open_parens = '([{' 4 opposite = {')': '(', ']': '[', '}': '{'} 5 6 ls = LStack() 7 for i in text: 8 if i not in parens: 9 continue 10 if i in open_parens: 11 ls.push(i) 12 continue 13 14 try: 15 s = ls.pop() 16 except: # 當出現一個閉括號,但是沒有與之相對的開括號時 17 print('匹配失敗') 18 return False 19 if opposite[i] != s: 20 print('匹配失敗') 21 return False 22 23 if ls.is_empty(): 24 print('匹配成功') 25 return True 26 else: 27 print('匹配失敗') 28 return False 29 30 31 def check_parens2(text): 32 parens = '()[]{}' 33 open_parens = '([{' 34 opposite = {')': '(', ']': '[', '}': '{'} 35 36 def parentheses(text): 37 i, text_len = 0, len(text) 38 while True: 39 while i < text_len and text[i] not in parens: 40 i += 1 41 if i >= text_len: 42 return 43 yield text[i], i 44 i += 1 45 46 ls = LStack() 47 for pr, i in parentheses(text): 48 if pr in open_parens: 49 ls.push(pr) 50 elif ls.pop() != opposite[pr]: 51 print('匹配失敗') 52 return False 53 print('匹配成功') 54 return True 55 56 57 text = '((((' 58 check_parens(text) 59 check_parens2(text)5.3.2 表達式的表示、計算和交換
中綴表示很難統一地貫徹。首先是一元和多元運算符都難以中綴形式表示。其次是不足以表示所有可能的運算順序,需要通過輔助符號、約定和 /或輔助描述機制。
后綴表達式特別適合計算機處理。
前綴表達式和后綴表達式都不需要引進括號,也不需要任何有關優(yōu)先級或結合性的規(guī)定。對于前綴表示,每個運算符的運算對象,就是它后面出現的幾個完整表達式,表達式個數由運算符元數確定。對于后綴表示,情況類似但位置相反。
中綴表達式的表達能力最弱,而給中綴表達式增加了括號后,幾種表達式具有同等表達能力。
。。。
?
5.3.3 棧與遞歸
如果在一個定義中引用了被定義的對象本身,這種定義被稱為遞歸定義。類似地,如果在一種數據結構中的某個或某幾個部分具有與整體同樣的結構,這也是一種遞歸結構。
在遞歸定義或結構中,遞歸部分必須比原來的整體簡單,這樣才有可能達到某種終結點(即遞歸定義的出口),這種終結點必須是非遞歸的。例如,單鏈表中,結點鏈的空鏈接就是遞歸的終點。
階乘函數的遞歸運算
以fact(6)為例,
這種后進先出的使用方式和數據項數的無明確限制,就說明需要用一個棧來支持遞歸函數的實際運行,這個棧被稱為程序運行棧(算法圖解上叫調用棧)。
棧與遞歸/函數調用
對遞歸定義的函數,實現方式就是用一個運行棧,對函數的每個調用都在這個棧上為之開辟一塊區(qū)域,其中保存這個調用的相關信息,這個區(qū)域稱為一個函數幀。
一般的函數調用和退出的方式也與此類似。例如函數f里調用函數g,函數g里調用函數h,函數h里調用函數r,其執(zhí)行流程與遞歸完全一樣。
遞歸和非遞歸
對遞歸定義的函數,每個實際調用時執(zhí)行的都是該函數體的那段代碼,只是需要在一個內部運行棧里保存各次調用的局部信息。這種情況說明,完全有可能修改函數定義,把一個遞歸定義的函數改造為一個非遞歸的函數。在函數里自己完成上面這些工作,用一個棧保存計算中的臨時信息,完成同樣的計算工作。
1 def norec_fact(n): 2 res = 1 3 ls = LStack() 4 while n > 0: 5 ls.push(n) 6 n -= 1 7 while not ls.is_empty(): 8 res *= ls.pop() 9 return res實際上,任何一個遞歸定義的函數,都可以通過引入一個棧保存中間結果的方式,翻譯為一個非遞歸的過程。
棧的應用:簡單背包問題
。。。
?
5.4 隊列
5.4.1 隊列抽象數據類型
隊列的基本操作也是一個封閉集合,通常包括創(chuàng)建新隊列對象、判斷隊列是否為空、入隊、出隊、檢查隊列當前元素。
隊列操作與棧操作一一對應,但通常采用另一套習慣的操作名:
5.4.2 隊列的鏈接表實現
采用線性表技術實現隊列,就是利用元素位置的順序表示入隊時間的先后關系。隊列操作要求先進先出,這就要求在表的兩端進行操作,所以實現起來也稍微麻煩一些。
由于需要在鏈接表的兩端操作,在一端插入元素,在另一端刪除。最簡單的單鏈表只支持首端高效操作,在另一端操作需要O(n)時間,不適合作為隊列的實現基礎。
帶表尾指針的單鏈表,支持O(1)時間的尾端插入操作,再加上表首端的高效訪問和刪除,可作為隊列的實現基礎。
5.4.3 隊列的順序表實現
基于順序表實現隊列的困難
另一種可能是隊首元素出隊后表中元素不前移,但記住新隊首位置。從操作效率上看,每個操作都是O(1)時間,但表中隊列卻好像隨著操作向表尾方向”移動“,表前端留下越來越多的空位。
這樣經過反復的入隊和出隊操作,一定會在某次入隊時出現隊尾溢出的情況,而此時隊首卻有大量空位,所以這是一種假性溢出,并不是真的用完了整個元素區(qū)。假如元素存儲區(qū)能自動增長,隨著操作進行,表首端就會留下越來越大的空區(qū),而且這片空區(qū)永遠也不會用到。顯然不應該允許程序中出現這種情況。
從圖5.7可以看到:在反復入隊和出隊操作中,隊尾最終會到達存儲區(qū)末端。但與此同時,存儲區(qū)首端可能有些空位可以利用。這樣就得到了一種順理成章的設計:如果入隊時隊尾已滿,應該考慮轉到存儲區(qū)開始的位置去入隊新元素。
循環(huán)順序表
1)在隊列使用中,順序表的開始位置并未改變。變量q.elems始終指向表元素區(qū)開始。
2)隊頭變量q.head記錄當前隊列里第一個元素的位置;隊尾變量q.rear記錄當前隊列里最后元素之后的第一個空位。
3)隊列元素保存在順序表的一段連續(xù)單元里,python的寫法是[q.head, q.rear],左閉右開區(qū)間。
上面這幾條也是這種隊列的操作必須維護的性質。初始時隊列為空,應該讓q.hear和q.rear取相同的值,表示順序表里一個空段。具體取值無關緊要。不變操作都不改變有關變量的值,不會破壞上述性質;變動操作可能修改有關變量的值,因此要特別注意維護上面的性質。
出隊和入隊操作分別需要更新變量q.head和q.rear。
1 q.head = (q.head+1) % q.len # 出隊 # 通過求余控制循環(huán) 2 q.rear = (q.rear+1) % q.len # 入隊判斷隊列狀態(tài)。q.head == q.rear表示隊空。
從圖5.8的隊列狀態(tài)出發(fā)做幾次入隊操作,可以到達圖5.9所示的狀態(tài),這時隊列里已有7個元素。如果再加入一個元素順序表就滿了,但又會出現q.head == q.rear的情況,這個狀態(tài)與隊列空的判斷無法區(qū)分。
一種解決辦法是直接把圖5.9“看作”隊列已滿,即把隊滿條件定義為
1 (q.rear+1) % q.len == q.head # 隊尾攆上隊首了采用這種方法,將在表里留下一個不用的空位。基于循環(huán)順序表,存在多種隊列實現方式。
5.4.4 隊列的 list 實現
最直截了當的實現方法將得到一個O(1)時間的enqueue操作和O(n)時間的dequeue操作。
現在考慮定義一個可以自動擴充存儲的隊列類。這里很難直接利用list的自動存儲擴充機制,兩個原因:首先是隊列元素的存儲方式與list元素的默認存儲方式不一致。list元素總在其存儲區(qū)的最前面一段,而隊列元素可能是其中的任意一段,有時還分為頭尾兩端(圖5.9就是這樣,但隊列本身仍然是連續(xù)的)。如果list自動擴充,其中的隊列元素就有可能失控。另一方面,list沒提供檢查元素存儲區(qū)容量的機制,隊列操作中無法判斷系統何時擴容。由于沒有很好的辦法處理這些困難,下面考慮自己管理存儲。
基本設計
首先,隊列可能為空而無法dequeue,為此自定義一個異常:
1 class QueueUnderflow(ValueError): 2 pass基本設計:
1)在SQueue對象里用一個list類型的成分_elems存放隊列元素。
2)_head記錄隊列首元素所在位置的下標。
3)_num記錄表中元素個數。
4)_len記錄當前表的長度,必要時替換存儲區(qū)。
數據不變式
變動操作可能會改變一些對象屬性的取值,如果操作的實現有錯誤,可能會破壞對象的狀態(tài)。實現一種數據結構里的操作時,最基本的問題就是這些操作需要維護對象屬性之間的正確關系,這樣一套關系被稱為這種數據結構的數據不變式。(1)對象的初始狀態(tài)應該滿足數據不變式。(2)每個對象操作都應該保證不破壞數據不變式。有了這兩條,這類對象在使用中就能始終處于良好狀態(tài)。
下面是隊列實現中考慮的數據不變式:
1)_elems屬性引用著隊列的元素存儲區(qū),它是一個list對象,_len屬性記錄存儲區(qū)的有效容量(python無法獲知list對象的實際大小)。
2)_head是隊首元素的下標,_num始終記錄著隊列中元素的個數。
3)隊列里的元素總保存在_elems里從_head開始的連續(xù)位置中,新入隊元素存入_head+_num算出的位置,但如果需要把元素存入下標_len的位置時,改為在下標0位置存入該元素(前提是隊列未滿,0位置處有空位,否則擴容)。
4)在_num == _len時(隊滿時)出現入隊操作,就擴大存儲區(qū)。
總之,需要時刻注意_elems、_len、_head、_num四個變量的狀態(tài)。
隊列類的實現
根據前面的設計,隊列的首元素在self._elems[self._head],peek和dequeue操作應該取這里的元素;下一個空位在self._elems[(self._head+self._num) % self._len],入隊的新元素應該存入這里。
另外,隊列空就是self._num == 0,當前隊列滿就是self._num = self._len,這時應該替換存儲區(qū)。
完整實例:
1 #!coding:utf8 2 3 class QueueUnderflow(ValueError): 4 pass 5 6 class SQueue(): 7 def __init__(self, init_len=8): 8 self._len = init_len 9 self._elems = [0]*init_len # 感覺用None比較好 10 self._head = 0 11 self._num = 0 12 13 def is_empty(self): 14 return self._num == 0 15 16 def peek(self): 17 if self.is_empty(): 18 raise QueueUnderflow('none') 19 return self._elems[self._head] 20 21 # 時刻注意四個變量的狀態(tài) 22 # 不能直接使用pop,pop隊首的時間為O(n) 23 def dequeue(self): 24 if self.is_empty(): 25 raise QueueUnderflow('none') 26 e = self._elems[self._head] 27 self._head = (self._head + 1) % self._len 28 self._num -= 1 29 return e 30 31 # 不能直接使用append 32 # 可能出現擴容操作 33 def enqueue(self, e): 34 if self._num == self._len: 35 self.__extend() # 擴容 36 37 self._elems[(self._head + self._num) % self._len] = e 38 self._num += 1 39 40 # 擴容(2倍) 41 def __extend(self): 42 old_len = self._len 43 self._len *= 2 44 new_elems = [0]*self._len # 這個一直沒有理解?? 45 for i in range(old_len): 46 new_elems[i] = self._elems[(self._head + i) % self._len] 47 self._elems, self._head = new_elems, 0特別提醒:peek和dequeue操作都需要事先判斷隊列是否為空。
基于循環(huán)順序表實現的隊列,加入和取出操作都是O(1)操作。循環(huán)順序表實際上是為了解決假性溢出問題。
小結:基于鏈表的隊列,鏈表是指帶尾端指針的鏈表;基于順序表的隊列,順序表是指循環(huán)順序表。
?
5.5 迷宮求解和狀態(tài)空間搜索
5.5.1 迷宮求解:分析和設計
迷宮問題
搜索從入口到出口的路徑,具有遞歸性質:
1)從迷宮的入口開始檢查,這是初始的當前位置。
2)如果當前位置就是出口,已經找到出口,問題解決。
3)如果從當前位置已無路可走,當前正在進行的探查失敗,需要按一定方式另行繼續(xù)搜索。
4)從可行方向中取一個,向前進一步,從那里繼續(xù)探索通往出口的路徑。
先考慮一種簡單形式的迷宮。如圖5.10左圖,
其形式是一組位置構成的矩形陣列,空白格子表示可通行,陰影格子表示不可通行。這種迷宮是平面的,形式比較規(guī)范,每個空白位置的上/下/左/右四個方向有可能也是空白位置,每次允許在某個方向上移動一步。這種迷宮可以直接映射到二維的0/1矩陣,因此很容易在計算機里表示。
迷宮問題分析
問題的目標是找到從入口到出口的一條路徑,而不是所有的可行路徑。
由于不存在其他指導信息,這里的工作方式只能是試探并設法排除無效的探索方向。這顯然需要緩存一些信息:如果當前位置有多個可能的繼續(xù)探查方向,由于下一步只能探查一種可能,因此必須記錄不能立即考慮的其他可能方向。不記錄而丟掉了重要信息,就可能出現實際有解但不能找到的情況。
存在不同的搜索方式,可以比較冒進,也可以穩(wěn)扎穩(wěn)打。如果搜索到當前位置還沒找到出口,可以繼續(xù)向前走,直到無路可走才考慮后退,換一條沒走過的路繼續(xù)。也可以在每一步都從最早記錄的有選擇位置向前進,找到下一個可達位置并記錄它。
顯然這里需要保存已經發(fā)現但尚未探索的分支方向信息。由于無法確定需要保存的數據項數,只能用某種緩存結構,首先應該考慮使用棧或隊列。搜索過程只要求找到目標,對如何找到沒有任何約束,因此這兩種結構都有可能使用。采用不同的緩存結構,會對所實現的搜索過程有重要影響:
1)按棧的方式保存和使用信息,實現的探索過程是每步選擇一種可能方向一直向前,直到無法前進才退回到此前最后選擇點,換路徑繼續(xù)該過程。
2)按隊列方式保存和使用信息,就是總從最早遇到的搜索點不斷拓展。這種方式倒不好理解。。。
問題表示和輔助結構
要解決上述迷宮問題,首先要設計一種問題表示形式。用計算機解決問題,第一步就是選擇合適的數據表示。python里可以用二維數組表示。迷宮入口和出口可以各用一對下標表示。
有一個情況需要注意:搜索過程有可能在某些局部路徑中兜圈子,這樣即使存在到出口的路徑,程序也可能無休止地運行卻永遠也找不到解。為防止出現這種情況,程序運行中必須采用某種方法記錄已經探查過的位置,方法是把對應矩陣元素標記為2。計算中發(fā)現元素值為2就不再去探索。(0表示可通行,1表示不可通行,2表示已探查)
還需要找到一種確定當前位置可行方向的技術。在到達某個位置后,搜索過程需要選一個方向前進。如果再次退回到這里就需要改走另一方向,直到所有方向都已探查為止,如果還沒找到出口就應該退一步。
這里需要記錄方向信息。顯然,每個格子有四個相鄰位置,為正確方便地處理這幾個可能方向,需要確定一種系統檢查方法。對于單元(i, j),其四個相鄰位置的數組元素下標如圖所示:(i 表示上下,j 表示左右)
為了方便地計算相鄰位置,需要定義一個二元組列表,其元素是從位置(i, j)得到其四鄰位置應該加的數對:dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
為了使算法的描述更加方便,先定義兩個簡單的輔助函數。其中參數pos都是形式為(i, j)的二元序對。
1 def mark(maze, pos): # 給迷宮maze的pos位置標2,表示“到過了” 2 maze[pos[0]][pos[1]] = 2 3 4 def passable(maze, pos): # 檢查迷宮maze的位置pos是否可行 5 return maze[pos[0]][pos[1]] == 05.5.2 求解迷宮的算法
迷宮的遞歸求解
示例,
1 def mark(maze, pos): # 給迷宮maze的pos位置標2,表示“到過了” 2 maze[pos[0]][pos[1]] = 2 3 4 def passable(maze, pos): # 檢查迷宮maze的位置pos是否可行 5 return maze[pos[0]][pos[1]] == 0 6 7 def find_path(maze, pos, end): 8 mark(maze, pos) 9 if pos == end: 10 print(pos, end=' ') 11 return True 12 13 for i in range(4): 14 nextP = (pos[0]+dirs[i][0], pos[1]+dirs[i][1]) 15 if passable(maze, nextP): 16 if find_path(maze, nextP, end): 17 print(pos, end=' ') 18 return True 19 return False 20 21 maze = [ 22 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 23 [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1], 24 [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1], 25 [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1], 26 [1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1], 27 [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1], 28 [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], 29 [1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1], 30 [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1], 31 [1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1], 32 [1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], 33 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 34 ] 35 pos = (1, 1) 36 end = (10, 12) 37 dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] 38 print(find_path(maze, pos, end))棧和回溯法
迷宮的回溯法求解
1 def mark(maze, pos): # 給迷宮maze的pos位置標2,表示“到過了” 2 maze[pos[0]][pos[1]] = 2 3 4 def passable(maze, pos): # 檢查迷宮maze的位置pos是否可行 5 return maze[pos[0]][pos[1]] == 0 6# 自己翻譯的, 7 def find_path(maze, start, end): 8 if start == end: 9 print(start) 10 return 11 ls = LStack() 12 ls.push(start) 13 while not ls.is_empty(): 14 pos = ls.pop() 15 for dir in dirs: 16 nextp = pos[0]+dir[0], pos[1]+dir[1] 17 if nextp == end: 18 print(nextp) 19 while not ls.is_empty(): 20 print(ls.pop()) 21 return 22 if passable(maze, nextp): 23 ls.push(pos) 24 ls.push(nextp) 25 break 26 mark(maze, pos) 27
# 書上的,在棧中保存的是(位置序對, 探索方向)序對。 28 def find_path2(maze, start, end): 29 if start == end: 30 print(start) 31 return 32 ls = LStack() 33 mark(maze, start) 34 ls.push((start, 0)) 35 while not ls.is_empty(): 36 pos, nxt = ls.pop() 37 for i in range(nxt, 4): 38 nextp = pos[0]+dirs[i][0], pos[1]+dirs[i][1] 39 if nextp == end: 40 print(nextp) 41 while not ls.is_empty(): 42 print(ls.pop()) 43 return 44 if passable(maze, nextp): 45 ls.push((pos, i+1)) 46 mark(maze, nextp) 47 ls.push((nextp, 0)) 48 break 49 print('not found') 50 51 maze = [ 52 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 53 [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1], 54 [1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1], 55 [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1], 56 [1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1], 57 [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1], 58 [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1], 59 [1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1], 60 [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1], 61 [1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1], 62 [1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1], 63 [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 64 ] 65 pos = (1, 1) 66 end = (10, 12) 67 dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)] 68 find_path(maze, pos, end)
兩種方法的區(qū)別在于標記的時機不同:方法1是標記當前位置,方法2是標記可達位置。兩種方法的關鍵點相同:一是棧中所存元素相同,一次操作入棧的都是當前位置和可達位置。二是到達出口后打印路徑的方式相同(書中沒給)。
5.5.3 迷宮問題和搜索
狀態(tài)空間搜索:棧和隊列
基于棧和隊列的搜索過程
?*深度和寬度優(yōu)先搜索的性質
5.6.1 幾種與棧或隊列相關的結構
雙端隊列
python的deque類
5.6.2 幾個問題的討論
?
轉載于:https://www.cnblogs.com/yangxiaoling/p/9924474.html
總結
- 上一篇: STS安装lombok
- 下一篇: python-23 xml.etree