【数据结构】队列的基础知识


队列是线性表的一种,同栈一样,它也属于 操作受限的线性表

1. 队列的定义和特点

队列只允许在表的一端插入元素,另一端删除元素。

允许插入的一端被称为队尾;允许删除的一端被称为对头。

下图为入队列、出队列示意图:

入队和出队

a 1 a_1 a1为对头元素, a n a_n an为队尾元素。栈中元素按 a 1 , a 2 , ⋯   , a n a_1, a_2, \cdots , a_n a1,a2,,an的次序入对栈, 出队也只能按照这个顺序进行。

队列的修改规则:先进先出(First In First Out, FIFO)

2. 循环队列

2.1 存储结构表示

循环队列是利用一组地址连续的存储单元依次存放从对头到队尾的数据元素。

附设front指针来指示队列头元素;rear指针指示队列尾元素。

队列的顺序存储结构如下:

#define MAXQSIZE 100 //队列可能达到的最大长度
typedef struct
{
    QElemType *base; //存储空间的基地址
    int front;  //头指针 -> 更像是偏移量
    int rear;	//尾指针 -> 更像是偏移量
}SqQueue;

初始化创建空队列时,令front= rear = 0 ,

每当插入新的队列尾元素时,尾指针rear增1;每当删除队列头元素时,头指针front增1。

队列的假溢出:假设当前队列分配的最大空间为4,则当队列处于下图所示的状态时,不可再继续插入新的队尾元素,否则会出现溢出现象, 即因数组越界而导致程序的非法操作错误。而此时队列的实际可用空间并未占满。
队列假溢出

将顺序队列变为一个循环队列即可解决这类问题。

下图中,对头元素的 a 3 a_3 a3,入队前队尾指针Q.rear是3;当下一个元素 a 4 a_4 a4入队后,队尾指针Q.rear回到了0

Q.rear = (Q.rear + 1) % 4

循环队列

循环队列为空状态:

空循环队列

此时Q.front == Q.rear

下图为队满的状态,共有两种:

上面一种队列空间被占满,但是此时**Q.front == Q.rear**,与队空的判断条件一致,判断条件冲突了。

Q.front == Q.rear时,无法区分队列是空还是满的状态。

因此通常使用下面一种:少用一个元素空间,此时(Q.rear+1)%MAXSIZE == Q.front

满循环队列

循环队列队空和对满的条件:

  1. 队空的条件:Q.front == Q.rear
  2. 队满的条件:(Q.rear+1)%MAXSIZE == Q.front

2.2 初始化

循环队列的初始化操作就是动态分配一个预定义大小为MAXSIZE的数组空间。

算法步骤:

  1. 为队列分配一个最大容量为MAXSIZE的数组空间,base指向数组空间的首地址。
  2. 头指针front和尾指针rear置为零, 表示队列为空。
Status InitQueue(SqQueue *Q)
{
    Q->base = (QElemType*)malloc(sizeof(QElemType)*MAXQSIZE); //分配容量为MAXSIZE的数组
    if (Q->base==NULL) exit(OVERFLOW); //存储分配失败
    Q->front = Q->rear = 0; //头指针和尾指针设置为零,队列为空
    return OK;
}

2.3 求队列长度

对于非循环队列,尾指针和头指针的差值便是队列长度。

对于循环队列,差值可能为负数,所以需要将差值加上MAXSIZE, 然后与MAXSIZE求余。

Status QueueLength(const SqQueue *Q)
{
    return (Q->rear-Q->front+MAXQSIZE)%MAXQSIZE;
}

2.4 入队

入队操作就是在队尾插入一个新元素。

算法步骤:

  1. 判断队列是否满,若满则返回ERROR
  2. 将新元素插入队尾
  3. 队尾指针加1
Status EnQueue(SqQueue *Q, QElemType e)
{
    if ((Q->rear+1)%MAXQSIZE == Q->front) return ERROR; //判断队列是否满
    Q->base[Q->rear] = e; //新元素插入队尾
    Q->rear = (Q->rear+1)%MAXQSIZE; //队尾指针加1
    return OK;
}

2.5 出队

出队操作就将对头元素删除。

算法步骤:

  1. 判断队列是否空,若空则返回ERROR
  2. 保存对头元素
  3. 队头指针加1
Status DeQueue(SqQueue *Q, QElemType *e)
{
    if (Q->rear == Q->front) return ERROR; //判断队列是否空
    *e = Q->base[Q->front]; //保存对头元素
    Q->front = (Q->front+1)%MAXQSIZE;; //队头指针加1
    return OK;
}

2.6 取得循环队列的队头元素

当队列为非空时,返回当前对头元素的值,对头指针不变。

QElemType GetHead(SqQueue *Q)
{
    if (Q->rear != Q->front)	return Q->base[Q->front];
}

2.7 案例

给出一个简单的案例,将数据元素类型QElemType设置为int,所以这是一个数据元素为int的循环队列。

包含上述4种操作,便于理解、应用。

下述代码运行环境:VS2019,其他环境可移植。

#include <stdio.h>
#include <malloc.h>

typedef struct
{
    int* base; //存储空间的基地址
    int front;  //头指针 -> 更像是偏移量
    int rear;	//尾指针 -> 更像是偏移量
}SqQueue;

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;

#define MAXQSIZE 100 //队列可能达到的最大长度

// 初始化循环队列
// 函数参数: Q -> 操作台地址
// 返回值:1代表运行正常
Status InitQueue(SqQueue* Q)
{
    Q->base = (int*)malloc(sizeof(int) * MAXQSIZE); //分配一个容量为MAXSIZE的数组
    if (Q->base == NULL) exit(OVERFLOW); //存储分配失败
    Q->front = Q->rear = 0; //头指针和尾指针设置为零,队列为空
    return OK;
}

// 求队列长度
// 函数参数: Q -> 操作台地址
// 返回值:1代表运行正常
Status QueueLength(const SqQueue* Q)
{
    return (Q->rear - Q->front + MAXQSIZE) % MAXQSIZE;
}

// 入队
// 函数参数: Q -> 操作台地址; e -> 入队参数
// 返回值:1代表运行正常; 0代表运行有问题
Status EnQueue(SqQueue* Q, int e)
{
    if ((Q->rear + 1) % MAXQSIZE == Q->front) return ERROR; //判断队列是否满
    Q->base[Q->rear] = e; //新元素插入队尾
    Q->rear = (Q->rear + 1) % MAXQSIZE; //队尾指针加1
    return OK;
}

// 出队
// 函数参数: Q -> 操作台地址; e -> 存放出队元素
// 返回值:1代表运行正常; 0代表运行有问题
Status DeQueue(SqQueue* Q, int* e)
{
    if (Q->rear == Q->front) return ERROR; //判断队列是否空
    *e = Q->base[Q->front]; //保存对头元素
    Q->front = (Q->front + 1) % MAXQSIZE;; //队头指针加1
    return OK;
}

// 取得队头元素
// 函数参数: Q -> 操作台地址
// 返回值:0代表运行有问题
int GetHead(SqQueue* Q)
{
    if (Q->rear == Q->front) return ERROR; //判断队列是否空
    return Q->base[Q->front];
}

int main()
{
    SqQueue queue; //创建队列的操作台
    int elem = 0;  //存放出栈元素

    InitQueue(&queue); //初始化队列

    EnQueue(&queue, 6);  //将数据6入队
    EnQueue(&queue, 89);  //将数据89入队
    EnQueue(&queue, 101);  //将数据101入队

    printf("The length of queue is : %d\n", QueueLength(&queue)); //打印队列长度
    printf("%d\n", GetHead(&queue)); //打印队头元素
    
    DeQueue(&queue, &elem); //出队
    printf("elem: %d\n", elem); //打印出队元素
    printf("The length of queue is : %d\n", QueueLength(&queue)); //打印队列长度
    printf("%d\n", GetHead(&queue));  //打印队头元素

    DeQueue(&queue, &elem); //出队
    printf("elem: %d\n", elem); //打印出队元素
    printf("The length of queue is : %d\n", QueueLength(&queue)); //打印队列长度
    printf("%d\n", GetHead(&queue));  //打印队头元素

	return 0;
}

3. 链队

3.1 链队的结构表示

链队是指采用链式存储结构实现的队列。通常链队用单链表来表示。

链队

如上图所示,一个链队需要两个指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。

这里和单链表一样, 为了操作方便起见,给链队添加一个头结点, 并令头指针始终指向头结点。

typedef struct QNode
{
    QElemType data; 	//数据
    struct QNode *next;	//指针
}QNode, *QueuePtr;

typedef struct
{
    QueuePtr front; //队头指针
    QueuePtr rear;	//队尾指针
}LinkQueue;

3.2 初始化

链队的初始化操作就是构造一个只有一个头结点的空队。

链队初始化

算法步骤:

  1. 生成新的头结点作为头结点,队尾和对头指针都指向该结点
  2. 将头结点的指针域置空
Status InitQueue(LinkQueue *Q)
{
    Q->front = Q->rear = (QNode*)malloc(sizeof(QNode)); //生成新结点
    Q->front->next = NULL; //将头结点的指针域置空
    return OK;
}

3.3 入队

链队在入队前不需要判断队是否满,需要为入队元素动态分配一个结点空间。

链队入队

算法步骤:

  1. 为入队元素分配结点空间,用指针p指向
  2. 将新结点数据域置为e
  3. 将新结点插入到队尾
  4. 修改队尾指针为p
Status EnQueue(LinkQueue *Q, QElemType e)
{
    QNode* p = (QNode*)malloc(sizeof(QNode)); //1. 为入队元素分配结点空间
    p->data = e; //2. 将新结点数据域置为e
    p->next = NULL;
    Q->rear->next = p; //3. 将新结点插入到队尾
    Q->rear = p; //4. 修改队尾指针为p
    return OK;
}

3.4 出队

链队在出队前需要判断队列是否为空,同时链队在出队后需要释放出队头元素的所占空间。

  1. 判断队列是否为空,若空则返回ERROR。
  2. 临时保存队头元素的空间,以备释放。
  3. 修改队头指针,指向下一个结点。
  4. 判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值, 指向头结点。
  5. 释放原队头元素的空间。

链队出队

Status DeQueue(LinkQueue* Q, QElemType* e)
{
    if (Q->rear == Q->front) return ERROR; //1. 判断队列是否空
    QNode* p = Q->front->next; //2. 临时保存队头元素的空间,以备释放
    *e = p->data;
    Q->front->next = p->next; //3. 修改队头指针,指向下一个结点
    if (Q->rear == p) Q->rear = p->next; //4. 判断出队元素是否为最后一个元素
    free(p); //5. 释放原队头元素的空间
    return OK;
}

3.5 取得队头元素

当队列非空时,此操作返回当前队头元素的值,队头指针保持不变。

QElemType GetHead(LinkQueue *Q)
{
	if (Q->front != Q->rear) return Q->front->next->data;
}

3.6 案例

给出一个简单的案例,将数据元素类型QElemType设置为int,所以这是一个数据元素为int的链队。

包含上述4种操作,便于理解、应用。

下述代码运行环境:VS2019,其他环境可移植。

#include <stdio.h>
#include <malloc.h>

typedef struct QNode
{
    int data; 	//数据
    struct QNode* next;	//指针
}QNode, * QueuePtr;

typedef struct
{
    QueuePtr front; //队头指针
    QueuePtr rear;	//队尾指针
}LinkQueue;

#define OK 1
#define ERROR 0
#define OVERFLOW -2
typedef int Status;

#define MAXQSIZE 100 //队列可能达到的最大长度

// 初始化顺序栈
// 函数参数: Q -> 指针地址
// 返回值:1代表运行正常
Status InitQueue(LinkQueue* Q)
{
    Q->front = Q->rear = (QNode*)malloc(sizeof(QNode)); //生成新结点
    Q->front->next = NULL; //将头结点的指针域置空
    return OK;
}

// 入队
// 函数参数: Q -> 指针地址; e -> 入队参数
// 返回值:1代表运行正常; 0代表运行有问题
Status EnQueue(LinkQueue* Q, int e)
{
    QNode* p = (QNode*)malloc(sizeof(QNode)); //1. 为入队元素分配结点空间
    p->data = e; //2. 将新结点数据域置为e
    p->next = NULL;
    Q->rear->next = p; //3. 将新结点插入到队尾
    Q->rear = p; //4. 修改队尾指针为p
    return OK;
}

// 出队
// 函数参数: Q -> 指针地址; e -> 出队地址
// 返回值:1代表运行正常; 0代表运行有问题
Status DeQueue(LinkQueue* Q, int* e)
{
    if (Q->rear == Q->front) return ERROR; //1. 判断队列是否空
    QNode* p = Q->front->next; //2. 临时保存队头元素的空间,以备释放
    *e = p->data;
    Q->front->next = p->next; //3. 修改队头指针,指向下一个结点
    if (Q->rear == p) Q->rear = p->next; //4. 判断出队元素是否为最后一个元素
    free(p); //5. 释放原队头元素的空间
    return OK;
}

// 取得队头元素
// 函数参数: Q -> 指针地址; e -> 出队地址
// 返回值:队头元素
int GetHead(LinkQueue* Q)
{
    if (Q->front != Q->rear) return Q->front->next->data;
}

int main()
{
    LinkQueue queue; //创建指向队列的指针
    int elem = 0;  //存放出栈元素

    InitQueue(&queue); //初始化队列

    EnQueue(&queue, 6);  //将数据6入队
    EnQueue(&queue, 89);  //将数据89入队
    EnQueue(&queue, 101);  //将数据101入队

    printf("%d\n", GetHead(&queue)); //打印队头元素
    
    DeQueue(&queue, &elem); //出队
    printf("elem: %d\n", elem); //打印出队元素
    printf("%d\n", GetHead(&queue));  //打印队头元素

    DeQueue(&queue, &elem); //出队
    printf("elem: %d\n", elem); //打印出队元素
    printf("%d\n", GetHead(&queue));  //打印队头元素

	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴回答你关于数据结构和算法的问题!在Java中,有许多基础数据结构和算法可以使用。下面是一些常见的数据结构和算法的介绍: 1. 数组(Array):数组是一种线性数据结构,可以存储相同类型的元素。它们在内存中是连续存储的,并且可以通过索引来访问元素。 2. 链表(Linked List):链表是另一种常见的线性数据结构,它由节点组成,每个节点都包含一个数据项和一个指向下一个节点的指针。 3. 栈(Stack):栈是一种后进先出(LIFO)的数据结构。它支持两个基本操作:push(将元素放入栈顶)和pop(从栈顶删除元素)。 4. 队列(Queue):队列是一种先进先出(FIFO)的数据结构。它支持两个基本操作:enqueue(将元素放入队尾)和dequeue(从队头删除元素)。 5. 树(Tree):树是一种非线性数据结构,它由节点和边组成。每个节点可以有零个或多个子节点。常见的树包括二叉树、二叉搜索树和平衡二叉树。 6. 图(Graph):图是由节点和边组成的非线性数据结构。节点表示对象,边表示节点之间的关系。图可以是有向的或无向的。 在Java中,还有一些常见的算法,包括但不限于以下几种: 1. 排序算法:例如冒泡排序、选择排序、插入排序、快速排序和归并排序等。 2. 查找算法:例如线性查找和二分查找等。 3. 图算法:例如广度优先搜索(BFS)和深度优先搜索(DFS)等。 4. 动态规划:一种通过将问题分解成子问题来解决复杂问题的算法。 这只是数据结构和算法的一小部分基础知识,希望对你有所帮助!如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值