数据结构详谈队列的顺序存储及C语言实现

循环队列及其基本操作的C语言实现

  • 前言
  • 一、队列的顺序存储
    • 1.1 队尾指针与队头指针
    • 1.2 基本操作实现的底层逻辑
      • 1.2.1 队列的创建与销毁
      • 1.2.2 队列的增加与删除
      • 1.2.3 队列的判空与判满
      • 1.2.4 逻辑的局限性
  • 二、循环队列
    • 2.1 循环队列的实现逻辑一
    • 2.2 循环队列的实现逻辑二
    • 2.3 循环队列的实现逻辑三
  • 三、如何实现队列的循环
  • 四、循环队列的C语言实现
    • 4.1 空间置换法的C语言实现
      • 4.1.1 数据类型的定义
      • 4.1.2 队列的初始化
      • 4.1.3 队列的判空
      • 4.1.4 队列的判满
      • 4.1.5 队列的入队
      • 4.1.6 队列的出队
      • 4.1.7 队列的查找
      • 4.1.8 队列的销毁
      • 4.1.9 空间置换法的演示
    • 4.2 标志法的C语言实现
      • 4.2.1 数据类型的定义
      • 4.2.2 队列的初始化
      • 4.2.3 队列的判空
      • 4.2.4 队列的判满
      • 4.2.5 队列的入队
      • 4.2.6 队列的出队
      • 4.2.7 队列的查找
      • 4.2.8 队列的销毁
      • 4.2.9 标志法的C语言实现
  • 结语

封面

前言

大家好,很高兴又和大家见面啦!!!
在上一篇内容中,我们在介绍完队列的基本概念、重要术语以及基本操作后,又回顾了一下数据结构的三要素——数据的逻辑结构、数据的存储结构以及数据的运算。
队列这种数据结构我们已经介绍了它的逻辑结构以及数据运算的定义,从这一篇开始,我们将详细介绍队列的数据的存储结构以及数据的运算的实现。
在今天的内容中,我们要介绍的是队列在内存中的顺序存储结构以及如何通过C语言来实现相关的基本操作。

一、队列的顺序存储

顺序存储想必大家都并不陌生了,顺序存储指的是逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系有存储单元的邻接关系来体现。简单的理解就是在内存中分配一块连续的存储单元来存放队列中的元素。

1.1 队尾指针与队头指针

由队列的操作特性可知,我们在对队列进行入队时需要有一个指向队尾的标志,在进行出队时需要有一个指向队头的标志,这样我们才能正常实现数据元素的入队和出队操作。

在栈中,我们将指向栈顶的标志称为栈顶指针,在队列中同理:

  • 指向队尾的标志称为队尾指针(rear)
  • 指向队头的标志称为队头指针(front)

这两个指针的主要做用就是告诉我们元素从哪里入队,从哪里出队。现在了解完了这两个指针,下面我们就来探讨一下如何在队列的顺序存储上来实现队列的基本操作;

1.2 基本操作实现的底层逻辑

为了更好的介绍这些基本操作,我们还是以创建、销毁、增删改查的顺序来进行介绍;

1.2.1 队列的创建与销毁

  1. 队列的创建

对于队列的创建实际上就是定义数据类型、定义队列变量以及初始化一个队列。那如果要定义一个数据类型,按照前面的介绍,队列的数据类型中至少有三个内容:

  1. 存放元素的一块连续的存储空间;
  2. 指向队尾的队尾指针rear
  3. 指向队头的队头指针front

对于这三个内容的实现起始并不复杂,我们可以通过静态数组来实现一块连续的存储空间;
既然是静态数组,那么我们要想找到数组中不同位置的元素那就需要数组下标,因此队头指针与队尾指针就需要是两个存放数组下标的整型变量,因此我们可以将其用C语言表述为:

//队列的顺序存储类型
#define MaxSize 10 //定义队列的最大长度
typedef int ElemType;//重命名队列中数据元素的数据类型,可以修改为其它数据类型
typedef struct SqQueue {
	ElemType data[MaxSize];//存放队列数据元素的静态数组
	int front, rear;	   //定义队列的队头指针与队尾指针
}SqQueue;				   //重命名后的队列数据类型

那这样是不是就没问题了呢?这里我们先放一放,后面再来讨论;

在定义好数据类型后,我们只需要通过类型来定义一个变量并即将该变量进行初始化,即可完成队列的创建。定义变量都很简单,关键是这个初识我们应该如何表示?

为了更好的理解这个问题,那我们来看一下图片:
队列的创建
按照两个指针的描述,我们在创建队列时应该是如图所示,但是现在就有一个问题,队尾是负责进行入队操作的,队首是负责进行删除操作的,我们应该如何来表示队列的插入与删除呢?

这里我们需要联想一下栈的插入与删除。在栈中如果栈为空栈时,栈顶指针指向的是栈底,入栈一个新元素,栈顶指针就需要往上移动一位来表示入栈;当我们的栈不为空栈时,栈顶指针每往下移动一位就是表示出栈。

也就是说在栈中,我们是借助指针的移动来表示栈顶的出栈与入栈,在队列中,我同样也可以仿照这种思路了,通过指针的移动来表示入队与出队。因此我们在创建一个队列时,可以将frontrear都指向队头,如下所示:
队列的创建2
当有新元素入队时,我们可以将队尾指针往上移动,当有元素出栈时,我们同样可以将队头指针往上移动,入下图所示:
队列的创建3
既然这样,那我们就可以在初始化时将队头指针与队尾指针都初始化为0,如下所示:

	Q->front = Q->rear = 0;//赋值语句的连续赋值形式
	//等价于下面两句代码
	//Q->rear = 0;
	//Q->front = Q->rear;

具体是不是如此,我们还是先继续往下看。

  1. 队列的销毁

如果我们想要销毁一个队列,无非就是将队列中的所有元素依次出队,那如果一个存满元素的队列要销毁的话,那我们就需要队头指针一直上移如下图所示:

队列的销毁
如图所示,当我们在完全销毁队列后,此时的队列的两个指针又重合了,并且都指向了队尾,转化成C语言,则可以表述为:

	if (Q->front == Q->rear && Q->rear == MaxSize - 1)
		printf("队列已销毁
");

如果只是这样判断,还有没有什么问题呢?我们先继续往后看;


1.2.2 队列的增加与删除

队列的增加与删除,在创建与销毁的讨论中我们已经提到过了,当增加一个新的数据元素时,我们可以通过队尾指针的移动来表示,那现在的问题是,队尾指针是应该如何进行移动?下面就有几种情况:

  1. 先入队新的元素再移动,指针指向队尾元素的下一个位置;
  2. 先移动指针再入队新元素,指针指向队尾元素;

用C语言来表达则是:

	//先入队再移动
	Q->data[Q->rear++] = x;
	//先移动再入队
	Q->data[++Q->rear] = x;

PS:在介绍栈的入栈与出栈时已经介绍过这里的代码简写的意思,这里我就不再重复介绍了。

队列的入队逻辑具体选择哪一种我们先不着急,接着往下看;

当我们要删除一个数据元素时,我们可以通过队头指针的移动来表示,同样的,队头指针的移动和队尾指针一样,也有下面几种情况:

  1. 先出队元素再移动,指针指向的是队头元素;
  2. 先移动指针再出队,指针指向的是队头元素的前一个位置;

用C语言表示则是:

	//先出队再移动
	x = Q->data[Q->front++];
	//先移动再出队
	x = Q->data[++Q->front];

队列的出队逻辑具体选择哪一种我们也不着急,接着往下看;


1.2.3 队列的判空与判满

队列的判空与判满的实现取决于队列初始化的方式,当我们创建好一个队列时,此时的队列中是不存在任何元素的,因此刚创建好的队列是一个空队列,这相信大家应该都能理解。
那么我们在判空时,只要按照初始化的方式即可进行判空操作也就是:

	if (Q.front == Q.rear && Q.front == 0)
		printf("队列为空
");

那判满的话我们则可以根据队头指针与队尾指针来同时判断,如下所示:

if (Q.front == 0 && Q.rear == MaxSize - 1)
		return true;

但是具体能不能像这样实现呢?下面我们就来仔细探讨一下;


1.2.4 逻辑的局限性

在前面对基本操作的实现中,我们有提到过以下几个问题:

  1. 数据类型只定义静态数组和两个指针行不行?
  2. 队列的初始化能不能将两个指针都初始化为0?
  3. 队列的销毁能不能通过两个指针都指向MaxSize-1来判断是否成功销毁?
  4. 队列的增加与删除的逻辑应该是什么?
  5. 队列的判满与判空能不能实现?

我们来看一下下面的图片:
队列操作的演示
从上图中我们可以看到按照前面的分析,在创建数据类型时只定义静态数组与两个指针并将指针初始化为0的情况下,我们要实现一个队列,那我们的入队操作与出队操作都应该选择先执行入队或者出队,后执行指针的移动,并且判满与销毁的判定应该是rear==MaxSize;
但是这样就会造成一个问题,如下图所示:
逻辑的局限性
在这种情况下,我们此时的队列并不是满队列,但是rear指针已经指向了MaxSize,如果我们要继续入队的话此时就会出现数组越界的情况。

也就是说我们前面的分析只适合与创建好队列后从初始化开始到销毁结束,中间的流程都不能发生任何变化,即入队就要全部元素入满,出队就要直接到销毁。

这样实现的队列就好比一次性碗筷,只能够使用一次,感受一下,并不能重复利用,那我们应该如何调整才能实现一个完整的队列呢?这里就需要引入一个新的概念——循环队列;

二、循环队列

2.1 循环队列的实现逻辑一

所谓的循环队列并不是指像循环链表那种头尾相连的结构,队列的存储结构依然是顺序存储,只不过指针存储的范围是0~MaxSize-1;通过指针的这种存储的循环方式将队列看做一个环,如下图所示:

循环队列
此时我们各个操作的执行逻辑如下所示:

  1. 数据类型的定义:此时我们只需要定义三个对象——存储数据元素的一维数组,指向队头元素的队头指针与指向队尾元素下一个位置的队尾指针;
  2. 初始化:在初始化时我们将队头指针与队尾指针都初始化为0,使他们同时指向数组首元素的位置;
  3. 判空:我们在进行判空时只需要判断front == rear 这个条件是否成立,成立则为空队列,否则为非空队列;
  4. 入队:入队时的逻辑是先入队后移动,即data[rear] = x;rear++;简写为data[rear++] = x
  5. 判满:为了保证判空的逻辑正常,此时我们需要舍弃最后一个空间来作为判满的条件,也就是说当队尾指针的下一个位置为队头指针时表示队列已满,也就是rear++ == front;当条件成立时表示队列已满,否则表示队列未满;
  6. 出队:在进行出队操作时的逻辑则是先出队后移动,即x = data[front];front++;简写为x = data[front++]
  7. 销毁:在进行销毁时我们需要重复进行出队操作,当队头指针与队尾指针再一次指向同一个位置时,表示队列中的元素已经全部出队,此时队列为空队列,之后我们在将对应的空间释放掉就行,因为是通过静态数组实现,所以当程序结束后,空间就会被操作系统自动回收;

这时可能就有朋友会说了,你像这个样子不就造成了空间的浪费吗?有没有什么办法可以将这一块空间也给利用起来呢?

2.2 循环队列的实现逻辑二

首先对于空间浪费的问题,我想说的是我们这里只浪费了一个数据类型所需的空间,相比于之前的一次性队列来说,我们在利用上面会节省更多的内存空间;
最后我们也可以通过对数据类型的调整来完成队列空间的饱和运用,如下所示:
循环队列2
我们通过设定一个记录当前队列长度的变量len,在这种情况下,我们的基本操作就需要做出如下调整:

  1. 数据类型的定义:增设一个记录当前队列长度的变量len
  2. 初始化:在初始化时需要将当前队列长度初始化为0;
  3. 判空:在判空时,我们可以直接判断len == 0是否成立,成立,则为空队列,否则,为非空队列;
  4. 入队:每次完成一个新元素入队后,我们都需要执行一次len++
  5. 判满:在判满时,我们可以直接判断len == MaxSize是否成立,成立队列已入满,否则队列还未满;
  6. 出队:每完成一个元素的出队后,我们都需要执行一次len--
  7. 销毁:在完成所有的元素出队后,此时的队列长度又会变为0,所以我们只需要指向判空操作即可;

这时可能又有朋友会问,你这里是将队尾指针指向的是队尾元素的下一个位置,那如果我将其指向队尾元素又应该如何操作呢?

2.3 循环队列的实现逻辑三

其实这个实现方式也有很多种,这里我们还是以初始化为0的情况下来实现,如下所示:
循环队列3

我们通过增设一个入队标志tag来表示当前空间元素的入队情况,对应的基本操作就有如下调整:

  1. 数据类型的定义:增设一个标志变量tag,当tag = 0时表示此时的空间没有元素入队,当tag = 1时表示此时的空间有元素入队;
  2. 初始化:在初始化阶段,我们需要将队头指针初始化为0,入队标志初始化为0,队尾指针初始化为MaxSize - 1
  3. 判空:在进行判空时,我们需要通过判断rear++ ==front && tag == 0是否成立,成立,则队列为空队列,否则,队列为非空队列;
  4. 入队:在进行入队操作时,此时的入队逻辑是先移动后入队,即rear++;data[rear] = x;简写为data[++rear] = x,并且我们还需要将入队标志tag修改为1,如下图所示:
    循环队列4
  5. 判满:在判断队列是否入满时,我们需要判断rear++ == front && tag == 1是否成立,成立则说明此时的队列已满,否则此时的队列未满;
  6. 出队:在进行出队操作时,我们需要将入队标志tag修改为0,如下图所示:
    循环队列5
  7. 销毁:在完成所有元素出队后,队列又会变为空队列,因此,我们只需要进行判空即可;

循环队列的实现逻辑主要是有以上三种方式,当然肯定还有其他的方式,但是操作上基本上都是大同小异,现在我们还有一个最重要的问题还有有提到——如何实现队列的循环?

三、如何实现队列的循环

在前面的逻辑中,我们来判断空队列和满队列时有提过通过判断队尾指针的下一个空间是否为队头指针,我们前面也是通过rear++ == front来说明这种判断的过程,但是在实际实现中,这是有问题的,因为我们是通过静态数组实现的队列,它在内存中还是以顺序存储的形式进行存放的,如下所示:
指针的循环
我们是将这种连续存放的空间臆想为一个环状结构,但并不代表它就是一个环状结构,因此,我们只能够通过指针来完成队列的循环。

那如何实现呢?我们现在先来思考一下这两个指针存放的是什么内容?

没错,就是数组下标,在前面我们也有提到过它们的取值范围是0~MaxSize - 1,只要我能够保证不管如何进行入队和出队操作,它们的取值都是在这个范围内,那就能实现队列的循环操作。

在C语言中我们有介绍过一个只能够在整型中使用的算术操作符——%(取模),这个操作符的作用就是获取两个整数相除后的余数,就比如在整型运算中我们知道

5

/

2

=

2

1

5/2=2……1

5/2=2……1,在C语言中我们通过

/

/

/来获取两个整数的商,通过

%

\%

%来获取两个整数的余数,如下所示:
指针的循环2
在除法

a

/

b

=

c

d

a / b = c …… d

a/b=c……d中,d的取值肯定是在[0 , b - 1]之间的,也就是说如果通过对下标进行与MaxSize取模的话,那是不是就能保证指针存储的值一定是在[0 , MaxSize - 1]之间了呢?

下面我们可以通过代码来测试一下,如下所示:
指针的循环3
可以看到,确实如此,通过取模运算后得到的值正好是在[0 , 8]之间。因此循环队列的实现就需要借助取模运算符来完成。下面我们就来看一下循环队列的C语言实现;

四、循环队列的C语言实现

前面我们介绍了3中实现方式,对于记录队列长度的实现相比之下会简单一点,这里我就不多做介绍了,我们主要是介绍另外两种方式,这里我们将这两种方式分别叫做空间置换法标志法
这里的空间置换指的是舍弃小块空间来获取整个队列的空间复用
标志法指的是通过设立入队标志来完成循环队列

4.1 空间置换法的C语言实现

4.1.1 数据类型的定义

空间置换法在定义类型时,总共定义了三个对象——静态数组、队头指针与队尾指针,它的实现就比较简单,如下所示:

//队列的顺序存储类型——空间置换法
#define MaxSize 10 //定义队列的最大长度
typedef int ElemType;//重命名队列中数据元素的数据类型,可以修改为其它数据类型
typedef struct SqQueue {
	ElemType data[MaxSize];//存放队列数据元素的静态数组
	int front, rear;	   //定义队列的队头指针与队尾指针
}SqQueue;				   //重命名后的队列数据类型

在定义好数据类型后,我们就可以通过数据类型来定义一个队列Q,如下所示:

void test_Queue1() {
	SqQueue Q;//定义队列Q
}

4.1.2 队列的初始化

在初始化阶段,我们只需要将两个指针初始化为0就行,如下所示:

//队列的初始化
bool InitQueue(SqQueue* Q) {
	if (!Q)
		return false;//当指针Q为空指针时,返回false
	Q->front = Q->rear = 0;//赋值语句的连续赋值形式
	return true;
}

一定要注意,当我们需要对实参进行修改时,是需要通过传址的方式进行传参,而形参则需要通过指针来接收。我们如果需要调用对应的函数时,一定要对形参进行判空,如果此时的形参为空指针,那说明我们的传参出现了问题,这里千万要记得;

4.1.3 队列的判空

对于空间置换法的判空,我们是根据两个指针是否相等来判断的,所以此时直接判断就是,如下所示:

//队列的判空
bool EmptyQueue(SqQueue Q) {
	if (Q.rear == (Q.front))
		return true;
	return false;
}

4.1.4 队列的判满

在进行判满时,因为此时的两个指针在逻辑上应该是处于相邻的位置,也就是队尾指针的下一个空间就是队头指针指向的空间,因此这里我们就需要通过

%

\%

%操作符来完成逻辑上的相邻,如下所示:

//队列的判满
bool FullQueue(SqQueue Q) {
	if ((Q.rear + 1) % MaxSize == Q.front)
		return true;
	return false;
}

4.1.5 队列的入队

在进行入队操作时,因为我们要确保队尾指针的取值是循环的,因此我们在移动队尾指针时就需要借助取模操作符,如下所示:

//队列的入队
bool EnQueue(SqQueue* Q, ElemType x) {
	if (!Q)
		return false;
	if (FullQueue(*Q))//判满
		return false;
	Q->data[Q->rear] = x;//先入队
	Q->rear = ++(Q->rear) % MaxSize;//再移动
	return true;
}

我们可以执行入队操作的前提条件是此时的队列还未满,因此,在入队前我们需要调用一下判满函数,来确保此时的队列还未满。

在调用函数的时候一定要注意,此时在入队函数中的参数Q是一个指针,但是判满函数的参数Q是一个变量,这里我们需要先对指针Q进行解引用,再传参

4.1.6 队列的出队

在进行出队操作时,我们同样也是需要借助

%

\%

%操作符来确保队头指针的取值是循环的,如下所示:

//队列的出队
bool DeQueue(SqQueue* Q, ElemType* x) {
	if (!Q)
		return false;
	if (EmptyQueue(*Q))//判空
		return false;
	*x = Q->data[Q->front];//先出队
	Q->front = ++(Q->front) % MaxSize;//再移动
	return true;
}

执行出队的前提条件是此时的队列为非空队列,因此,在出队前我们需要调用一下判空函数,来确保此时的队列为非空队列;

4.1.7 队列的查找

在队列中,我们的查找也是受到限制的,我们不能越过队头或者队尾来访问其他元素,因此,这里我们在实现查找时,只能够查找队头或者队尾元素,如下所示:

//队列的查找
bool GetHead(SqQueue Q,ElemType* x) {
	if (EmptyQueue(Q))//判空
		return false;
	*x = Q.data[Q.front];//查找队头元素
	return true;
}

我们在进行队列元素访问时,只能从出队的一端来访问元素,因此,队列的查找只支持访问队头元素。

4.1.8 队列的销毁

队列的销毁就是通过重复进行出队操作使队列变成空队列,最后再将队列的空间回收即可完成销毁,因此我们指向销毁时需要判断队列是否为空,如下所示:

//队列的销毁
bool DestroyQueue(SqQueue* Q) {
	if (!Q)
		return false;
	while (EmptyQueue(*Q)) {
		int x = 0;
		if (DeQueue(Q, &x))
			printf("元素%d已成功出队
", x);
		else
			printf("出队失败
");
	}
	return true;
}

4.1.9 空间置换法的演示

在完成了这些基本操作后,下面我们就来演示一下空间置换法,代码如下所示:

//队列的顺序存储类型——空间置换法
#define MaxSize 10 //定义队列的最大长度
typedef int ElemType;//重命名队列中数据元素的数据类型,可以修改为其它数据类型
typedef struct SqQueue {
	ElemType data[MaxSize];//存放队列数据元素的静态数组
	int front, rear;	   //定义队列的队头指针与队尾指针
}SqQueue;				   //重命名后的队列数据类型
//队列的初始化
bool InitQueue(SqQueue* Q) {
	if (!Q)
		return false;//当指针Q为空指针时,返回false
	Q->front = Q->rear = 0;//赋值语句的连续赋值形式
	return true;
}
//队列的判空
bool EmptyQueue(SqQueue Q) {
	if (Q.rear == (Q.front))
		return true;
	return false;
}
//队列的判满
bool FullQueue(SqQueue Q) {
	if ((Q.rear + 1) % MaxSize == Q.front)
		return true;
	return false;
}
//队列的入队
bool EnQueue(SqQueue* Q, ElemType x) {
	if (!Q)
		return false;
	if (FullQueue(*Q))//判满
		return false;
	Q->data[Q->rear] = x;//先入队
	Q->rear = ++(Q->rear) % MaxSize;//再移动
	return true;
}
//队列的出队
bool DeQueue(SqQueue* Q, ElemType* x) {
	if (!Q)
		return false;
	if (EmptyQueue(*Q))//判空
		return false;
	*x = Q->data[Q->front];//先出队
	Q->front = ++(Q->front) % MaxSize;//再移动
	return true;
}
//队列的查找
bool GetHead(SqQueue Q,ElemType* x) {
	if (EmptyQueue(Q))//判空
		return false;
	*x = Q.data[Q.front];//查找队头元素
	return true;
}

//队列的销毁
bool DestroyQueue(SqQueue* Q) {
	if (!Q)
		return false;
	while (EmptyQueue(*Q)) {
		int x = 0;
		if (DeQueue(Q, &x))
			printf("元素%d已成功出队
", x);
		else
			printf("出队失败
");
	}
	return true;
}
void test_Queue1() {
	SqQueue Q;//定义队列Q
	if (InitQueue(&Q))//队列初始化
		printf("初始化成功
");
	else {
		printf("初始化失败
");
		return;
	}
	int x = 0;
	if (GetHead(Q,&x))//查找队头元素
		printf("此时的队列为非空队列,队头元素为%d
", x);
	else {
		printf("此时的队列为空队列
");
	}
	//元素入队
	while (scanf("%d", &x) == 1) {
		if (EnQueue(&Q, x)) {
			printf("元素%d入队成功
", x);
		}
		else {
			printf("元素%d入队失败
", x);
			if (FullQueue(Q))//插入失败后进行判满
				printf("此时的队列已满
");
			else {
				printf("此时的队列未满
");
			}
		}
	}
	//元素出队
	if (DeQueue(&Q, &x)) {
		printf("元素%d出队成功
", x);
		if (GetHead(Q, &x))//查找队头元素
			printf("此时的队列为非空队列,队头元素为%d
", x);
		else {
			printf("此时的队列为非空队列
");
		}
	}
	else {
		printf("出队失败
");
		if (EmptyQueue(Q))//队列判空
			printf("此时的队列为空队列
");
		else
			printf("此时的队列为非空队列
");
	}
	//队列销毁
	if (DestroyQueue(&Q)) {
		printf("队列成功销毁
");
	}
	else {
		printf("队列销毁失败
");
	}
}

接下来我们在主函数中调用一下test_Queue1函数来测试一下空间置换法:
空间置换法的演示
可以看到,此时所有的功能都能够正常运行,也就是说我们完成了通过空间置换法完成了一个循环队列;

4.2 标志法的C语言实现

4.2.1 数据类型的定义

在标志法中,我们增设了一个出入队的标志,对应的数据类型如下所示:

//队列的顺序存储类型——标志法
#define MaxSize 10 //定义队列的最大长度
typedef int ElemType;//重命名队列中数据元素的数据类型,可以修改为其它数据类型
typedef struct SqQueue {
	ElemType data[MaxSize];//存放队列数据元素的静态数组
	int front, rear;	   //定义队列的队头指针与队尾指针
	int tag;			   //出入队标志
}SqQueue;				   //重命名后的队列数据类型

4.2.2 队列的初始化

出入队的标志取值,我们将其设定为出队为0,入队为1,

//队列的初始化
bool InitQueue(SqQueue* Q) {
	if (!Q)
		return false;//当指针Q为空指针时,返回false
	Q->front = 0;//队头指针指向的是队头元素所在空间
	Q->rear = MaxSize - 1;//队尾指针指向的是队尾元素所在空间
	Q->tag = 0;//此时队列为空队列,相当于执行了出队操作
	return true;
}

4.2.3 队列的判空

当队列为空队列时,此时队头指针与队尾指针在逻辑上是相邻的,我们可以通过队尾指针向上移动找到队头指针,并且此时的入队标志为0,表示的是通过出队操作的到的对应关系,代码如下所示:

//队列的判空
bool EmptyQueue(SqQueue Q) {
	if (((Q.rear + 1) % MaxSize) == Q.front && Q.tag == 0)
		return true;
	return false;
}

同样的,为了保证指针的可循环性,我们这里在判断时是借助了

%

\%

%操作符实现的相邻判断;

4.2.4 队列的判满

在标志法的实现下,判空与判满两个指针的位置关系是一致的,唯一不同的就是入队标志,当标志为1时说明此时是通过入队得到的对应的位置关系,那就说明此时是队列已满的情况,代码如下所示:

//队列的判满
bool FullQueue(SqQueue Q) {
	if (((Q.rear + 1) % MaxSize) == Q.front && Q.tag == 1)
		return true;
	return false;
}

4.2.5 队列的入队

当队尾指针指向的是队尾元素时,我们在进行入队操作是需要先将队尾指针往后移动一位后,再进行入队操作,由于设立了入队标志,所以当我们执行入队操作时,需要将入队标志改为1,对应的代码如下所示:

//队列的入队
bool EnQueue(SqQueue* Q, ElemType x) {
	if (!Q)
		return false;
	if (FullQueue(*Q))//判满
		return false;
	Q->rear = ++(Q->rear) % MaxSize;//先移动
	Q->data[Q->rear] = x;//再入队
	Q->tag = 1;//执行入队操作,入队标志改为1
	return true;
}

4.2.6 队列的出队

队列的出队操作唯一需要改动的就是将入队标志修改为0,代码如下所示:

//队列的出队
bool DeQueue(SqQueue* Q, ElemType* x) {
	if (!Q)
		return false;
	if (EmptyQueue(*Q))//判空
		return false;
	*x = Q->data[Q->front];//先出队
	Q->front = ++(Q->front) % MaxSize;//再移动
	Q->tag = 0;//指向出队操作,入队标志改为0
	return true;
}

4.2.7 队列的查找

对于两种方法的查找而言,都是一致的,因为我此时只需要找到队头或者队尾元素即可;

//队列的查找
bool GetHead(SqQueue Q, ElemType* x) {
	if (EmptyQueue(Q))//判空
		return false;
	*x = Q.data[Q.front];//查找队头元素
	return true;
}

4.2.8 队列的销毁

标志法的销毁操作,同样是通过重复调用出队操作,因此,这里也是不需要有任何修改,代码如下所示:

//队列的销毁
bool DestroyQueue(SqQueue* Q) {
	if (!Q)
		return false;
	while (EmptyQueue(*Q)) {
		int x = 0;
		if (DeQueue(Q, &x))
			printf("元素%d已成功出队
", x);
		else
			printf("出队失败
");
	}
	return true;
}

4.2.9 标志法的C语言实现

下面我们就来看一下整体的代码:

//队列的顺序存储类型——标志法
#define MaxSize 10 //定义队列的最大长度
typedef int ElemType;//重命名队列中数据元素的数据类型,可以修改为其它数据类型
typedef struct SqQueue {
	ElemType data[MaxSize];//存放队列数据元素的静态数组
	int front, rear;	   //定义队列的队头指针与队尾指针
	int tag;			   //出入队标志
}SqQueue;				   //重命名后的队列数据类型
//队列的初始化
bool InitQueue(SqQueue* Q) {
	if (!Q)
		return false;//当指针Q为空指针时,返回false
	Q->front = 0;//队头指针指向的是队头元素所在空间
	Q->rear = MaxSize - 1;//队尾指针指向的是队尾元素所在空间
	Q->tag = 0;//此时队列为空队列,相当于执行了出队操作
	return true;
}
//队列的判空
bool EmptyQueue(SqQueue Q) {
	if (((Q.rear + 1) % MaxSize) == Q.front && Q.tag == 0)
		return true;
	return false;
}
//队列的判满
bool FullQueue(SqQueue Q) {
	if (((Q.rear + 1) % MaxSize) == Q.front && Q.tag == 1)
		return true;
	return false;
}
//队列的入队
bool EnQueue(SqQueue* Q, ElemType x) {
	if (!Q)
		return false;
	if (FullQueue(*Q))//判满
		return false;
	Q->rear = ++(Q->rear) % MaxSize;//先移动
	Q->data[Q->rear] = x;//再入队
	Q->tag = 1;//执行入队操作,入队标志改为1
	return true;
}
//队列的出队
bool DeQueue(SqQueue* Q, ElemType* x) {
	if (!Q)
		return false;
	if (EmptyQueue(*Q))//判空
		return false;
	*x = Q->data[Q->front];//先出队
	Q->front = ++(Q->front) % MaxSize;//再移动
	Q->tag = 0;//指向出队操作,入队标志改为0
	return true;
}
//队列的查找
bool GetHead(SqQueue Q, ElemType* x) {
	if (EmptyQueue(Q))//判空
		return false;
	*x = Q.data[Q.front];//查找队头元素
	return true;
}

//队列的销毁
bool DestroyQueue(SqQueue* Q) {
	if (!Q)
		return false;
	while (EmptyQueue(*Q)) {
		int x = 0;
		if (DeQueue(Q, &x))
			printf("元素%d已成功出队
", x);
		else
			printf("出队失败
");
	}
	return true;
}
void test_Queue2() {
	SqQueue Q;//定义队列Q
	if (InitQueue(&Q))//队列初始化
		printf("初始化成功
");
	else {
		printf("初始化失败
");
		return;
	}
	int x = 0;
	if (GetHead(Q, &x))//查找队头元素
		printf("此时的队列为非空队列,队头元素为%d
", x);
	else {
		printf("此时的队列为空队列
");
	}
	//元素入队
	while (scanf("%d", &x) == 1) {
		if (EnQueue(&Q, x)) {
			printf("元素%d入队成功
", x);
		}
		else {
			printf("元素%d入队失败
", x);
			if (FullQueue(Q))//插入失败后进行判满
				printf("此时的队列已满
");
			else {
				printf("此时的队列未满
");
			}
		}
	}
	//元素出队
	if (DeQueue(&Q, &x)) {
		printf("元素%d出队成功
", x);
		if (GetHead(Q, &x))//查找队头元素
			printf("此时的队列为非空队列,队头元素为%d
", x);
		else {
			printf("此时的队列为非空队列
");
		}
	}
	else {
		printf("出队失败
");
		if (EmptyQueue(Q))//队列判空
			printf("此时的队列为空队列
");
		else
			printf("此时的队列为非空队列
");
	}
	//队列销毁
	if (DestroyQueue(&Q)) {
		printf("队列成功销毁
");
	}
	else {
		printf("队列销毁失败
");
	}
}

下面我们就来在主函数中调用并测试一下看看结果:
标志法的演示
可以看到此时的标志法实现时能够比空间置换法要多存储一个元素,我们现在也成功使用C语言通过标志法实现了循环队列。

结语

在今天的篇章中,我们详细介绍了队列的顺序存储结构——循环队列,并详细分析了三种实现循环队列的方式,最后通过C语言实现了两种循环队列——空间置换法与标志法,希望今天的内容能够帮助大家在了解队列的顺序存储结构的同时,还能帮助大家熟悉对应的基本操作并能够实现对应的操作。

今天的内容到这里就全部结束了,在下一个篇章中,我们将开始介绍队列的链式存储结构,通过链式存储的队列的基本操作又应该如何实现呢?就让我们期待一下下一篇的内容介绍吧!最后感谢大家的翻阅,咱们下一篇再见!!!