重學資料結構之佇列

TM0831發表於2020-05-12

一、佇列的概念

1.基本概念

  佇列(queue)又被稱為隊,也是一種儲存資料元素的容器。佇列時一種特殊的線性表,只允許在表的前端(front)進行刪除操作,只允許在表的後端(rear)進行插入操作,進行刪除操作的一端叫做對頭,進行插入操作的一端稱為隊尾。

  佇列按照先進先出的原則(FIFO,First In First Out)儲存資料,先存入的元素會先被取出來使用。在佇列中插入一個佇列元素稱為入隊,從佇列中刪除一個佇列元素稱為出隊。

2.抽象資料描述

  佇列的基本操作和棧是類似的,只不過操作名稱有些許不一樣,下面是一個基本佇列的抽象資料描述:

ADT Queue:

  Queue(self)  # 建立一個空佇列

  is_empty(self)  # 判斷佇列是否為空

  enqueue(self, elem)  # 入隊,向佇列中插入一個元素

  dequeue(self)  # 出隊,從佇列中刪除一個元素

  peek(self)  # 檢視對頭位置的元素

3.用 Python 實現

 1 # 自定義佇列
 2 class MyQueue:
 3     def __init__(self):
 4         self.data = []
 5 
 6     def is_empty(self):
 7         return len(self.data) == 0
 8 
 9     def enqueue(self, elem):
10         self.data.append(elem)
11 
12     def dequeue(self):
13         if self.data:
14             ret, self.data = self.data[0], self.data[1:]
15             return ret
16         return None
17 
18     def peek(self):
19         return self.data[0] if self.data else None

 

二、佇列

1.順序佇列

  順序佇列是佇列的順序儲存結構,順序佇列實際上是運算受限的順序表。和順序表一樣,順序佇列使用一個向量空間來存放當前佇列中的元素。由於佇列的隊頭和隊尾的位置是變化的,設定兩個指標front和rear分別指示隊頭元素和隊尾元素的位置,它們的初值在初始化時都置為0。

2.迴圈佇列

  迴圈佇列將向量空間想象為一個首尾相接的迴環,在迴圈佇列中,由於入隊時隊尾指標向前追趕隊頭指標,出隊時隊頭指標追趕隊尾指標,因而隊空和隊滿時頭尾指標均相等。

3.雙端佇列

  雙端佇列是一種具有佇列和棧的性質的資料結構。雙端佇列中的元素可以從兩端彈出,其限定插入和刪除操作在表的兩端進行。雙端佇列是限定插入和刪除操作在表的兩端進行的線性表。

4.優先順序佇列

  在優先順序佇列中,會給每一個元素都分配一個數字用來標記其優先順序,例如給其中最小的數字以最高的優先順序,這樣就可以在一個集合中訪問優先順序最高的元素並對其進行查詢和刪除操作了。

  使用 Python 實現一個優先順序佇列,可以藉助 Python 中的 heapq 模組來實現,heapq 是一個二叉堆的實現,其內部使用內建的 list 物件,對於列表中的每一個元素都滿足 a[k] <= a[2 * k + 1] and a[k] <= a[2 * k + 2],因此其預設是一個最小堆,a[0] 是佇列中最小的元素。下面是利用 heapq 模組實現的一個優先順序佇列程式碼示例:

 1 # 自定義優先順序佇列
 2 class PriorityQueue:
 3     def __init__(self):
 4         self.data = []
 5         self.index = 0
 6 
 7     def push(self, elem, priority):
 8         """
 9         入隊,將元素插入到佇列中
10         :param elem: 待插入元素
11         :param priority: 該元素的優先順序
12         :return:
13         """
14         # priority 加上負號是因為 heapq 預設是最小堆
15         heapq.heappush(self.data, (-priority, self.index, elem))
16         self.index += 1
17 
18     def pop(self):
19         """
20         出隊,從佇列中取出優先順序最高的元素
21         :return:
22         """
23         return heapq.heappop(self.data)[-1]
24 
25     def size(self):
26         """
27         獲取佇列中元素數量
28         :return:
29         """
30         return len(self.data)
31 
32     def is_empty(self):
33         """
34         判斷佇列是否為空
35         :return:
36         """
37         return len(self.data) == 0

 

三、佇列的應用

1.迷宮問題

1)問題描述

  將一個迷宮對映成一個由0和1組成的二維矩陣,迷宮裡的空位置用0來表示,障礙和邊界用1來表示,最左上角為入口,最右下角為出口,求是否能從迷宮中走出來?

2)問題分析

  首先在演算法初始時,可行的位置用0標識,不可行的位置用1標識,但在搜尋路徑的過程中需要將已經走過的位置給標記上,這裡可以用數字2來標記,在後面的搜尋過程中碰到2也就不會重複搜尋了。

  當到達一個位置時,需要確定該位置的可行方向,而除了邊界點,每個位置都有四個可行的方向需要探索,例如對於座標點(i,j),其四個相鄰位置如下圖:

  

  為了能夠方便的計算相鄰位置,可以用一個列表來記錄:

DIR = [(0, 1), (1, 0), (0, -1), (-1, 0)]

  對於任何一個位置(i,j),都可以分別加上 DIR[0]、DIR[1]、DIR[2]、DIR[3],就能得到相鄰位置了。

  為了使演算法變得簡單,這裡可以先定義兩個輔助用的函式,一個用於標記走過的點,一個用於判斷輸入的位置是否可以通行,具體程式碼如下:

 1 def mark(maze, pos):
 2     """
 3     將迷宮中已經走過的位置進行標記,設定為2
 4     :param maze: 迷宮
 5     :param pos: 位置
 6     :return:
 7     """
 8     maze[pos[0]][pos[1]] = 2
 9 
10 
11 def passable(maze, pos):
12     """
13     檢車迷宮中指定位置是否能走
14     :param maze: 迷宮
15     :param pos: 位置
16     :return:
17     """
18     if pos[0] < len(maze) and pos[1] < len(maze[0]):
19         return maze[pos[0]][pos[1]] == 0
20     return False

3)遞迴演算法求解

  在使用遞迴演算法求解的過程中,對於每一個位置,都有如下的演算法過程:

  • 標記當前位置;
  • 檢查當前位置是否為出口,如果則表明找到路徑,演算法結束,不是則進行下一步;
  • 遍歷該位置的相鄰位置,使用遞迴呼叫自身;
  • 如果相鄰位置都不可行,表明無法從迷宮中走出來,演算法結束。

  遞迴演算法的核心函式程式碼如下:

 1 def solution(maze, pos, end):
 2     """
 3     遞迴演算法的核心函式
 4     :param maze: 迷宮,二維矩陣
 5     :param pos: 當前位置
 6     :param end: 出口
 7     :return:
 8     """
 9     mark(maze, pos)
10     if pos == end:
11         print(pos)
12         return True
13     for i in range(4):
14         next_pos = pos[0] + DIR[i][0], pos[1] + DIR[i][1]
15         if passable(maze, next_pos):
16             if solution(maze, next_pos, end):
17                 print(pos)
18                 return True
19     return False

4)使用佇列求解

  基於佇列的求解演算法還是使用之前的表示方法和輔助函式,但演算法過程有變化。

  首先將入口標記為已經到過併入隊,然後當佇列裡還有未探索的位置時,取出一個位置並檢查該位置的相鄰位置,當相鄰位置可行時,如果這個位置就是出口則結束演算法,否則加入到佇列中,當整個佇列中的元素都被取出來後,佇列為空,表示未找到從迷宮中走出來的路徑,結束演算法。使用佇列求解迷宮問題的演算法程式碼如下:

 1 def queue_solution(maze):
 2     """
 3     使用佇列求解迷宮問題
 4     :param maze: 迷宮,二維矩陣
 5     :return: 
 6     """
 7     start, end = (0, 0), (len(maze) - 1, len(maze[0]) - 1)
 8     q = MyQueue()
 9     mark(maze, start)
10     q.enqueue(start)
11     while not q.is_empty():
12         pos = q.dequeue()
13         for i in range(4):
14             next_pos = pos[0] + DIR[i][0], pos[1] + DIR[i][1]
15             if passable(maze, next_pos):
16                 if next_pos == end:
17                     print("Find Path!")
18                     return True
19                 mark(maze, next_pos)
20                 q.enqueue(next_pos)
21     print("No Path!")
22     return False

2.生產者消費者模式

  所謂生產者消費者模式,簡而言之就是兩個執行緒,一個扮演生產者,負責產生資料,另一個扮演消費者,不斷獲取這些資料並進行處理。而只有生產者和消費者,還算不上生產者消費者模式,還需要有一個緩衝區位於兩者之間,生產者把資料放入緩衝區,消費者從緩衝區中取出資料。使用生產者消費者模式有幾個好處:解耦,支援併發等。

  在編寫爬蟲爬取資料時,就可以使用該模式,一個模組不斷獲取 URL,另一個模組就負責傳送請求和解析資料,而在其中就可以使用佇列作為緩衝區,將待爬取的 URL 都存放在這個佇列中,然後爬取模組再從中取出 URL 進行爬取,直至佇列為空。這裡可以參考我以前寫過的一篇部落格:https://www.cnblogs.com/TM0831/p/10510319.html

 

相關文章