队列:顺序队列和循环队列
和栈的先进后出不同,队列的形式是先进先出,队列的想法来自于生活中排队的策略, 顾客在付款结账的时候,按照到来的先后顺序排队结账。先来的顾客先结账,后来的顾客后结账。
队列有两种实现形式:1 顺序表实现 2 循环顺序表
首先来看下顺序表的实现,在python中,队列的实现用list来写十分的方便。实现方式如下:
class line_queue():
def __init__(self):
self._elem=[]
def push(self,elem):
self._elem.append(elem)
def pop(self):
elem=self._elem.pop(0)
return elem
def queue_length(self):
return len(self._elem)
和栈唯一的区别是,这里pop是pop(0),也就是首先进队列的数据出列。这个实现很简单,但是有一个问题,每次有元素出队列的时候,元素都必须要进行前移。这就带来了一个问题,它的操作复杂度为O(n),而不是O(1)。只有从尾部弹出元素也就是先进后出的时候复杂度为O(1).
那么如何才能满足O(1)的出列复杂度呢。我们可以考虑记住队头和队尾的位置。每次出队的时候直接将队头位置的元素弹出就可以了。具体的实现可以参考下图
下面来看下代码的实现:
class line_queue_update():
def __init__(self):
self._elem=[]
self.head=self.rear=0
def push(self,elem):
self._elem.append(elem)
self.rear+=1
def pop(self):
elem=self._elem[self.head]
self.head+=1
return elem
def queue_length(self):
return len(self._elem)
def get_elem(self):
print self._elem
if __name__=="__main__":
q=line_queue_update()
for i in range(10):
q.push(i)
print 'The length is %d' % q.queue_length()
q.pop()
q.pop()
q.push(90)
q.push(100)
q.push(200)
print 'The length is %d' % q.queue_length()
运行结果如下:
/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter5.py
The length is 10
The length is 13
这个方法的实现出队列的复杂度就是O(1)。但是我们同时也注意到另外一个问题,那就是尽管我们曾经pop元素。但是整个list的长度在不断的增加。这是为什么呢,是因为队头/对尾位置变量的值将随着操作移动。从操作效率来看,每个操作都在O(1)时间完成。但另一方面,表中元素序列随着操作向表尾方向移动,这就导致了在表的前端留下来了越来越多的空间,这些空间就是那些已经出队的元素曾经占据的空间。在c语言中,表元素存储的大小是固定的,经过反复的入队和出队操作。一定会在某次入队时出现队尾溢出也就是表满的情况。对于python而言,由于list是自动增长的,随着操作进行,表前端留下了越来越大的空区,而且这片空区永远也不会用到,完全浪费了。
前面介绍了顺序队列的两种实现方式,第一种时间效率无法满足,空间效率满足。 第二种时间效率满足,空间效率无法满足。两种策略都有各自的缺点,那么是否有一种方式既能满足时间效率也能满足空间效率呢。这就需要用到队列的第二种实现方式:循环顺序表
循环顺序表是在顺序表的第二种方式进行的变种。前面介绍过,通过head,rear记录表头和表尾元素的方式可以做到时间效率为O(1)但是会导致大量的空间浪费。但是如果我们将顺序表看做一种环形结构。那么空间浪费的问题就可以解决了,结构如下所示。
那么在这个结构里面,数据就保存在q.front到q.rear的位置里面。两个变量的差就是队列里面的元素个数。这种结构下元素始终在一个环里进行轮转。空位置可以被新插入的元素占据。
那么在环形结构中,需要注意以下3点:
1当队列为空或者为满的时候,q.front=q.rear。由于q.front=q.rear对应队空和队满两种情况,因此为了区分,可以通过队列长度来判断,当q.front=q.rear且q.len=q.max的时候认为为队满,否则为队空。
2 出队的时候q.front=(q.front+1)%q.len
3 入队的时候q.rear=(q.rear+1)%q.len
第2点和第3点的实现方式和顺序表不一样,在更新队头和队尾位置的时候需要进行求余操作,这是因为循环队列的空间大小固定,队头和队尾的位置是相对增长的,不是绝对增长的。下面来看下具体的实现代码
class squeue():
def __init__(self,init_len=8):
self._len=init_len
self._elem=[0]*init_len
self._head=0
self._rear=0
self._num=0
def is_empty(self):
return self._num == 0
def is_full(self):
return self._num == self._len
def dequeue(self):
if self.is_empty():
raise BaseException('queue is empty')
e=self._elem[self._head]
self._head=(self._head+1) % self._len
self._num-=1
return e
def enqueue(self,elem):
if self.is_full():
raise BaseException('queue is full')
self._elem[self._rear] = elem
self._rear=(self._rear+1) % self._len
self._num+=1
def get_elem(self):
print self._elem
if __name__=="__main__":
q=squeue()
for i in range(8):
q.enqueue(i)
q.get_elem()
q.dequeue()
q.enqueue(100)
q.get_elem()
首先插入8个元素,然后出队一个元素
/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter5.py
[0, 1, 2, 3, 4, 5, 6, 7]
[100, 1, 2, 3, 4, 5, 6, 7]
当继续插入元素的时候,q.enqueue(100)会提示队列已满
/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter5.py
Traceback (most recent call last):
File "/home/zhf/py_prj/data_struct/chapter5.py", line 196, in <module>
q.enqueue(100)
File "/home/zhf/py_prj/data_struct/chapter5.py", line 173, in enqueue
raise BaseException('queue is full')
BaseException: queue is full
我们可以添加一个扩充队列的操作,防止队列满导致插入失败
def expand(self):
old_len=self._len
self._len*=2
new_elems=[0]*self._len
for i in range(old_len):
new_elems[i] = self._elem[(self._head+i-1) % old_len]
self._elem,self._head,self._rear=new_elems,0,self._head+self._num
if __name__=="__main__":
q=squeue()
for i in range(8):
q.enqueue(i)
q.get_elem()
q.dequeue()
q.enqueue(100)
q.get_elem()
q.enqueue(200)
q.get_elem()
运行结果如下:
/usr/bin/python2.7 /home/zhf/py_prj/data_struct/chapter5.py
[0, 1, 2, 3, 4, 5, 6, 7]
[100, 1, 2, 3, 4, 5, 6, 7]
[100, 1, 2, 3, 4, 5, 6, 7, 0, 200, 0, 0, 0, 0, 0, 0]