3.队列
队列其实可以看作是一种特别的线性表,一种对存储有特定要求的线性表。队列的特性就是FIFO——先进先出,就像平常的排队一样,先排队的先处理,队列中的进、出一般用PUSH、POP表示。队列是一种使用频率非常高的数据结构,在很多算法中(如图的广度优先遍历等)队列是实现的关键。
由于队列是一种特殊的线性表,因而队列的实现是基于线性表的实现的,只不过在对线性表的操作上,要特殊化,使其符合队列的特征。在C++中,队列的实现被封装在类queue中,该类存在于头文件<queue>中,使用时,可以指定队列的底层实现(即vector、list。类vector是C++使用数组对线性表的封装实现,而list类则是使用链表的封装实现)。Java中也有Queue类封装了对队列的实现。从Queue的实现——Queue aa = new LinkedList();——可以看出,Java的Queue类是基于链表实现的。
用C语言实现的话,是可以用数组或者链表实现的。
使用数组实现:
如同线性表的实现,首先需要定义一个结构体类表示队列这一个数据结构。可以这样定义:
typedefstruct
{
inthead;//指示队列头在数组中位置
inttail; //指示队列尾在数组中的位置
intcount;
intmaxSize; //数组的大小
int*array;
}iqueue_a;
这里使用head与tail指示队列头尾的位置非常重要。head指示读取的位置(即队列头),而tail指示元素插入的位置(即队列尾)。这里使用定长数组,并将数组当做环形来使用,如图:
这里为什么不使用自动增长的数组,像线性表那样?如果这样做的话,那么每读取一个元素后,head之前的元素都不可用了,因为队列只能在后面添加元素,这样会导致大量的存储空间浪费,甚至导致缓冲区溢出,而使用环形数组就是为了解决这个问题。如上图所示,tail可以跨过数组的最大位置,然后从0开始,从而重新利用数组的空闲空间。
实现的函数:
intiqueue_a_init(iqueue_a*queue);
intiqueue_a_init_size(iqueue_a*queue,intsize);
voidiqueue_a_delete(iqueue_a*queue);
intiqueue_a_push(iqueue_a*queue,intelement);
intiqueue_a_pop(iqueue_a*queue,int*var);
由于并不存在环形数组这种东西,因而我们在实现iqueue_a_push和iqueue_a_pop函数时,需要检查head与tail是否大于数组长度的最大值maxSize,若大于的话,需要进行取模操作,让它们小于maxSize。
使用链表实现:
如同线性表的实现,使用链表时,首先需要定义一个节点结构体:
typedefstructiq_node
{
intdata;
structiq_node*next;
}iqueue_node;
当然也需要一个代表队列的数据结构,封装链表:
typedefstruct
{
intcount;
iqueue_node*head;
iqueue_node*tail;
}iqueue_l;
这里的head与tail同样是代表队列的头和尾。由于队列空间分配的灵活性,可以轻易避免使用数组实现中空间浪费的问题。当然,也同时带来了其他问题,如每个元素需要额外的空间存储指针,增加了内存释放问题等。在数据量大且不确定的情况下,使用链表实现更为适合。
实现的函数:
voidiqueue_l_init(iqueue_l*queue);
voidiqueue_l_delete(iqueue_l*queue);
intiqueue_l_push(iqueue_l*queue,intelement);
intiqueue_l_pop(iqueue_l*queue,int*var);
4.栈
栈这个概念在计算机科学中是十分重要的,重要到几乎所有的CPU都在硬件上实现对栈的支持(如x86处理器上有ss、sp、bp寄存器用于栈操作,更有特定的栈命令如push、pop等)。栈跟队列一样,也是一种特殊的线性表,其特殊的地方也是在存取这方面。队列的特性是FIFO,而栈则是LIFO,是后进先出的意思,像叠盘子一样。
栈之所以重要,是因为它在计算机中使用十分的广泛。比如说,在操作系统中,中断处理、多任务切换等,在执行前都要将当前上下文先入栈,然后在执行中断服务程序或者切换到其他进程去执行,在执行完中断服务程序或其他进程任务后,要从栈中读出数据恢复上下文。但是,在中断服务程序或其他进程执行过程中有可能又需要执行其他中断或者其他进程,这样的话,栈LIFO的特性就可以有效的保证返回上一级中断或进程时能够正确恢复上下文。
在C++中,有类stack类封装了对栈的实现,跟类queue一样,stack也可以在创建对象的时候定义底层实现是vector还是list。而在Java中,也有累Stack封装对栈的实现。
使用C语言实现时,可以基于数组或者链表,跟线性表实现很相似,差异点在于,栈需要维护一个栈顶指针来指示元素插入或者取出的位置。
使用数组实现:
同样,需要定义一个结构体表示栈的结构:
typedefstruct
{
inttop;
intmaxSize;
int*array;
}istack_a;
这里top指示栈顶位置,maxSize指示数组大小——当栈顶超过maxSize时给数组分配多一倍的空间,使数组可自动增长,array即是存储数据的数组。
实现的函数:
intistack_a_init(istack_a*stack);
intistack_a_init_size(istack_a*stack,intsize);
voidistack_a_delete(istack_a*stack);
intistack_a_push(istack_a*stack,intelement);
intistack_a_pop(istack_a*stack,int*var);
函数istack_a_init、istack_a_init_size用于初始化栈,前者使用默认大小创建数组,而后者可以指定创建数组的初始大小。函数istack_a_delete用于销毁栈变量并释放存储空间。函数istack_a_push由于向栈顶压入元素,而istack_a_pop则为弹出元素,并将该元素存于变量var中。这些函数的返回值都是用于表示操作是否成功的(0表示失败)。
使用链表实现:
同样,需要定义存储元素的节点以及表示封装元素的结构体:
typedefstructis_node
{
intdata;
structis_node*next;
}istack_node;
typedefstruct
{
intcount;
istack_node*top;
}istack_l;
实现的函数:
voidistack_l_init(istack_l*stack);
voidistack_l_delete(istack_l*stack);
intistack_l_push(istack_l*stack,intelement);
intistack_l_pop(istack_l*stack,int*var);
这些函数的功能与使用数组实现的都一样。