队列也是一种容器,任何时候可访问的元素为最早存入队列而未出队的元素,队列是先进先出结构。
队列的实现
队列抽象数据结构
ADT Queue:
Queue(self) #创建空队列
is_empty(self) #判断是否为空
enqueue(self,elem) #元素elem入队
dequeue(self) #删除入队最早的元素,并返回
peek(self) #查看入队最早的元素,不删除
链接表实现
考虑到队列需要两端的高效操作,所以用带尾端指针的单链表来实现队列,尾端插入时间为 O(1) O ( 1 ) ,首端访问和删除也为 O(1) O ( 1 ) ;入队操作把元素加在链接表的尾端,出队操作在链接表的首端获取元素。
class Queue:
def __init__(self):
self._first = None
self._last = None
def is_empty(self):
return self._first is None
def enqueue(self, elem):
n = LNode(elem)
if self._first is None:
self._first, self._last = n, n
else:
self._last.next, self._last = n, n
def dequeue(self):
if self._first is None:
raise QueueUnderflow("The Queue is empty!")
elif self._first == self._last:
m = self._first
self._first, self._last = None, None
return m.elem
else:
m = self._first
self._first = self._first.next
return m.elem
def peek(self):
if self._first is None:
raise QueueUnderflow("The Queue is empty!")
else:
return self._first.elem
顺序表实现
根据顺序表的性质,不管哪边作为队列头部,入队出队总会有一个为 O(n) O ( n ) 操作,效率不是很理想。
循环顺序表
将顺序表看作一种环形结构,首尾相连,队列元素保存在环状结构中的一段。
-
q.elems
q
.
e
l
e
m
s
始终指向表的开始。
-
q.head
q
.
h
e
a
d
指向队列的首元素,
q.rear
q
.
r
e
a
r
指向队列结束的第一个空位。
- 队列元素保存在顺序表的一块连续单元里
[q.head:q.rear)
[
q
.
h
e
a
d
:
q
.
r
e
a
r
)
,两个变量对表长取模的和为队列长度。
-
q.head
q
.
h
e
a
d
与
q.rear
q
.
r
e
a
r
相同时队列为空;当表中位置都有元素时,
q.head
q
.
h
e
a
d
与
q.rear
q
.
r
e
a
r
也相同,但是队列已经满了,所以留一个空位出来,把
(q.rear+1)
(
q
.
r
e
a
r
+
1
)
%
q.len==q.head
q
.
l
e
n
==
q
.
h
e
a
d
时定义为队列满。
出入队的操作如下:
q.head = (q.head + 1) % q.len
q.rear = (q.rear + 1) % q.len
队列的list实现
由于表空而无法出队的异常:
class QueueUnderflow(ValueError):
pass
队列SQueue的基本设计:
- SQueue对象里用一个list类型的成分_elems存放队列元素。
- 用两个属性_head和_num分别记录队列首元素所在位置的下标和表中元素个数。
- 用list作为存储区,需要检查当前表是否已满,必要时换一个存储表,因此要记录当前表的长度_len。
class SQueue:
def __init__(self, init_len=8):
self._len = init_len #存储区首次默认长度为8
self._elems = [0] * init_len #存储区为8个[0]
self._head = 0 #首元素位置
self._num = 0 #元素总数
def is_empty(self):
return self._num == 0
def peek(self):
if self._num == 0:
raise QueueUnderflow
return self._elems[self._head]
def dequeue(self):
if self._num == 0:
raise QueueUnderflow
e = self._elems[self._head]
self._head = (self._head + 1) % self._len #出队时首元素链接后移一位,可能会超过储存区长度,所以进行取模运算
self._num -= 1
return e
def enqueue(self, e):
if self._num == self._len:
self.__extend()
self._elems[(self._head+self._num) % self._len] = e
self._num += 1
def __extend(self):
old_len = self._len
self._len *= 2
new_elems =[0]*self._len
for i in range(old_len): #把原存储区的元素按顺序从_head开始存入新存储区
new_elems[i] = self._elems[(self._head+i)%old_len]
self._elems, self._head = new_elems, 0
迷宫求解
迷宫问题的一般思路为:
- 从迷宫入口开始检查,这是初始位置。
- 如果当前位置为出口,则问题已解决。
- 从可行的方向选取一个方向,继续探寻,如果无路可走就返回前一个路口,选取下一个方向继续。
现在考虑一种简单形式的迷宫,可以直接映射到二维的
0/1
0
/
1
矩阵,空位置用0表示,障碍和边界用1表示;这里需要保存已经发现但尚未探索的分支信息,首先考虑使用栈或队列,采用不同的缓存结构,会对所实现的搜索过程有重要影响:
- 按栈的方式保存和使用信息,实现的探索过程是每步选择一种可能方向一直前进,直到无法前进才退回到此前最后选择点,换路径继续该过程。
- 按队列方式保存和使用信息,就是总从最早遇到的搜索点不断拓展。
问题表示和辅助结构
选择合适的数据表示,迷宫本身可以用一个元素值为
0/1
0
/
1
的矩阵表示,在
python
p
y
t
h
o
n
里可以用以
list
l
i
s
t
为元素的
list
l
i
s
t
表示,迷宫入口和出口各用一对下标表示。
为了防止搜索时在某个位置绕圈子,程序运行中必须记录已探查过的位置,在搜索过程中不断检查有关记录,确保不重复。在搜索过程中把检查过的位置标记为
2
2
,计算中发现元素值为非就是不能通行。
对于单元
(i,j)
(
i
,
j
)
,它的四个相邻位置分别为:
(i−1,j)
(
i
−
1
,
j
)
,
(i,j+1)
(
i
,
j
+
1
)
,
(i+1,j)
(
i
+
1
,
j
)
,
(i,j−1)
(
i
,
j
−
1
)
。为了方便得到四个位置,这里定义一个二元组的表:
dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]
对于任何一个位置
(i,j)
(
i
,
j
)
,给它加上dirs[0],dirs[1],dirs[2],dirs[3],就分别得到了该位置的四个相邻位置。
另外两个辅助函数:
给
maze
m
a
z
e
的位置
pos
p
o
s
标
2
2
表示已经检查过。
def mark(maze, pos):
maze[pos[0]][pos[1]] = 2
检查迷宫的位置 pos p o s 是否可行。
def passable(maze, pos):
return maze[pos[0]][pos[1]] == 0
求解迷宫
递归求解
因为
python
p
y
t
h
o
n
解释器为支持递归程序采用了运行栈,所以直接递归求解:
-
mark
m
a
r
k
当前位置
- 检查当前位置是否为出口,如果是,则成功结束
- 逐个检查当前位置的四邻是否可以通达出口
- 如果四周的探索都失败,报告失败
maze = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,1,1,0,0,0,1,0,0,0,1],
[1,0,1,0,0,0,0,1,0,1,0,1,0,1],
[1,0,1,0,1,1,1,1,0,1,0,1,0,1],
[1,0,1,0,0,0,0,0,0,1,1,1,0,1],
[1,0,1,1,1,1,1,1,1,1,0,0,0,1],
[1,0,1,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,0,1,1,1,0,1,0,1,1,0,1],
[1,0,1,0,1,0,1,0,1,0,1,0,0,1],
[1,0,1,0,1,0,1,0,1,1,1,1,0,1],
[1,0,1,0,0,0,1,0,0,1,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1]
]
#递归的方法如下:
def find_path(maze, pos, end):
mark(maze, pos) #首先改变该点数值
if pos == end: #判断该点是否为终点
print "the end is:", pos
return True
for i in range(4): #迭代改点周围的四个点
nextp = [pos[0] + dire[i][0], pos[1] + dire[i][1]]
if passable(maze, nextp): #判断是否可以走
if find_path(maze, nextp, end): #递归自身
print "the end is:", pos
return True
return False
回溯法求解
从入口开始搜索,遇到分支记录信息,然后继续探查,如果没有出口就回溯到分支处探查下一个方向。搜索中位置入栈有两种情况:1,把从入口到当前位置途径的所有位置入栈;2,只在栈里保存分支节点的未探查方向。
上图的迷宫中还有一种情况,环形路线,开始时一个节点有两个方向,顺着一个走到最后时,该节点不再存在未探查方向了;算法最后还要输出正确路径的所有位置。
入口start相关信息(位置和尚未探索的方向)入栈:
while 栈不空:
弹出栈顶元素作为当前位置继续搜索:
while 当前位置存在未探索方向:
求出下一探索位置nextp
if nextp 是出口:
输出路径并结束
if nextp 尚未探索:
将当前位置和nextp顺序入栈并退出内层循环
入栈用序对 (pos,nxt) ( p o s , n x t ) 表示, pos p o s 为坐标, nxt n x t 为整数,表示回溯到该位置的下一探索方向,为 dirs d i r s 的下标。
def maze_solver(maze, start, end):
if start == end:
print start
return
st = SStack()
mark(maze, start)
st.push((start, 0))
while not st.is_empty():
pos, nxt = st.pop()
for i in range(nxt, 4): #在for中依次循环当前位置的上下左右四个方向,如果有一个方向可以passable的话,把当前位置余下的方向入栈,再把这次探查的方向入栈,回到while循环,
#每次向前走一步就会把当前位置余下的探索方向压入栈;由于每次都mark了当前位置,所以回溯时,while每次取出栈中的非分支点都不会通过passable
#(非分支点只有前后两个方向,路过后前后都会改变标记),每次都可以回溯到分支节点。
nextp = (pos[0] + dirs[i][0], pos[1] + dirs[i][1])
if nextp == end:
print_path(end, pos, st)
return
if passable(maze, nextp):
st.push((pos, i+1))
mark(maze, nextp)
st.push((nextp, 0))
break
print "No path found."
队列求解
队列有先进先出的特性,每次把未探查的位置入队,再从队中取出检查相邻位置,未探查的位置再入队。
基本框架:
将start标记
start入队
while 队列中有未充分探查的位置:
取出一个位置pos
检查pos的相邻位置
遇到end成功结束
尚未探查的都mark并入队
队列为空,搜索失败
def maze_solver_queue(maze, start, end):
if start == end:
print "Path finds."
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)
print "No path."