前言

本文介绍c语言实现栈的相关内容。

(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手——通义,DeepSeek等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)


C语言实现栈的详细解析

栈(Stack)一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端,称为栈顶,另一端称为栈底。具有后进先出(LIFO, Last In First Out)的特性。它在函数调用、表达式求值、括号匹配等场景有广泛应用。下面我将详细讲解C语言中实现栈的两种主要方式:顺序栈(基于数组-最常用)和链式栈(基于链表)。

一、栈的基本概念

栈的核心特性

  • 后进先出:最后进入的元素最先被取出
  • 操作限制:只能在栈顶进行插入(push)和删除(pop)操作
  • 基本操作时间复杂度:均为O(1)

栈的两个经典操作

  1. 压栈(Push):栈的插入操作,将元素放入栈顶
  2. 出栈(Pop):栈的删除操作,将栈顶元素移除

二、栈的实现方式

1. 顺序栈(基于数组实现)

结构体定义
typedef struct {
int* data;      // 存储元素的数组
int size;       // 栈的总容量
int top;        // 栈顶指针(初始为-1,表示空栈)
} Stack;

关键点

  • top初始值为-1表示空栈
  • size限制栈的最大容量
  • 使用动态数组实现顺序存储
代码实现

初始化栈

Stack* initStack(int n) {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->data = (int*)malloc(sizeof(int) * n);
s->size = n;
s->top = -1;  // 初始化栈顶指针
return s;
}

入栈操作

int push(Stack* s, int val) {
if (s->top == s->size - 1) {
// 栈已满,返回错误
return -1;
}
s->top++;
s->data[s->top] = val;
return 0;  // 成功
}

出栈操作

int pop(Stack* s, int* val) {
if (s->top == -1) {
// 栈为空,返回错误
return -1;
}
*val = s->data[s->top];
s->top--;
return 0;  // 成功
}

判空操作

int empty(Stack* s) {
return s->top == -1;
}

获取栈顶元素

int top(Stack* s) {
if (empty(s)) {
return -1;  // 空栈返回错误值
}
return s->data[s->top];
}
顺序栈的特点
  • 优点:实现简单,内存连续,访问速度快
  • 缺点:容量固定,可能有空间浪费;满栈时需要扩容(增加复杂性)

完整数组实现

#include<stdio.h>
  #include<stdlib.h>
    #include<stdbool.h>
      #include<assert.h>
        // 定义栈中存储的数据类型为整型
        typedef int STDataType;
        // 定义栈结构体
        typedef struct Stack
        {
        STDataType* a;      // 动态数组,用于存储栈元素
        int top;            // 栈顶指针(指向栈顶元素的下一个位置)
        int capacity;       // 栈的容量(当前分配的数组大小)
        }ST;
        // 函数声明(栈操作接口)
        void STInit(ST* ps);                // 初始化栈
        void STDestroy(ST* ps);              // 销毁栈(释放内存)
        void STPush(ST* ps, STDataType x);  // 入栈(压栈)
        void STPop(ST* ps);                 // 出栈(弹栈)
        STDataType STTop(ST* ps);           // 获取栈顶元素
        int STSize(ST* ps);                 // 获取栈中元素个数
        bool STEmpty(ST* ps);               // 判断栈是否为空
        // 初始化栈
        void STInit(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 初始化栈成员:
        // 1. 将动态数组指针置为NULL(表示尚未分配内存)
        // 2. 栈顶指针初始化为0(表示栈为空,栈顶元素在位置-1,但实际存储从0开始)
        // 3. 栈容量初始化为0
        ps->a = NULL;
        ps->top = 0;
        ps->capacity = 0;
        }
        // 销毁栈(释放内存并重置状态)
        void STDestroy(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 释放动态数组内存
        free(ps->a);
        // 重置栈状态(避免野指针)
        ps->a = NULL;
        ps->top = 0;
        ps->capacity = 0;
        }
        // 入栈操作(压栈)
        void STPush(ST* ps, STDataType x)
        {
        // 确保传入的指针有效
        assert(ps);
        // 检查栈是否已满(top等于容量表示已无可用空间)
        if (ps->top == ps->capacity)
        {
        // 计算新容量:如果当前容量为0(空栈),则分配4个元素空间
        // 否则,容量翻倍(避免频繁扩容,提高效率)
        int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        // 重新分配内存,扩展栈容量
        STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
        // 检查内存分配是否成功
        if (tmp == NULL)
        {
        perror("realloc fail"); // 打印错误信息
        return; // 分配失败,退出函数
        }
        // 更新栈的数组指针和容量
        ps->a = tmp;
        ps->capacity = newcapacity;
        }
        // 将元素放入栈顶位置(top指向的位置)
        ps->a[ps->top] = x;
        // 栈顶指针后移(指向下一个空位置)
        ps->top++;
        }
        // 出栈操作(弹栈)
        void STPop(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 检查栈是否为空(不能从空栈弹出元素)
        assert(!STEmpty(ps));
        // 栈顶指针前移(相当于移除栈顶元素)
        ps->top--;
        }
        // 获取栈顶元素
        STDataType STTop(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 检查栈是否为空
        assert(!STEmpty(ps));
        // 栈顶元素位于top-1位置(因为top指向下一个空位置)
        return ps->a[ps->top - 1];
        }
        // 获取栈中元素个数
        int STSize(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 栈中元素个数 = top(因为top表示已使用的元素数量)
        return ps->top;
        }
        // 判断栈是否为空
        bool STEmpty(ST* ps)
        {
        // 确保传入的指针有效
        assert(ps);
        // 如果栈顶指针为0,则栈为空
        return ps->top == 0;
        }
        int main()
        {
        ST s;
        STInit(&s);
        STPush(&s, 1);
        STPush(&s, 2);
        STPush(&s, 3);
        int top = STTop(&s);
        printf("%d ", top);
        STPop(&s);
        STPush(&s, 4);
        STPush(&s, 5);
        while (!STEmpty(&s))
        {
        int top = STTop(&s);
        printf("%d ", top);
        STPop(&s);
        }
        STDestroy(&s);
        return 0;
        }

主要掌握上面数组实现即可


2. 链式栈(基于链表实现)

结构体定义
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef struct {
Node* top;  // 栈顶指针
int size;   // 栈的大小
} Stack;

关键点

  • 栈顶即为链表的头节点
  • top指向栈顶元素
  • size记录栈中元素个数
代码实现

初始化栈

Stack* initStack() {
Stack* s = (Stack*)malloc(sizeof(Stack));
s->top = NULL;
s->size = 0;
return s;
}

入栈操作

void push(Stack* s, int val) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = val;
newNode->next = s->top;
s->top = newNode;
s->size++;
}

出栈操作

int pop(Stack* s, int* val) {
if (s->top == NULL) {
return -1;  // 栈为空
}
Node* temp = s->top;
*val = temp->data;
s->top = temp->next;
free(temp);
s->size--;
return 0;
}

判空操作

int empty(Stack* s) {
return s->top == NULL;
}

获取栈顶元素

int top(Stack* s) {
if (empty(s)) {
return -1;  // 空栈返回错误值
}
return s->top->data;
}
链式栈的特点
  • 优点:空间利用率高,无需扩容,动态增长
  • 缺点:实现相对复杂,内存不连续,访问速度稍慢

三、两种实现方式的对比

特性顺序栈链式栈
实现基础数组链表
空间利用率低(可能有浪费)高(动态分配)
扩容需要扩容(可能有性能开销)无需扩容
插入/删除效率O(1)O(1)
代码复杂度较低较高
适用场景已知栈大小,需要频繁操作不知道栈大小,需要动态增长

四、栈的应用场景

栈在C语言中有以下广泛应用:

  1. 表达式求值

    • 栈可以用于存储运算符和操作数
    • 实现表达式的求值算法,如中缀表达式转后缀表达式并计算结果
  2. 函数调用

    • 函数调用时,需要保存函数的返回地址、参数和局部变量等信息
    • 这些信息使用栈来保存和管理
  3. 括号匹配

    • 栈可以用于检查括号是否匹配
    • 遇到左括号入栈,遇到右括号出栈,最终检查栈是否为空
  4. 逆波兰表达式求值

    • 逆波兰表达式是一种后缀表达式
    • 栈可以实现逆波兰表达式的求值
  5. 递归算法

    • 递归算法中,每次递归调用时需要保存当前函数的状态
    • 这些状态可以使用栈来保存和管理
  6. 深度优先搜索

    • 栈可以用于实现图的深度优先搜索算法

五、实际应用示例

括号匹配检查

#include <stdio.h>
  #include <stdlib.h>
    typedef struct {
    char* data;
    int size;
    int top;
    } Stack;
    Stack* initStack(int n) {
    Stack* s = (Stack*)malloc(sizeof(Stack));
    s->data = (char*)malloc(sizeof(char) * n);
    s->size = n;
    s->top = -1;
    return s;
    }
    int push(Stack* s, char c) {
    if (s->top == s->size - 1) return -1;
    s->top++;
    s->data[s->top] = c;
    return 0;
    }
    int pop(Stack* s, char* c) {
    if (s->top == -1) return -1;
    *c = s->data[s->top];
    s->top--;
    return 0;
    }
    int empty(Stack* s) {
    return s->top == -1;
    }
    int isMatching(char c1, char c2) {
    return (c1 == '(' && c2 == ')') ||
    (c1 == '[' && c2 == ']') ||
    (c1 == '{' && c2 == '}');
    }
    int checkBrackets(char* str) {
    Stack* s = initStack(100);
    for (int i = 0; str[i] != '\0'; i++) {
    if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
    push(s, str[i]);
    } else if (str[i] == ')' || str[i] == ']' || str[i] == '}') {
    char c;
    if (empty(s) || !isMatching(pop(s, &c), str[i])) {
    return 0;  // 括号不匹配
    }
    }
    }
    return empty(s);  // 检查栈是否为空
    }
    int main() {
    char str1[] = "({[()])}";
    char str2[] = "({[]})";
    printf("str1: %s\n", checkBrackets(str1) ? "匹配" : "不匹配");
    printf("str2: %s\n", checkBrackets(str2) ? "匹配" : "不匹配");
    return 0;
    }

六、总结

  1. 栈的实现

    • 顺序栈:基于数组,实现简单,但容量固定
    • 链式栈:基于链表,空间利用率高,动态增长
  2. 选择建议

    • 如果已知栈的大小,且对性能要求高,选择顺序栈
    • 如果栈的大小不确定,需要动态增长,选择链式栈
  3. 栈的核心价值

    • 通过后进先出的特性,简化了复杂问题的解决
    • 为表达式求值、括号匹配、函数调用等提供了高效解决方案
  4. 重要原则

    • 所有栈操作必须检查栈是否为空(避免空栈出栈错误)
    • 顺序栈需注意容量限制,满栈时需要处理扩容
    • 链式栈需注意内存管理,出栈时需释放节点内存

栈作为基础数据结构,理解其原理和实现方式对学习更复杂的数据结构和算法至关重要。希望这篇详细解析能帮助你深入理解C语言中栈的实现和应用。