数据结构 04 栈和队列

1 链栈基本操作的实现

链栈是一种基于链表实现的栈结构,栈遵循 "先进后出"(FILO)的原则。下面详细解释链栈的基本操作,并给出代码实现(以 C 语言为例)。

1. 链栈的结构定义

首先需要定义链栈的结点结构,每个结点包含数据域和指针域:

cpp 复制代码
typedef struct StackNode {
    int data;               // 数据域,存储栈中元素
    struct StackNode *next; // 指针域,指向下一个结点
} StackNode;

typedef struct {
    StackNode *top; // 栈顶指针,指向链栈的栈顶结点
} LinkStack;

2. 初始化(Initialization)

初始化链栈时,将栈顶指针设置为 NULL,表示栈为空:

cpp 复制代码
void InitStack(LinkStack *S) {
    S->top = NULL; // 栈顶指针置空,链栈初始为空
}
  • 解释:此时链栈中没有任何结点,top 指向 NULL,后续入栈操作会从这里开始构建链栈。

3. 入栈(Push)

入栈操作是在栈顶添加一个新的元素,步骤如下:

  1. 申请一个新的结点 p
  2. 将待入栈的元素 e 存入新结点的数据域。
  3. 将新结点的 next 指针指向当前的栈顶结点(因为新结点要成为新的栈顶,所以要先连接原来的栈顶)。
  4. 将栈顶指针 top 指向新结点,使其成为新的栈顶。

代码实现:

cpp 复制代码
int Push(LinkStack *S, int e) {
    StackNode *p = (StackNode *)malloc(sizeof(StackNode)); // 申请新结点
    if (p == NULL) {
        return 0; // 内存分配失败,入栈失败
    }
    p->data = e;        // 存入数据
    p->next = S->top;   // 新结点的 next 指向原栈顶
    S->top = p;         // 栈顶指针指向新结点,新结点成为栈顶
    return 1;           // 入栈成功
}
  • 解释:比如原来栈顶是结点 A,现在入栈元素 B,新结点 pnext 指向 A,然后 top 指向 p,此时栈顶就是 B 了,符合 "先进后出",后续出栈会先取出 B

4. 判空(Empty)

判断链栈是否为空,只需检查栈顶指针是否为 NULL

cpp 复制代码
int IsEmpty(LinkStack S) {
    return S.top == NULL; // 栈顶指针为 NULL 则栈空,返回 1;否则返回 0
}
  • 解释:如果 topNULL,说明链栈中没有结点,栈为空;否则栈不为空。

5. 出栈(Pop)

出栈操作是从栈顶取出一个元素,步骤如下:

  1. 先判断栈是否为空,如果为空则无法出栈。
  2. 如果栈不为空,记录栈顶结点的数据。
  3. 将栈顶指针指向栈顶结点的下一个结点(原来的第二个结点成为新的栈顶)。
  4. 释放原来的栈顶结点的内存。

代码实现:

cpp 复制代码
int Pop(LinkStack *S, int *e) {
    if (IsEmpty(*S)) {
        return 0; // 栈空,出栈失败
    }
    StackNode *p = S->top; // 暂存栈顶结点
    *e = p->data;          // 获取栈顶元素的数据
    S->top = p->next;      // 栈顶指针指向原栈顶的下一个结点
    free(p);               // 释放原栈顶结点的内存
    return 1;              // 出栈成功
}
  • 解释:比如栈顶是结点 B,其 next 指向结点 A,出栈时,先取出 B 的数据,然后 top 指向 A,释放 B 的内存,此时栈顶就是 A 了,下次出栈就会取出 A

完整示例代码

下面是包含所有操作的完整示例,还添加了获取栈顶元素和遍历栈的函数:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct StackNode {
    int data;
    struct StackNode *next;
} StackNode;

typedef struct {
    StackNode *top;
} LinkStack;

// 初始化链栈
void InitStack(LinkStack *S) {
    S->top = NULL;
}

// 入栈
int Push(LinkStack *S, int e) {
    StackNode *p = (StackNode *)malloc(sizeof(StackNode));
    if (p == NULL) {
        return 0;
    }
    p->data = e;
    p->next = S->top;
    S->top = p;
    return 1;
}

// 判空
int IsEmpty(LinkStack S) {
    return S.top == NULL;
}

// 出栈
int Pop(LinkStack *S, int *e) {
    if (IsEmpty(*S)) {
        return 0;
    }
    StackNode *p = S->top;
    *e = p->data;
    S->top = p->next;
    free(p);
    return 1;
}

// 获取栈顶元素(不出栈)
int GetTop(LinkStack S, int *e) {
    if (IsEmpty(S)) {
        return 0;
    }
    *e = S.top->data;
    return 1;
}

// 遍历栈(从栈顶到栈底)
void TraverseStack(LinkStack S) {
    StackNode *p = S.top;
    printf("栈中元素(从栈顶到栈底):");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

int main() {
    LinkStack S;
    InitStack(&S);

    // 入栈操作
    Push(&S, 1);
    Push(&S, 2);
    Push(&S, 3);
    Push(&S, 4);

    TraverseStack(S); // 输出:栈中元素(从栈顶到栈底):4 3 2 1 

    int topElem;
    GetTop(S, &topElem);
    printf("栈顶元素:%d\n", topElem); // 输出:栈顶元素:4

    // 出栈操作
    int popElem;
    Pop(&S, &popElem);
    printf("出栈元素:%d\n", popElem); // 输出:出栈元素:4

    TraverseStack(S); // 输出:栈中元素(从栈顶到栈底):3 2 1 

    return 0;
}

2 课件PPT关于括号检查的解法

Leetcode 27-CSDN博客https://blog.csdn.net/AYheyheyhey/article/details/153258951?spm=1001.2014.3001.5501

区别在于这个基础是链栈,题目可以看卡片附上的。

3 队列以及其基本操作

这是队列的抽象数据类型(ADT)定义,队列是一种 ** 先进先出(FIFO)** 的线性数据结构,类似排队,先到的先处理。下面详细解释各部分并给出基于链式存储的 C 语言实现。

一、解释

  1. 基本操作

    • InitQueue(&Q):初始化队列 Q,使其成为空队列。
    • DestroyQueue(&Q):销毁队列 Q,释放所有资源。
    • QueueEmpty(Q):判断队列 Q 是否为空,返回布尔值。
    • QueueLength(Q):返回队列 Q 的长度(元素个数)。
    • GetHead(Q, &e):获取队头元素,存入 e(不删除队头)。
    • EnQueue(&Q, e):将元素 e 入队(添加到队尾)。
    • DeQueue(&Q, &e):出队(删除队头元素),并将队头元素存入 e

二、链式队列的 C 语言实现

队列的存储有顺序存储(数组) 链式存储(链表)两种方式。这里用链式存储(更灵活,无固定容量限制)实现,步骤如下:

1. 定义结构体
cpp 复制代码
// 队列结点结构
typedef struct QNode {
    int data;           // 数据域(假设元素是 int 型,可根据需求修改)
    struct QNode *next; // 指针域,指向下一个结点
} QNode, *QueuePtr;

// 队列结构(包含队头、队尾指针)
typedef struct {
    QueuePtr front; // 队头指针
    QueuePtr rear;  // 队尾指针
} LinkQueue;
2. 初始化队列 InitQueue
cpp 复制代码
void InitQueue(LinkQueue *Q) {
    // 创建头结点(不存数据,仅用于统一操作)
    Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));
    if (Q->front == NULL) {
        printf("内存分配失败!\n");
        exit(1); // 异常退出
    }
    Q->front->next = NULL; // 头结点 next 置空(队列为空)
}
3. 销毁队列 DestroyQueue
cpp 复制代码
void DestroyQueue(LinkQueue *Q) {
    // 循环释放所有结点,直到队头为 NULL
    while (Q->front) {
        Q->rear = Q->front->next; // 暂存下一个结点
        free(Q->front);           // 释放当前队头
        Q->front = Q->rear;       // 队头后移
    }
}
4. 判断队列是否为空 QueueEmpty
cpp 复制代码
int QueueEmpty(LinkQueue Q) {
    // 队头和队尾指向同一结点(头结点),则为空
    return Q.front == Q.rear;
}
5. 队列长度 QueueLength
cpp 复制代码
int QueueLength(LinkQueue Q) {
    int len = 0;
    QueuePtr p = Q.front->next; // 从第一个元素结点开始遍历
    while (p) {
        len++;
        p = p->next;
    }
    return len;
}
6. 获取队头元素 GetHead
cpp 复制代码
int GetHead(LinkQueue Q, int *e) {
    if (QueueEmpty(Q)) {
        return 0; // 队空,获取失败
    }
    *e = Q.front->next->data; // 队头元素是头结点的下一个结点的数据
    return 1; // 获取成功
}
7. 入队 EnQueue
cpp 复制代码
int EnQueue(LinkQueue *Q, int e) {
    QueuePtr p = (QueuePtr)malloc(sizeof(QNode));
    if (p == NULL) {
        printf("内存分配失败!\n");
        return 0; // 分配失败,入队失败
    }
    p->data = e;   // 存入数据
    p->next = NULL; // 新结点为队尾,next 置空
    Q->rear->next = p; // 原队尾的 next 指向新结点
    Q->rear = p;       // 队尾指针指向新结点
    return 1; // 入队成功
}
8. 出队 DeQueue
cpp 复制代码
int DeQueue(LinkQueue *Q, int *e) {
    if (QueueEmpty(Q)) {
        return 0; // 队空,出队失败
    }
    QueuePtr p = Q.front->next; // 暂存队头元素结点
    *e = p->data;               // 获取队头元素
    Q->front->next = p->next;   // 头结点的 next 指向原队头的下一个结点
    if (Q->rear == p) {
        Q->rear = Q->front; // 若队中只有一个元素,出队后队尾指向头结点
    }
    free(p); // 释放原队头结点
    return 1; // 出队成功
}

三、测试示例

cpp 复制代码
int main() {
    LinkQueue Q;
    InitQueue(&Q); // 初始化队列

    // 入队测试
    EnQueue(&Q, 1);
    EnQueue(&Q, 2);
    EnQueue(&Q, 3);

    // 队列长度
    printf("队列长度:%d\n", QueueLength(Q)); // 输出:3

    // 获取队头
    int head;
    GetHead(Q, &head);
    printf("队头元素:%d\n", head); // 输出:1

    // 出队测试
    int e;
    DeQueue(&Q, &e);
    printf("出队元素:%d\n", e); // 输出:1

    // 再次获取队头
    GetHead(Q, &head);
    printf("新队头元素:%d\n", head); // 输出:2

    // 销毁队列
    DestroyQueue(&Q);
    return 0;
}

四、总结

队列的核心是先进先出,链式实现通过 "头指针 + 尾指针" 快速操作队头和队尾,适合频繁入队 / 出队的场景(如任务调度、消息队列等)。若需固定容量,也可改用 ** 顺序存储(数组)** 实现(需处理 "假溢出" 问题,通常用循环队列优化)。

4 顺序队列的"溢出"问题

一、顺序队列的存储基础

顺序队列是用数组来存储队列元素的,就像一个固定长度的 "管子"。数组有两个关键的指针(其实是下标):

  • front:队头指针,指向队列中第一个元素的位置。
  • rear:队尾指针,指向队列中最后一个元素的下一个位置 (这样方便操作)。还有一个 MaxSize,表示数组的最大容量(队列最多能存多少元素)。

二、真溢出(真正的没地方了)

rear - front ≥ MaxSize 时,就发生了真溢出

  • 解释:rear - front 可以理解为当前队列中元素的个数(因为 rear 指向队尾下一个位置,front 指向队头)。当这个差值大于等于数组最大容量 MaxSize 时,说明数组里真的装满了,没有任何空闲位置可以再入队了。
  • 比如:数组能存 5 个元素(MaxSize = 5),现在队列里已经有 5 个元素了(rear - front = 5),这时候再想入队,就没地方了,这就是真溢出。

三、假溢出(有地方但装不下)

假溢出是顺序队列很 "坑" 的一个问题:数组还有空闲空间,但因为队头和队尾的位置,导致无法入队

  • 例子:假设数组长度是 5,一开始队列是空的(front = rear = 0)。然后进行了 3 次入队(元素存在下标 0、1、2),再进行 2 次出队(把下标 0、1 的元素删了)。这时候队列里只有下标 2 的元素,front 移到了 2,rear 还是 3。现在数组下标 0、1 是空闲的,但如果要入队,rear 想往后移,可后面没位置了(因为 rear 到 4 就到数组末尾了),但其实前面 0、1 是空的。这就是 "假溢出"------ 有空间,但因为队列的 "头重脚轻",没法用。

四、解决假溢出的方法

方法 1:按最大需求设置队列大小
  • 解释:如果能预估队列最多需要存多少元素,就把数组 MaxSize 设得足够大,大到能装下所有可能的入队元素。
  • 比如:你知道程序里最多会同时有 100 个元素在队列里,那你就把数组设为能存 100 个元素。这样只要元素个数不超过 100,就不会溢出。
  • 缺点:如果预估错了,或者元素个数波动大,要么浪费空间(设太大),要么还是会溢出(设太小)。
方法 2:出队后,剩余元素向队头移动
  • 解释:每次出队(删除队头元素)后,把队列里剩下的所有元素,都往队头方向挪一个位置。这样队尾的位置就空出来了,后续入队可以继续用。
  • 例子:队列里元素是 [a, b, c]front=0rear=3),出队 a 后,把 b 移到下标 0,c 移到下标 1,rear 变成 2。这样下次入队,元素就可以存在下标 2 的位置。
  • 缺点:每次出队都要移动所有元素,效率很低(比如队列有 1000 个元素,每次出队都要移 999 个,特别费时间)。
方法 3:入队时,先移动元素再入队
  • 解释:当要入队时,先检查是不是假溢出。如果是,就把队列里已有的元素往队头方向挪,腾出队尾的位置,然后再把新元素入队。
  • 例子:队列元素存在 [_, _, c]front=2rear=3,数组长度 5),现在要入队 d。发现 rear 到末尾了,就把 c 移到下标 0,然后 d 存在下标 1,front=0rear=2
  • 缺点:和方法 2 一样,移动元素很费时间,效率不高。
方法 4:采用循环队列(最常用、最聪明的方法)
  • 核心思想:把数组想象成一个 ,队头和队尾可以 "绕圈"。当 rear 到数组末尾时,下一个位置可以回到数组开头(用取模运算实现)。
  • 实现要点:
    • 入队时:rear = (rear + 1) % MaxSize。这样当 rearMaxSize - 1 时,(rear + 1) % MaxSize 就会回到 0。
    • 出队时:front = (front + 1) % MaxSize
    • 判空:front == rear(和普通顺序队列一样)。
    • 判满:通常用 "牺牲一个空间" 的方法,即 (rear + 1) % MaxSize == front 时,认为队列满了。
  • 例子:数组长度 5,front=2rear=4。现在入队,rear = (4 + 1) % 5 = 0,元素存在下标 0 的位置。这样就利用了数组开头的空闲空间,解决了假溢出。
  • 优点:不用移动元素,效率高,还能充分利用数组空间。

5 循环队列

一、循环队列的基本思想

顺序队列会有 "假溢出" 问题(前面有空位,但队尾到数组末尾了,没法入队)。循环队列的想法是:把数组想象成一个环,队头和队尾可以 "绕圈"。比如数组最后一个位置的下一个位置,不是越界,而是回到数组开头。这样就能充分利用数组的所有空间,解决假溢出。

二、循环队列的示意图

左边是顺序队列 :数组下标从 0 到 6,队头 Q.front 在 2,队尾 Q.rear 在 5,元素是 J₁(下标 2)、J₂(下标 3)、J₃(下标 4)。这时候如果再入队,rear 到 6 就没地方了,但其实下标 0、1 是空的。

右边是循环队列 :把数组变成一个环,下标 0 接在 6 后面。队头 Q.front 在 2,队尾 Q.rear 绕到后面,元素 J₁J₂J₃ 像绕圈一样排列,这样前面的空位(下标 0、1)就可以被利用了。

三、循环队列的 "二义性" 问题

在循环队列里,队空 的条件是 Q.front == Q.rear(队头和队尾重合,没有元素)。但如果队列满了,有时候也会出现 Q.front == Q.rear(比如数组全装满了,队头和队尾绕圈后重合)。这就导致 "Q.front == Q.rear" 既可能是队空,也可能是队满,这就是二义性(一个条件有两种意思,没法判断)。

四、解决二义性的三种方法

方法 1:用计数器记录元素个数
  • 思路:搞一个变量 count,专门记录队列里有多少个元素。
  • 判队空:count == 0(队列里一个元素都没有)。
  • 判队满:count == MAXQSIZE(MAXQSIZE 是数组最大容量,元素个数达到上限)。
  • 解释:因为有 count 明确记录个数,所以不管 Q.frontQ.rear 怎么绕,只要看 count 就知道是满还是空。
方法 2:加设标志位 tag
  • 思路:用 tag 标记最近一次操作是 "入队" 还是 "出队"。
    • tag = 0:最近一次是出队操作。
    • tag = 1:最近一次是入队操作。
  • 判队空:tag == 0 && Q.rear == Q.front(最近是出队,现在队头队尾重合,说明队空了)。
  • 判队满:tag == 1 && Q.rear == Q.front(最近是入队,现在队头队尾重合,说明队满了,因为入队导致没地方了)。
  • 解释:通过 tag 区分 "是出队导致的重合(队空)" 还是 "入队导致的重合(队满)"。
方法 3:少用一个存储单元
  • 思路:故意让数组少用一个位置,这样队满和队空的条件就不一样了。
  • 判队空:Q.rear == Q.front(和原来一样)。
  • 判队满:Q.front == (Q.rear + 1) % MAXQSIZE(队尾的下一个位置是队头,说明队满了)。
  • 例子:数组最大容量 MAXQSIZE = 5,实际只用 4 个位置存元素。当 Q.rear 在 4,(Q.rear + 1) % 5 = 0,如果此时 Q.front = 0,就说明队满了(队尾的下一个位置是队头)。
  • 解释:因为少用了一个位置,所以队满时 Q.rear 的下一个位置是 Q.front,而队空时是 Q.rear == Q.front,这样就区分开了。
相关推荐
尘似鹤3 小时前
微信小程序学习(六)--多媒体操作
学习·微信小程序·小程序
要做朋鱼燕3 小时前
密码学安全:CIA三元组与三大核心技术
网络·笔记·密码学·嵌入式·加密·aes
UpYoung!3 小时前
无广技术贴!【PDF编辑器】Solid Converter PDF保姆级图文下载安装指南——实用推荐之PDF编辑软件
学习·数学建模·pdf·编辑器·运维开发·个人开发
金水谣3 小时前
考研10.5笔记
笔记
达瓦里氏1234 小时前
重排反应是什么?从分子变化到四大关键特征解析
数据库·学习·化学
LiJieNiub4 小时前
基于 PyTorch 实现 MNIST 手写数字识别
pytorch·深度学习·学习
Geek攻城猫4 小时前
02117 信息组织【第六章】
学习
流***陌4 小时前
线上教学小程序:构建高效互动的云端学习空间
学习·小程序
CAU界编程小白4 小时前
数据结构系列之堆
数据结构·c