文章目录
- queue的应用举例(用了先进先出的排队思想的都算)
- 队列的抽象数据类型
- 头尾相接的顺序存储结构:循环队列
- 空队列和满队列中,front和rear重合
- 办法一:加一个标志变量flag
- 办法二:改变满队列的定义(用更多)
- 队列是否满的判断条件
- 队列长度
- 循环队列顺序存储结构相关代码
- 队列的链式存储结构:链队列
队列是一种FIFO(first in first out)线性表,它和栈一样,本质是线性表,但是不同的是他只允许在一端插入,另一端输出。
允许插入的一端叫做队尾,允许删除的一端叫做队头。总是删除队头元素,插入总是插入到队尾。这无疑是一种非常符合人类生活习惯的数据结构,所以很好学。
虽然符合人类生活习惯,但是在程序设计中也一样用的极其频繁,下面是一些例子
queue的应用举例(用了先进先出的排队思想的都算)
- 电脑的鼠标操作
我有!原来如此! - 客服电话排队
- 键盘输入
各种被敲击到的键会进入到一个队列 - 记事本软件等的输出
把字符流按照队列顺序显示在记事本中,比如你敲击的是god,显示的就也是god,而不是dog,哈哈哈,先进先出
队列的抽象数据类型
ADT Queue
Data//元素具有相同数据类型,相邻元素具有前驱后继关系。
OperationInitQueue(*Q);//建立一个空队列DestroyQueue(*Q);//若Q存在,则销毁ClearQueue(*Q);QueueEmpty(*Q);GetHead(*Q, *e);//把队头元素存入eEnQueue(*Q, e);//把新元素插入到Q中DeQueue(*Q, *e);//删除Q的队头元素,用e返回其值QueueLength(*Q);
endADT
头尾相接的顺序存储结构:循环队列
队列也是线性表,所以和栈一样,也有顺序存储结构和链式存储结构两种。
顺序存储结构还是要用数组实现,但是并不是像栈那样简单地使用,而是要做成一个循环的队列。因为栈只在一端插入或删除,所以直接用数组,把数组为0那端作为栈底就妥了。
但是如果简单地用数组直接实现队列,则数组索引较大的一端是队尾,这样便于插入新元素时直接插在数组下一个空位,则数组索引为0那边是队头,继续思考你就会发现问题了。队尾增加元素确实还比较方便的,时间复杂度是O(1)。但是队头元素的删除呢?如果我们假设只能把数据存在数组的前n个位置,则每次删除队头元素,就需要把所有后续元素往前移动一个位置,时间复杂度是O(n),如下图。
为了不移动后续所有元素,我们取消元素必须一直存在前N个位置的假设,而是用一个头指针front指向队头元素,和一个尾指针rear指向队尾元素的下一个内存位置。这样的话,删除队头元素只需要改变front指针,时间复杂度也变成O(1)了。插入到队尾只需要改一下rear指针,当然时间复杂度还是O(1)。
如果rear指向队尾元素,则队列长度为1时,front和rear指向同一个位置,重合了,这样不便于处理,我们不希望front和rear指向同一个位置。
你以为这样子的队列就丝滑完美了吗?不,还有问题。你想,不断删除队头结点会使得front指针不断后移,而rear指针不变。这时候用来实现队列的数组的前端有很多空位。如果这时候再往队列中不断添加新元素,知道队尾达到了数组的长度,这时候rear指针不断增大,直到指向数组后面那个内存位置,如上图右边,就不能再增加新元素了(这叫做假溢出),否则就会内存越界,可是明明数组的前端还有很多空位可用啊。
为了解决假溢出,我们不要让rear指针指向数组后方内存,本身这样做也是很危险的,一般只有迫不得已才这样做,而让rear指针指向数组的头部,索引为0处,这样形成一个环,头尾相接,则可以利用数组前端空位。也永远不会出现指针指向不明的问题。
所以,经过上面一番问题的出现和解决,我们设计出了顺序存储结构的队列:循环队列。我们也理解了为什么队列用顺序结构存储时必须是循环的,而栈不是。
空队列和满队列中,front和rear重合
让rear指针指向队尾元素的下一个位置,还是会有两种情况下,front会和rear相等:
- 空队列:front == rear == someValue(我之前以为是NULL,后来发现并不是!因为使用数组实现,所以这里的front和rear指针实际是数组下标,是整数,所以不会是NULL)
- 满队列: front == rear == someValue
由于空队列时俩指针也不是NULL,而是某个整数(数组下标),那怎么解决这个问题呢?有两个办法,都是用多用点空间的办法来换取两指针重合的情况判断。
办法一:加一个标志变量flag
设置一个标志变量flag,当标志为0且俩指针重合,则是空队列。这用了额外的一个字节,因为我们一般不会只分配一个比特,虽然表示这个标志位只需要一个比特。
- 空队列:front == rear && flag == 0
- 满队列:front == rear && flag == 1
办法二:改变满队列的定义(用更多)
满队列会遇到front和rear重合是因为我们把数组的空间全用完了。如果我们省出一个位置不用,即还剩一个位置为空时我们就认为队列已经满了,不能再插入新元素了,那么两个指针在满队列下也不会重合了。
这样一来,就只有空队列会让两个指针重合了。
无需利用外部变量,直接利用队列本身就可以实现,所以这种办法用的更多。后面我们也只实现这种办法。
如下图,左右都是新方案的满队列。用一个数组位置来换取指针重合的唯一性。
队列是否满的判断条件
从上图可以看到,rear可能比front大,也可能比front小。满队列时,二者的差值可能是1,也可能是N-1(N是数组长度)。
可以确定的是,如果rear比front大且队列为满,则一定是上图左边这种满队列情况,一大就大一整圈。front一定指向第一个位置(front = 0),而rear一定指向最后一个位置(rear = N - 1)。
如果rear比front小且队列为满,则一定是上图右边这样,rear比front小1
综合这两种情况可以发现,队列满一定有
队列长度
另外,不考虑队列是否为满,如果
,则队列长度为
,如上图左子图。
如果
,则队列长度f分为两段,一段为
,另一端为
,如上图右子图。所以队列长度是
综合这两种情况,队列长度的通用公式则为:
总结来说,如果队列中数据元素的类型占用空间比较大,比如是某个类的对象,这个类有很多私有数据成员,则适合用第一种办法,如果队列中存储的数据的类型是int等类型,占用内存和1字节差不多的,对内存又不是特别严苛,就可以使用第二种办法。
循环队列顺序存储结构相关代码
队列结点的结构体
Typedef struct
{ElemType data[SIZE];int front;//无论数据类型是什么,指针类型都是整型int rear;
}SqQueue;
初始化
Status InitQueue(SqQueue * Q)
{Q->front = Q->rear = 0;return OK;//使用数组,没有要求自己分配数组的内存,所以不需要malloc
}
求队列长度
int QueueLength(SqQueue * Q)
{return (Q->rear - Q->front + SIZE) % SIZE;
}
入队列操作
Status EnQueue(SqQueue * Q, ElemType e)
{if ((Q->rear + 1) % SIZE == Q->front)return ERROR;//已满Q->data[Q->rear] = e;if (Q->rear == SIZE - 1)Q->rear = 0;else++(Q->rear);return OK;
}
改变尾指针的那段代码可以简化为如下:
Status EnQueue(SqQueue * Q, ElemType e)
{if ((Q->rear + 1) % SIZE == Q->front)return ERROR;//已满Q->data[Q->rear] = e;Q->rear = (Q->rear + 1) % SIZE;return OK;
}
出队列操作
Status DeQueue(SqQueue * Q, ElemType * e)
{if (Q->rear == Q->front)return ERROR;//空*e = Q->data[Q->front];Q->front = (Q->front + 1) % SIZE;return OK;
}
到此为止,我们学会了队列的顺序存储结构,用数组实现的循环队列,可以通过上面几个基本函数看到循环队列的时间复杂度还是很不错,入队,出队,返回队长度,初始化的时间复杂度全都是O(1)。
但是他再好,也是顺序结构,即要底层要用数组实现,那么只要用数组就一定会有数组的缺点:需要提前设置数组长度,长度固定后不能改。这使得循环队列的灵活性差了一些,存的项最多就N-1个(有一个空着,为了使得满队列时两指针不重合,只有空队列才重合)。
要让长度灵活,当然还是要选择链表,动态实时地分配,随用随分配内存,十分方便。
队列的链式存储结构:链队列
本质上就是线性表的单链表,只是他只能尾进头出罢了。
front指针指向头结点(链表喜欢设置一个头结点以使得空队列和普通队列处理统一),rear指针指向最后一个结点(和循环队列不同)。
队列的结点
typedef struct QNode
{ElemType data;struct QNode * next;
}QNode, *QueuePtr;//这里写两个则struct不能是匿名的,且第一个和结构名一样.QueuePtr即QNode *
队列的链表结构
typedef struct
{QueuePtr front, rear;//只有队头队尾指针
}LinkQueue;
入队操作
Status EnQueue(LinkQueue * Q, ElemType e)
{//不用判断是否已满,因为是链表(QueuePtr) q = (QueuePtr)malloc(sizeof(ONode));\if (!q)return ERROR;//分配失败,或者写exit(OVERFLOW);q->data = e;q->next = NULL;Q->rear->next = q;Q->rear = q;return OK;
}
出队操作
Status DeQueue(LinkQueue * Q, ElemType * e)
{QNode * q;if (Q->front == Q->rear)return ERROR;//空*e = Q->front->next->data;q = Q->front->next;Q->front = Q->front->next;free(q);return OK;
}