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

一文揭开「队列」的迷雾

[复制链接]
gaosmile 发布时间:2020-9-29 22:14
队列的概念
, x: v+ P$ _+ A9 K) S- h- {& C

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

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

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

微信图片_20200929220933.png

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

我们可以总结如下

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

微信图片_20200929220938.png

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

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

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

    ; y5 B) @- D% U3 ]

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

队列的结点设计与初始化

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

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

微信图片_20200929220941.png

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

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

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

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

微信图片_20200929220944.png

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

//结点定义
  s9 B/ e" D! b. @- ?- v4 _( E9 etypedef struct node{
. o  v) q( S- L( n8 f    int data;8 f/ C2 J. p% L% W; }
    struct node *next;! E0 M* l9 C( |8 k/ k% v$ l3 m
}node;2 h; b. L8 K' U3 w
//队列定义,队首指针和队尾指针
! o0 {+ _% l6 [7 g% D9 [6 |$ ?typedef struct queue{( J5 n( [9 j" g1 M
    node *front;    //头指针
1 u. A' M2 c; W6 t    node *rear;     //尾指针$ ]0 C0 G& Z4 Z3 W
}queue;
1 T( \  r# r2 Z/ g

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

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

//初始化结点2 z0 ~( _, }' w; |% N0 s
node *init_node(){7 }0 l& e5 t- J7 d( @
    node *n=(node*)malloc(sizeof(node));
2 b9 G" p3 X, ~% ?6 [! l8 A. Y9 ^    if(n==NULL){    //建立失败,退出
1 s% S0 U" N& u        exit(0);
. B0 [0 P! g- V8 u9 R& i    }
/ T. s/ n' d4 T* K1 r    return n;
5 u! q+ V, E( J6 p0 V- G  ^}
9 g4 _5 m  d2 y; e& n
//初始化队列7 N4 \" o# g. E; K# B, Y
queue *init_queue(){
/ }* C4 I+ S! O1 F    queue *q=(queue*)malloc(sizeof(queue));, @. X/ x- E" T6 }" c# F9 r8 S
    if(q==NULL){    //建立失败,退出
0 D, Q% X6 z8 [        exit(0);
0 Q! a0 p; J+ F$ t# t" D    }3 g3 D& s0 Q% M* L! m
    //头尾结点均赋值NULL
- B: a" o1 G4 ]4 ?, Y    q->front=NULL;  / Q) \- t' u6 i4 o! y( b$ ^
    q->rear=NULL;6 s0 y* n6 ^9 a, W% e6 q  x& E
    return q;
' `- ?. l0 L" m; ?7 Z$ q3 L9 a}5 J- S8 C6 f1 P+ [
判断队列是否为空

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

其代码可以表示为:

//队列判空$ e7 x" r5 K6 O9 ]1 s
int empty(queue *q){6 `. t& [% r8 p( k9 |' O5 X" q
    if(q->front==NULL){! F" C/ e+ e. o' m$ i) d, S! {5 H
        return 1;   //1--表示真,说明队列非空( f" T" W9 K) o5 D$ o
    }else{, k. E- p$ r" M: d& z& d0 `
        return 0;   //0--表示假,说明队列为空
6 h: z( [9 D3 h/ R  b8 T; t) y    }$ G% O' w; v/ S* ^8 Z! m
}
. ]5 C! d! \- T

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

int empty(queue *q){4 U: F' y! {2 L$ Z- P
    return q->front==NULL;
( h' L; H: q+ b* D; p}0 c( e5 K7 V+ [
入队操作

入队操作变化如下图:

微信图片_20200929220947.png

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

front=n;3 _" R9 c( `- g
rear=n;8 n7 d) Q) p* l) F" u

如图所示:

微信图片_20200929220951.png 唯一结点n

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

微信图片_20200929220954.png

其代码可以表示为:

//入队操作5 P3 D. h* x, K: O3 i
void push(queue *q,int data){
" |6 i, v7 @" n    node *n =init_node();1 u  @8 ~" |: z% ]: c8 ~
    n->data=data;) u1 J( X# T5 U% X+ F# Z  q
    n->next=NULL;   //采用尾插入法; X6 ?  s8 _3 K2 `8 i% X
    //if(q->rear==NULL){     //使用此方法也可以4 f! ^. n, H' }% m4 ^
    if(empty(q)){: M" e2 k% M8 O' N. ?
        q->front=n;
4 p! k0 L& `: |. @# S        q->rear=n;% w2 \, u8 i. w( `; h
    }else{
$ R8 k' s9 F* d4 o3 z        q->rear->next=n;    //n成为当前尾结点的下一结点
- z2 |& C! G$ e        q->rear=n;  //让尾指针指向n
+ W( ^! h& Z6 _  E/ x9 v2 \: l    }
( s4 n" S- F" o  \; t3 n}
# x2 l: K  m4 y7 c; z5 ?' B
出队操作

出队操作变化如下图:

微信图片_20200929220957.png

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

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

微信图片_20200929221001.png

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

微信图片_20200929221004.png

其代码可以表示为:

//出队操作8 g, e" n( h* Q% F& T
void pop(queue *q){
* |5 p9 r' j9 N  Y; |# F! N    node *n=q->front;
- \+ |. R5 |+ ~' E    if(empty(q)){. W- P1 [! v1 ~) y. ?! m# M1 c3 i
        return ;    //此时队列为空,直接返回函数结束
7 f, |; I8 f, X$ U& f# J  `    }
( c/ c, o8 U* @4 {    if(q->front==q->rear){) P2 D0 W! g8 v
        q->front=NULL;  //只有一个元素时直接将两端指向置为空即可
! l# U% m* K, Y& W0 H+ |, v/ C        q->rear=NULL;# G! s" G8 e, w# v
        free(n);        //记得归还内存空间- u# A: ]/ o/ i1 x. m9 I, k
    }else{
: j  S; I5 h! e% @4 b: c. H. L        q->front=q->front->next;$ p) w' r3 m$ N, k: _* `2 y
        free(n);3 r. _/ L  [/ X
    }
1 _" I* L; T4 ~, x4 E; I$ p}- ?4 h7 `( M7 O0 e' [
打印队列元素(遍历)

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

其代码可以表示为

//打印队列元素
7 x( A: x" H" d- a( u' Ivoid print_queue(queue *q){
& r0 j5 ?' h- w8 k8 m    node *n = init_node();
+ o; q& d3 Z5 Y/ f0 p    n=q->front;% a* p; k1 S. h9 o. v+ e0 T1 Z
    if(empty(q)){
, }3 r1 _6 o% G! j; k' N        return ;    //此时队列为空,直接返回函数结束
+ \  s6 u$ O& F6 {' R9 D    }
& l5 j/ }. U; L1 T, b' ?    while (n!=NULL){
% F9 Q  D) @  V3 r, w        printf("%d\t",n->data);
3 u) u0 N; ~- v        n=n->next;
( x1 y. r+ u. N$ r    }2 Y* x5 I" C+ e1 m
    printf("\n");   //记得换行
1 f' ~" d% @* j" p}$ x. l; [1 W: |9 p" E

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

int calac(queue *q){( f0 X8 e3 ?6 K9 f5 m- i0 N
    node *n = init_node();& ?$ L- L4 {) w
    n=q->front;* S3 G6 v! S+ U- p; h5 h3 m+ G/ I
    int cnt=0;    //计数器设计
1 {6 h% q  |9 r! R    if(empty(q)){
1 A# v2 S* L& @: {; b; ~- l        return 0;    //此时队列为空,直接返回函数结束3 R8 Q3 x' ^4 E" k. ^, m- [0 u2 K
    }
# B0 ]3 Z9 _7 l; J# _1 x    while (n!=NULL)
2 L* W; h4 c( _- J( }1 I4 B. x# n    {& z- c3 j4 g; R# \
        n=n->next;
) {0 U) a: h* n- ]0 L0 Q. j        cnt++;) v* Z: Q6 V4 T+ O* g
    }/ g, Y4 {# j0 |' ~3 U
    return cnt;1 G* s% f. U: G
}
6 d  f7 S8 D( C9 w7 i1 J( ~  g
顺序队列的假溢出

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

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

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

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

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

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

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

微信图片_20200929221011.png

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

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

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

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

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

循环队列的概念

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

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

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

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

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

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

微信图片_20200929221018.png

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

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

循环队列的结构设计

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

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

微信图片_20200929221021.png

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

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

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

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


    ( l6 n) h+ \% j) `% ?7 k6 B

其代码可以表示为:

#define maxsize 10      //表示循环队列的最大容量; y2 p8 E7 d7 W9 b) W6 y1 c
  
: u. G! q3 E/ d+ `7 ^//循环队列的结构设计
7 W! l, ~7 ]' J2 Rtypedef struct cir_queue{
, {7 o) K0 I$ d* t    int data[maxsize];
" k/ ^0 h5 d3 R! R$ W# }6 H    int rear;
3 h" {/ Z4 f" m. [    int front;4 {8 Z4 v  I" P5 L5 h  J- U
}cir_queue;  P/ \9 }* h- s8 P/ y; T
循环队列的初始化

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

微信图片_20200929221024.png

其代码可以表示为:

//初始化$ T# `' ^# N8 r& C
cir_queue *init(){6 x7 n6 \. i( G( u' L* l$ P
    cir_queue *q = (cir_queue*)malloc(sizeof(cir_queue));
  ~6 J; A; Z: f$ A  p: M4 L. W    if(q==NULL){
, e* v' M0 G) S0 u) J        exit(0);   //申请内存失败,退出程序, b/ k: U- t2 ^# Q$ |2 ^
    }# \* g* E+ E" O5 Q
    q->front=0;; G( M+ ?  S9 k
    q->rear=0;4 A( b) ~; E0 T9 R9 {
    return q;
+ G5 e, ~- H* o( P- D* E" O6 ~# `}
3 u+ [; P. K  Q; d! k, c
入队操作

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

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

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

微信图片_20200929221028.png 入队操作

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

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

如图:

微信图片_20200929221031.png 队列已满

其代码可以表示为:

- L. |$ h9 H8 }
//入队操作push7 G( K1 K8 L% y$ K* M; G/ @
void push(cir_queue *q,int data){& @6 Q6 q4 @$ O/ u5 k
    if((q->rear+1)%maxsize==q->front){( t4 q% p4 `6 l: r; {% K
        printf("溢出,无法入队\n");
/ M- m- d* A) q        return;' T8 P/ Q' \, C1 w9 G  d. M; Q
    }else{1 K- U4 X5 ]( ?4 T9 B2 ~4 T7 W
        q->rear=(q->rear+1)%maxsize;" {" l3 y' C7 q2 K8 f" M/ s
        q->data[q->rear]=data;$ i; f2 g! P6 v5 B* A  M; }+ j
    }( H; }* N7 |+ @8 G; ]7 g
}/ Q" n# W! h3 u! c8 K* |
出队操作

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

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

微信图片_20200929221034.png 出队操作

其代码可以表示为:

//出队操作pop/ j$ \7 K# v6 Z4 r
void pop(cir_queue *q){7 R4 {: z& m- _% w+ F( C6 i9 c
    if(q->rear==q->front){
: n4 j/ l& t4 c  L4 f        printf("队列为空,无法出队\n");
4 v, d$ \" q. b9 j8 A- X0 o        return;% p1 J& z- C, E# _
    }else{" k) O! z; I# Y& I  ]3 j! J
        q->data[q->front]=0;
$ s: A5 D  \; M. X4 a        q->front=(q->front+1)%maxsize;0 X' B2 h3 g9 A, B! G
    }$ Q7 V" ~6 H( d
}
, w+ F& S, i4 j- T7 v, `/ f: H
遍历操作

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

//遍历队列
; l, u5 D7 B) \$ j4 N$ q; u; ]void print(cir_queue *q){
! M" C0 v/ H  o/ v2 K    int i=q->front;
, i' l& {2 z. `( A    while(i!=q->rear){- X2 z0 h! A- M) ?7 ?
        i=(i+1)%maxsize;
$ q% L% f. @4 v" r, S, a0 ^  |$ B% w        printf("%d\t",q->data);   
+ x" {& Q9 T& r+ B    }7 b( |$ W/ m; l' ]$ \3 |! M
    printf("\n");       //记得换行8 t" i8 w: t3 Q1 j1 @' v0 t0 u( C
}
. n5 o+ S& ]' b( M: r2 w; M$ P6 I
关于队列的总结

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

今天队列基础就讲到这里,下一期,我们再见!

% x8 C! A1 E- w( Y' z' r/ O( C9 A
赞 收藏 评论0 发布时间:2020-9-29 22:14

举报

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