31. 【C语言】堆栈与队列的实现

前面我们掌握了数组、链表,以及双向/循环链表的灵活操作。今天,我们要用这些基本工具来构建两种经典的抽象数据结构 ------栈(Stack)队列(Queue)

它们不是全新的数据类型,而是一种访问规则的约定。比如栈规定"后进先出",队列规定"先进先出"。你可以用数组或链表来承载这些规则,具体怎么实现,就看你的选择。这种"接口与实现分离"的思想,正是数据结构的精髓。


一、栈:后进先出(LIFO)

就像一摞盘子:你每次只能取最上面的那个,放也只能放在最上面。最后放上去的(Last In),最早被取出(First Out),所以叫 LIFO

栈有三个核心操作:

  • push(压栈):把元素放到栈顶。
  • pop(出栈):把栈顶元素取出来。
  • peek(查看栈顶):只看不取。

我们用两种方式来实现栈:数组链表

1. 数组实现栈

用一个固定大小的数组存储数据,用一个整数 top 指示栈顶位置。top == -1 表示空栈。

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

#define STACK_MAX 100

typedef struct {
    int data[STACK_MAX];
    int top;
} ArrayStack;

void stack_init(ArrayStack *s) {
    s->top = -1;
}

bool stack_is_empty(ArrayStack *s) {
    return s->top == -1;
}

bool stack_is_full(ArrayStack *s) {
    return s->top == STACK_MAX - 1;
}

bool stack_push(ArrayStack *s, int value) {
    if (stack_is_full(s)) return false;
    s->data[++(s->top)] = value;
    return true;
}

bool stack_pop(ArrayStack *s, int *out) {
    if (stack_is_empty(s)) return false;
    *out = s->data[(s->top)--];
    return true;
}

bool stack_peek(ArrayStack *s, int *out) {
    if (stack_is_empty(s)) return false;
    *out = s->data[s->top];
    return true;
}

特点:访问快速,但容量固定。适合元素数量可预知的场景。

2. 链表实现栈

用动态内存构建无容量限制的栈。栈顶就是链表头------每次 push 就是头插,pop 就是删除头结点。

c 复制代码
typedef struct SNode {
    int data;
    struct SNode *next;
} SNode;

typedef struct {
    SNode *top;   // 指向栈顶结点
} LinkedStack;

void lstack_init(LinkedStack *s) {
    s->top = NULL;
}

bool lstack_is_empty(LinkedStack *s) {
    return s->top == NULL;
}

bool lstack_push(LinkedStack *s, int value) {
    SNode *node = (SNode*)malloc(sizeof(SNode));
    if (node == NULL) return false;
    node->data = value;
    node->next = s->top;   // 新结点的 next 指向旧栈顶
    s->top = node;         // 栈顶更新为新结点
    return true;
}

bool lstack_pop(LinkedStack *s, int *out) {
    if (lstack_is_empty(s)) return false;
    SNode *temp = s->top;
    *out = temp->data;
    s->top = temp->next;   // 栈顶下移
    free(temp);
    return true;
}

bool lstack_peek(LinkedStack *s, int *out) {
    if (lstack_is_empty(s)) return false;
    *out = s->top->data;
    return true;
}

void lstack_free(LinkedStack *s) {
    while (!lstack_is_empty(s)) {
        int dummy;
        lstack_pop(s, &dummy);
    }
}

注意:pop 操作要释放结点内存,并更新 top 指针。链表栈不会满,但可能因内存不足而 push 失败。


二、栈应用一:括号匹配

编译器检查代码中大括号、中括号、圆括号是否匹配,就是栈的经典案例。

思路:遇到左括号就压栈,遇到右括号时检查栈顶。如果栈顶是匹配的左括号则弹出,否则报错。扫描完后栈必须为空。

c 复制代码
#include <stdbool.h>
#include <string.h>

bool is_balanced(const char *expr) {
    LinkedStack s;
    lstack_init(&s);

    for (int i = 0; expr[i] != '\0'; i++) {
        char ch = expr[i];
        if (ch == '(' || ch == '[' || ch == '{') {
            lstack_push(&s, ch);
        } else if (ch == ')' || ch == ']' || ch == '}') {
            if (lstack_is_empty(&s)) {
                return false;   // 右括号多出来
            }
            int top_val;
            lstack_pop(&s, &top_val);
            char top = (char)top_val;
            if ((ch == ')' && top != '(') ||
                (ch == ']' && top != '[') ||
                (ch == '}' && top != '{')) {
                return false;   // 类型不匹配
            }
        }
    }
    bool ok = lstack_is_empty(&s);
    lstack_free(&s);
    return ok;
}

测试:

c 复制代码
printf("%d\n", is_balanced("({[]})"));  // 1 (true)
printf("%d\n", is_balanced("({[})"));   // 0 (false)

三、栈应用二:迷宫回溯(简单版)

栈可以用来做深度优先搜索(DFS)。想象一个二维迷宫,用 0 表示通路,1 表示墙。从起点开始,把走过的位置压栈,如果遇到死路就退栈(回溯),直到找到出口。

简化实现:用一个栈保存每一步的坐标。每一步尝试上、右、下、左四个方向,如果可走就前进一步,压栈;如果四个方向都走不通,就出栈(回退)。

代码较长,这里只给出核心思想,完整代码可参考课后练习。栈在这里扮演的角色是记录历史路径------正是"后进先出"让你能沿着原路返回。


四、队列:先进先出(FIFO)

队列 就像排队买票:先来的先服务,后来的排后面。这就是 FIFO(First In First Out)。

核心操作:

  • enqueue(入队):将元素放到队尾。
  • dequeue(出队):从队头取走元素。

1. 数组实现循环队列

如果用普通数组,出队后前面的空间就浪费了,或者要频繁移动元素。循环队列解决此问题:通过模运算让数组"头尾相连"。

维护 front(队头索引)和 rear(队尾索引)。rear 指向下一次插入的位置。为了区分队空和队满,我们牺牲一个存储单元。

c 复制代码
#include <stdbool.h>

#define QUEUE_MAX 100

typedef struct {
    int data[QUEUE_MAX];
    int front;
    int rear;
} ArrayQueue;

void aqueue_init(ArrayQueue *q) {
    q->front = 0;
    q->rear = 0;
}

bool aqueue_is_empty(ArrayQueue *q) {
    return q->front == q->rear;
}

bool aqueue_is_full(ArrayQueue *q) {
    return (q->rear + 1) % QUEUE_MAX == q->front;
}

bool aqueue_enqueue(ArrayQueue *q, int value) {
    if (aqueue_is_full(q)) return false;
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % QUEUE_MAX;
    return true;
}

bool aqueue_dequeue(ArrayQueue *q, int *out) {
    if (aqueue_is_empty(q)) return false;
    *out = q->data[q->front];
    q->front = (q->front + 1) % QUEUE_MAX;
    return true;
}

(rear + 1) % MAX == front 是判满条件,那个空间始终空闲,以避免 front == rear 既表示空也表示满的歧义。

2. 链表实现队列

用链表实现队列需要维护头指针 front 和尾指针 tail。入队是在尾部插入,出队是删除头结点。双向链表当然可以,但单向链表配合尾指针足矣。

c 复制代码
typedef struct QNode {
    int data;
    struct QNode *next;
} QNode;

typedef struct {
    QNode *front;  // 队头
    QNode *rear;   // 队尾
} LinkedQueue;

void lqueue_init(LinkedQueue *q) {
    q->front = NULL;
    q->rear = NULL;
}

bool lqueue_is_empty(LinkedQueue *q) {
    return q->front == NULL;
}

bool lqueue_enqueue(LinkedQueue *q, int value) {
    QNode *node = (QNode*)malloc(sizeof(QNode));
    if (node == NULL) return false;
    node->data = value;
    node->next = NULL;

    if (lqueue_is_empty(q)) {
        q->front = node;
        q->rear = node;
    } else {
        q->rear->next = node;  // 旧尾连接新结点
        q->rear = node;        // 更新尾
    }
    return true;
}

bool lqueue_dequeue(LinkedQueue *q, int *out) {
    if (lqueue_is_empty(q)) return false;
    QNode *temp = q->front;
    *out = temp->data;
    q->front = temp->next;
    if (q->front == NULL) {
        q->rear = NULL;   // 队列空了,尾也要置空
    }
    free(temp);
    return true;
}

void lqueue_free(LinkedQueue *q) {
    while (!lqueue_is_empty(q)) {
        int dummy;
        lqueue_dequeue(q, &dummy);
    }
}

链表队列没有容量限制,内存足够即可。


五、队列应用:广度优先搜索(BFS)简介

栈对应 DFS(深度优先搜索),队列对应 BFS(广度优先搜索)。在迷宫问题中,BFS 能找到最短路径。

思路:从起点入队。每次取出队首,将其可达且未访问的邻居全部入队,并标记已访问。一层层扩散,直到找到出口。

栈和队列就像算法的左右手:DFS 用栈(隐式或显式),BFS 用队列。它们本质上只是规定了数据进出的顺序,却带来了截然不同的遍历策略。

复制代码
#include <stdio.h>
#include <stdbool.h>

#define N 5
#define MAX_QUEUE 25

// 坐标结构体
typedef struct {
    int x, y;
} Point;

// 队列结构体(循环队列)
typedef struct {
    Point data[MAX_QUEUE];
    int front, rear;
} Queue;

void initQueue(Queue *q) {
    q->front = q->rear = 0;
}

bool isEmpty(Queue *q) {
    return q->front == q->rear;
}

bool enqueue(Queue *q, Point p) {
    if ((q->rear + 1) % MAX_QUEUE == q->front) return false; // 队满
    q->data[q->rear] = p;
    q->rear = (q->rear + 1) % MAX_QUEUE;
    return true;
}

Point dequeue(Queue *q) {
    // 调用前确保非空
    Point p = q->data[q->front];
    q->front = (q->front + 1) % MAX_QUEUE;
    return p;
}

// 检查坐标合法性
bool isValid(int maze[N][N], bool visited[N][N], int x, int y) {
    return (x >= 0 && x < N && y >= 0 && y < N &&
            maze[x][y] == 0 && !visited[x][y]);
}

// BFS 求解迷宫,返回 true 表示有解,并打印路径
bool solveMazeBFS(int maze[N][N]) {
    bool visited[N][N] = { false };
    Point parent[N][N];                 // 记录前驱节点,用于回溯路径
    int dir[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 上、下、左、右

    Queue q;
    initQueue(&q);

    Point start = {0, 0};
    enqueue(&q, start);
    visited[0][0] = true;
    parent[0][0] = (Point){-1, -1};     // 起点的父节点设为(-1,-1)

    while (!isEmpty(&q)) {
        Point cur = dequeue(&q);

        // 到达出口
        if (cur.x == N - 1 && cur.y == N - 1) {
            // 回溯路径,存入数组以便正序输出
            Point path[N * N];
            int len = 0;
            Point p = cur;
            while (p.x != -1 && p.y != -1) {
                path[len++] = p;
                p = parent[p.x][p.y];
            }
            // 正序输出(从入口到出口)
            printf("找到路径(BFS):\n");
            for (int i = len - 1; i >= 0; i--) {
                printf("(%d,%d)", path[i].x, path[i].y);
                if (i > 0) printf(" -> ");
            }
            printf("\n");
            return true;
        }

        // 探索四个方向
        for (int i = 0; i < 4; i++) {
            int nx = cur.x + dir[i][0];
            int ny = cur.y + dir[i][1];
            if (isValid(maze, visited, nx, ny)) {
                visited[nx][ny] = true;
                parent[nx][ny] = cur;          // 记录来路
                Point next = {nx, ny};
                enqueue(&q, next);
            }
        }
    }

    return false;   // 队列空,无解
}

int main() {
    // 5×5 迷宫,0:通路,1:障碍
    int maze[N][N] = {
        {0, 1, 0, 0, 0},
        {0, 1, 0, 1, 0},
        {0, 0, 0, 1, 0},
        {0, 1, 1, 1, 0},
        {0, 0, 0, 0, 0}
    };

    printf("迷宫 (0:路, 1:墙):\n");
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            printf("%d ", maze[i][j]);
        }
        printf("\n");
    }

    if (!solveMazeBFS(maze)) {
        printf("无解\n");
    }

    return 0;
}

输出:

复制代码
迷宫 (0:路, 1:墙):
0 1 0 0 0 
0 1 0 1 0 
0 0 0 1 0 
0 1 1 1 0 
0 0 0 0 0 
找到路径(BFS):
(0,0) -> (1,0) -> (2,0) -> (3,0) -> (4,0) -> (4,1) -> (4,2) -> (4,3) -> (4,4)

六、 BFS与DFS路径对比

BFS(队列实现)的特点:

  • 最短路径保证:BFS按层扩展,第一次到达终点时找到的路径一定是最短的
  • 空间复杂度:最坏情况O(n²),需要存储所有可达节点
  • 时间复杂度:O(V+E),每个节点和边访问一次
  • 路径特征:找到的路径通常"绕路少",直接走向目标

DFS(栈实现)的特点:

  • 不一定最短:DFS深度优先,可能找到很长很绕的路径
  • 空间复杂度:O(h),h为递归深度,通常比BFS节省空间
  • 时间复杂度:同样O(V+E),但实际访问顺序不同
  • 路径特征:可能找到"钻胡同"的路径,先深入再回溯

对比示例:

在同一个迷宫中 (0:路, 1:墙):

复制代码
0 1 0 0 0 
0 1 0 1 0 
0 0 0 1 0 
0 1 1 1 0 
0 0 0 0 0 
  • BFS找到的路径:(0,0) -> (1,0) -> (2,0) -> (3,0) -> (4,0) -> (4,1) -> (4,2) -> (4,3) -> (4,4)
  • DFS可能找到的路径:(0,0) -> (1,0) -> (2,0) -> (2,1) -> (2,2) -> (1,2) -> (0,2) -> (0,3) -> (0,4) -> (1,4) -> (2,4) -> (3,4) -> (4,4)(相同)或更长的路径

选择建议:

  • 需要最短路径 → 用BFS
  • 需要探索所有可能/路径数量少 → 用DFS
  • 图很大但深度有限 → 用DFS(节省内存)
  • 图较宽但深度不大 → 用BFS

这个BFS实现展示了队列在算法中的核心作用:先进先出的特性确保了按"层"探索,这正是找到最短路径的关键。

七、数组 vs 链表:实现选择

数组实现 链表实现
大小限制 有(可预设或动态扩容)
内存使用 可能浪费预留空间 每个元素多一个指针开销
操作复杂度 O(1) O(1)
实现难度 简单 稍复杂(需管理内存)
缓存友好 是(连续内存)

如果元素数量可预知且性能要求高,用数组;如果数量不定或频繁增删,用链表。无论选择哪种,暴露给使用者的接口(push/pop/enqueue/dequeue)是一样的------这就是抽象的力量。


八、小结

今天我们学习了两种基础抽象数据结构:

  • 栈(Stack) :LIFO,后进先出。用数组(固定大小)或链表(动态)实现。核心操作 pushpoppeek。应用:括号匹配、表达式求值、迷宫回溯、函数调用栈。
  • 队列(Queue):FIFO,先进先出。数组循环队列高效利用空间,链表队列灵活无界。应用:BFS、任务调度、缓冲区管理。

栈和队列是最简单的"受限访问"结构,但它们构建了无数算法的基础。当你在高级语言中使用 list 当作栈,或使用 deque 时,底层的原理就来自这里。

现在你已经有了链表、栈、队列三件套。下一篇文章,我们将进入另一个迷人的世界------二叉树与递归。递归函数本质上就是系统栈的应用,而树是自然界中最常见的数据结构之一。理解树,你将开启算法新篇章。


课后小练习

  1. 用链表栈实现一个简单的四则运算表达式求值器(只含 +、-、*、/,无括号)。提示:使用两个栈(操作数栈、运算符栈),按照优先级规则处理。
  2. 用循环队列实现一个简单的"生产者-消费者"模拟器:生产者向队列中添加数字(1 到 20),消费者从中取出并打印。两个操作交替进行。
  3. 实现一个最小栈:在 O(1) 时间内能获取栈中的最小值。提示:使用辅助栈记录每个状态下的最小值。
  4. (小挑战)基于栈实现一个迷宫求解程序:将迷宫定义为一个 5×5 的 0/1 矩阵,(0,0) 是入口,(4,4) 是出口。用栈记录路径,输出从入口到出口的完整坐标序列。如果无解,输出"无解"。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库

相关推荐
万联WANFLOW2 小时前
多分支企业组网,IP 网段到底该怎么规划
开发语言·php
阿里嘎多学长2 小时前
2026-07-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
ChaoZiLL2 小时前
我的数据结构3——链表(link list)
数据结构·链表
cookies_s_s4 小时前
C++ 字符串动态创建对象 -- 工厂模式、自动注册、模板递归动态调用
服务器·开发语言·c++
王老师青少年编程4 小时前
2026年6月GESP真题及题解(C++七级):消消乐
数据结构·c++·算法·真题·gesp·2026年6月
海清河晏1114 小时前
数据结构 | 二叉搜索树
数据结构·c++·visual studio
盐焗鹌鹑蛋5 小时前
【C++】继承
开发语言·c++
Yang_jie_035 小时前
笔记:数据结构(顺序表)
数据结构·windows·笔记
栈溢出了5 小时前
Java Lambda 表达式笔记
java·开发语言·intellij-idea