一篇文章掌握“栈”

目录

一、栈的本质与核心特性

(一)栈的定义与本质

(二)核心术语

(三)关于栈顶与栈底指针的指向

(四)逻辑与物理结构

二、栈的底层实现方案

(一)链表实现(不太推荐)

(二)数组实现(推荐方案)

(三)选择数组实现的原因

三、环境搭建与结构体定义

(一)工程文件结构

[(二)Stack.h 完整代码](#(二)Stack.h 完整代码)

四、核心函数的实现

(一)初始化栈

(二)辅助函数

(三)栈操作

五、测试代码(test.c)

六、栈的算法题:有效的括号

(一)题目简述

[(二)解题思路(利用栈的 LIFO 特性)](#(二)解题思路(利用栈的 LIFO 特性))

(三)代码实现

(四)边界场景处理


一、栈的本质与核心特性

(一)栈的定义与本质

一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底,完全封闭。

栈中的数据元素遵守**后进先出LIFO(Last In First Out)**的原则。

(二)核心术语

1、栈顶(Top)

允许操作的一端,用指针 / 下标标记,既表示 "下次插入位置",也隐含 "有效元素个数"

2、栈底(Bottom):固定封闭的一端,是栈的起始点,空栈时栈顶与栈底重合

**3、压栈:**栈的插入操作叫做进栈 / 压栈 / 入栈,入数据在栈顶。

**4、出栈:**栈的删除操作叫做出栈。出数据也在栈顶。

**5、空栈:**栈中无任何数据时,栈顶与栈底重合(下标均为 0),此时不可执行出栈操作。

**6、满栈:**数组实现中栈的有效元素个数等于容量(Top = Capacity),需触发扩容。

**★**从数学角度,栈可定义为一个有序集合 S,仅支持两种核心操作:

**① Push(S, x):**将元素 x 插入到集合 S 的顶端(栈顶);

**② Pop(S):**从集合 S 的顶端移除并返回元素(若 S 非空)。

(三)关于栈顶与栈底指针的指向

**1、**空栈时,栈顶 = 栈底。

**2、**有元素时,栈底指针指向栈的底部元素,栈顶指针指向下一个可入栈的位置,即当前栈顶元素的下一个地址。

**Tip:**这种设计可以通过栈顶与栈底指针的差值快速计算栈中元素个数(栈顶指针 - 栈底指针 = 元素个数),是一种常见的实现方式。

(四)逻辑与物理结构

**1、逻辑结构:**必为线性结构,数据元素间存在一对一的线性关系。

**2、物理结构:**取决于底层实现方式(数组或链表)。如果实现方式是数组,那么就是连续存储;如果实现方式是链表,那么就是非连续存储。

二、栈的底层实现方案

(一)链表实现(不太推荐)

1、设计思路与关键决策

链表实现栈的核心是将栈顶定义在链表头部,原因如下:

**(1)**链表头部插入 / 删除元素时,仅需修改指针指向,时间复杂度为 O(1)

**(2)**若栈顶定义在链表尾部,插入/删除需遍历链表找尾结点,时间复杂度升至 O(n),效率极低。

(二)数组实现(推荐方案)

1、设计思路与关键决策

数组实现栈的核心是将栈顶定义在数组尾部,原因如下:

**(1)**数组尾部插入 / 删除元素时,无需移动其他元素,时间复杂度为 O(1)

**(2)**若栈顶定义在数组头部,插入 / 删除需移动所有元素,时间复杂度升至 O(n),效率极低。

2、数据结构设计

cpp 复制代码
// 栈结构定义(数组实现)
typedef int STDataType; 
typedef struct Stack {
    STDataType* arr;     // 动态数组:存储栈元素(避免固定大小限制)
    int top;             // 栈顶指针:标记下次插入位置,同时等于有效元素个数
    int capacity;        // 容量:当前数组可容纳的最大元素个数
} ST;

3、关键操作逻辑

**(1)**动态扩容机制

初始容量为 4,空间不足时按 2 倍扩容(避免初始容量为 0 时直接倍增失效)。

**(2)初始化:**数组指针置空,栈顶(top)和容量(capacity)均设为 0。

**(3)入栈:**先检查容量,不足则扩容,再将数据插入栈顶(top 位置),随后 top 自增。

**(4)出栈:**直接将 top 自减(无需物理删除元素,通过 top 标记有效数据范围)。

**(5)取栈顶元素:**返回数组下标为top - 1的元素(top 指向下次插入位置)。

(三)选择数组实现的原因

数组实现的栈,元素是连续存储的,没有额外的指针开销且无内存碎片,所有空间都用于存储有效数据,因此空间效率更高。

什么是内存碎片呢?

内存碎片:当链表结点频繁插入、删除后,内存中会出现很多零散的小空间**(这些空间太小,无法存储新的大对象),这就是**内存碎片,因此链表的内存特性是 "有碎片"。

再形象一点就是,假如第一个结点与第二个结点之间,相隔了60个字节,我现在如果要存储一个80个字节空间的数据,这里就存不下,就造成了内存碎片的浪费。

即使加上后面的空间足够,但是也不行,因为这是两个独立的空闲块

简单说:碎片不是 "空间不够",而是 "空间散了,拼不成大块"

三、环境搭建与结构体定义

(一)工程文件结构

需创建 3 个文件,分工明确,便于维护:

**1、Stack.h:**结构体定义、函数声明、头文件引入(对外提供接口)。

**2、Stack.c:**所有函数的具体实现(内部逻辑)。

**3、test.c:**测试用例编写,验证功能正确性(调用接口)。

(二)Stack.h 完整代码
cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

// 栈数据类型抽象(支持后续修改)
typedef int STDataType;

// 栈结构定义
typedef struct Stack {
    STDataType* arr;     // 动态数组存储数据
    int top;             // 栈顶指针(标记下次插入位置,同时表示有效元素个数)
    int capacity;        // 栈的容量
} ST;


// 核心操作声明
//1、初始化与销毁
void STInit(ST* ps);         // 初始化栈
void STDestroy(ST* ps);      // 销毁栈

//2、辅助函数
void Print(ST* ps);          // 打印栈元素(辅助操作)
bool StackEmpty(ST* ps);     // 判断栈是否为空

//3、栈操作
void StackPush(ST* ps, STDataType x);  // 入栈
void StackPop(ST* ps);       // 出栈
STDataType StackTop(ST* ps); // 取栈顶元素
int STSize(ST* ps);          // 获取有效元素个数

四、核心函数的实现

(一)初始化栈
cpp 复制代码
// 初始化栈
void STInit(ST* ps) 
{
    assert(ps); // 断言避免空指针
    ps->arr = NULL; // 动态数组初始为空(未申请内存)
    ps->top = 0;// 栈顶初始为0:空栈时下次插入位置为数组下标0
    ps->capacity = 0; // 初始容量为0:未申请内存时容量为0
}
(二)辅助函数

1、栈状态判断

cpp 复制代码
// 判断栈是否为空
bool StackEmpty(ST* ps) 
{
    assert(ps);
    return ps->top == 0; // 空栈标志:top为0则表示无有效元素
}

**Tip:**这个栈状态判断主要用于出栈操作,只有栈不为空,才可以进行出栈;进行判空的断言判断是 assert(!StackEmpty(ps));

2、打印操作

cpp 复制代码
void Print(ST* ps) 
{
    assert(ps);
    for (int i = 0; i < ps->top; i++) {
        printf("%d  ", ps->arr[i]);
    }
    printf("\n");
}
(三)栈操作

1、入栈

入栈需先检查容量,不足时触发动态扩容,扩容逻辑需处理 "初始容量为 0" 的特殊场景:

cpp 复制代码
void StackPush(ST* ps, STDataType x)
{
    assert(ps != NULL);

    // 1. 检查容量:栈满(top == capacity)时扩容
    if (ps->top == ps->capacity) 
    {
        //(1)扩容:初始容量为4,后续按2倍扩容(平衡效率与空间)
        int newCapacity;
        if (ps->capacity == 0) 
            newCapacity = 4;
        else 
            newCapacity = ps->capacity * 2;
        
        
        //(2)申请新内存
        STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));
        if (tmp == NULL) 
        {
            perror("realloc fail: "); 
            exit(1);       
        }
        
        //(3)更新栈的数组指针和容量【realloc可能返回新地址】
        ps->arr = tmp;
        ps->capacity = newCapacity;
    }

    // 2. 空间足够则插入元素
    ps->arr[ps->top] = x;
    ps->top++;
}

扩容细节说明:

**(1)****初始容量设为 4:**避免频繁扩容(若初始为 1,插入 4 个元素需扩容 3 次);

**(2)****2 倍扩容:**符合 "amortized O (1)" 时间复杂度(多次插入的平均时间为 O (1));

**(3)**使用 realloc 而非 malloc:若后续空间足够,realloc 可直接复用原有内存,减少内存拷贝开销;如果不够,则再另一块内存创建,返回新地址,所以可能返回新地址。

2、出栈

cpp 复制代码
void StackPop(ST* ps) 
{
    // 断言:栈不能为空(空栈出栈是非法操作)
    assert(ps != NULL && !StackEmpty(ps));
    // 仅需top自减:后续插入会覆盖原数据,无需显式删除
    ps->top--;
}

**关键注意:**出栈不删除物理元素是为了效率,即O(1) 时间复杂度。但需注意:若栈存储敏感数据(如密码),需手动覆盖原数据,以避免数据泄露。

3、取栈顶元素

栈顶元素存储在 top - 1 位置(因 top 指向下次插入位置):

cpp 复制代码
STDataType StackTop(ST* ps) 
{
    // 断言:栈不能为空(空栈无栈顶元素)
    assert(ps != NULL && !StackEmpty(ps));
    return ps->arr[ps->top - 1]; // 返回top前一个位置的元素
}

4、获取中有效元素个数

cpp 复制代码
// 获取有效元素个数(直接返回top值)
int STSize(ST* ps) 
{
    assert(ps);
    return ps->top;
}

五、测试代码(test.c)

cpp 复制代码
#include"Stack.h"

void test01() {
    ST st;
    STInit(&st); // 初始化栈

    // 入栈操作:1、2、3、4
    StackPush(&st, 1);
    StackPush(&st, 2);
    StackPush(&st, 3);
    StackPush(&st, 4);

    // 打印栈元素(输出:1  2  3  4)
    Print(&st);

    // 输出有效元素个数(输出:4)
    printf("有效元素个数:%d\n", STSize(&st));

    // 出栈操作(依次弹出4、3、2、1)
    while (!StackEmpty(&st)) {
        int top = StackTop(&st); // 取栈顶元素
        printf("出栈元素:%d\n", top);
        StackPop(&st);
    }

    STDestroy(&st); // 销毁栈
}

int main() {
    test01();
    return 0;
}

六、栈的算法题:有效的括号

(一)题目简述

1、题目描述

给定仅包含 '('、')'、'{'、'}'、'['、']' 的字符串,判断字符串是否有效。

有效字符串需满足: 左括号必须用相同类型的右括号闭合;左括号必须按正确顺序闭合(后出现的左括号先闭合); 每个右括号必有对应的左括号。

**2、题目链接:**https://leetcode.cn/problems/valid-parentheses/description/

(二)解题思路(利用栈的 LIFO 特性)

**1、遍历字符串:**遇到左括号('('、'{'、'[')直接入栈;

2、遇到右括号时

**(1)**若栈为空(无对应左括号),直接返回无效。

**(2)**取出栈顶元素,判断是否为匹配的左括号,如 ')' 匹配 '(';不匹配则返回无效,匹配则执行出栈操作。

**3、遍历结束后:**若栈为空,即所有左括号均匹配闭合,则字符串有效;否则无效。

(三)代码实现
cpp 复制代码
bool isValid(char* s) 
{
    // 1. 初始化栈(存储左括号)
    ST st;
    STInit(&st);
    char* p = s; // 指针遍历字符串

    // 2. 遍历字符串
    // 循环终止条件:遍历到字符串结束符('\0')
    while (*p != '\0') 
    { 
        if (*p == '(' || *p == '{' || *p == '[') {
            StackPush(&st, *p);//左括号入栈
        }
        else {
            // 右括号处理:栈空则无效
            if (StackEmpty(&st)) {
                STDestroy(&st); // 销毁栈(避免内存泄漏)
                return false;
            }
            // 取栈顶元素匹配
            char topChar = StackTop(&st);
            // 取栈顶元素匹配
            char top = StackTop(&st);
            if ((top == '(' && *p != ')') || 
                (top == '{' && *p != '}') || 
                (top == '[' && *p != ']')) {
                STDestroy(&st);
                return false;
            }
            StackPop(&st); // 匹配成功,出栈
        }
        p++; // 指针移动到下一个字符
    }

    // 3. 遍历结束:检查栈是否为空(即是否存在未闭合左括号)
    bool ret;
    if (StackEmpty(&st)) 
        ret = true;
    else 
        ret = false;
    STDestroy(&st); // 销毁栈(必须执行,避免内存泄漏)
    return ret;
}

**Tip:**代码中用到的 StackEmpty(&st) 等栈操作函数,前文已完成实现。答题时,只需将这些函数复制到当前代码中,即可直接调用,无需重复编写。

(四)边界场景处理

**1、**字符串以右括号开头(栈空直接返回无效);

**2、**左右括号类型不匹配【 如 :(] 】;

**3、**单侧括号(仅左括号或仅右括号)。

**4、**遍历结束后栈非空【左括号未完全闭合,如 :'(' 】;

相关推荐
Vic101011 小时前
链表算法三道
java·数据结构·算法·链表
琢磨先生David2 小时前
Java每日一题
数据结构·算法·leetcode
im_AMBER2 小时前
Leetcode 125 验证回文串 | 判断子序列
数据结构·学习·算法·leetcode
List<String> error_P2 小时前
蓝桥杯高频考点练习:模拟问题“球队比分类”
数据结构·python·算法·模拟·球队比分
daxi1502 小时前
C语言从入门到进阶——第8讲:VS实用调试技巧
c语言·开发语言·c++·算法·蓝桥杯
m0_531237172 小时前
C语言-数组
c语言·开发语言·算法
2401_876907522 小时前
Type-C连接器的常见故障和解决方法
c语言·开发语言
宇木灵2 小时前
C语言基础-四、函数
c语言·开发语言·前端·学习
We་ct2 小时前
LeetCode 114. 二叉树展开为链表:详细解题思路与 TS 实现
前端·数据结构·算法·leetcode·链表·typescript