数据和算法【栈和队列】
概述
栈和队列都是保存数据元素的容器,这就意味着可以把元素存入其中,或者从中取出元素使用。这两种结构支持的元素访问数据操作包括查看,即只是取得元素的有关信息;还包括元素弹出,即在取得元素的同时将其从容器中删除。
栈、队列和数据使用顺序
栈和队列也是最简单的缓存结构,它们只支持数据项的存储和访问,不支持数据项之间的任何关系。因此,这两种结构的操作集合都很小,很简单,其中最重要的就是存入元素和取出元素两个操作。作为数据结构,它们还需要提供几个任何数据结构都需要的操作,如结构的创建、检查空状态等。
栈和队列的实现只需要保证元素存入和取出的顺序,并不需要记录或保证新存入的元素与容器中已有元素之间的任何关系:
栈是保证元素后进先出(Last In First Out, LIFO)关系的结构,简称为LIFO结构。
队列是保证元素先进先出(First In First Out, FIFO)关系的结构,简称为FIFO结构。
栈:概念和实现
栈(stack,在一些书籍里称为堆栈)是一种容器,可存入数据元素、访问元素、删除元素等。存入栈中的元素之间相互没有任何具体关系,只有到来的时间先后顺序。在这里没有元素的位置、元素的前后顺序等概念。
栈的基本性质保证,在任何时刻可以访问、删除的元素都是在此之前最后存入的那个元素。因此,栈确定了一种默认元素访问顺序,访问时无需其他信息。
栈的顺序表实现
python的list及其操作实际上提供了与栈的使用方式有关的功能,可以直接作为栈来使用。相关情况可以如下考虑:
1)建立空栈对应于创建一个空表[],判空栈对应于检查是否为空表。
2)由于list采用动态顺序技术(分离式实现),作为栈的表不会满。
3)压入元素操作应在表的尾端进行,对应于lst.append(x)。
4)访问栈顶元素应该用lst[-1]。
5)弹出操作也应该在表尾端进行,无参的lst.pop()默认弹出表尾元素。
class StackUnderflow(ValueError): pass class SStack(object): def __init__(self): self._elems = [] def is_empty(self): return self._elems == [] def top(self): if self._elems == []: raise StackUnderflow("in SStack.top()") return self._elems[-1] def push(self, elem): self._elems.append(elem) def pop(self): if self._elems == []: raise StackUnderflow("in SStack.pop()") return self._elems.pop()
栈的链表实现
class StackUnderflow(ValueError): pass class LStack(object): def __init__(self): self._top = None def is_empty(self): return self._top is None def top(self): if self._top is None: raise StackUnderflow("in LStack.top()") return self._top.elem def push(self, elem): self._top = LNode(elem, self._top) def pop(self): if self._top is None: raise StackUnderflow("in LStack.pop()") p = self._top self._top = p.next return p.elem
队列
队列的链接表实现
由于需要在链接表的两端操作,从一端插入元素,从另一端删除。最简单的单链表只支持首端高效操作,在另一端操作需要O(n)时间,不适合作为队列的实现基础。可以使用带表首端的高效访问和删除,基于单链表实现队列的技术就已经很清晰了。
队列的顺序表实现
基于顺序表实现队列的困难
首先假设用顺序表的尾端插入实现enqueue操作,根据队列的性质,出队操作应该在表的首端进行。为了维护顺序表的完整性(表元素在表前端连续存放),出队操作取出当时的首元素后,就需要把表中其余元素全部前移,这样将得到一个O(n)时间的操作。另一种可能是在队首元素出队后表中的元素不前移,但记住新队头位置。但是会出现如下问题:
表元素存储区大小是固定的,经过反复的入队和出队操作,一定会在某次入队时出现队尾溢出表尾(表满)的情况。而在出现这种溢出时,表前部通常会有些空位,因此这是一种“假性溢出”,并不是真的用完了整个元素区。假如元素存储区能自动增长,随着操作进行,表前端就会留下越来越大的空区,而且这片空区永远也不会用到,完全浪费了。显然不应该允许程序运行中出现这种情况。
循环顺序表
把顺序表看作一种环形结构,认为其最后存储位置之后是最前的位置,形成了一个环形:
1)在队列使用中,顺序表的开始位置并不改变,变量q.elems始终指向表元素区开始。
2)队头变量q.head记录当前队列里第一个元素的位置;队尾变量q.rear记录当前队列里最后元素之后的一个空位。
3)队列元素保存在顺序表的一段单元里,两个变量的值之差就是队列里的元素个数。
当q.head == q.rear时表示队空,但是如果队满了的话也会出现q.head == q.rear的情况,于是就把(q.rear + 1) % q.len == q.head表示为队满。
队列的list实现
现在考虑定义一个可以自动扩充存储的队列类。这里很难直接利用list的自动存储扩充机制,有两方面原因:首先是队列元素的存储方式与list元素的默认存储方式不一致。list的元素总在其存储区的最前面一段;而队列的元素可能是表里的任意一段,有时还分为头尾两部分。如果list自动扩充,其中的队列元素就有可能失控;另一方面,list没提供检查元素存储区容量的机制,队列操作中无法判断系统何时扩容。于是自己管理存储(list)。
基本设计
class QueueUnderflow(ValueError): pass class SQueue(): def __init__(self, init_len=8): self._len = init_len self._elems = [0] * init_len 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): new_elems[i] = self._elems[(self._head + i) % old_len] self._elems, self._head = new_elems, 0