栈和队列的实现与详解(C语言版):从底层原理到代码实战

🎬 博主名称键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》

⛺️指尖敲代码,雾霭皆可破


文章目录

一、引言

栈(Stack)和队列(Queue)是两种最基本、最常用的线性数据结构,广泛应用于各种算法和系统底层实现中。它们的主要区别在于元素的存取顺序:栈是后进先出(LIFO,Last In First Out) ,而队列是先进先出(FIFO,First In First Out)

本文将结合一份完整的C语言实现代码,详细讲解栈和队列的结构定义、接口设计、核心函数的实现细节,并通过测试用例演示它们的使用方法。同时,我们也会分析两种实现方式的优缺点,并指出代码中需要注意的地方。

二、栈的实现与分析

栈可以用数组或链表实现,这里采用的是动态数组的方式,优点是数据在内存中连续,缓存友好,且入栈出栈的时间复杂度均为O(1)。缺点是扩容时需要重新分配内存,可能造成空间浪费或拷贝开销。

1. 栈的结构定义(Stack.h)

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

typedef int Datatype;

typedef struct Stack
{
    Datatype* arr;      // 动态数组指针
    int top;            // 栈顶位置(指向栈顶元素的下一个位置)
    int capacity;       // 当前数组容量
} ST;

// 函数声明
void STInit(ST* pst);
void STPush(ST* pst, Datatype x);
void STPop(ST* pst);
Datatype STTop(ST* pst);
bool STEmpty(ST* pst);
int STSize(ST* pst);
void STDestroy(ST* pst);
  • top 的含义:这里约定 top 指向栈顶元素的下一个位置 ,即栈中元素个数正好等于 top。初始时 top = 0
  • capacity 为数组当前能容纳的最大元素个数,当 top == capacity 时表示栈满,需要扩容。

2. 核心函数实现(Stack.c)

初始化与销毁
c 复制代码
void STInit(ST* pst)
{
    pst->arr = NULL;
    pst->capacity = pst->top = 0;
}

void STDestroy(ST* pst)
{
    if (pst->arr)
    {
        free(pst->arr);
    }
    pst->capacity = pst->top = 0;
}

初始化将指针置空,容量和大小置零。销毁时释放动态数组内存,并将指针置空,防止野指针。

入栈(Push)
c 复制代码
void STPush(ST* pst, Datatype x)
{
    assert(pst);
    // 检查是否需要扩容
    if (pst->capacity == pst->top)
    {
        int newcapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
        Datatype* ptr = (Datatype*)realloc(pst->arr, sizeof(Datatype) * newcapacity);
        if (ptr == NULL)
        {
            perror("realloc");
            return;
        }
        pst->arr = ptr;
        pst->capacity = newcapacity;
    }
    pst->arr[pst->top++] = x;
}
  • 扩容策略:当容量为0时,先分配4个元素的空间;之后每次扩容为原来的2倍。
  • 使用 realloc 重新分配内存,若失败则打印错误并返回。注意realloc 失败时原内存依然有效,但此处直接返回会导致入栈失败,且原数据仍然存在,可以接受;但更严谨的做法是保留原数组不变,等后续操作。在教学中可以提醒读者根据实际需求处理。
  • 入栈操作直接在当前 top 位置存入 x,然后 top 自增。
出栈(Pop)
c 复制代码
void STPop(ST* pst)
{
    assert(pst);
    assert(pst->top > 0);   // 栈不能为空
    pst->top--;
}

出栈非常简单,只需将 top 减1,逻辑上删除了栈顶元素。实际内存并未释放,后续入栈会覆盖该位置。

获取栈顶元素
c 复制代码
Datatype STTop(ST* pst)
{
    assert(pst);
    return pst->arr[pst->top - 1];
}

注意 top 指向栈顶下一个位置,所以栈顶元素下标为 top - 1

判空与大小
c 复制代码
bool STEmpty(ST* pst)
{
    return pst->top == 0;
}

int STSize(ST* pst)
{
    return pst->top;
}

3. 栈的测试用例

test.c 中有一段被注释的栈测试代码:

c 复制代码
//int main()
//{
//    ST st;
//    STInit(&st);
//    STPush(&st, 1);
//    STPush(&st, 2);
//    STPush(&st, 3);
//    while (!STEmpty(&st))
//    {
//        printf("%d\n", st.arr[st.top - 1]);
//        STPop(&st);
//    }
//    STDestroy(&st);
//    return 0;
//}

这段代码演示了栈的基本使用流程:

  1. 初始化栈。
  2. 连续压入三个元素:1, 2, 3。
  3. 循环弹出并打印栈顶元素,直到栈空。由于栈的LIFO特性,打印顺序为:3, 2, 1。
  4. 最后销毁栈释放内存。

三、队列的实现与分析

队列通常使用链表实现,因为数组实现的队列在出队时需要移动大量元素(除非使用循环队列)。这里采用单链表结构,包含头指针和尾指针,使得入队(尾插)和出队(头删)的时间复杂度均为O(1)。

1. 队列的结构定义(Queue.h)

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

typedef int QDatatype;

typedef struct QueueNode
{
    QDatatype a;
    struct QueueNode* next;
} QNode;

typedef struct Queue
{
    QNode* phead;   // 队头指针
    QNode* ptail;   // 队尾指针
    int size;       // 队列中元素个数
} Queue;

// 函数声明
void QueueInit(Queue* q);
void QueuePush(Queue* q, QDatatype x);
void QueuePop(Queue* q);
QDatatype QueueBack(Queue* q);
QDatatype QueueFront(Queue* q);
bool QEmpty(Queue* q);
int QSize(Queue* q);
void QueueDestroy(Queue* q);
  • 队列节点 QNode 包含数据域 a 和指向下一个节点的指针 next
  • 队列结构 Queue 持有头指针和尾指针,并维护一个 size 记录节点个数,方便判空和获取大小。

2. 核心函数实现(Queue.c)

初始化与销毁
c 复制代码
void QueueInit(Queue* q)
{
    assert(q);
    q->phead = q->ptail = NULL;
    q->size = 0;
}

void QueueDestroy(Queue* q)
{
    assert(q);
    while (q->phead != NULL)
    {
        QNode* next = q->phead->next;
        free(q->phead);
        q->phead = next;
    }
    q->phead = q->ptail = NULL;
}

初始化时头尾指针置空,size为0。销毁时遍历链表释放每个节点,最后将头尾指针置空。

入队(Push)
c 复制代码
void QueuePush(Queue* q, QDatatype x)
{
    assert(q);
    QNode *ptr = (QNode*)malloc(sizeof(QNode));
    // 注意:此处应先检查malloc是否成功,再给ptr->a赋值
    ptr->a = x;
    ptr->next = NULL;
    if (ptr == NULL)
    {
        perror("malloc");
        return;
    }
    if (q->phead == NULL)
    {
        q->phead = q->ptail = ptr;
    }
    else
    {
        q->ptail->next = ptr;
        q->ptail = ptr;
    }
    q->size++;
}
  • 代码中的一个小问题 :在检查 ptr == NULL 之前就使用了 ptr->aptr->next,这可能导致对空指针的解引用。正确的做法是先将 malloc 的结果赋给 ptr,然后立即判断 ptr 是否为空,若不为空再进行赋值。应该调整顺序:

    c 复制代码
    QNode *ptr = (QNode*)malloc(sizeof(QNode));
    if (ptr == NULL)
    {
        perror("malloc");
        return;
    }
    ptr->a = x;
    ptr->next = NULL;

    读者在实际编码中务必注意此细节。

  • 入队时,若队列为空(phead == NULL),则新节点既是队头也是队尾;否则将新节点链接到尾节点之后,并更新尾指针。最后 size 加1。

出队(Pop)
c 复制代码
void QueuePop(Queue* q)
{
    assert(q);
    assert(q->size > 0);
    if (q->phead->next == NULL)
    {
        free(q->phead);
        q->phead = q->ptail = NULL;
    }
    else
    {
        QNode* next = q->phead->next;
        free(q->phead);
        q->phead = next;
    }
    q->size--;
}
  • 出队时先断言队列非空(size > 0)。
  • 如果队列中只有一个节点(phead->next == NULL),则释放该节点后头尾指针都置空。
  • 否则,保存头节点的下一个节点,释放当前头节点,然后头指针指向下一个节点。
  • 最后 size 减1。
获取队首和队尾元素
c 复制代码
QDatatype QueueFront(Queue* q)
{
    assert(q);
    assert(q->phead);
    return q->phead->a;
}

QDatatype QueueBack(Queue* q)
{
    assert(q);
    assert(q->ptail);
    return q->ptail->a;
}

直接返回头节点或尾节点的数据,需要确保指针非空。

判空与大小
c 复制代码
bool QEmpty(Queue* q)
{
    assert(q);
    return q->size == 0;
}

int QSize(Queue* q)
{
    assert(q);
    return q->size;
}

利用维护的 size 直接返回。

3. 队列的测试用例

test.c 中当前启用的 main 函数是队列测试:

c 复制代码
int main()
{
    Queue q;
    QueueInit(&q);
    QueuePush(&q, 1);
    QueuePush(&q, 2);
    printf("%d ", QueueFront(&q));  // 输出队首元素 1
    QueuePop(&q);                    // 出队
    QueuePush(&q, 3);
    while (!QEmpty(&q))
    {
        printf("%d ", QueueFront(&q));
        QueuePop(&q);
    }
    QueueDestroy(&q);
}

运行过程分析:

  1. 初始化队列。
  2. 入队1、2,此时队列为 [1, 2]。
  3. 打印队首元素 1,然后出队,队列变为 [2]。
  4. 入队3,队列变为 [2, 3]。
  5. 循环打印并出队,输出 2 3,队列变空。
  6. 销毁队列。

最终输出结果为:1 2 3,符合队列的先进先出特性。

四、总结与对比

特性 栈 (Stack) 队列 (Queue)
存储结构 动态数组 单链表
存取顺序 后进先出 (LIFO) 先进先出 (FIFO)
核心操作 Push / Pop / Top Push / Pop / Front / Back
时间复杂度 均为 O(1)(Pop 仅逻辑删除) 均为 O(1)(需维护头尾指针)
适用场景 函数调用、括号匹配、深度优先等 广度优先搜索、任务调度、缓冲区等

优缺点分析

  • 动态数组实现的栈:内存连续,访问效率高;扩容时有拷贝开销,但总体入栈均摊O(1)。适合元素个数可预测或频繁访问的场景。
  • 链表实现的队列:无需扩容,元素个数不受限制;每个节点需要额外存储指针,内存开销稍大。适合频繁入队出队、元素数量变化大的场景。

五、结语

栈和队列是数据结构学习的基石,理解它们的实现原理对于后续学习更复杂的结构(如树、图)以及算法设计至关重要。本文通过完整的C语言代码,详细剖析了两种结构的内部细节和接口设计,希望能帮助读者不仅会用,更能自己实现。读者也可以尝试用数组实现循环队列,或用链表实现栈,进一步加深理解。

(完)

相关推荐
智者知已应修善业1 小时前
【冰雹猜想过程逆序输出】2025-4-19
c语言·c++·经验分享·笔记·算法
无名之逆2 小时前
你可能不需要WebSocket-服务器发送事件的简单力量
java·开发语言·前端·后端·计算机·rust·编程
Remember_9932 小时前
一文吃透Java WebSocket:原理、实现与核心特性解析
java·开发语言·网络·websocket·网络协议·http·p2p
锅包一切2 小时前
一、C++ 发展与程序创建
开发语言·c++·后端·学习·编程
一株菌子3 小时前
10.12 总结
开发语言·python
枷锁—sha3 小时前
【CTFshow-pwn系列】03_栈溢出【pwn 051】详解:C++字符串替换引发的血案与 Ret2Text
开发语言·网络·c++·笔记·安全·网络安全
沙白猿3 小时前
【TJXT】Day3
java·开发语言
一个处女座的程序猿O(∩_∩)O3 小时前
Python面向对象的封装特性详解
开发语言·python
zhaoyin19943 小时前
python基础
开发语言·python