棧和隊列


目錄

  • 一、概述
  • 二、棧:概念和實現
  • 三、棧的應用
  • 四、隊列
  • 五、迷宮求解和狀態空間搜索
  • 六、補充
  • 七、部分課后編程練習

一、概述

棧跟隊列都是保存數據的容器。還有前面的線性表。

棧和隊列主要用於計算過程中保存的臨時數據,如果數據在編程時就可以確定,那么使用幾個變量就可以臨時存儲,但是如果存儲的數據項數不能確定,就需要復雜的存儲機制。這樣的存儲機制稱為緩存。棧和隊列就是使用最多的緩存結構。

1、棧、隊列和數據使用順序

棧和隊列是最簡單的緩存結構,它們只支持數據項的存儲和訪問,不支持數據項之間的任何關系。因此,它們最重要的兩個操作就是存入元素和取出元素。

當然還有其他的操作,比如創建,檢查空(或者滿)的操作。

按照數據在時間上生成的先后順序進行不同的處理:

  • 后生成的數據需要先處理。就需要先進后出的數據結構進行存儲(棧,Last In First Out)。
  • 先生成的數據需要先處理。就需要先進先出的數據結構進行存儲(隊列,First In First Out)。

那么如何實現這兩種結構呢,最自然跟最簡單的實現方式就是使用線性表。

2、應用環境

很多,從略。python中可以直接使用list實現棧的功能。可以使用deque,實現隊列的功能。

二、棧:概念和實現

存入棧中的元素之間沒有任何具體關系,只有到來的時間先后順序。因此,就沒有元素的位置和元素的前后順序等概念。

棧的基本性質保證,在任何時刻可以訪問、刪除的元素都是在此之前最后存入的那個元素。

1、棧抽象數據對象

棧的線性表實現相關問題

在線性表的一端進行插入和刪除操作,這一端稱為棧頂,另一端稱為棧底。

對於順序表,后端插入和刪除是O(1)操作,因此考慮使用這一端作為棧頂。

對於鏈接表,前端插入和刪除是O(1)操作,因此用這端作為棧頂。

2、棧的順序實現

把list直接當做棧使用,也是可以的,只是這樣的對象跟list無法區分,並且提供了棧不應該支持的list所有操作,這樣就會威脅到棧的安全。

class StackUnderFlow(ValueError):
    '''
    棧下溢,也就是空棧訪問
    '''
    pass

class SStack:
    def __init__(self):
        self.elems = []
    
    def is_empty(self):
        return self.elems == []
    
    def push(self, elem):
        self.elems.append(elem)
    
    def pop(self):
        if self.is_empty():
            rasise StackUnderFlow('in SStack pop')
        return self.elems.pop()
    
    def top(self):
        if self.is_empty():
            raise StackUnderFlow('in SStack top')
        return self.elems[-1]
list實現的棧

3、棧的鏈接表實現

順序表實現的優缺點:

  • 優點:表元素放在一個連續的存儲塊內,方便管理。
  • 缺點:當擴大內存的時候,需要一次復制操作,這是一次高代價的操作。

采用鏈接表實現的優點,不需要一個連續的存儲塊就可以,而且沒有替換存儲的高代價操作。但是它的缺點零散的存儲比較依賴python的存儲管理,還有每個結點的鏈接也會帶來開銷。

class LStack:
    def __init__(self):
        self.top = None
    
    def is_empty(self):
        return self.top is None
    
    def push(self, elem):
        self.top = LNode(elem, slef.top)
    
    def pop(self):
        if self.is_empty():
            raise StackUnderFlow('in LStack pop')
        e = self.top.elem
        self.top = self.top.next
        return e
    
    def top(self):
        if self.is_empty():
            raise StackUnderFlow('in LStack top')
        return self.top.elem
鏈表實現的棧

三、棧的應用

棧的用途主要是兩方面:

  • 作為程序中的輔助結構,用於保存需要前進后出的數據。
  • 利用棧的先進后出的性質,做一些特定的事情。
s1 = SStack()
for i in list1:
    s1.push(i)
list2 = []
while not s1.is_empty():
    list2.append(s1.pop())
棧的小應用:反轉線性表的元素

1、簡單應用:括號匹配問題

問題

  • 考慮python程序中的括號匹配,這里只考慮三種括號,(){}[]。
  • 每種括號都包括一個開括號和一個閉括號,相互對應。
  • 對於括號里面的嵌套,也要正確匹配。

分析:

  • 由於程序中無法預知要處理的括號數量,因此不能使用固定的變量來進行保存,必須使用緩存結構。
  • 需要存儲的開括號的使用原則是后存入先使用,符合LIFO原則。
  • 如果一個開括號完成配對,就刪除這個括號。

實現過程:

  • 順序掃描檢查正文里的一個個字符。
  • 檢查的時候跳過無關的字符
  • 遇到開括號將其壓入棧中
  • 遇到閉括號,彈出棧頂元素與之配對。
  • 如果匹配成功繼續,直到正文結束。
  • 不過不匹配,以檢查失敗結束。
def check_parens(text):
    '''
    括號配對檢查函數,text是被檢查的正文串
    '''
    one_parens = '[{('
    opposite = {']':'[',')':'(','}':'{'}
    
    st = SStack()
    for s in text:
        if s in one_parens:
            st.push(s)
        if s in opposite and st.pop() != opposite[s]:
            return False
    if st.is_empty():  #書中沒有最后這一步的判斷。
        return True
    else:
        return False
括號匹配問題

2、表達式的表示,計算和變換

表達式和計算的描述

我們常用的表達式是用二元運算符連接起來的中綴表達式。例如:(1+2)*3

中綴表達式是習慣的表達式,但是它有一些缺點,比如不能准確描述計算的順序,通常需要借助一些輔助符號(比如圓括號),或者優先級(比如先乘除后加減)。

比如上面的例子中,如果沒有(),那么計算順序將會完全不一樣。

因此,當計算機處理表達式的時候,通過會把中綴表達式轉化為后綴表達式(逆波蘭表達式)。當然,還有一種表達式是前綴表達式(波蘭表達式)。

后綴表達式和前綴表達式都不需要括號或者優先級,或者結合性的規定。

例子:

中綴形式:(3-5) * (6+17*4) / 3
前綴形式:/ * - 3 5 + 6 * 17 4 3
后綴形式:3 5 - 6 17 4 * + * 3 /

后綴表達式的計算

分析后綴表達式的計算規則

  • 遇到運算對象,把它保存起來。
  • 遇到運算符,取出最近保存的兩個運算對象,進行運算,將結果保存起來。

遇到問題:

  • 使用什么結構保存數據
  • 表達式里的元素如何表示(可以字符串)
  • 處理失敗的情況。

解決方法:

  • 使用棧保存數據。
  • 表達式的元素使用字符串存儲
  • 在計算的過程中,如果棧中的元素不足兩個,那么操作就失敗(說明后綴表達式寫的是錯誤的)。還有,當計算完成之后,棧里最終只能有一個元素(也就是計算的結果)。

為判斷不足兩個元素的情況,必須給棧添加一個屬性(寫成方法也可以),就是棧的深度。

def suffix_exp_evaluator(line):
    '''
    將表達式的字符串變為項的表
    '''
    return suf_exp_evaluator(line.split())
    
class ESStack(SStack):
    @property
    def depth(self):
        return len(self.elems)
    
class suf_exp_evaluator(exp):
    oper = '-+*/'
    st = ESStack()
    
    for x in exp:
        if x not in oper:
            st.push(float(x))
            continue
        
        a = st.pop()
        b = st.pop()
        if x == '+':
            c = a + b
        elif x  == '-':
            c = b - a
        elif x  == '*':
            c = a * b
        elif x == '/':
            c = b / a
        else:
            break
        st.push(c)
        
    if st.depth == 1:
        return st.pop()
    raise SyntaxError('Extra operand(s)')
后綴表達式的計算

中綴表達式到后綴表達式的轉換

在中綴表達式轉換后綴表達式中要注意兩點:

  • 在掃描中綴表達式的過程中,如果遇到運算對象,就直接將它作為后綴表達式的一個項。
  • 把運算符放入后綴表達式要注意送出的時機,必須要處理好優先級和結合性的問題。
infix_operators = '+-*/()'
def tokens(lines):
    '''
    生成器函數,逐一生成line中的一個個項。項是浮點數或者運算符。
    本函數不能處理一元運算符,也不能處理帶符號的浮點數。
    '''
    i, llen = 0, len(line)
    while i < llen:
        while i < llen and line[i].isspace():
            i += 1
        if i >= llen:
            break
        if line[i] in infix_operators:
            yield line[i]
            i += 1
            continue
        j = i + 1
        
        while (j < llen and not line[j].isspace() and line[j] not in infix_operators):
            if ((line[j] == 'e' or line[j] == 'E') and j+1 < llen and line[j+1] == '-'):  # 處理負指數
                j += 1
            j += 1
        
        yield line[i:j]
        i = j

def trans_infix_suffix(line):
    st = SStack()
    exp = []

    for x in tokens(line):  # tokens是一個待定義的生成器
        if x not in infix_operators:  # 運算對象直接送出
            exp.append(x)
        elif st.is_empty() or x == '(':  # 左括號進棧
            st.push(x)
        elif x == ')':  # 處理右括號的分支
            while not st.is_empty() and st.top() != '(':
                exp.append(st.pop())
            if st.is_empty():  # 沒找到左括號,就是不配對
                raise SyntaxError("Missing '('.")
            st.pop()  # 彈出左括號,右括號也不進棧
        else:  # 處理算術運算符,運算符都看作是左結合
            while (not st.is_empty() and
                   priority[st.top()] >= priority[x]):
                exp.append(st.pop())
            st.push(x)  # 算術運算符進棧

    while not st.is_empty():  # 送出棧里剩下的運算符
        if st.top() == '(':   # 如果還有左括號,就是不配對
            raise SyntaxError("Extra '('.")
        exp.append(st.pop())

    return exp
中綴表達式轉換為后綴表達式

3、棧與遞歸

遞歸滿足兩個條件:

  • 調用自身
  • 結束遞歸的條件

遞歸調用的過程

以階乘函數為例:

def fact(n):
    if n == 0:
        return 1
    retrun n * fact(n-1)

看一下fact(3)的計算過程:

  • 如果要得到fact(3)的結果,必須先算出fact(2)。
  • 在計算fact(3)時參數n是3,在fact(2)時,參數n是2。如此遞歸下去。
  • 那么這個n是不斷變化的,因此是需要提前保存的。
  • 顯然,n的個數不能確定,所以不能使用變量來保存。
  • 在這樣一系列的遞歸調用中,n的保存性質符合后進先出的條件,因此需要一個棧來保存與n相關的信息。

我們把這種棧稱為程序運行棧。

 

上面兩幅圖分別描述了fact(3)的調用和返回過程和它的程序運行棧信息變化。

棧和遞歸/函數調用

一般而言,對於遞歸函數,其執行的時候都有局部狀態,包括函數的形參和局部變量等數據,遞歸函數在調用自己之前需要先把這些變量保存在程序運行棧中,以備后來使用。

編程語言實現遞歸函數就是運行一個棧,每個遞歸函數調用都在棧上開辟一塊區域,稱為函數幀,用來保存相關信息。位於棧頂的幀是當前幀,所有局部變量都在這里。在進入下一個遞歸調用時,再建立一個新幀。當函數從下一層遞歸中返回時,遞歸函數的上一層執行取得下層函數調用的結果,執行彈出已經結束的對於的幀,然后回到調用前的那一層的執行狀態。

其實不只是遞歸函數,實際上一般函數的調用和退出的方式也與此類似。例如上圖中,在f里調用g,在g中調用h,在h中調用r。也是使用程序運行棧來保存局部數據。

當然,一般函數的調用跟遞歸函數調用還是有不同的,比如一般函數調用的每個函數局部狀態不同,因此,他們的參數個數,局部變量的個數就不同,那么在分配函數幀的容量時就不同。

棧與函數調用*

在程序執行中,函數的嵌套調用是按照‘后調用先返回’的規則進行的,這種規則符合棧的使用模式,因此棧可以很自然的支持函數調用的實現。

函數調用時的內部動作分兩部分:

  • 新函數調用之前,保存一些信息。(前序動作)
  • 退出函數調用時,恢復調用前的狀態。(后序動作)

函數的前序動作包括:

  • 為被調用函數的局部變量和形式參數分配存儲區(函數幀)。
  • 將所有實參和函數的返回地址存入函數幀。
  • 將控制轉到被調用的函數入口。

函數的后序動作包括:

  • 將被調用函數的計算結果存入執行位置。
  • 釋放被調用函數的函數幀。
  • 按以前保存的返回地址將控制轉回調用函數。

每次函數的調用都要執行這些動作,因此可以看出函數調用是有代價的。

遞歸與非遞歸

對於遞歸函數的調用,實際上就是把信息保存到運行棧中,然后不斷執行函數體的那段代碼。因此,完全可以修改遞歸函數,將其變成一個非遞歸函數。

def norec_fact(n):
    '''
    自己管理棧來模擬函數調用的過程。
    '''
    res = 1
    st = SStack()
    while n > 0:
        st.push(n)
        n -= 1
    while not st.is_empty():
        res *= st.pop()
    return res
階乘的循環實現

遞歸函數和非遞歸函數

任何一個遞歸函數都可以通過引入一個棧來保存中間結果的方式,翻譯為一個非遞歸函數。同理,任何一個包含循環的程序也可翻譯成一個不包含循環的遞歸函數。

在目前的新型計算機上,函數調用的效率多半都可以接受,通常如果遞歸的思想比較簡單,不一定要用非遞歸函數定義。當然,除了一些對效率要求特別高的特殊情況之外。

棧的應用:簡單背包問題

問題:一個背包可以放重量為w的物品,現在有n件物品,它的集合是S,重量分別是w0,w1,...wn-1。然后,從中挑選出若干件物品,它們的重量之和正好等於w。如果存在,就說這個背包問題有解,如果不存在就無解。

先考慮遞歸思想的求解思路。
現在用knap(w, n)表示n件物品相對於總總量w的背包問題,那么在通常考慮一件物品選還是不選,有兩種情況:

  • 如果不選最后一件物品wn-1,那么knap(w, n-1)的解就是knap(w, n)的解,如果找到了前者的解,就找到了后者的解。
  • 如果選擇最后一件物品,那么knap(w-wn-1, n-1)有解,其解加上最后一件物品就是knap(w, n)的解,也就是前者和后者都有解。

那么n件物品的背包問題就變成了n-1件物品的背包問題。區別就是,一種情況是重量一樣,種類減一。一種情況是重量少了,種類減一。

def knap_rec(weight, wlist, n):
    if weight == 0:
        return True
    if weight < 0 or (weight > 0 n < 1):
        return False
    if knap_rec(weight-wlist[n-1], wlist, n-1):
        return True
    if knap_rec(weight, wlist, n-1):
        return True
    else:
        return False
簡單背包問題遞歸實現

四、隊列

1、隊列抽象數據類型

2、隊列的鏈接表實現

沒有尾指針的實現,如果把鏈表尾端作為隊頭,那么加入隊列的時間復雜度復雜度是O(n),而從隊列中取出元素的操作(也就是從鏈表首端刪除第一個元素)時間復雜度是O(1)。

如果有尾指針,上面的兩個操作都是O(1)的時間復雜度。

具體實現很簡單,跟前面的鏈表類似。

3、隊列順序表實現

基於順序表實現的問題

利用順序表現有的結構實現隊列:

  • 使用順序表的尾端實現隊列的入隊操作(append(elem))O(1),使用順序表的首端實現隊列的出隊操作(pop(0))O(n)。
  • 使用順序表的首端實現隊列的入隊操作(insert(0, elem))O(n),使用順序表的尾端實現隊列的出隊操作(pop())O(n)。

這兩種情況都有一個O(n)操作,因此都不理想。

還有一種是在隊首出隊之后,元素不前移,只需要記住新隊頭的位置就可以。這個設計雖然能達到O(1)的操作,但是有一個致命的問題,隨着不斷的入隊元素,隊列前面會有越來越多的空位,並且這些空位永遠不會被利用,這就浪費了存儲資源。詳情見下圖。

因此,以上的設計都不是最合理的。

循環順序表

如果把順序表看做一個環形結構,認為其最后存儲位置之后是最前的位置,形成一個環形。那么就會循環利用空間,解決了隊首出現空位的情況。

上圖是一個包含8個元素的順序表

  • 在隊列使用中,順序表的開始位置並不改變。q.elems始終指向表元素的開始。
  • 隊頭變量q.head記錄隊列里的第一個元素的位置。q.rear記錄當前隊列里最后元素的第一個空位。
  • 隊列元素保存在順序表的一段連續單元里[q.hear:q.rear]。兩個變量之差取模就是隊列里元素的個數。

注意變動操作維護上面變量的值,出隊和入隊時變量更新操作。
q.head = (q.head+1)%q.len
q.rear = (q.rear)%q.len

隊空操作。q.head == q.rear
隊滿操作。(q.rear+1)%q.len = q.head

此外,當隊滿的時候進行存儲空間擴充。這里很難利用list的自動擴充機制,因此需要自己管理list的存儲。

4、隊列list實現

數據不變式

隊列里設計到四個屬性,head,rear,num,len。還有一些變動屬性的操作。因此,在進行變動操作的時候就應該遵守數據不變式的原則,維護數據的正確性。

數據不變式說明對象的不同屬性,描述它們應該滿足的邏輯約束關系,如果一個對象的成分取值滿足數據不變式,就說明這是一個狀態完好的對象。

隊列類的實現

class SQueue:
    def __init__(self, init_len=8):
        self.head = 0
        self.num = 0
        self.len = init_len
        self.elems = [0] * self.len
    
    def is_empty(self):
        return self.num == 0
    
    def peek(self):
        if self.is_empty():
            raise ValueError
        return self.elems[(self.head]
    
    def dequeue(self):
        if self.is_empty():
            raise ValueError
        e = self.elems[self.head]
        self.head  = (self.head + 1) % self.len
        self.num -= 1
        return e
    
    def enqueue(self, elem):
        # 表滿的時候,需要擴充。
        if self.num == self.len:
            self.extend()
        self.elems[(self.head + self.num)%self.len] = elem
        self.num += 1
    
    def extend(self):
        '''
        表的擴充采用2倍的策略。並且是新表的頭是從下邊0開始。
        也可以采用新表的頭跟原表的下標一樣的策略。看設計需求。
        '''
        old_len = self.len
        self.len *= 2
        new_elems = [0] * self.len
        for i in range(old_len):
            new_elems[i] = self.elems[(self.head+i)%old_len]
        self.elems, self.head = new_elems, 0
循環順序表實現隊列

5、隊列的應用

  • 文件打印。打印機是文件系統,由於多個進程共享一個文件系統,因此必須使用隊列等待一個一個排隊打印。
  • 萬維網服務器。將來不及處理的請求放到一個待請求的隊列中。等系統調用。
  • windows系統和消息隊列。不同進程或者線程通信。
  • 離散事件系統模擬。

五、迷宮求解和狀態空間搜索

1、迷宮求解:分析和設計

迷宮問題

問題:給定一個迷宮圖,包括圖中的一個入口點和出口點。要求找到一條從入口到出口的路徑。

初步分析:

  • 從迷宮的入口開始檢查,也就是初始位置開始檢查。
  • 如果找到出口,問題解決。
  • 如果當前方向無路可走,檢查失敗,需要按照一定方式另外繼續搜索。
  • 從可行方向中取一個方向繼續前進,從那找到出口路徑。

迷宮問題分析

分析問題

  • 迷宮里面有一集位置,其中有些可以通行的空位置相互直接連通(表現為陣列中鄰接的空格,或者矩陣里鄰接的0元素),一步可達。
  • 一個空位置有幾個相鄰元素,也就是幾個可能前進的方向。
  • 最終目標是找到從入口到出口的一條路徑。
  • 為找到所需路徑,可能更需要逐一探查前進的不同可能性。

這個過程顯然需要緩存一些信息:如果當前位置有多個方向可供探查,但是下一步只能探查一個方向,因此需要記錄其他暫時不能探查的方向,以后后面使用。

那么該使用什么方式進行緩存呢?

存在兩種不同的方式,可以比較冒進,也可以穩扎穩打。

  • 如果使用棧的方式緩存,實現的探索過程就是每一步選擇一種可能的方向,直到無法前進,退回到此前最后的選擇點,更換方向繼續探索。也就是所謂的冒進型。
  • 如使用隊列方式保存,就是總是從最早遇到的索索點不斷拓展。也就是穩扎穩打型。

問題表示和輔助結構

迷宮本身使用一個元素值為0/1的矩陣表示,在python中可以用嵌套list表示。迷宮的入口和出口各用一個下標表示。

有一個特殊情況,在探索過程中可能會出現兜圈子的情況,為防止這種情況,必須采用某種方法記錄已經探查過的位置信息。

這種位置信息有兩種方式表示:

  • 使用另外一種專門結構保存這種信息。
  • 把已經檢查過的信息直接標記在地圖上。

現在假設使用一個矩陣表示迷宮圖,0表示通路,1表示非通路,2表示已經探查過的路徑。

表示當前位置的可行方向。假設用一個二元組表示元素位置(i,j)。i和j分別元素是在矩陣中的下標。那么它的四個方向通過上圖就可以表示出來。

此外,還應確定一個位置可探查方向的順序,在這里規定為‘東南西北’。

2、求解迷宮的算法

迷宮的遞歸求解

dirs = [(0,1), (1,0), (0,-1), (-1,0)]  #按照東南西北的順序,給出它與四個方向的差距值。
def mark(maze, pos):
    '''
    用來標記走過的路徑。
    '''
    maze[pos[0]][pos[1]] = 2

def passable(maze, pos):
    '''
    檢查是否可行
    '''
    return maze[pos[0]][pos[1]] == 0

def find_path(maze, pos, end):
    mark(maze, pos)
    if pos == end:
        print(pos, end=' ')
        return True
    for i in range(4):
        nextp = pos[0]+dirs[i][0], pos[1]+dirs[i][1]
        if passable(maze, nextp):
            if find_path(maze, nextp, end):
                print(pos, end=' ')
                return True
    return False
遞歸求解迷宮

棧和回溯法

回溯法在工作中執行兩種基本動作:前進和后退。

前進:

  • 條件:當前位置存在尚未探查的四鄰位置。
  • 操作:選定下一個位置並向前探查。如果還存在其他可能未探查的分支,就記錄相關的信息,以便將來使用。
  • 如果找到出口,成功結束。

后退:

  • 條件:遇到死路,不存在尚未探查的四鄰位置。
  • 操作:退回最近記錄的那個分支點,檢查那里是否有分支,如果有,就取下一個未探查鄰位置作為當前位置並前進,沒有就將其刪除並繼續回溯。
def maze_solver(maze, start, end):
    if start == end:
        print(start)
        return 
    st = SStack()
    mark(maze, start)
    st.push((start, 0))  # 0指的是需要從哪個方向開始探索。
    while not st.is_empty():
        pos, nxt = st.pop()
        for i in range(nxt, 4):
            nextp = pos[0] + dirs[i][0], pos[1] + dirs[i][1]
            if nextp == end:
                print(end, pos, st)
                return 
            if passable(maze, nextp):
                st.push((pos, i+1)) # 將原位置和下一個方向存入棧中。
                mark(maze, nextp)
                st.push((nextp, 0))
                break
棧求解迷宮

關於這個算法,有兩點說明:

  • 發現一個新位置之后,先標記它,然后再將其壓入棧中。保證不會兜圈子。
  • 一個棧的下一個元素總是到達它路徑上的前一個位置,這是搜索中的一個不變性質,保證了棧里的元素構成一條路徑。

3、迷宮問題和搜索

其實迷宮問題,是一類問題的代表,這類問題稱為狀態空間搜索問題。基本特征:

  • 存在一集可能狀態。
  • 有一個初始狀態s0,一個或多個結束狀態,或者右判斷成功結束的方法。
  • 對於每個狀態s,都有表示與s相鄰的一組狀態。
  • 有一個判斷函數valid判斷s是否是可行狀態。
  • 問題:找出從s0觸發到達某個結束狀態的路徑,或者從s0出發,設法找到一個或者全部解。

狀態空間搜索:棧和隊列

用計算的方法解決問題,根據人們對於問題的認識深度,存在兩種不同的處理方法:

  • 如果對問題研究比較深入,可以做出一個專門的算法用來解決問題。
  • 但更多的情況是對問題認識不夠全面,這樣就有可能把該問題轉化成一個狀態空間的搜索問題。

搜索法就是一種通用問題的求解方法。但是在搜索的過程中需要緩存一些已知信息,原則上棧跟隊列都可以使用,但是不同的選擇,會對搜索進展方式有不同的影響。

在前面的迷宮算法里使用的是棧,它的特點是后進先出,因此會產生回溯。

如果使用隊列作為緩存,那么實際上是一種從各種可能齊頭並進的搜索。這個過程中沒有回溯,只是一種逐步擴張的過程。

基於隊列的迷宮求解算法

def maze_solver_queue(maze, start, end):
    if start == end:
        return 
    
    qu = SQueue()
    mark(maze, start)
    qu.enqueue(start)
    while not qu.is_empty():
        pos = qu.dequeue()
        for i in range(4):
            nextp = pos[0] + dirs[i][0], pos[1] + dirs[i][1]
            
            if passable(maze, nextp):
                if nextp == end:
                    print('Path find')
                    return
                mark(maze, nextp)
                qu.enqueue(nextp)
隊列求解迷宮

上面的算法有一個問題沒有解決,就是找到的路徑沒有存儲。如果想只要找到一條路徑,必須另行記錄與路徑有關的信息。可以考慮使用字典結構實現

def maze_solver_queue(maze, start, end):
    if start == end:
        return 
    
    qu = SQueue()
    mark(maze, start)
    dic = {}
    qu.enqueue(start)
    while not qu.is_empty():
        pos = qu.dequeue()
        for i in range(4):
            nextp = pos[0] + dirs[i][0], pos[1] + dirs[i][1]
            
            if passable(maze, nextp):
                dic[nextp] = pos  # 記錄搜索過程中的對象關系key是后一元素,value是前一個元素
                if nextp == end:
                    # 根據記錄關系,從后往前推,找到路徑
                    p = end
                    while p != start:
                        p = dic[p]
                        print(p)
                    return
                mark(maze, nextp)
                qu.enqueue(nextp)
隊列求解迷宮並輸出路徑信息

基於棧和隊列的搜索過程

上圖中分別是迷宮示例中基於棧和隊列的兩種不同的搜索路徑圖。其中棧的特點是,先找一個方向,直到不能再繼續的時候,返回最近的那個分叉處,繼續探索。因此它是深度優先的搜索。

隊列的特點是一步一步搜索,如果有通路就進行前一步探索,不會出現回溯現象。因此它是寬度優先的搜索。

深度和寬度優先搜索的性質

  • 假設搜索問題有解,能否保證找到解。深度優先總是沿着一條路徑前行,如果存在無窮多的狀態無解區域,那么即是其他地方有解,深度優先算法也有可能找不到。寬度優先的算法是這樣的,它是從近到遠進行掃盪,只要存在到達解的有窮長路徑,這種算法就能找到最優解(一定是最短路徑的)。
  • 如果找到解,如何得到相應路徑。深度優先遍歷出棧中的路線即可。寬度優先需要借助其他方法記錄相關信息。
  • 搜索所有可能的解和最優解。深度優先在找到一個解之后可以回溯,找到所有解,直到遍歷完整個狀態找到所有的解。然后再選出最有解。寬度優先找到的第一個解就是最優解,當然也可以繼續搜索,找到其他解。
  • 搜索的時間開銷。求解搜索問題的時間代價受限於狀態空間的規模,實際求解的代價正比於找到解之前訪問的狀態個數。
  • 搜索的空間開銷。對於深度優先搜索,所需要的棧空間由找到一個解之前遇到過的最長的那一條搜索路徑確定。對於寬度優先搜索,所需的隊列空間由搜索過程中可能路徑分支最多的那一層確定。

如果一個問題可使用空間搜索方法解決,那是用深度優先還是寬度優先,需要根據具體情況,也就是上面的性質來進行選擇。

六、補充

1、與棧和隊列相關的結構

雙端隊列

deque,它允許在兩端插入和刪除元素。
雙端隊列的實現方式

  • 雙鏈表可以支持在兩端都是O(1)時間的插入和刪除元素。
  • 循環表。也可以支持O(1)時間的兩端插入和刪除。

python的deque類

python標准庫中collections定義的deque類型,提供了一種雙端隊列的操作,它是采用雙鏈表的技術實現的,但是其中每個鏈接結點里順序存儲一組元素。

2、問題討論

問題:對於棧和隊列,既然鏈表能實現統一的O(1),那么為什么還要考慮順序實現呢?

答:因為計算機的分級緩存結構(參考csapp第1章的存儲管理)。如果連續進行一批內存訪問是局部的,操作速度就會很快。因此,在考慮程序的效率時,一個重要線索就是對計算機內存使用的局部化。而鏈接結構的一個特點是,其中結點在內存中是任意分散的,因此,對鏈表元素的訪問是在很多位置的內存單元跳來跳去的,因此它的效率沒有順序表實現的高。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM