数据结构和算法(6)队列的操做和实现

数据结构和算法(1)线性表实现git

数据结构和算法(2)单向循环链表的建立插入删除实现github

数据结构和算法(3)双向链表与双向循环链表的实现面试

数据结构和算法(4)链表相关面试题算法

数据结构和算法(5)栈和队列的操做和实现shell

数据结构和算法(6)队列的操做和实现数组

@[TOC]数据结构

1. 数据结构和算法(6)队列的操做和实现

代码下载

本篇博客代码下载:数据结构和算法

1.1 队列简介

队列是一种先进先出(First In First Out)的线性表,也就是FIFO。一般,称进数据的一端为 "队尾",出数据的一端为 "队头",数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。post

与栈结构不一样的是,队列的两端都"开口",要求数据只能从一端进,从另外一端出,以下图 所示:单元测试

若是对栈结构不熟悉能够参考我以前的博客:“数据结构和算法(五)栈和队列的操做和实现”

队列的存储结构
不只如此,队列中数据的进出要遵循 "先进先出" 的原则,即最早进队列的数据元素,一样要最早出队列。拿图 1 中的队列来讲,从数据在队列中的存储状态能够分析出,元素 1 最早进队,其次是元素 2,最后是元素 3。此时若是将元素 3 出队,根据队列 "先进先出" 的特色,元素 1 要先出队列,元素 2 再出队列,最后才轮到元素 3 出队列。

栈和队列不要混淆,栈结构是一端封口,特色是"先进后出";而队列的两端全是开口,特色是"先进先出"。

所以,数据从表的一端进,从另外一端出,且遵循 "先进先出" 原则的线性存储结构就是队列

队列存储结构的实现有如下两种方式:

  1. 顺序队列:在顺序表的基础上实现的队列结构;
  2. 链队列:在链表的基础上实现的队列结构; 二者的区别仅是顺序表和链表的区别,即在实际的物理空间中,数据集中存储的队列是顺序队列,分散存储的队列是链队列。

实际生活中,队列的应用随处可见,好比排队买 XXX、医院的挂号系统等,采用的都是队列的结构。

拿排队买票来讲,全部的人排成一队,先到者排的就靠前,后到者只能从队尾排队等待,队中的每一个人都必须等到本身前面的全部人所有买票成功并从队头出队后,才轮到本身买票。这就不是典型的队列结构吗?

明白了什么是队列,接下来开始学习顺序队列和链队列的基本实现和注意事项。

1.2 队列顺序存储

因为顺序队列的底层使用的是数组,所以需预先申请一块足够大的内存空间初始化顺序队列。除此以外,为了知足顺序队列中数据从队尾进,队头出且先进先出的要求,咱们还须要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元素,以下图 所示:

顺序队列的实现
因为顺序队列初始状态没有存储任何元素,所以 top 指针和 rear 指针重合,且因为顺序队列底层实现靠的是数组,所以 toprear 其实是两个变量,它的值分别是队头元素和队尾元素所在数组位置的下标。

当有数据元素进队列时,对应的实现操做是将其存储在指针 rear 指向的数组位置,而后 rear+1;当须要队头元素出队时,仅需作 top+1 操做。

举个栗子:

若是咱们要将 {1,2,3,4} 用顺序队列存储的实现,

元素1 进队的过程以下:

元素1 入队

元素4入队过程:

元素4入队

那么咱们接下来要将1,2,3,4这四个元素出队。出队过程以下:

元素1出队

元素4出队:

元素4出队

咱们先看一下一个简单的顺序队列操做代码:

#include <stdio.h>

 int enQueue(int *a,int rear,int data){
	 a[rear]=data;
	 rear++;
	 return rear;
 }
void deQueue(int *a,int front,int rear){
//若是 front==rear,表示队列为空
	while (front!=rear) {
		printf("出队元素:%d\n",a[front]);
		 front++;
	 }
 }
 
int main() {
	int a[100];
	int front,rear;
	//设置队头指针和队尾指针,当队列中没有元素时,队头和队尾指向同一块地址
	front=rear=0;
	//入队
	rear=enQueue(a, rear, 1);
	rear=enQueue(a, rear, 2);
	rear=enQueue(a, rear, 3);
	rear=enQueue(a, rear, 4);
	//出队
	deQueue(a, front, rear);
	return 0;
 }

复制代码

输出结构为:

出队元素:1
出队元素:2
出队元素:3
出队元素:4
复制代码

上面这种顺序存储队列会存在一些问题,如假溢出问题,以下图:

顺序队列假溢出问题
如上图,咱们先将A,B,C三个元素依次入队,而后将A,B 出队,而后又入队了D,E,元素,这个时候rear队尾指针指向了数组的最后,实际上咱们还有两个空间能够利用,这个时候队列并无满,可是因为rear指向了最后,也就是顺序队列总体发生了后移,这样形成的影响是:

  • 顺序队列以前的数组存储空间将没法再被使用,形成了空间浪费;也就是假溢出。
  • 另外如若是顺序表申请的空间不足够大,则直接形成程序中数组溢出,产生溢出错误;

为了解决上面问题,咱们有一种比较优的解决办法,就是用循环队列来解决假溢出问题。

接下来将介绍循环队列。

1.2.1 循环队列

循环队列就是,当队尾指针移动到数组末尾时,下次再有元素入队时,能够将队尾指针从新移到数组前面没有元素的位置。

循环队列操做以下图:

循环队列解决假溢出问题

如上图: (a) 咱们用Q.front == Q.rear表示队列为空。 当咱们依次入队a,b,c三个元素后,如上图(b)所示。 接下来,咱们将元素a 从队头出队,如上图(c)所示。 而后,咱们又依次入队了d ,e ,f, g这个时候实际上队列已经存满了,单咱们发现 Q.front == Q.rear,如上图(d1)所示。可是咱们前面(a)中用Q.front == Q.rear表示队列为空,可是队列满了的时候咱们Q.front == Q.rear就没法区分究竟是队列为空仍是满了。为了解决这个问题,咱们通常采用牺牲一个存储空间的方式。也就如上图(d2) 咱们用Q.front = Q.rear + 1表示堆满,就是牺牲一个存储单元不存放数据。

在循环队列中咱们判断队列为空,队列为满的条件以下:

  1. 队列为满: (Q.rear+1)%maxSize==Q.front
  2. 队列为空: Q.rear==Q.front
  3. 队列中有效的数据的个数: (Q.rear+maxSize-Q.front)%maxSize

1.2.2 循环队列的代码实现

  • 循环队列的顺序存储结构
/* 循环队列的顺序存储结构 */
typedef struct KQueue {
    KQueueElementType data[MAXSIZE];
    int front;        /* 头指针 */
    int rear;        /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}KQueue;
复制代码

1.2.2.1 初始化

//1. 初始化一个队列
KStatus initQueue(KQueue *Q) {
    Q->front = Q->rear = 0;
    return OK;
}
复制代码

1.2.2.2 队列清空

//2. 将队列清空
KStatus clearQueue(KQueue *Q) {
    Q->front = Q->rear = 0;
    return OK;
}
复制代码

1.2.2.3 队列判空

//3. 队列判空
KStatus isEmpty(KQueue Q) {
    return Q.front == Q.rear ;
}
复制代码

1.2.2.4 队列是否满了

//4. 队列是否满了
KStatus isFull(KQueue Q) {
    return Q.front == (Q.rear + 1 ) % MAXSIZE;
}

复制代码

1.2.2.5 查询队列长度

//5. 查询队列长度
int getLength(KQueue Q) {
    return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;
}
复制代码

1.2.2.6 获取队头元素

//6. 获取队头元素
//若队列不空,则用e返回Q的队头元素,并返回OK,不然返回ERROR;
KStatus getHead(KQueue Q, KQueueElementType *e) {
    //判断是否队列为空
    if (isEmpty(Q)) {
        return ERROR;
    }
    //取出元素值
    *e = Q.data[Q.front];
    return OK;
}
复制代码

1.2.2.7 入队

//7. 入队
// 若队列未满,则插入元素e为新队尾元素
KStatus enQueue(KQueue *Q, KQueueElementType e) {
    //判断队列是否满了
    if (isFull(*Q)) {
        return ERROR;
    }
    //将元素e赋值给队尾
    Q->data[Q->rear] = e;
    
    //rear指针向后移动一位,若到最后则转到数组头部
    Q->rear = (Q->rear + 1) % MAXSIZE;
    
    return OK;
}
复制代码

1.2.2.8 出队

//8. 出队
//若队列不空,则删除Q中队头的元素,用e返回值
KStatus deQueue(KQueue *Q, KQueueElementType *e) {
    if (isEmpty(*Q)) return ERROR;
    //从队头取出元素赋值给e
    *e = Q->data[Q->front];
    
    //front指针向后移动一位,删除对头元素
    Q->front = (Q->front + 1) % MAXSIZE;
    
    return OK;
}
复制代码

1.2.2.9 遍历队列

//9. 遍历队列
KStatus traverseQueue(KQueue Q) {
    int i = Q.front;
    while ((i + Q.front) != Q.rear) {
        //从队头遍历到队尾,依次输出元素,i表示当前已经输出到第几个元素了
        //(i + Q.front) != Q.rear 表示 已经遍历到了队尾了,
        //因为咱们不能修改front和rear指向,因此须要一个临时变量记录当前位置
        printf("%d ", Q.data[i]);
        i = (i + 1) % MAXSIZE;
    }
    printf("\n");
    
    return OK;
}

复制代码

1.2.2.10 单元测试

//10. 单元测试
void test() {
    printf("循环队列操做单元测试\n");
    KStatus j;
    int i=0;
    KQueueElementType d;
    KQueue Q;
    initQueue(&Q);
    printf("初始化队列后,队列空否?%u(1:空 0:否)\n",isEmpty(Q));
    
    printf("入队:\n");
    while (i < 10) {
        enQueue(&Q, i);
        i++;
    }
    traverseQueue(Q);
    printf("队列长度为: %d\n",getLength(Q));
    printf("如今队列空否?%u(1:空 0:否)\n",isEmpty(Q));
    printf("出队:\n");
   
   //出队
    deQueue(&Q, &d);
    printf("出队的元素:%d\n",d);
    traverseQueue(Q);

    //获取队头
    j=getHead(Q,&d);
    if(j)
        printf("如今队头元素为: %d\n",d);
    clearQueue(&Q);
    printf("清空队列后, 队列空否?%u(1:空 0:否)\n",isEmpty(Q));

}
复制代码

1.2.2.11 完整代码

//
// main.c
// 010_Queue
//
// Created by 孔雨露 on 2020/4/18.
// Copyright © 2020 Apple. All rights reserved.
//

#include <stdio.h>
#include "stdlib.h"
#include "math.h"
#include "time.h"

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int KStatus;
typedef int KQueueElementType; /* KQueueElementType类型根据实际状况而定,这里假设为int */

/* 循环队列的顺序存储结构 */
typedef struct KQueue {
    KQueueElementType data[MAXSIZE];
    int front;        /* 头指针 */
    int rear;        /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}KQueue;

//1. 初始化一个队列
KStatus initQueue(KQueue *Q) {
    Q->front = Q->rear = 0;
    return OK;
}

//2. 将队列清空
KStatus clearQueue(KQueue *Q) {
    Q->front = Q->rear = 0;
    return OK;
}

//3. 队列判空
KStatus isEmpty(KQueue Q) {
    return Q.front == Q.rear ;
}

//4. 队列是否满了
KStatus isFull(KQueue Q) {
    return Q.front == (Q.rear + 1 ) % MAXSIZE;
}

//5. 查询队列长度
int getLength(KQueue Q) {
    return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;
}

//6. 获取队头元素
//若队列不空,则用e返回Q的队头元素,并返回OK,不然返回ERROR;
KStatus getHead(KQueue Q, KQueueElementType *e) {
    //判断是否队列为空
    if (isEmpty(Q)) {
        return ERROR;
    }
    //取出元素值
    *e = Q.data[Q.front];
    return OK;
}

//7. 入队
// 若队列未满,则插入元素e为新队尾元素
KStatus enQueue(KQueue *Q, KQueueElementType e) {
    //判断队列是否满了
    if (isFull(*Q)) {
        return ERROR;
    }
    //将元素e赋值给队尾
    Q->data[Q->rear] = e;
    
    //rear指针向后移动一位,若到最后则转到数组头部
    Q->rear = (Q->rear + 1) % MAXSIZE;
    
    return OK;
}

//8. 出队
//若队列不空,则删除Q中队头的元素,用e返回值
KStatus deQueue(KQueue *Q, KQueueElementType *e) {
    if (isEmpty(*Q)) return ERROR;
    //从队头取出元素赋值给e
    *e = Q->data[Q->front];
    
    //front指针向后移动一位,删除对头元素
    Q->front = (Q->front + 1) % MAXSIZE;
    
    return OK;
}

//9. 遍历队列
KStatus traverseQueue(KQueue Q) {
    int i = Q.front;
    while ((i + Q.front) != Q.rear) {
        //从队头遍历到队尾,依次输出元素,i表示当前已经输出到第几个元素了
        //(i + Q.front) != Q.rear 表示 已经遍历到了队尾了,
        //因为咱们不能修改front和rear指向,因此须要一个临时变量记录当前位置
        printf("%d ", Q.data[i]);
        i = (i + 1) % MAXSIZE;
    }
    printf("\n");
    
    return OK;
}


//10. 单元测试
void test() {
    printf("循环队列操做单元测试\n");
    KStatus j;
    int i=0;
    KQueueElementType d;
    KQueue Q;
    initQueue(&Q);
    printf("初始化队列后,队列空否?%u(1:空 0:否)\n",isEmpty(Q));
    
    printf("入队:\n");
    while (i < 10) {
        enQueue(&Q, i);
        i++;
    }
    traverseQueue(Q);
    printf("队列长度为: %d\n",getLength(Q));
    printf("如今队列空否?%u(1:空 0:否)\n",isEmpty(Q));
    printf("出队:\n");
   
   //出队
    deQueue(&Q, &d);
    printf("出队的元素:%d\n",d);
    traverseQueue(Q);

    //获取队头
    j=getHead(Q,&d);
    if(j)
        printf("如今队头元素为: %d\n",d);
    clearQueue(&Q);
    printf("清空队列后, 队列空否?%u(1:空 0:否)\n",isEmpty(Q));

}

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    test();
    return 0;
}

复制代码
  • 单元测试,输出结果:
Hello, World!
循环队列操做单元测试
初始化队列后,队列空否?1(1:空 0:否)
入队:
0  1  2  3  4  5  6  7  8  9  
队列长度为: 10
如今队列空否?0(1:空 0:否)
出队:
出队的元素:0
1  2  3  4  5  6  7  8  
如今队头元素为: 1
清空队列后, 队列空否?1(1:空 0:否)
Program ended with exit code: 0
复制代码

1.3 队列链式存储

链式队列的实现思想同顺序队列相似,只需建立两个指针(命名为 top 和 rear)分别指向链表中队列的队头元素和队尾元素,如图下图1 所示:

图 1 链式队列的初始状态

图 1 所示为链式队列的初始状态,此时队列中没有存储任何数据元素,所以 top 和 rear 指针都同时指向头节点。

下面咱们来说解一下 链式队列入队,出队操做,链式队列入队,出队操做基本跟单链表类似,若是不熟悉链表的操做能够参考我以前的链表相关的博客:

  1. "数据结构和算法(一)线性表实现"
  2. 数据结构和算法(二)单链表与单向循环链表的实现

基本就是入队就是在链表尾部结点插入一个元素,出队就是在链表头结点删除一个结点。

  • 链式队列入队操做: 以下图2 表示链式队列依次入队{1,2,3} 三个元素:

图 2 {1,2,3} 入链式队列

如上图所示,在链队队列中,当有新的数据元素入队,只需进行如下 3 步操做:

  1. 将该数据元素用节点包裹,例如新节点名称为 elem;
  2. 与 rear 指针指向的节点创建逻辑关系,即执行 rear->next=elem;
  3. 最后移动 rear 指针指向该新节点,即 rear=elem;
  • 链式队列出队操做: 当链式队列中,有数据元素须要出队时,按照 "先进先出" 的原则,只需将存储该数据的节点以及它以前入队的元素节点按照原则依次出队便可。出队过程就是从链表头依次删除首结点的过程, 如今咱们须要将上图2中的1,2 两个元素出队,其操做过程 以下图3所示:

图 3 链式队列中数据元素出队

如上图3所示,咱们能够知道,在链式队列中队头元素出队,须要作如下 3 步操做:

  1. 经过 top 指针直接找到队头节点,建立一个新指针 p 指向此即将出队的节点;
  2. 将 p 节点(即要出队的队头节点)从链表中摘除;
  3. 释放节点 p,回收其所占的内存空间;

1.3.2 队列链式存储的代码实现

  • 链式队列的结构定义
复制代码

1.3.2.1 初始化

复制代码

1.3.2.2 队列清空

复制代码

1.3.2.3 队列判空

复制代码

1.3.2.4 队列是否满了

复制代码

1.3.2.5 查询队列长度

复制代码

1.3.2.6 获取队头元素

复制代码

1.3.2.7 入队

复制代码

1.3.2.8 出队

复制代码

1.3.2.9 遍历队列

复制代码

1.3.2.10 单元测试

复制代码

1.3.2.11 完整代码

复制代码
  • 单元测试,输出结果:
复制代码
相关文章
相关标签/搜索