你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

队列基础讲解

[复制链接]
gaosmile 发布时间:2020-9-19 10:20
队列的概念

首先我们联想一下链表,在单链表中,我们只能对他的链表表尾进行插入,对链表的表头进行结点的删除,这样强限制性的链表,就是我们所说的队列。

也就是说,队列(queue)是限定在表的一端进行插入,表的另一端进行删除的数据结构。

如下图所示,假如你去买票排队,每一列队伍都有一个队尾和对头,先来的先买票,后来的后买,买好的就从对头出去,新来买票的就需要从队尾继续排队。

微信图片_20200919101423.png

通常,称进数据的一端为 队尾,出数据的一端为 队头,数据元素进队列的过程称为 入队,出队列的过程称为 出队。

我们可以总结如下

队列是一个线性的数据结构,并且这个数据结构只允许在一端进行插入,另一端进行删除,禁止直接访问除这两端以外的一切数据,且队列是一个先进先出的数据结构。

微信图片_20200919101426.png

如上图,队列就像一个两端相通的水管,只允许一端插入,另一端取出,取出的球就不在水管里面了,而先放入管中的球就会先从管中拿出。

队列存储结构的实现有以下两种方式
  • 顺序队列:在顺序表的基础上实现的队列结构

  • 链队列:在链表的基础上实现的队列结构


    9 [3 _& L5 f0 K1 g) L( S4 E

两者的区别仅是顺序表和链表的区别,即在实际的物理空间中,数据集中存储的队列是顺序队列,分散存储的队列是链队列。

队列的结点设计与初始化

队列只有链式的设计方法,其本身分为多种队列,如顺序队列和循环队列,还有衍生的优先队列等等,我们以顺序队列的设计为例。

首先是队列的结点设计,我们可以设计出两个结构体,一个结构体Node表示结点,其中包含有一个data域和next指针,如图所示:

微信图片_20200919101429.png

其中data表示数据,其可以是简单的类型,也可以是复杂的结构体。

next指针表示,下一个的指针,其指向下一个结点,通过next指针将各个结点链接。

然后我们再添加一个结构体,其包括了两个分别永远指向队列的队尾和队头的指针,看到这里是不是觉得和栈很像?

我们主要的操作只对这两个指针进行操作,如图所示:

微信图片_20200919101432.png

其结构体设计的代码可以表示为:

//结点定义7 W, a" [! v% v1 s  v; ^! X' R
typedef struct node{
& m7 U( f4 z1 ?& ^    int data;! e6 y; O+ Y- @3 ]( z! y" h
    struct node *next;' l- P, H! k: l0 P/ r) H: c
}node;
( H$ |& J; i4 [- t$ X4 U0 @5 x7 ?//队列定义,队首指针和队尾指针. {% i1 W9 W6 g# U0 v* w
typedef struct queue{
7 O$ a- O) b. C3 I0 L' N$ f' Q    node *front;    //头指针
7 ~' C- A0 _8 p. ?+ C$ \; F# }+ @    node *rear;     //尾指针( @6 d4 ?& x0 u+ E+ s
}queue;, r* G" ^) u! G. r5 U5 o" |

对于初始化需要初始化两个类型,一个是初始化结点,一个是初始化队列。

我们看到代码中的描述,初始化队列有些不同,当初始化队列的时候,需要将头尾两个结点指向的内容统统置为空,表示是一个空队列,两个创建的函数代码可以表示为:

//初始化结点
) x4 J! _  R2 @node *init_node(){3 H* D  T1 Y( F3 f4 J7 ]+ n
    node *n=(node*)malloc(sizeof(node));/ A$ S& Y! [/ t: P% N8 Z/ f' I' q6 j
    if(n==NULL){    //建立失败,退出. w5 O8 r4 j& a
        exit(0);
1 r( y/ e$ B+ c5 `& p9 C4 n    }, E; K/ B, N3 s+ E5 }% Z, M! ]
    return n;% k, \* ?* `& `7 S
}9 ?/ ~: ^' B& l$ |% d
//初始化队列" I4 d7 [& y. q3 e* P
queue *init_queue(){) X5 D. {: \' W
    queue *q=(queue*)malloc(sizeof(queue));
( Q8 N" x7 y& [# ?    if(q==NULL){    //建立失败,退出5 D2 h# p0 n" N7 a& r4 K
        exit(0);* F# _% h# x- |" v
    }
: t% D  p; ~4 ]2 K1 ~6 Y. k8 G    //头尾结点均赋值NULL
- j0 C* j: K' k/ Z    q->front=NULL;  
) R; A5 x# ~9 k( {    q->rear=NULL;! c' ?$ Q, {) S( z: H& z$ f
    return q;
! m7 P0 ^" S3 J$ E/ Y}9 Y4 i( |1 U% Z+ k' G1 M8 l
判断队列是否为空

这是一个既简单也很要紧的操作,判断队列是否为空直接就是判断队列头指针是否是空值即可,判断队列是否为空是比较常用的操作,切勿忘记。

其代码可以表示为:

//队列判空
' ~8 h  [/ O$ C8 S  wint empty(queue *q){
9 j8 C2 g# @  f' V3 c- y    if(q->front==NULL){
3 E& x4 E$ ]1 L8 L5 C- ?        return 1;   //1--表示真,说明队列非空
& ^4 [) p9 f8 i. C4 h" c& c    }else{7 M9 H, D1 }$ i* E, z# I! p% X+ [* X
        return 0;   //0--表示假,说明队列为空
. t3 Z$ K' W/ Z6 l8 X& S$ t    }* D$ c4 i0 i  K, k4 A+ L' g- Q
}
0 ]) R1 t1 c* {+ E) }

或者直接利用返回值进行更简单的判断也可以,代码如下:

int empty(queue *q){
  H2 j/ o( i) L; o! o/ y    return q->front==NULL;
2 n5 j- y  b% k* E! u/ A}8 t' ^0 F8 `( C
入队操作

入队操作变化如下图:

微信图片_20200919101435.png

进行入队(push)操作的时候,同样的,我们首先需要特判队列是否为空,如果队列为空的话,需要将头指针和尾指针一同指向第一个结点,代码如下

front=n;
3 U2 y; q! {" @: i- Irear=n;' v2 c% @2 v; _3 m0 H" n' }# [

如图所示:

微信图片_20200919101439.png ) A$ E$ R' K. ^5 z1 D) i
唯一结点n

当如果队列不为空的时候,这时我们只需要将尾结点向后移动,通过不断移动next指针指向新的结点构成队列即可。如图所示:

微信图片_20200919101442.png

其代码可以表示为:

//入队操作8 {0 R4 k* v  @2 [# S4 w/ i
void push(queue *q,int data){9 Q% e) R9 A7 g$ Y2 F
    node *n =init_node();
! T# p3 s) V, E* O) o$ `    n->data=data;
+ ?1 v8 Y9 b+ n    n->next=NULL;   //采用尾插入法1 g2 }% d6 M8 r
    //if(q->rear==NULL){     //使用此方法也可以' p( X; ^* m1 ]. q! x0 c3 l6 B7 D* ]" k4 v
    if(empty(q)){, k; V0 ~# Y+ d7 g
        q->front=n;
! W' R/ h2 d5 E* n4 O        q->rear=n;6 h3 J, C. x! N7 E% D
    }else{
2 F. E. z+ z3 b6 ?7 E. f7 `7 Z        q->rear->next=n;    //n成为当前尾结点的下一结点/ J2 A& @" y( m/ B
        q->rear=n;  //让尾指针指向n7 d# }: U' z9 l; I3 S
    }. u6 h: {; }8 z
}) c5 d0 Y. t, y; Z4 L
出队操作

出队操作变化如下图:

微信图片_20200919101445.png

出队(pop)操作,是指在队列不为空的情况下进行的一个判断,当然我们在此也一定要进行队列判空的操作,你懂的。

如图,如果队列只有一个元素了,也就是说头尾指针均指向了同一个结点,那么我们直接将头尾两指针制空NULL,并释放这一个结点即可,如下图所示:

微信图片_20200919101449.png

当队列含有以上个元素时,我们需要将队列的头指针指向头指针当前指向的下一个元素,并释放掉当前元素即可,如下图所示

微信图片_20200919101452.png

其代码可以表示为:

//出队操作
4 c9 X8 ^' J' ]# p9 ]& G% Fvoid pop(queue *q){2 @) W* f4 l4 m, \
    node *n=q->front;
) P8 S& i) `. ^% @+ K5 ^    if(empty(q)){/ y& t3 a8 i3 K0 H
        return ;    //此时队列为空,直接返回函数结束/ S$ Q) o1 H" f$ w! I: J. c! e
    }& d$ q9 \1 g! y0 l+ z
    if(q->front==q->rear){
; `# E2 P' O$ M        q->front=NULL;  //只有一个元素时直接将两端指向置为空即可3 ~' s9 ?( e8 U9 h& R& n
        q->rear=NULL;0 o# F9 i, R+ E: [7 V6 \
        free(n);        //记得归还内存空间* w' M$ G; j1 ~8 q6 H! t% e
    }else{  x5 Q% D7 G" c% E+ z$ v
        q->front=q->front->next;
$ V8 ]0 y$ |# r/ f* l- D        free(n);
9 N7 o7 t4 c* N% N$ ?    }
  Y1 N. S5 G+ h+ i4 j# J  z}
1 o  U' |4 M7 F1 i3 W( V
打印队列元素(遍历)

打印队列的全部元素可以帮助我们调试,看到队列中具体的数据,在队列不为空的情况下,通过结点的next指向依次遍历并输出元素既可。

其代码可以表示为

//打印队列元素8 f% W4 U: }/ w5 V
void print_queue(queue *q){
* G+ {, U9 X' F! v$ \# O8 f3 Z    node *n = init_node();
( l" f6 [; r9 G' `7 h$ b    n=q->front;
; ]8 n* Z, s& ~    if(empty(q)){5 Y5 D' c6 w) _" B3 G; u( X
        return ;    //此时队列为空,直接返回函数结束
+ t. f7 r) s( w+ T2 y6 n2 {$ j    }
, d/ m# D) h* \" A    while (n!=NULL){
$ F6 y) ~) L; e! [* j7 d        printf("%d\t",n->data);
' `8 ~& c$ F( Q" Y9 j* f' Z5 d        n=n->next;
+ W: Y* W1 q$ O    }% k- D3 o# ?9 b& L6 W: o
    printf("\n");   //记得换行
, v5 @, i# X" y# x/ {% Z  O}# x; y2 u5 h5 }

遍历操作还有很多别的表示方法,比如说进行计算队列中含有多少元素,代码如下:

int calac(queue *q){+ f( j; [0 T! q8 N
    node *n = init_node();% e5 K. e# B9 L1 Y& M
    n=q->front;
3 w$ Q/ a$ u5 Z: `+ D8 K    int cnt=0;    //计数器设计5 o/ L( B- ?: U
    if(empty(q)){
7 w, P! J  l8 Z        return 0;    //此时队列为空,直接返回函数结束
! {4 S/ r4 _9 i# V' a5 S1 K    }
8 d" i, t7 u5 P& u3 b5 J    while (n!=NULL)
. V! k" g) U. x2 V) K& u    {
$ e9 Y* k# z+ I" O. Q. a3 j        n=n->next;. E( w5 k' w+ O
        cnt++;$ g' L; s% `! D4 V* V# q8 x
    }
: D: N' T8 _: |5 U) x; E    return cnt;
. ^5 W$ t7 {3 J- B* S. S+ n6 Q7 h/ D}
2 i9 K6 z0 b- \# _3 h
顺序队列的假溢出

什么是假溢出?我们可能会有疑问,溢出还有假的!

这里我们也需要考虑到顺序队列有什么缺点,对于顺序队列而言,其存在已经足够解决大多时候的设计问题了,但是其依旧存在一些缺陷和不足。

从上面的解析中我们看到,入队和出队操作均是直接在其后面进行结点的链接和删除,这种操作会造成其使用空间不断向出队的那一边偏移,产生假溢出。

我们来打打一个比方,先看看下面的图:

微信图片_20200919101501.png 示例顺序队列

上图所示,有一个顺序队列,这个队列的大小为5,其已经包含了四个元素data1,data2,data3,data4。

接着,我们对这个队列进行出队操作,出队2个元素,队列就变成了这个样子,如下图所示:

微信图片_20200919101505.png

从图上看到似乎没有什么问题,但是当我们接着再进行入队操作,比如我们入队2个元素,分别是data5和data6。

此时我们已经发现问题了,尾指针移动到我们可以进行队列操作的范围之外去了,有没有发现?

这种现象我们称呼作为队列用的存储区还没有满,但队列却发生了溢出,我们把这种现象称为假溢出。如下图所示:

微信图片_20200919101509.png 出队产生假溢出

那么我们有什么办法解决这个问题呢?这就要涉及到循环队列的性质了!

循环队列的概念

可能这个时候会产生一个疑问,我们学习的队列不是使用链表实现的动态队列么?

没有空间的时候会开辟空间,这难道还会产生假溢出么?

的确,当进行动态创建队列的时候,也只不过是向后继续不断的申请内存空间;

即使前面出队操作释放掉了前面的空间,但是指针依旧会向后进行移动,直到达到系统预留给程序的内存上界被强行终止;

这对于极为频繁的队列操作和程序而言是致命的,这时候,就需要对我们的队列进行优化,使用更为优秀的结构——循环队列。

循环队列就是将队列存储空间的最后一个位置转而绕到第一个位置,形成逻辑上的环状空间,以此来供队列循环使用,如下图。

微信图片_20200919101512.png

循环队列就是给定我们队列的大小范围,在原有队列的基础上,只要队列的后方满了,就从这个队列的前面开始进行插入,以达到重复利用空间的效果;

由于循环队列的设计思维更像一个环,因此常使用一个环图来表示,但我们需要注意,实际上循环队列不是一个真正的环,它依旧是单线性的。

循环队列的结构设计

由于循环对列给定了数据范围的大小,所以不需要使用链式的动态创建方法了。

因为如果使用链式存储,会无法确定何时再回到队头进行插入操作,所以我们采用模拟的方法,如图所示:

微信图片_20200919101516.png

其中,data表示一个数据域,int为类型,其可以修改为任意自定义的类型,比如说简单的char,float类型等等,也可以是复杂的结构体类型。

  • maxsize表示循环队列的最大容纳量,其表示队列的全部可操作空间。

  • rear代表尾指针,入队时移动。

  • front代表头指针,出队时移动。


    ! S# S# O+ C1 r8 Z. k

其代码可以表示为:

#define maxsize 10      //表示循环队列的最大容量1 s8 j' z" T: C
  ( d! E3 r8 M( P# m
//循环队列的结构设计
& H1 r' s+ }2 R- G1 ltypedef struct cir_queue{
- O1 }4 V% i+ [    int data[maxsize];2 i0 a: s# V2 C4 h/ n6 s, E
    int rear;
% R2 v! K* e) l3 W6 G' ^    int front;
0 L5 c* R6 A4 R& ?. c7 n7 }}cir_queue;
6 j; |: n! P' h9 K3 S- w
循环队列的初始化

循环队列的初始化核心就在于申请空间,并且将front指针和rear指针内容赋值为0,即指向第0个元素即可,这里要注意第 0个元素内容为空,如下图所示:

微信图片_20200919101519.png

其代码可以表示为:

//初始化
* k% d" w: Z, w" D( m6 A7 j& r" w8 ?cir_queue *init(){
. O) A! u4 I( P" c) F5 R8 V3 S" K    cir_queue *q = (cir_queue*)malloc(sizeof(cir_queue));: L2 y6 G) J9 _7 l; z  r1 [/ s
    if(q==NULL){% f" V( e) a8 p) L( B! e
        exit(0);   //申请内存失败,退出程序" d* h% d' ]% V1 I  e8 F
    }
4 v4 ]% o& y8 U5 J' n    q->front=0;. x( W& q8 N% [& r7 A* X# m/ Q2 W
    q->rear=0;
: a" h3 f6 w/ p# n( U2 Y* q' m; Z5 U( W    return q;
6 H$ F0 ], [+ G. H& B" l}
5 U8 r6 k- W' ?, A( C  x
入队操作

入队操作同顺序队列的方法,直接将rear向后移动即可。

但是要注意判断,如果rear达到了队列的空间上线,将要从头继续开始移动。

这里推荐使用余数法,即无论如何求余都是在这片空间内进行操作,防止一次错误执行就直接整体崩溃,而且也相对而言更为简洁,不推荐使用if语句,这样显得比较累赘。

微信图片_20200919101523.png 入队操作

注意进行加一移动位置操作的时候,不能直接q->rear++这样的操作,这样计算机判断优先级会产生让自己意想不到的后果。

此外这里还需要进行一次是否队列已满的判断,当我们rear指针的下一个位置就是front的位置的时候,即改循环队列已满。

如图:

微信图片_20200919101526.png 队列已满

其代码可以表示为:


9 O5 Y" r- \+ M# O2 B% i//入队操作push
; F( ?2 X8 k! qvoid push(cir_queue *q,int data){1 @& D7 Z; X$ S  ~. j
    if((q->rear+1)%maxsize==q->front){% X5 J2 V% A' D9 h2 M
        printf("溢出,无法入队\n");
. U3 N5 ?6 a2 {6 j7 f( m        return;
8 C* j' V  Z7 m% N    }else{7 h0 j) ?. P1 O- y, h
        q->rear=(q->rear+1)%maxsize;
& }* r1 [4 ?, v        q->data[q->rear]=data;
4 m; l4 E; k& q" G    }7 T5 ?! L+ c+ `
}
/ B2 G$ C" J) s
出队操作

如果顺序队列的出队操作,直接将front进行后移一位即可。

这里上面很多地方都提过了,有一个需要留意的地方,即队列是否为空,当队列为空的时候是无法进行出队操作的。

微信图片_20200919101529.png 出队操作

其代码可以表示为:

//出队操作pop: ]+ Q1 I9 w$ i% n, F4 A; a
void pop(cir_queue *q){) }  q& O9 Z, _* Y$ {; }
    if(q->rear==q->front){) S6 G2 Y* A6 \7 N; Z0 \8 `% C$ |! Q  h
        printf("队列为空,无法出队\n");  V* t! n# f' D$ k: X0 c2 K  I0 q, X
        return;
* ]& R+ y6 j! N4 o    }else{
7 H0 F4 L9 q7 P        q->data[q->front]=0;) H" v) L0 ~* W7 X- Z0 _( [
        q->front=(q->front+1)%maxsize;- d5 d- m0 [9 h9 |
    }1 F" q, F' [+ o! z
}0 w: i+ B  W( M3 H2 y
遍历操作

遍历操作需要借助一个临时变量储存位置front的位置信息,利用i逐步向后移动,直到i到达了rear的位置即可宣告遍历的结束。

//遍历队列
; c- T) w$ i% i& u4 J3 \- e" [' Fvoid print(cir_queue *q){
4 x. D* j: E  F! I; N    int i=q->front;
# F3 I5 v  T. s6 q2 N( H' [    while(i!=q->rear){7 K; G# ^6 n# B0 N
        i=(i+1)%maxsize;
. s8 M: m# v3 n# e, ^+ H        printf("%d\t",q->data);   
5 n1 w# a* Q/ q0 ^% F    }' [7 |# c9 V: u2 ^3 K/ z3 c7 S
    printf("\n");       //记得换行
9 _; V& b/ K3 I' w}
% c# }' }5 N  p& ^' R* f! m2 J
关于队列的总结

请牢记这句话:队列是一个先进先出的数据结构。


& K+ N8 |0 u9 A( }- N
赞 收藏 1 评论0 发布时间:2020-9-19 10:20

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版