数据结构小扫尾——栈

数据结构小扫尾------栈

@jarringslee

文章目录

  • 数据结构小扫尾------栈
    • 栈本质上是一种特殊的线性表。
      • (一)线性表的定义
      • (二)线性表的运算
    • 什么是栈。
      • (一)栈的定义
      • (二)栈的分类
      • (三)栈的存储结构
      • (四)栈的示例
    • 二、栈的实现
      • (一)顺序栈的实现
      • (二)链栈的实现
    • 栈的应用
      • (一)括号匹配
      • (二)中缀表达式转后缀表达式
    • 例题
      • 20. 有效的括号 - 力扣(LeetCode)\](https://leetcode.cn/problems/valid-parentheses/)

​ 栈stack作为一种重要的数据结构,在实际开发中有着广泛的应用,如函数调用、表达式求值等。

栈本质上是一种特殊的线性表。

(一)线性表的定义

线性表是一种最基础的数据结构,它由具有相同特性的数据元素构成,这些数据元素之间存在着线性关系,即第一个元素称为表头,最后一个元素称为表尾,除第一个元素外,每个元素有且只有一个前驱,除最后一个元素外,每个元素有且只有一个后继。线性表可以顺序存储,也可以链式存储。

(二)线性表的运算

复制代码
1. **插入** :在表中指定位置插入一个元素。
2. **删除** :删除表中指定位置的元素。
3. **查找** :查找表中满足特定条件的元素。

什么是栈。

(一)栈的定义

栈是一种特殊的线性表,它满足后进先出的特性,即最后被插入的元素最先被删除。栈的插入和删除操作都只能在栈顶进行。

就像一个桶。我们把东西按顺序放进去,最后一个就放在了桶的最顶端。我们需要拿出来的时候,先拿出来最后放进去的,最后拿出来第一个放进去的。起到了一个逆序的过程。

(二)栈的分类

  • 顺序栈 :使用数组实现的栈,元素在数组中连续存储。
  • 链栈 :使用链表实现的栈,元素通过指针链接在一起。

(三)栈的存储结构

栈通常使用顺序栈和链栈来存储。顺序栈的存储结构如下:

c 复制代码
typedef int DataType;
typedef struct Stack {
    DataType* a; //存放栈元素的数组
    int top;     //栈顶指针
    int capacity; //栈的最大容量
} Stack;

链栈的存储结构如下:

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

typedef struct LinkStack {
    Node* top; //栈顶指针
} LinkStack;

(四)栈的示例

假设我们有一个顺序栈 [1, 2, 3, 4, 5],其中 5 是栈顶元素。当插入元素 6 时,栈变为 [1, 2, 3, 4, 5, 6]。当删除元素时,栈变为 [1, 2, 3, 4, 5]。

二、栈的实现

(一)顺序栈的实现

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

// 定义栈中存储的数据类型为整型
typedef int DataType;

// 定义顺序栈的结构体,包含存储元素的数组、栈顶指针和栈的最大容量
typedef struct Stack {
    DataType* a; // 存放栈元素的数组
    int top;     // 栈顶指针,初始值为 -1 表示栈为空
    int capacity; // 栈的最大容量
} Stack;

// 栈的初始化函数
void StackInit(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    pstack->a = (DataType*)malloc(sizeof(DataType) * 4); // 分配初始大小为 4 的动态数组
    if (pstack->a == NULL) { // 判断内存分配是否成功
        perror("malloc fail"); // 若分配失败,输出错误信息
        return;
    }
    pstack->top = -1; // 栈顶指针初始化为 -1
    pstack->capacity = 4; // 栈的初始容量设为 4
}

// 栈的销毁函数
void StackDestroy(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    free(pstack->a); // 释放动态分配的数组内存
    pstack->a = NULL; // 将数组指针置为空,避免野指针
    pstack->top = -1; // 栈顶指针重置为 -1
    pstack->capacity = 0; // 栈的容量重置为 0
}

// 栈的插入(压栈)操作
void StackPush(Stack* pstack, DataType x) {
    assert(pstack); // 断言确保传入的指针非空
    // 判断栈是否已满,若栈满则扩容
    if (pstack->top == pstack->capacity - 1) {
        DataType* tmp = (DataType*)realloc(pstack->a, sizeof(DataType) * pstack->capacity * 2);
        if (tmp == NULL) { // 判断内存重新分配是否成功
            perror("realloc fail"); // 若扩容失败,输出错误信息
            return;
        }
        pstack->a = tmp; // 更新数组指针
        pstack->capacity *= 2; // 栈的容量翻倍
    }
    pstack->a[++pstack->top] = x; // 栈顶指针增 1 后存入新元素
}

// 栈的删除(弹栈)操作
void StackPop(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    assert(!StackEmpty(pstack)); // 断言确保栈不为空,防止溢出
    pstack->top--; // 栈顶指针减 1,实现元素出栈
}

// 判断栈是否为空的函数
bool StackEmpty(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    return pstack->top == -1; // 若栈顶指针为 -1 则栈为空,返回 true,否则返回 false
}

// 获取栈顶元素的函数
DataType StackTop(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    assert(!StackEmpty(pstack)); // 断言确保栈不为空,防止访问越界
    return pstack->a[pstack->top]; // 返回栈顶元素的值
}

// 获取栈中元素个数的函数
int StackSize(Stack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    return pstack->top + 1; // 栈顶指针加 1 即为栈中元素个数
}

(二)链栈的实现

c 复制代码
// 定义栈中存储的数据类型为整型
typedef int DataType;

// 定义链栈节点的结构体,包含存储的数据和指向下一个节点的指针
typedef struct Node {
    DataType data; // 节点存储的数据
    struct Node* next; // 指向下一个节点的指针
} Node;

// 定义链栈的结构体,包含指向栈顶的指针
typedef struct LinkStack {
    Node* top; // 栈顶指针,初始值为 NULL 表示栈为空
} LinkStack;

// 链栈的初始化函数
void LinkStackInit(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    pstack->top = NULL; // 栈顶指针初始化为空
}

// 链栈的销毁函数
void LinkStackDestroy(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    Node* cur = pstack->top; // 从栈顶开始遍历
    while (cur) { // 遍历链栈中的每个节点
        Node* next = cur->next; // 保存下一个节点的地址
        free(cur); // 释放当前节点的内存
        cur = next; // 移动到下一个节点
    }
    pstack->top = NULL; // 销毁后栈顶指针置为空
}

// 链栈的插入(压栈)操作
void LinkStackPush(LinkStack* pstack, DataType x) {
    assert(pstack); // 断言确保传入的指针非空
    Node* new_node = (Node*)malloc(sizeof(Node)); // 动态分配新节点内存
    if (new_node == NULL) { // 判断内存分配是否成功
        perror("malloc fail"); // 若分配失败,输出错误信息
        return;
    }
    new_node->data = x; // 将数据存入新节点
    new_node->next = pstack->top; // 新节点的 next 指针指向原栈顶
    pstack->top = new_node; // 更新栈顶指针为新节点
}

// 链栈的删除(弹栈)操作
void LinkStackPop(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    assert(!LinkStackEmpty(pstack)); // 断言确保栈不为空,防止溢出
    Node* tmp = pstack->top; // 保存当前栈顶节点
    pstack->top = pstack->top->next; // 更新栈顶指针为下一个节点
    free(tmp); // 释放原栈顶节点内存
}

// 判断链栈是否为空的函数
bool LinkStackEmpty(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    return pstack->top == NULL; // 若栈顶指针为空则栈为空,返回 true,否则返回 false
}

// 获取链栈栈顶元素的函数
DataType LinkStackTop(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    assert(!LinkStackEmpty(pstack)); // 断言确保栈不为空,防止访问越界
    return pstack->top->data; // 返回栈顶元素的值
}

// 获取链栈中元素个数的函数
int LinkStackSize(LinkStack* pstack) {
    assert(pstack); // 断言确保传入的指针非空
    int size = 0; // 初始化计数器为 0
    Node* cur = pstack->top; // 从栈顶开始遍历
    while (cur) { // 遍历链栈中的每个节点
        size++; // 每遍历一个节点计数器加 1
        cur = cur->next; // 移动到下一个节点
    }
    return size; // 返回链栈中元素的总个数
}

栈的应用

(一)括号匹配

括号匹配是一种经典的栈应用,用于检查表达式中的括号是否正确匹配。

c 复制代码
// 判断括号匹配的函数
bool isValid(char* s) {
    Stack stack; // 定义一个顺序栈
    StackInit(&stack); // 初始化栈
    for (int i = 0; s[i] != '\0'; i++) { // 遍历字符串中的每个字符
        if (s[i] == '(' || s[i] == '[' || s[i] == '{') { // 若字符为左括号
            StackPush(&stack, s[i]); // 将左括号压入栈中
        } else { // 若字符为右括号
            if (StackEmpty(&stack)) { // 判断栈是否为空,若为空说明没有匹配的左括号
                StackDestroy(&stack); // 销毁栈以释放资源
                return false; // 返回不匹配的结果
            }
            char top = StackTop(&stack); // 取出栈顶元素(最近的左括号)
            StackPop(&stack); // 将栈顶元素弹出
            // 判断右括号与左括号是否匹配
            if ((s[i] == ')' && top != '(') || (s[i] == ']' && top != '[') || (s[i] == '}' && top != '{')) {
                StackDestroy(&stack); // 销毁栈以释放资源
                return false; // 若不匹配,返回不匹配的结果
            }
        }
    }
    bool result = StackEmpty(&stack); // 若遍历结束栈为空,则所有括号匹配;否则存在未匹配的左括号
    StackDestroy(&stack); // 销毁栈以释放资源
    return result; // 返回判断结果
}

(二)中缀表达式转后缀表达式

中缀表达式转后缀表达式是另一种经典的栈应用。

c 复制代码
// 判断字符是否为运算符的辅助函数
bool isOperator(char c) {
    return c == '+' || c == '-' || c == '*' || c == '/'; // 若字符是其中之一,则返回 true 表示是运算符
}

// 获取运算符优先级的辅助函数
int precedence(char op) {
    if (op == '+' || op == '-') { // 加减运算符优先级较低,返回 1
        return 1;
    } else if (op == '*' || op == '/') { // 乘除运算符优先级较高,返回 2
        return 2;
    } else { // 其他字符不是运算符,返回 0
        return 0;
    }
}

// 中缀表达式转后缀表达式的函数
void infixToPostfix(char* infix, char* postfix) {
    Stack stack; // 定义一个顺序栈
    StackInit(&stack); // 初始化栈
    int j = 0; // 用于记录后缀表达式中字符的位置
    for (int i = 0; infix[i] != '\0'; i++) { // 遍历中缀表达式的每个字符
        if (infix[i] == '(') { // 若遇到左括号,直接压栈
            StackPush(&stack, infix[i]);
        } else if (infix[i] == ')') { // 若遇到右括号
            // 依次弹出栈中的运算符直到遇到左括号,并将弹出的运算符加入后缀表达式
            while (!StackEmpty(&stack) && StackTop(&stack) != '(') {
                postfix[j++] = StackTop(&stack);
                StackPop(&stack);
            }
            StackPop(&stack); // 弹出左括号,但不加入后缀表达式
        } else if (isOperator(infix[i])) { // 若遇到运算符
            // 将栈中优先级大于等于当前运算符的运算符依次弹出并加入后缀表达式
            while (!StackEmpty(&stack) && precedence(infix[i]) <= precedence(StackTop(&stack))) {
                postfix[j++] = StackTop(&stack);
                StackPop(&stack);
            }
            StackPush(&stack, infix[i]); // 将当前运算符压栈
        } else { // 若是操作数,直接加入后缀表达式
            postfix[j++] = infix[i];
        }
    }
    // 弹出栈中剩余的运算符并加入后缀表达式
    while (!StackEmpty(&stack)) {
        postfix[j++] = StackTop(&stack);
        StackPop(&stack);
    }
    postfix[j] = '\0'; // 在后缀表达式末尾添加字符串结束标志
    StackDestroy(&stack); // 销毁栈以释放资源
}

例题

20. 有效的括号 - 力扣(LeetCode)

给定一个只包含三种括号字符 '('')''{''}''['']' 的字符串 s,判断 s 是否是有效的括号字符串。

示例 1:

复制代码
输入:s = "()"
输出:true

示例 2:

复制代码
输入:s = "()[]{}"
输出:true

示例 3:

复制代码
输入:s = "(]"
输出:false

​ 我们首先初始化一个空栈,然后遍历字符串中的每一个字符。如果字符串是左括号,则压入栈。如果字符是右括号,则检查栈顶元素是否匹配,匹配则弹栈,否则返回 false。如果字符是右括号,则检查栈顶元素是否匹配,匹配则弹栈,否则返回 false

c 复制代码
// 判断括号匹配的函数(与前面的 isValid 函数一致)
bool isValid(char* s) {
    Stack stack;
    StackInit(&stack);
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
            StackPush(&stack, s[i]);
        } else {
            if (StackEmpty(&stack)) {
                StackDestroy(&stack);
                return false;
            }
            char top = StackTop(&stack);
            StackPop(&stack);
            if ((s[i] == ')' && top != '(') || (s[i] == ']' && top != '[') || (s[i] == '}' && top != '{')) {
                StackDestroy(&stack);
                return false;
            }
        }
    }
    bool result = StackEmpty(&stack);
    StackDestroy(&stack);
    return result;
}

150. 逆波兰表达式求值 - 力扣(LeetCode)

根据逆波兰表示法,求表达式的值。

有效的算符包括 +-*/。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

示例 1:

复制代码
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀逆波兰表达式为:((2 + 1) * 3) = 9

示例 2:

复制代码
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀逆波兰表达式为:(4 + (13 / 5)) = 6

​ 依旧是初始化一个空栈,然后遍历逆波兰表达式。如果元素是数字,则压入栈。如果元素是运算符,则弹出栈顶的两个数字进行运算,将结果压入栈。最后栈顶元素即为结果。

c 复制代码
// 逆波兰表达式求值的函数
int evalRPN(char** tokens, int tokensSize) {
    Stack stack;
    StackInit(&stack);
    for (int i = 0; i < tokensSize; i++) { // 遍历逆波兰表达式的每个标记
        char* token = tokens[i];
        // 判断是否为运算符
        if (strcmp(token, "+") == 0) {
            assert(!StackEmpty(&stack)); // 确保栈中有足够的操作数
            int b = StackTop(&stack); // 取出栈顶元素作为第二个操作数
            StackPop(&stack);
            int a = StackTop(&stack); // 取出下一个栈顶元素作为第一个操作数
            StackPop(&stack);
            StackPush(&stack, a + b); // 将运算结果压栈
        } else if (strcmp(token, "-") == 0) {
            assert(!StackEmpty(&stack));
            int b = StackTop(&stack);
            StackPop(&stack);
            int a = StackTop(&stack);
            StackPop(&stack);
            StackPush(&stack, a - b);
        } else if (strcmp(token, "*") == 0) {
            assert(!StackEmpty(&stack));
            int b = StackTop(&stack);
            StackPop(&stack);
            int a = StackTop(&stack);
            StackPop(&stack);
            StackPush(&stack, a * b);
        } else if (strcmp(token, "/") == 0) {
            assert(!StackEmpty(&stack));
            int b = StackTop(&stack);
            StackPop(&stack);
            int a = StackTop(&stack);
            StackPop(&stack);
            StackPush(&stack, a / b);
        } else { // 若是操作数,则转换为整型后压栈
            StackPush(&stack, atoi(token));
        }
    }
    int result = StackTop(&stack); // 最终结果位于栈顶
    StackDestroy(&stack);
    return result;
}
相关推荐
方方土3331 分钟前
ABC 404
数据结构·算法·图论
学习噢学个屁4 分钟前
基于51单片机的红外人体感应报警器
c语言·单片机·嵌入式硬件·51单片机
梁下轻语的秋缘1 小时前
每日c/c++题 备战蓝桥杯(P1886 滑动窗口 /【模板】单调队列)
c语言·c++·蓝桥杯
zm2 小时前
五一假期作业
数据结构·算法
JANYI20182 小时前
Linux 常用指令详解
linux·c语言·网络
梁下轻语的秋缘4 小时前
C/C++滑动窗口算法深度解析与实战指南
c语言·c++·算法
hallo-ooo4 小时前
【C/C++】函数模板
c语言·c++
iFulling4 小时前
【数据结构】第八章:排序
数据结构·算法
一只鱼^_4 小时前
力扣第448场周赛
数据结构·c++·算法·leetcode·数学建模·动态规划·迭代加深