​ 栈和队列是两种重要的线性结构。从数据结构的角度看,栈和队列也是线性表,其特特殊在于栈和队列的基本操作是线性表操作的子集,他们是操作受限的线性表,因此,可以称为限定性的数据结构。

​ 但是另一方面,从数据类型角度来看,栈和队列是和线性表大不相同的两类重要的抽象数据类型。并且栈和队列因为其特性,被广泛的应用于各种软件系统中,学习他们对我们有很大的帮助。因此本文除了讨论栈和队列的定义、表示方法和实现外,还将给出一些应用的例子。

​ 上一文中,我们讨论了的相关概念,本文我们来继续讨论下队列。

1.队列的基本概念

队列的定义:队列简称为队,也是一种操作受限制的线性表,其限制为仅允许在表的一端进行插入,在表的另一端进行删除。可进行插入的一端称为队尾(Rear),可进行删除的一端称为队头(front)。

数据结构--队列的基本概念与应用_数据结构

队列的特点:队列的特点概括起来就是先进先出(First in First out, FIFO)。

​ 队列在我们生活中更为常见,比如我们经常排队买东西、12306的购票等待队列;开进隧道的火车,各节车厢就是队中的元素,最先进去的车厢总是最先驶出隧道。

数据结构--队列的基本概念与应用_队列_02

2.队列的存储结构

​ 队列的存储结构主要有两种,分别是顺序队列和链队。顺序队列其定义如下,可以看到,顺序队列的核心就是一个连续的存储空间,在这里使用数组来实现。

typedef struct {
  //用于存放队列中的元素
  int data[maxSize];
  //队首指针
  int front;
  //队尾指针
  int rear;
} SqQueue;		

​ 链队的定义如下,可以看到,链队就是采用链表来存储栈,采用尾插法创建队。链队的定义较为复杂,分为队结点、链队两部分:

//队结点
typedef struct QNode {
  //数据域
  int data;
  //指针域
  struct QNode * next;
} QNode;

//链队类型
typedef struct {
  //队头指针
  struct QNode * front;
  //队尾指针
  struct QNode * rear;
} LiQueue;
数据结构--队列的基本概念与应用_出队_03

​ 对于队列而言,也有两种存储结构,一般而言,我们也是选择顺序队列。

​ 注:有些地方,在定义队列时,为了存储方便,会给链队增加一个头结点,并令头指针指向头结点。此时,空队的判断条件变为头指针和尾指针均指向头结点(尾指针指向头结点,因为front的指向一直不变)。存储结构下图所示:

数据结构--队列的基本概念与应用_数据结构_04

3.顺序队列的定义和基本操作

3.1循环队列

和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个指针 front和rear分别指示队列头元素及队列尾元素的位置。为了在C语言中描述方便起见,在此我们约定:初始化建空队列时,令 front=rear=0,每当插入新的队列尾元素时,“尾指针增1”;每当删除队列头元素时,“头指针增1”。因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向
队列尾元素的下一个位置,如下图所示。

数据结构--队列的基本概念与应用_数据结构_05

​ 假设当前为队列分配的最大空间为6,则当队列处于上图(d)的状态时不可再继续插入新的队尾元素,否则会因数组越界产生越界异常。然而此时又不宜如顺序栈那样,进行存储再分配扩大数组空间,因为队列的实际可用空间并未占满。一个较巧妙的办法是将顺序队列臆造为一个环状的空间,如下图,称之为循环队列

数据结构--队列的基本概念与应用_出队_06

​ 想象一下,数组“变成”一个环,让rear和front沿着环走,这样就永远不会出现两者来到数组尽头无法继续往下走的情况,这就是循环队列。循环队列是改进的顺序队列,元素的进队出队如下图所示:

数据结构--队列的基本概念与应用_出队_07

​ 在上图中,队列进度、出队的变化情况如下:

​ ①由空队进队两个元素,此时front指向0,rear指向2.

​ ②进队4个元素,出队3个元素,此时 front指向3,rear指向6.

​ ③进队2个元素,出队4个元素,此时 front 指向7,rear 指向0.

​ 由①到③的变化过程可以看出,经过元素的进进出出,即便是 rear和 front 都到了数组尾端(图中③所示),依然可以让元素继续入队,因为两指针不是沿着数组下标递增地直线行走,而是沿着一个环行走,走到数组尽头的时候自动返回数组的起始位置。

​ 从上述分析,我们也能看到,普通的顺序队列存在的缺陷,因此我们在实际使用中,都会使用循环队列,我们后面的讨论的顺序队列主要是循环队列。而循环队列为了保障队空和队满的判断,需要牺牲一个存储空间。

3.2 循环队列的要素

​ 循环队列qu也是有四个要素,分别为两个特殊状态:队空队满;两个操作:入队出队

队空状态:qu.rear == qu.front

队满状态:(qu.rear+1)%maxSize == qu.front

入队操作qu.data[qu.rear]=x; qu.rear = (qu.rear+1)%maxSize;

出队操作x = qu.data[qu.front]; qu.front = (qu.front+1)%maxSize;

​ 注:上面的代码是入队、出队时均是先操作数据,在移动指针;可能也会有别的次序,不过本质上是相同的。

3.3 循环队列的基本操作

初始化队列

void initQueue(SqQueue *qu){
  qu->front = qu->rear = 0;
}

判断队是否为空

//如果为空,返回1,否则返回0
int isEmpty(SqQueue qu){
    if(qu.rear == qu.front){
        return 1;
    }
    return 0;
}

进队操作

int enQueue(SqQueue *qu, int x){
    if((qu->rear+1)%maxSize == qu->front){
        printf("队满,无法入队!");
        return 0;
    }
    qu->data[qu->rear] = x;
    qu->rear = (qu->rear+1) % maxSize;
    return 1;
}

出队操作

int deQueue(SqQueue *qu, int *x){
    if(qu->rear == qu->front){
        printf("队空,无法出队!");
        return 0;
    }
    *x = qu->data[qu->front];
    qu->front = (qu->front+1) % maxSize;
    return 1;
}

​ 在考试中,队一般是作为辅助的结构来解决其他问题,因此一般情况下,队的定义和操作可以写的很简单,比如:

//两句话定义一个队
int queue[maxSize];
int front = rear = 0;
//入队
queue[rear] = x;
rear = (rear + 1) % maxSize;

//出队
x = queue[front];
front = (fron + 1) % maxSize;

4.链队的定义和基本操作

4.1 链队的要素

​ 对于链队lqu,也有四个要素,分别为两个特殊状态:队空队满;两个操作:入队出队

队空:lqu->rearNULL或lqu->frontNULL,表明栈是空的。

队满:一般情况下,不存在队满的情况(除非内存不足,无法申请新的结点)。

入队操作:元素所处结点由指针p指向,使用尾插法插入节点。

//尾插法
lqu->rear->next = p;
lqu->rear = p;

出队操作:出栈元素保存在x中

//p指向出栈结点
p = lqu->front;
x = p->data;
//队首指针指向下一个结点
lqu->front = p->next;
free(p);

​ 入队、出队操作的动态示意图如下所示,

数据结构--队列的基本概念与应用_数据结构_08

4.2 链队的基本操作

初始化队列

void InitLinkedQueue(LiQueue *lqu){
    lqu = (LiQueue *) malloc(sizeof(LiQueue));
    lqu -> front =NULL;
    lqu -> rear = NULL;
}

判断队是否为空

int isLinkedEmpty(LiQueue lqu){
    if(lqu.rear == NULL || lqu.rear == NULL){
        return 1;
    }
    return 0;
}

入队操作

void enLinkedQueue(LiQueue *lqu, int x){
    //创建一个节点p
    QNode *p = (QNode *) malloc(sizeof(QNode));
    p->data = x;
    p->next = NULL;
    
    //需要判断队列是否为空,如果为空,需要同时修改front指针的值
    if(lqu->rear == NULL){
        lqu->rear = lqu ->front = p;
    } else{
        lqu->rear->next = p;
        lqu->rear = p;
    }
}

出队操作

int deLinkedQueue(LiQueue *lqu, int *x){
    if(lqu->rear == NULL){
        printf("队空,无法出队!");
        return 0;
    }
    QNode *p = lqu->front;
    //如果队中只有一个结点,需要同时修改rear指针的值
    if(lqu->front == lqu->rear){
        lqu->rear = lqu ->front = NULL;
    } else{
        lqu->front = p->next;
    }
    *x = p->data;
    free(p);
    return 1;
}

主函数测试代码

int main(int argc, const char * argv[]) {
    // insert code here...
    LiQueue lqu;
  	int x;
    InitLinkedQueue(&lqu);
    printf("st.top:%d\n", lqu.front);
    printf("Queue is Empty:%d\n", isLinkedEmpty(lqu));
    enLinkedQueue(&lqu, 1);
    enLinkedQueue(&lqu, 2);
    enLinkedQueue(&lqu, 3);
    printf("Queue is Empty:%d\n", isLinkedEmpty(lqu));
    deLinkedQueue(&lqu, &x);
    printf("出队的元素为:%d\n", x);
    deLinkedQueue(&lqu, &x);
    printf("出队的元素为:%d\n", x);
    deLinkedQueue(&lqu, &x);
    printf("出队的元素为:%d\n", x);
}

5.双端队列

​ 双端队列是一种插入和删除操作在两端均可进行的线性表。可以把双端队列看成栈底连在一起的两个栈。与共享栈不同的是,两个栈的栈顶指针是向两端延伸的。由于双端队列允许在两端插入、删除元素,因此需要设立两个指针:end1,end2,分别指向双端队列的中两端的元素。

数据结构--队列的基本概念与应用_双端队列_09

​ 允许在一端进行插入和删除,另一端只允许删除的双端队列称为输入受限的双端队列,如下图左边所示;允许在一端进行插入和删除,另一端只允许插入的双端队列称为输出受限的双端队列,如下图右边所示。

数据结构--队列的基本概念与应用_顺序表_10

​ 双端队列的基本操作可参考下题:

例题:某队列允许在其两端进行入队操作,但仅允许在一端进行出队操作,若元素a,b,c,d,e依次入此队列后再进行出队操作,则不可能得到的出队序列是( )。
A. b, a, c, d, e B. d, b, a, c, e
C. d, b, c, a, e D. e, c, b, a, d

答案:C.

6.总结

​ 栈的概念和操作都比较简单,不过他却是经常作为辅助结构出现在其他问题的求解中,比如二叉树的层次遍历等等。

​ 而且队列是一种应用非常广的数据结构,在我们以后的编程中,使用到频率是非常高的。一定以熟悉队的特性,在求解具体问题中,将之利用好。

​ 关于栈和队列的练习题,可以参考这篇文章

参考:

1.数据结构中的C语言编程基础

2.数据结构考研大纲

3.数据结构高分笔记

4.王道数据结构

5.数据结构–C语言版,严蔚敏


​ 又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主根据自身理解与对考研资料进行的整理,仅作为参考,大佬有什么问题,可以评论区留言,如果有什么错误,还请批评指正。

​ 本专栏为数据结构知识,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。

有任何疑问,可以评论区留言。