前言
堆栈和队列都是比较基本的数据结构,也是程序员应该掌握的结构,它们简单而又有用,以此,记录一下学习过程。
堆栈
堆栈是比较基本的ADT(抽象数据类型),这种结构最鲜明的特点就是List-In First-Out,LIFO方式。
堆栈接口
一般来说,传统的堆栈操作就是push和pop。
push就是把一个新值压入到堆栈顶部,pop就是把堆栈顶部的值移出堆栈并返回这个值。堆栈只提供对它顶部元素的访问。
但是,用于访问堆栈顶部元素只有pop操作,且这个元素还要被弹出堆栈。
所以要使用另一种堆栈接口,其含有三种基本操作push,pop和top:
push和传统堆栈一样,而pop只是将堆栈顶的值弹出,并不返回这个值,而top的操作就是返回堆栈顶元素的值,它并不把顶部元素从堆栈中移除。
同时,一个空的堆栈不支持pop操作,所以需要一个is_empty函数查看堆栈是否为空,同理,一个满的堆栈不支持push操作,需要一个is_full函数查看堆栈是否为满。
实现堆栈
所有的ADT都必须确定一件事,那就是内存分配,如何获取内存来储存值。有三种可选的方案:静态数组,动态分配的数组,以及动态分配的链式结构。
静态数组要求长度固定,而且这个长度在编译使就已经确定。但是这个方案最简单也最不容易出错。
动态数组可以在运行时才决定数组的长度。而且,需要的话,可以分配一个新的,更大的数组。把原先的数组的值复制到新的数组中,然后删除掉原先数组,从而达到动态增长数组大小的目的。
最后链式结构提供的最大程度上的灵活性。每个元素在需要时候才单独分配,所以除非超过机器内存大小的限制外,这种方式对元素数量没有什么限制。不过,链式结构在访问下一元素的时候需要消耗一定的内存空间,而且在访问一个特定的元素时候的效率不如数组。
下述代码正式用链式结构对堆栈的实现,其他两种同理也可以比较简单的实现:
1 | /* |
stack.h文件
1 | /* |
队列
队列同样是比较基本的ADT,这种结构和堆栈不同的是,其特点是First-In First-Out,FIFO方式。
队列接口
事实上队列并没有像堆栈那样具有约定俗成的用法,不过原理上队列都是一样,在这里我们用insert和delete来表示插入和删除,以及队列的方向是从队尾进入,从队首弹出。
队列也有insert+delete的用法,以及insert+delete+first的用法,和上述堆栈中差不多,也就不细说了。
实现队列
队列的实现有些和堆栈不同,队列需要两个指针,一个指向队首front,一个指向队尾rear。同时,一般的数组并不适合队列,这是因为队列使用内存的方式引起的。堆栈数据总是扎很于一端,而队列需要对数据进行挪移。
在实现队列时,可以用到循环数组实现:
再插入一个新的元素就为:
这种循环数组很容易实现:
1 | rear += 1; |
或者:
rear = (rear + 1) % QUEUE_SIZE
同理,对front也是一样的计算。
但是,这样的循环数组具有一个问题——那就是对队列为空和为满时候两种情况的front和rear是一样的。
向队列中不断添加元素使之为满。
从队列中不断释放元素使之为空。
这样的问题一种解决办法就是设置一个变量,这个变量用于记录队列大小。
还有一种解决方法,那就是重新定义 ‘ 满 ‘ 的含义,在队列中插入时剩一个元素的时候就为满了,这样在满的时候和空的时候front和rear的值就不一样了。
因为只定义了 ‘ 满 ‘ 的含义,那么队列为空的时候还是原先的判别条件:
(rear + 1) % QUEUE_SIZ = front
队列为满的时候,还保留一个元素未使用,所以在满的时候判别条件就为:
(rear + 2) % QUEUE_SIZE = front
这样就把这两种情况区分开了。
链表实现
上述所说的是队列使用线性结构数组的实现方式,在队列中,还可以使用链式结构——链表去实现,因为是动态分配了新元素的内存,只要不受机器内存大小的限制,理论上队列不存在为满的情况。
链表实现图