数据结构与算法分析(三)--- 队列、栈的实现与应用

一、队列的实现

想要理解队列很简单,因为它的性质和我们日常生活中的排队很类似,线性、先到先处理、后到排末尾。很显然,队列也是一种线性表,但比线性表的要求更严格,线性表可以对中间的元素进行访问和操作,队列则只能从首尾两端访问或操作元素,队列是一种操作受限的线性表,队列的所有特性都可以由线性表实现。

可能你会疑惑:既然链表、数组、游标数组都可以实现队列的所有特性,我们为何还要为队列单独实现一种数据结构呢?从功能上来说,数组或链表确实可以代替队列,但你要知道,特定的数据结构是对特定场景的抽象。而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。为队列专门实现一个数据结构,可以限制对队列的操作,保证其”先到先处理、后到排末尾“的规矩。
队列模型
队列作为一种符合”先进先出(FIFO—first in first out)“特性的数据结构,应用非常广泛,特别是一些具有额外特性的队列,比如循环队列、阻塞队列、并发队列,它们在很多偏底层系统、框架、中间件的开发中,起着关键的作用,比如环形缓存就用到了循环队列。

队列作为一种操作受限的线性表,完全可以由任意一种线性表来实现,顺序表实现的队列称为顺序队列,链式表实现的队列称为链式队列。由于队列只有入队enqueue()、出队dequeue()两个操作,不需要随机访问,所以顺序队列与链式队列的选择,主要考虑队列容量的可扩展性,假如队列容量大小预期变化不大,可以使用数组实现队列;假如队列容量在后期不断增加,则可以使用链表实现队列,当然也可以使用变长数组实现,C++ STL队列容器就是使用双向变长数组deque实现的。

1.1 顺序队列的实现

前面介绍顺序表时提到,数组随机访问效率比较高,插入、删除元素需要涉及其后元素的搬移操作,因此效率较低。队列需要的两种操作:队尾入队、队首出队,实际上就是元素的尾部插入与首部删除,数组删除首部元素需要后面的所有元素全部向前搬移一个位置,会导致数组实现的队列出队效率很低。

既然顺序队列出队效率低的原因是元素搬移操作,我们能否让存储单元不动,借鉴游标数组的技巧,分别使用一个队首游标与队尾游标来实现队列呢?就好像早期的工厂流水线,机器或零部件不动,工人移动,完成组装焊接等操作,比来回搬移机器零部件效率提升了不少。

使用队首与队尾游标完成入队与出队操作,确实能省去元素搬移的操作,提高效率。但细想后发现,伴随着入队、出队操作,队尾与队首游标会不断后移,数组的地址空间是有限的,当队尾游标移动到数组的末尾后,就没法继续插入新元素了,如果数组中还有空位置,就需要想办法重新利用这些空位置。怎么重复利用已从队首取出元素留下的空位置呢?

按照线性思维,很容易想到,当队尾游标移动到数组末尾后,把队首游标到队尾游标之间的元素(也即队列中的有效元素)全部搬移到数组最前面,不就能重新利用前面已被队列使用过的空位置吗?就像下面图示的:
顺序队列满时元素搬移
上面的方法虽说比每次出队都搬移元素的效率高了不少,每当队尾游标到末尾时才整体搬移一次全部队列元素,但还是免不了搬移元素导致的效率降低。有没有什么方法能避免元素的搬移呢?

回想下前面介绍的循环链表,既然链表可以实现循环访问,游标数组自然也可以模拟循环链表的特性。当队尾游标后移到数组末尾时,让其重新移回到数组起始位置不就可以实现循环访问了吗,毕竟游标也是显式存储的元素间指向关系。

实现循环访问的队列称为循环队列,循环队列既可以通过循环链表实现,也可以通过循环游标数组实现。循环队列的逻辑结构图示如下(为了更形象,以环形展示):
循环队列逻辑结构
循环队列就可以很好的避免数据搬移操作,效率比着前两种方案能提升很多。接下来看看循环队列如何实现?

循环队列的入队和出队操作比较简单,入队时只需要只需要将数据保存到队尾游标指向的存储单元,并将队尾游标后移一位即可;出队时只需要将队首游标指向的存储单元中的数据取出,并将队首游标后移一位即可。对于线性表来说,最重要的问题是防止访问越界,对于循环队列来说,最重要的问题则是确定队列为空和队列已满的判定条件。

队列为空比较好判断,队首与队尾游标指向同一个存储单元即表示队列为空。队列已满的判断条件在非循环队列中是队尾游标到数组末尾边界了,对于循环队列来说如何判定呢?下面先看一个循环队列已满的状态图示:
循环队列已满状态
从上面的循环队列已满状态图可以看出,循环队列已满时队尾游标指向的位置实际上并没有存储数据,循环队列会浪费一个数组的存储空间。当队列已满时,按照游标增长方向,队尾指针后移一位会与队首指针重合,这个可以看作循环队列已满的判定条件(也即tail + 1 = head)。但循环队列到末尾后,每次重新让其指向首部比较麻烦,既然涉及到循环,我们很容易想到取模运算,假设数组大小为n,则循环队列已满的判定条件就是(tail + 1)%n = head。

由于循环队列的队首游标与队尾游标处于变化中,我们需要经常访问队首与队尾游标,那么如何能快速访问该循环队列的队首与队尾游标呢?回顾下之前介绍单向链表时,使用了一个额外的链表头,不存放用户数据信息,只存放该链表的统计信息,包括链表内元素数目、指向首结点的指针、指向尾结点的指针等信息。我们可以借鉴这种技巧,也为循环队列提供一个队列头,用于存储该循环队列的队首游标、队尾游标、队列中有效元素数量、顺序表首地址、顺序表容量等信息,这些信息可以封装在一个结构体内,封装示例如下:

// datastruct\queue.c

struct QueueHead{
   
    int capacity;
    int haed;
    int tail;
    int size;
    ElementType *Array;
};
typedef struct QueueHead *Queue;
  • 循环队列创建与销毁

有了循环队列头,要想执行入队、出队操作,需要先创建循环队列,循环队列的创建与删除函数实现代码如下:

// datastruct\queue.c

Queue Create(int capacity)
{
   
    Queue Q = malloc(sizeof(struct QueueHead));
    if(Q == NULL)
    {
   
    	printf("Out of space!");
    	return NULL;
    }

    Q->Array = malloc(capacity * sizeof(ElementType));
    if(Q->Array == NULL)
        printf("Out of space!");

    Q->capacity = capacity;
    Q->head = 0;
    Q->tail = 0;
    Q->size = 0;

    return Q;
}

void Destroy(Queue Q)
{
   
    free(Q->Array);
    free(Q);
}
  • 循环队列入队与出队

有了上面的循环队列为空、已满的判定条件,实现其入队与出队函数就简单了,实现代码如下:

// datastruct\queue.c

bool Enqueue(Queue Q, ElementType x)
{
   
    if((Q->tail + 1)%Q->capacity == Q->head)
        return false;
    
    Q->Array[Q->tail] = x;
    Q->tail = (Q->tail + 1) % Q->capacity;
    Q->size++;

    return true;
}

ElementType Dequeue(Queue Q)
{
   
    if(Q->head == Q->tail)
        return (ElementType)NULL;

    ElementType ret = Q->Array[Q->head];
    Q->head = (Q->head + 1) % Q->capacity;
    Q->size--;

    return ret;
}

接下来给出一个循环队列操作的示例程序,为了更直观,实现了一个打印循环队列中所有有效元素的函数。上面出队的实现代码并不严谨,当队列为空时,假如ElementType为int,将返回0,我们无法判定返回的是队列已空还是队首元素值0(这个问题可以通过返回元素值地址的方式解决,返回NULL则表示队列已空,因为元素值地址不为NULL)。我们在操作循环队列时,最好养成入队与出队前,先判断队列是否已满或为空再继续操作。示例程序代码如下:

//datastruct\queue.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define ElementType int

struct QueueHead{
   
    int capacity;
    int head;
    int tail;
    int size;
    ElementType *Array;
};
typedef struct QueueHead *Queue;

Queue Create(int capacity);
bool Enqueue(Queue Q, ElementType x);
ElementType Dequeue(Queue Q);
void Destroy(Queue Q);

void printQueue(Queue Q)
{
   
    printf("Queue has %d elements: ", Q->size);
    int i = Q->head;
    while(i != Q->tail)
    {
   
        if(i != Q->head)
            printf(" ");
        printf("%d", Q->Array[i]);
        i = (i + 1) % Q->capacity;
    }
    printf("\n\n");
}

int main(void)
{
   
    Queue Q = Create(100);

    for(int i = 1; i < 9; i++)
    {
   
        if(Enqueue(Q, i * i) == false)
        {
   
            printf("The queue is full.\n");
            break;
        }
    }
    printf("\n");
    printQueue(Q);

    for(int i = 1; i < 7; i++)
    {
   
        if(Q->size == 0)
        {
   
            printf("The queue is empty.\n");
            break;
        }
        printf("%d ", Dequeue(Q));
    }
    printf("\n");
    printQueue(Q);

    Destroy(Q);

    return 0;
}

上述示例程序的执行结果如下:
循环队列示例程序执行结果

1.2 链式队列的实现

使用链表不需要元素搬移,也不需要维持一个循环链表,入队时将新元素插入链表尾部,出队时移除链表首节点,并将链表原

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值