数据结构——栈和队列

本文详细介绍了数据结构中的栈和队列,包括它们的顺序存储结构和链式存储结构,以及各自的基本操作。栈是后进先出的数据结构,适用于括号匹配、表达式求值等场景。队列遵循先进先出原则,常用于树的层次遍历、图的广度优先遍历等。同时,文中还探讨了栈和队列在实际问题中的应用,如迷宫求解和进制转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

栈和队列

文章目录



栈(Stack)

后进先出(LIFO)Last In First Out

栈的顺序存储结构(SqStack——Sequence Stack)

表示方法

#define MaxSize 10;
typedef struct SqStack {
    ElemType data[MaxSize];
    int top;				// 栈顶指针,指向栈顶数组的下标,而非位序
}SqStack;

SqStack S;

  • 栈顶指针:S.top,初始时 S.top = -1;

  • 栈顶元素:S.data[S.top];

  • 进栈操作(push):由于是顺序表,有最大存储空间限制,栈不满时,栈顶指针加 1,送值到栈顶元素。

  • 出栈操作(pop):出栈先判断是否空栈,非空,先取出栈顶元素,后栈顶指针减 1。

  • 栈空条件:S.top = -1;

  • 栈满条件:S.top = MaxSize - 1;

  • 栈的当前长度:S.top + 1;


基本操作

初始化 InitStack(SqStack &S)
InitStack(SqStack &S) {
    S.top = -1;				// 栈顶指针置为 -1,当第一个元素进栈,指针 + 1,便指向下标 0,即数组第一元素
}


判栈空 StackEmpty(SqStack S)
bool StackEmpty(SqStack S) {
    if (S.top == -1)
        return true;
    else
        return false;
}


进栈 Push(SqStack &S, ElemType x)
bool Push(SqStack &S, ElemType x) {
    if (S.top == MaxSize - 1)
        return false;			// 栈满
    S.top++;
    S.data[S.top] = x;			// 两步并一步:S.data[++S.top] = x;
    return true;
}


出栈 Pop(SqStack &S, ElemType &x)
bool Pop(SqStack &S, ElemType &x) {
    if (S.top == -1)
        return false;			// 栈空
    x = S.data[S.top--];		// 先返回,再减 1。【注】看变量和 ++ / -- 的位置,在前先执行。变量在前先返回。
    return true;
}


读栈顶元素 GetTop(SqStack &S, ElemType &x)
bool GetTop(SqStack &S, ElemType &x) {
    if (S.top = -1)
        return false;			// 栈空读取不到任何元素。
    x = S.data[S.top];
    return true;
}

读栈顶其实就是在判断栈是否为空罢了。


共享栈

栈 0 从前向后移动,栈 1 从后先前移动。

typedef struct SqStack {
    ElemType data[MaxSize];
    int top0;
    int top1;
}SqStack;
  • 栈顶指针:初始时,S.top0 = -1; S.top1 = MaxSize;

  • 栈顶元素:S.data[S.top0]; S.data[S.top1];

  • 进栈操作(push):

    • 栈 0:栈顶指针加 1,再存数。
    • 栈 1:栈顶指针减 1,再存数。
  • 出栈操作(pop):

    • 栈 0:取数,栈顶指针减 1
    • 栈 1:取数,栈顶指针加 1
  • 栈空:

    • 栈 0:S.top0 = -1;
    • 栈 1:S.top1 = MaxSize;
  • 栈满:S.top0 = S.top1 - 1;

  • 栈当前长度:

    • 栈 0:S.top0 + 1;
    • 栈 1:MaxSize - S.top1;



栈的链式存储结构(LiStack——LinkStack)

链式存储没有最大长度 MaxSize 的限制,不存在栈满上溢情况。

链式存储的栈使用单链表、双链表都可以实现,双链表实现非常简单,但是存储密度不高,一般使用单链表

栈的特点是后进先出,如果我们在单链表的尾部进栈,则栈顶指针指向尾部,进栈时,可以实现栈顶指针“先加 1”,再添加数据元素的操作;但是在出栈时,由于是单链表,数据元素取出后,栈顶指针无法“减 1”,因此在表尾进行操作并不合理。

所以基本操作都在表头实现,这时,回想前面单链表中的头插法,恰好满足了“后进先出”的特点。

表示方法

typedef struct Linknode {
    ElemType data;
    struct Linknode *next;
} *LiStack;

LiStack p 等价于 Linknode *p,前者强调栈,后者强调数据元素。

  • 栈顶指针:S 即单链表的第一个数据元素的地址,也就是栈顶地址,S 可看作栈顶指针。


基本操作

只需操作栈顶,因此使用不带头结点的单链表更加方便~

初始化:InitStack(LiStack &S)
void InitStack(LiStack S) {
    S = NULL;
}


判栈空:StackEmpty(LiStack S)
bool StackEmpty(LiStack S) {
    if (S == NULL)
        return true;
}


进栈:Push(LiStack &S, Linknode *p)
bool Push(LiStack S, Linknode *p) {
    if (p == NULL)
        return false;
    p -> next = S;		// 这里比较特殊,需要先将 p 连在 S 前面,也就是放在栈顶
    S = p;				// 再让栈顶指针指向 p 栈顶元素。❗ 这一步容易遗漏
    return true;
}


出栈:Pop(LiStack &S, ElemType e)
bool *Pop(LiStack &S) {
    if (S == NULL)
        return false;		// 空栈出个屁
    e = S -> data;			// 出栈的数据
    Linknode *p = S;		// 用一个 p 指向栈顶元素,取数后,需要将其释放
    S = S -> next;			// 栈顶指针 “减 1”
    free(p);
    return true;
}

注意出栈需要释放栈顶数据元素的存储空间


读栈顶元素:GetTop(LiStack S)
Linknode *GetTop(LiStack S) {
    if (S == NUll)
        return NULL;
    return S;
}

其实就是在判断栈是否为空。



栈的应用

括号匹配

主要流程如图所示

在这里插入图片描述

程序实现

在这里插入图片描述


表达式求值

后缀型:

后缀表达式中,越靠左的操作符,越先参与计算。

中缀表达式转后缀表达式(手算)

规则a + ba b + ,如法炮制即可。

  1. 在操作符下标注运算顺序①②③…按照“左优先原则”排序

    左优先原则:在确定好一个操作符的运算顺序并标号后,从最左向右检索找能第一个能参与运算的操作符,作为下一个操作符。

  2. 在该操作符下,将两个操作数和操作符按照转换规则转成后缀表达式,此时将它们整体看做一个新的操作数

  3. 还有操作符未处理,重复 2 直至完成。

在这里插入图片描述


💯中缀表达式转后缀表达式(机算)

思路:

根据 括号 > 乘除 > 加减 的原则,在从左往右扫描表达式的过程中,确定表达式操作符的运算顺序,

先初始化一个栈,栈中用来存储“暂时无法确定运算顺序的操作符” 和 “左括号”。

机器扫描顺序是从左往右,因此每得到一个运算顺序,就可以确定一部分后缀表达式,

  • 在不遇到括号情况下,需要多次判断加减乘除计算顺序,在每一次判断依赖当前扫描到的操作符和前面一个操作符,将整个表达式看做[已经转换为后缀表达式的部分——NumA] [前一个运算符op_prior] [一个单独的数字 NumB] [当前扫描到的运算符op_present] [尚未参与转换的部分 NumC] 的样式。

    [NumA] [op_prior] [NumB] [op_present] [NumC]			// 每一步的形式
    

    此时 op_prior 在栈顶(后面会解释为什么它在栈顶),循环扫描至 op_present 操作符

    每一步中,两个操作符的优先级将决定 op_prior是否能弹出栈参与运算。(这也是每一步的本质工作

    若 op_prior 运算优先级 >= op_present(另一种情况便是不能弹出 op_prior,此时它的有缘人便是右括号或循环结束),按照左优先原则,需先计算前面 A op_prior B 部分,从而op_prior 能弹出栈参与运算,将 op_prior 弹出并加入后缀表达式尾部NumA NumB op_prior,这部分可视作一个操作数 NumA1

    此时整体表达式形式变为 [NumA1] [op_present] [NumC]

    前面提到过 NumC 是尚未参与转换的部分(形式为Num1 op1 Num2 op2 ...),我们将再 NumC 中 num1、op1 和剩余部分视作 [Num1] [op1] [剩余部分Num2 op2 ...] 的形式。

    此时整体表达式便可视作 [NumA1] [op_present] [Num1] [op1] [Num2 op2 ...]

    这就变成了 我们最开始想要的 [NumA] [op_prior] [NumB] [op_present] [NumC] 的形式。

    此时原先的 op_present 将被视作新的 “op_prior” ,如果不依靠后面的操作符,无法得出**“NumA NumB”以何种顺序进行计算**,因此它是上面初始化栈时提到的“暂时无法确定运算顺序的操作符”,将其入栈。

    开始新的循环,重复以上步骤直至结束。

    【注】虽然每一步形式如 [NumA] [op_prior] [NumB] [op_present] [NumC] ,但是实质上每次比较的是 当前扫描的运算符和栈中暂时无法确定运算顺序的运算符

  • 在遇到括号时,遇到左括号压入栈顶,括号内部格式仍然符合上述形式,按上述执行,直至遇到右括号,弹出栈中运算符加入到后缀表达式中,再弹出左括号,表明结束了该组括号的匹配。


算法如下:

初始化一个栈,栈中用来存储“暂时无法确定运算顺序的操作符” 和 “左括号”。

  1. 从左往右扫描表达式的每一个元素,有 3 种情况:

    • 遇到 操作数 ,直接加入后缀表达式中。

    • 遇到 界限符(括号):

      • 括号:

        直接压入栈顶,等待一个右括号与其匹配,匹配到后,便可将括号里内容看做一个操作数加入到后缀表达式中。

      • 括号:

        弹出栈顶操作符并将其加入后缀表达式,重复,直至遇到与其匹配的左括号将左括号弹出左括号不加入后缀表达式

    • 遇到 操作符 :依次弹出栈中 优先级高于或等于 当前操作符的所有操作符,弹出时依次加入后缀表达式,直至栈顶元素为左括号或栈空为止。此时将当前操作符入栈。

      【注】上面的优先级指的是:* = / > + = -

  2. 扫描结束后,将栈中剩余运算符依次弹出、加入后缀表达式。


后缀表达式转中缀表达式计算(手算)
  1. 从左往右,找操作符
  2. 遇到操作符,找该操作符前面的两个操作数,按照 a b +a + b 这样的规则进行计算。将操作符和两个操作数整体看做一个新的操作数
  3. 若还有未处理的操作符,重复 2 直至结束。

在这里插入图片描述


后缀表达式转中缀表达式(机算)

算法如下:

由上面手算过程可以看到,每个操作符需要匹配前面两个操作数,而且我们自始至终都只操作这两个操作数,因此使用“栈”这种数据结构可以很好地完成该算法。

  1. 从左往右扫描下一个元素,执行 2 或执行 3,直至处理完所有元素。
  2. 若扫描到一个操作数,则执行“入栈”,将其压入栈顶,返回第 1 步。否则执行第 3 步。
  3. 若扫描到一个操作符,则执行两次“出栈”,弹出两个操作数(先出栈的是右操作数),执行相应运算,将运算结果压入栈顶,返回第 1 步。

【注】所有元素结束后,栈内应仅剩余一个元素——最终的运算结果


💯计算机计算中缀表达式(非常重要的算法,无处不在)

并非先把一个中缀表达式整体转成后缀表达式,随后整个后缀表达式执行后缀转中缀,在转的过程中完成计算。❌

而是在中缀表达式转后缀表达式的过程中,在每一步“弹出运算符”的时候,完成一步计算,最终计算完整个中缀表达式。

算法如下:

初始化两个栈,操作数栈运算符栈

操作数栈:每扫描到一个操作数,将其入栈,而非“中缀转后缀(机算)”中加入后缀表达式;栈存储尚未参与运算的操作数

运算符栈:每扫描到一个运算符或左界限符,按照“中缀转后缀(机算)”相同逻辑,对前一个运算符进行处理后,将运算符或左界限符入栈;栈存储暂时无法确定运算顺序的操作符和左界限符。

每弹出一个运算符时,表明弹出的运算符参与了一次运算,此时从操作数栈中弹出两个操作数——该操作符对应的左右操作数,进行计算,将运算结果压入操作数栈顶。

最终操作数栈内仅剩的元素便是计算结果


前缀型:
中缀表达式转前缀表达式(手算)

规则:a + b+ a b ,如法炮制即可。

  1. 在操作符下标注运算顺序①②③…按照“右优先原则”排序

    右优先原则:在确定好一个操作符的运算顺序并标号后,从最右向左检索找能第一个能参与运算的操作符,作为下一个操作符。

  2. 在该操作符下,将两个操作数和操作符按照转换规则转成后缀表达式,此时将它们整体看做一个新的操作数

  3. 还有操作符未处理,重复 2 直至完成。


前缀表达式转中缀表达式计算(手算)
  1. 从右往左,找操作符
  2. 遇到操作符,找该操作符前面的两个操作数,按照 + a ba + b 这样的规则进行计算。将操作符和两个操作数整体看做一个新的操作数
  3. 若还有未处理的操作符,重复 2 直至结束。


前缀表达式转中缀表达式计算(机算)

由上面手算过程可以看到,每个操作符需要匹配后面两个操作数,而且我们自始至终都只操作这两个操作数,因此使用“”这种数据结构可以很好地完成该算法。

  1. 从右往左扫描下一个元素,执行 2 或执行 3,直至处理完所有元素。
  2. 若扫描到一个操作数,则执行“入栈”,将其压入栈顶,返回第 1 步。否则执行第 3 步。
  3. 若扫描到一个操作符,则执行两次“出栈”,弹出两个操作数(先出栈的是左操作数),执行相应运算,将运算结果压入栈顶,返回第 1 步。

【注】所有元素结束后,栈内应仅剩余一个元素——最终的运算结果

在这里插入图片描述


迷宫求解

每一步都要入栈,走不通需要找上一步,这便用到了栈的思想


进制转换

每一步 % 一个数,最后需要逆序输出,这使用了栈的后进先出思想(Last In First Out)



队列(Queue)

操作受限的线性表——队列,就像检站口排队一样,🦮🐕‍🦺🐩🐕🐈🐅🐆🐎🦌🦏🦛,依次等候检票,进队列在队尾进出队列在队头出,即插入从后面出入,删除从前面删除。延伸一下:插入和删除操作只允许分别在固定一端进行。队列特点是:“先进先出”,FIFO(First In First Out)

队列的顺序存储结构(SqQueue——Sequence Queue)

表示方法

#define MaxSize 10;
typedef struct SqQueue {
    ElemType data[MaxSize];
    int front, rear;		// 队头指针,对尾指针,队头指向第一个数据,队尾指向末尾元素的后一位(空位置)
}SqQueue;
  • 初始状态:S.front == S.rear == 0; 队列长度为 0
  • 进队操作:判断队列是否满,非满,队尾元素赋值,队尾指针加 1
  • 出队操作:判断队列是否为空,非空,取出队头元素,队头指针加1
  • 判空操作:S.front == S.rear
  • 判满操作:无法判满

队列可以在两端操作,因此,若不用循环队列,队满后,不断执行出队操作,虽然前面的存储空间空出来了,但由于队尾指针不可再改变,无法入队,浪费存储空间,很难判断队列满与否。


🔹🔹🔹因此使用循环队列是一种好办法。

循环队列依赖 (下标改变) % MaxSize 来使得下标永远处于合法范围;总之,如果你搞不清楚那种情况用了 % ,无脑套就 vans。

  • 初始时:Q.front == Q.rear == 0;

  • 队头指针进 1Q.front = (Q.front + 1) % MaxSize;

  • 队尾指针进 1Q.rear = (Q.rear + 1) % MaxSize;

  • 出队入队都按照顺时针方向进行。

  • 判空:Q.front == Q.rear;

  • 判满

    循环队列即使是满队列,也会有 Q.front == Q.rear 的情况,我认为下面两种方法较好,一般使用第一种。

    1. 舍弃队列最后一个存储空间,当 Q.front = (Q.rear + 1) % MaxSize 时,队列满。
    2. struct 结构体内新增 int size 来存储当前长度,若 Q.size == MaxSize ,则队列满。
  • 队列长度:(Q.rear - Q.front + MaxSize) % MaxSize

  • 队头元素:Q.data[Q.front] ❗ Q.front 和 Q.rear 都是下标

  • 队尾元素:Q.data[Q.rear - 1] Q.rear 指向的是队尾元素后面的“空位”


循环队列基本操作

初始化: InitQueue(SqQueue &Q)
void InitQueue(SqQueue &S) {
    Q.front = Q.rear = 0;
}


判队空: StackEmpty(SqQueue Q)
bool StackEmpty(SqQueue Q) {
    if (Q.front == Q.rear)
        return true;
}


入队: EnQueue(SqQueue &Q, Elemtype x) (使用方式 1 判断队满情况下)
bool EnQueue(SqQueue &Q, Elemtype x) {
    if (Q.front == (Q.rear + 1) % MaxSize)
        return false;					// 判满
    Q.data[Q.rear] = x;					// 空位赋值
    Q.rear = (Q.rear + 1) % MaxSize;	// 指针进 1
    return ture;
}


出队: DeQueue(SqQueue &Q, ElemType &x)
bool DeQueue(SqQueue &Q, ElemType &x) {
    if (Q.front == Q.rear)
        return false;					// 判空
    x = Q.data[Q.front];				// 返回出队列的值
    Q.front = (Q.front + 1) % MaxSize;	// 队头指针进 1
    return true;
}


读队头元素: GetHead(SqQueue &Q, ElemType &x)
bool GetHead(SqQueue &Q, ElemType &x) {
    if (Q.front == Q.rear)				//队列空,读个屁
        return false;
    x = Q.data[Q.front];
    return true;
}


队尾指针指向队尾元素情况

初始化: InitQueue(SqQueue &Q)
void InitQueue(SqQueue &S) {
    Q.front =  0;
    Q.rear = -1;			// 队列中没有元素,rear 指向 -1
}


判队空: StackEmpty(SqQueue Q)
bool StackEmpty(SqQueue Q) {
    if (Q.front == (Q.rear + 1) % MaxSize)		// 若队列空,队列无数据,rear 在 front 的“前一位”
        return true;
}


入队: EnQueue(SqQueue &Q, Elemtype x) (使用方式 1 判断队满情况下)
bool EnQueue(SqQueue &Q, Elemtype x) {
    if (Q.front ==(Q.rear + 2) % MaxSize)
        return false;					// 若队列满,rear 在 front 的“前 2 位”,队尾后的一位“空出”。
    Q.rear = (Q.rear + 1) % MaxSize;	// 此时 rear 指向队尾元素,需要先指针进 1
    Q.data[Q.rear] = x;					// 再给新的队尾赋值
    return ture;
}


出队: DeQueue(SqQueue &Q, ElemType &x)
bool DeQueue(SqQueue &Q, ElemType &x) {
    if (Q.front == (Q.rear + 1) % MaxSize)
        return false;					// 判空
    x = Q.data[Q.front];				// 返回出队列的值
    Q.front = (Q.front + 1) % MaxSize;	// 队头指针进 1
    return true;
}


读队头元素: GetHead(SqQueue &Q, ElemType &x)

与队尾指针指向队尾后一位“空位”的情况相同。



队列的链式存储结构(LinkQueue)

表示方法

队列仅需要在队首和队尾进行操作,使用单链表可以轻松实现。

队列中的数据元素类似单链表,用一个结构体存储。但队列虽然可以像先前的链表、栈那样知道它们的首地址即可窥其全貌,但在队尾操作时间复杂度为O(n);为减少队尾操作时间复杂度,表示一个队列需要专门定义一个新的结构体,内部存储指向队头和队尾的指针

typedef struct LinkNode {
    ElemType data;
    struct LinkNode *next;
}LinkNode;

typedef struct LinkQueue {
    LinkNode *front, *rear;
}LinkQueue;						// 注意这里没有 define 成指针,因为结构体本身就包含的是两个指针!!!细品,你细品

此时 LinkQueue 代表队列,LinkNode 代表数据元素。

基本操作(带头结点更加方便)

初始化: InitQueue(LinkQueue &Q)
// 带头结点
bool InitQueue(LinkQueue &Q){
    Q.front = Q.rear = (LinkNode)malloc(sizeof(LinkNode));	// 申请内存空间,队头队尾同时指向它
    if (Q.front == NUll)				// 申请内存失败
        return false;
    Q.front -> next = NULL;				// 头结点初始化
    return true;
}
// 不带头结点
void InitQueue(LinkQueue &Q){
    Q.rear = Q.front = NULL;			// 队列中无元素,队头队尾指针 NULL。
}

【注】若插入操作,传入的数据元素类型为 LinkNode *,则不需要专门给 Q.rear 申请空间,直接令传入的指针插入在队尾,再让 Q.rear 指向即可。若传入数据元素类型为ElemType,则队尾指针指向新申请一片空间,赋值,再插入即可。


判队空: StackEmpty(LinkQueue Q)
// 带头结点
bool StackEmpty(LinkQueue Q) {
    if (Q.front == Q.rear)		// 或者 Q.front -> next == NULL
        return true;
    else
        return false;
}
// 不带头结点
bool StackEmpty(LinkQueue Q) {
    if (Q.front == Q.rear)		// 或者 Q.front == NULL 或 Q.rear == NULL 
        return true;
    else
        return false;
}


入队: EnQueue(LinkQueue &Q, ElemType *x)
// 带头结点
bool EnQueue(SqQueue &Q, ElemType *x) {
    LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));	// 插入结点申请空间
    if (s == NUll)
        return false;
    s -> data = x;				// 预备尾结点赋值
    s -> next = NULL;			// next 为 NULL
    Q.rear -> next = s;			// 将其链接到尾部
    Q.rear = s;					// 改变 rear 指向
    return true;
}
// 不带头结点——尾部插入结点操作涉及 2 个结点:尾部结点 和 新插入的结点,而队列空时,没有尾部结点,需特殊处理
bool EnQueue(LinkQueue &Q, ElemType *x) {
    LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode));	// 插入结点申请空间
    if (s == NUll)
        return false;
    s -> data = x;				// 预备尾结点赋值
    s -> next = NULL;			// next 为 NULL
    if (Q.front == NULL) {		// 不带头结点,需对空表进行特殊处理
        Q.front = s;
        Q.rear = s;
    }
    Q.rear -> next = s;			// 将其链接到尾部
    Q.rear = s;					// 改变 rear 指向
    return true;
}


出队: DeQueue(LinkQueue &Q, ElemType &x) ❗ 队尾元素出队列需要先将 rear 指向 front,否则丢失
// 带头结点
bool DeQueue(SqQueue &Q, ElemType &x) {
    if (Q.front -> next == NULL)
        return false;				// 队列空返回勾八啊
    LinkNode *p = Q.front -> next;	// p 指向队列第一个数据元素——队头
    x = p -> data;					// 传回数据 x
    Q.front -> next = p -> next;	// 队头指针的 next 后移
    if (Q.rear = p)					// 若此时 p 是尾结点,若直接 free(p),则 rear 就会“迷路”
        Q.rear = Q.front;			// 尾结点出队后,队列变空队列
    free(p);						// 释放出队数据元素占用的空间
    p = NULL;						// p 置为 NULL,防止野指针
}


读队头元素: GetHead(LinkQueue &Q, LinkNode *x)
bool GetHead(LinkQueue &Q, LinkNode *x) {
    if (Q.front == Q.rear)			// 判空
        return false;
    x = Q.front -> data;
    return true;
}



队列的应用

树的层次遍历


图的广度优先遍历


缓冲区

打印缓冲区,先进先出。


CPU 资源分配、竞争



Conclusion

传入参数

还是老生常谈,栈、队列、栈的数据元素、队列的数据元素传入时都需要进行合理判断——判空 of 判满


返回参数

  • 链表中,不带头结点一般初始化都可以成功,返回为 void

  • 栈顶指针 top 在顺序存储中是下标,在链式存储中是第一个数据元素。

  • 进栈判满,出栈判空

  • 进栈后不忘将栈顶指针再指向栈顶,出栈同理


队列

  • 顺序存储结构中,队头指针、队尾指针为下标;链式存储中指向队头和队尾数据元素。
  • 链式存储中,若无头结点,队头指向第一个数据元素;有头结点,指向头结点,即 Q.front -> next 为第一个数据元素。
  • 进队列后,不忘移动队头指针。
  • 出队列操作中,出队列元素需要增加其是否是队尾元素的判断——当队尾元素出队列时,若直接 free ,此时 Q.rear 还指向队尾结点,free 后,Q.rear 则“迷路”,因此需要先将 Q.rear 指向 Q.front,此时队列为空,再 free。
  • 不带头结点——尾部插入结点操作涉及 2 个结点:尾部结点 和 新插入的结点,而队列空时,没有尾部结点,需特殊处理


关于带不带头结点

  • 栈的链式存储结构:不带头结点
    • 链栈入栈、出栈操作都在链表头部,只有在表头一种情况。
  • 队列的链式存储结构:带头结点
    • 入队操作在队尾,通用的入队操作涉及两个结点——原尾部结点和新入队结点,若队列为空且无头结点,则相当于没有“原尾部结点”,需特殊处理。
    • 出队操作无需特殊处理。(相当于出栈)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值