嵌入式 - 数据结构与算法:(1-8)数据结构 - 栈(Stack)

上一篇 下一篇
顺序表和链表的对比

目 录

  • 栈(Stack)
    • 1)栈的定义与特点
    • 2)栈的基本操作
    • [3)C语言实现(顺序栈,Sequential Stack)](#3)C语言实现(顺序栈,Sequential Stack))
      • [3.1)头文件 Stack.h](#3.1)头文件 Stack.h)
      • [3.2)实现文件 Stack.c](#3.2)实现文件 Stack.c)
      • [3.3)测试文件 main.c](#3.3)测试文件 main.c)
      • 3.4)关键设计说明
    • [4)C语言实现(链栈,Linked stack)](#4)C语言实现(链栈,Linked stack))
      • 4.1)链栈的基本结构
      • 4.2)常用操作
        • [① 初始化 StackInit](#① 初始化 StackInit)
        • [② 入栈 StackPush(单链表的头插法)](#② 入栈 StackPush(单链表的头插法))
        • [③ 出栈并返回栈顶值 StackPop(头删法)](#③ 出栈并返回栈顶值 StackPop(头删法))
        • [④ 判空 StackEmpty](#④ 判空 StackEmpty)
        • [⑤ 获取大小 StackSize](#⑤ 获取大小 StackSize)
        • [⑥ 销毁 StackDestroy](#⑥ 销毁 StackDestroy)
      • 4.3)测试代码示例
      • 4.4)注意事项
    • [5)顺序栈 vs 链栈](#5)顺序栈 vs 链栈)

栈(Stack)

1)栈的定义与特点

栈是一种特殊的线性表,其限定只能在 表尾(栈顶)进行插入或删除操作。

  • 栈顶(Top):进行插入和删除操作的一端(表尾端)
  • 栈底(Bottom):另一端,通常不进行操作(表头端)
  • 空栈:不含任何元素的栈(空表)

核心特性:后进先出(LIFO - Last In First Out),因此栈又称为后进先出的线性表。

  • 最后入栈的元素最先出栈
  • 类比:弹夹、羽毛球筒、函数调用栈

举例: 假设有 S = ( a 1 , a 2 , . . . , a n ) S=(a_1,a_2,...,a_n) S=(a1,a2,...,an),则称 a 1 a_1 a1 为栈底元素, a n a_n an 为栈顶元素。栈中元素按 a 1 → a n a_1→a_n a1→an 的次序进栈,退栈的第一个元素应为栈顶元素 a n a_n an 。

既然栈也是线性表,那么它也应具有顺序存储结构和链式存储结构,也就是 顺序栈 和 链栈 。

2)栈的基本操作

操作 说明
InitStack() 初始化栈
DestroyStack() 销毁栈
Push() 入栈(压栈)
Pop() 出栈(弹栈)
Top() 获取栈顶元素
IsEmpty() 判断栈是否为空
Size() 获取栈中元素个数

3)C语言实现(顺序栈,Sequential Stack)

顺序栈和顺序表一样,本质上也是个大数组

顺序栈的基本结构:

c 复制代码
typedef int STDataType;	// 定义栈中元素类型(可根据需要修改)

/* 栈结构定义 */
typedef struct Stack {
    STDataType* data;	// 动态数组
    int top;        	// 栈顶索引(指向栈顶元素的下一个位置)
    int capacity;    	// 当前容量(MAXSIZE)
} Stack;

结构体中元素主要有三个:①动态数组、②栈顶索引(一开始是0)、③当前容量

访问某个元素应该用如下代码:

c 复制代码
Stack s;
s.data[...]

3.1)头文件 Stack.h

c 复制代码
#ifndef STACK_H
#define STACK_H

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

/* 定义栈中元素类型(可根据需要修改) */
typedef int STDataType;

/* 栈结构定义 */
typedef struct Stack {
    STDataType* data;  	// 动态数组
    int top;          	// 栈顶索引(指向栈顶元素的下一个位置,也可以指向当前栈顶元素,只是后续出栈压栈改一下就行)
    int capacity;     	// 当前容量
} Stack;

/* 函数声明 */
void StackInit(Stack* ps);                    // 初始化
void StackDestroy(Stack* ps);                 // 销毁
void StackPush(Stack* ps, STDataType x);      // 入栈
STDataType StackPop(Stack* ps);  			  // 出栈,并返回弹出的元素
bool StackEmpty(Stack* ps);                   // 判断是否为空
int StackSize(Stack* ps);                     // 获取元素个数

#endif

3.2)实现文件 Stack.c

具体会给出各个函数是如何实现的

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

/* 检查并扩容 */
static void CheckCapacity(Stack* ps) {
    if (ps->top == ps->capacity) {
        int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        STDataType* tmp = (STDataType*)realloc(ps->data, 
                                             newCapacity * sizeof(STDataType));
        if (tmp == NULL) {
            perror("realloc failed");
            exit(-1);
        }
        ps->data = tmp;
        ps->capacity = newCapacity;
    }
}

/* 初始化栈 */
void StackInit(Stack* ps) {
    if (ps == NULL) return;
    
    ps->data = NULL;
    ps->top = 0;		// 初始化栈顶索引为0(此时0索引处还没有值)
    ps->capacity = 0;
}

/* 销毁栈 */
void StackDestroy(Stack* ps) {
    if (ps == NULL) return;
    
    free(ps->data);
    ps->data = NULL;
    ps->top = 0;
    ps->capacity = 0;
}

/* 入栈 */
void StackPush(Stack* ps, STDataType x) {
    if (ps == NULL) return;
    
    CheckCapacity(ps);
    ps->data[ps->top] = x;	// 由于栈顶索引代表栈顶的下一个(未来)元素,所以这里直接复制给第top个元素
    ps->top++;				// 栈顶索引+1
}

/* 出栈(获取栈顶元素,并将栈顶索引-1) */
STDataType StackPop(Stack* ps) {
    if (ps == NULL || StackEmpty(ps)) {
        fprintf(stderr, "Stack is empty! Cannot pop.\n");
        exit(-1); // 或使用 assert, 或返回特殊值(需约定)
    }
    ps->top--;
    return ps->data[ps->top];
}

/* 判断栈是否为空 */
bool StackEmpty(Stack* ps) {
    if (ps == NULL) return true;
    return ps->top == 0;
}

/* 获取栈中元素个数 */
int StackSize(Stack* ps) {
    if (ps == NULL) return 0;
    return ps->top;
}

3.3)测试文件 main.c

c 复制代码
#include "sequential_stack.h"

void TestStack() {
    Stack s;
    StackInit(&s);
    
    printf("=== 栈测试开始 ===\n");
    
    /* 测试空栈 */
    printf("栈是否为空: %s\n", StackEmpty(&s) ? "是" : "否");
    printf("栈大小: %d\n", StackSize(&s));
    
    /* 入栈测试 */
    printf("\n--- 入栈操作 ---\n");
    for (int i = 1; i <= 5; i++) {
        StackPush(&s, i * 10);
        printf("入栈 %d, 栈顶元素: %d, 栈大小: %d\n", 
               i * 10, s.data[s.top-1], StackSize(&s));
    }
    
    // 获取栈顶元素就是:s.data[s.top-1]
    
    /* 出栈测试 */
    printf("\n--- 出栈操作 ---\n");
    while (!StackEmpty(&s)) {
        int val = StackPop(&s);
		printf("出栈 %d\n", val);
        printf("剩余大小: %d\n", StackSize(&s));
    }
    
    /* 再次测试空栈 */
    printf("\n栈是否为空: %s\n", StackEmpty(&s) ? "是" : "否");
    
    /* 销毁栈 */
    StackDestroy(&s);
    printf("\n=== 栈测试结束 ===\n");
}

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

运行结果为:

复制代码
=== 栈测试开始 ===
栈是否为空: 是
栈大小: 0

--- 入栈操作 ---
入栈 10, 栈顶元素: 10, 栈大小: 1
入栈 20, 栈顶元素: 20, 栈大小: 2
入栈 30, 栈顶元素: 30, 栈大小: 3
入栈 40, 栈顶元素: 40, 栈大小: 4
入栈 50, 栈顶元素: 50, 栈大小: 5

--- 出栈操作 ---
出栈 50
剩余大小: 4
出栈 40
剩余大小: 3
出栈 30
剩余大小: 2
出栈 20
剩余大小: 1
出栈 10
剩余大小: 0

栈是否为空: 是

=== 栈测试结束 ===

3.4)关键设计说明

  1. 动态扩容

    • 初始容量为0,首次入栈分配4个元素空间

    • 空间不足时自动扩容为原来的2倍

    • 使用 realloc 实现内存重分配

  2. 栈顶指针设计

    • top 指向栈顶元素的下一个位置

    • 空栈时 top = 0

    • 栈顶元素为 data[top - 1]

  3. 错误处理

    • 对空指针进行检查

    • 空栈时调用 StackTop() 会报错退出

    • 内存分配失败时程序终止

  4. 时间复杂度

    • 所有基本操作:O(1)(均摊时间复杂度)

    • 空间复杂度:O(n)

4)C语言实现(链栈,Linked stack)

链栈 = 头插头删的单链表

一般将单链表中的首元节点当作是栈顶(如果有哨兵头节点,那就是其下一个节点是栈顶;如果无哨兵头节点,那第一个节点就是栈顶)

核心思想用单链表实现栈,头插 + 头删 = 栈顶操作(所有操作都在头部进行)。

4.1)链栈的基本结构

节点定义:

c 复制代码
typedef int STDataType;  // 数据类型可泛化

typedef struct StackNode {
    STDataType data;          // 数据域
    struct StackNode* next;   // 指针域
} StackNode;

栈结构(推荐方案:带头指针 + 计数器):

c 复制代码
typedef struct LinkStack {
    StackNode* top;  // 指向栈顶节点(即链表头)
    int size;        // 元素个数(可选,方便获取大小)
} LinkStack;

4.2)常用操作

这里没有加入哨兵头节点

① 初始化 StackInit
c 复制代码
void StackInit(LinkStack* s) {
    s->top = NULL;
    s->size = 0;
}
② 入栈 StackPush(单链表的头插法)
c 复制代码
void StackPush(LinkStack* s, STDataType x) {
    StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
    if (!newNode) exit(-1);  // 内存分配失败
    
    newNode->data = x;
    newNode->next = s->top;  // 新节点指向原栈顶
    s->top = newNode;        // 更新栈顶为新节点
    s->size++;
}
③ 出栈并返回栈顶值 StackPop(头删法)
c 复制代码
STDataType StackPop(LinkStack* s) {
    if (StackEmpty(s)) {
        fprintf(stderr, "Error: Pop from empty stack!\n");
        exit(-1);
    }
    
    StackNode* del = s->top;
    STDataType val = del->data;
    
    s->top = del->next;  // 栈顶下移
    free(del);           // 释放内存
    s->size--;
    
    return val;
}
④ 判空 StackEmpty
c 复制代码
bool StackEmpty(LinkStack* s) {
    return s->top == NULL;
}
⑤ 获取大小 StackSize
c 复制代码
int StackSize(LinkStack* s) {
    return s->size;  // 若没存 size,需遍历链表(不推荐)
}
⑥ 销毁 StackDestroy
c 复制代码
void StackDestroy(LinkStack* s) {
    while (!StackEmpty(s)) {
        StackPop(s);  // 复用出栈逻辑
    }
    // s->top 已为 NULL,s->size 为 0
}

4.3)测试代码示例

linked_stack.h

c 复制代码
#ifndef LINKED_STACK_H
#define LINKED_STACK_H

#include <stdbool.h>

// 定义栈元素类型(可修改为其他类型)
typedef int STDataType;

// 链栈节点结构
typedef struct StackNode {
    STDataType data;
    struct StackNode* next;
} StackNode;

// 链栈结构:仅需指向栈顶的指针 + 元素个数(可选但推荐)
typedef struct LinkStack {
    StackNode* top;  // 指向栈顶节点(即链表头)
    int size;        // 当前元素个数
} LinkStack;

// 函数声明
void StackInit(LinkStack* s);
void StackDestroy(LinkStack* s);
void StackPush(LinkStack* s, STDataType x);
STDataType StackPop(LinkStack* s);
bool StackEmpty(LinkStack* s);
int StackSize(LinkStack* s);

#endif // LINKED_STACK_H

linked_stack.c

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

void StackInit(LinkStack* s) {
    if (!s) return;
    s->top = NULL;
    s->size = 0;
}

void StackDestroy(LinkStack* s) {
    if (!s) return;
    while (!StackEmpty(s)) {
        StackPop(s);  // 复用出栈逻辑释放内存
    }
    // 此时 s->top == NULL, s->size == 0
}

void StackPush(LinkStack* s, STDataType x) {
    if (!s) return;
    
    StackNode* newNode = (StackNode*)malloc(sizeof(StackNode));
    if (!newNode) {
        fprintf(stderr, "Error: malloc failed in StackPush!\n");
        exit(-1);
    }
    
    newNode->data = x;
    newNode->next = s->top;  // 新节点指向原栈顶
    s->top = newNode;        // 更新栈顶
    s->size++;
}

STDataType StackPop(LinkStack* s) {
    if (!s || StackEmpty(s)) {
        fprintf(stderr, "Error: Pop from empty stack!\n");
        exit(-1);
    }
    
    StackNode* delNode = s->top;
    STDataType val = delNode->data;
    
    s->top = delNode->next;  // 栈顶下移
    free(delNode);           // 释放内存
    s->size--;
    
    return val;
}

bool StackEmpty(LinkStack* s) {
    return s && s->top == NULL;
}

int StackSize(LinkStack* s) {
    return s ? s->size : 0;
}

main.c

c 复制代码
#include <stdio.h>
#include "linked_stack.h"

int main() {
    LinkStack s;
    StackInit(&s);

    printf("=== 链栈测试开始 ===\n");

    // 测试空栈
    printf("栈是否为空: %s\n", StackEmpty(&s) ? "是" : "否");
    printf("栈大小: %d\n", StackSize(&s));

    // 入栈测试(并直接访问栈顶元素)
    printf("\n--- 入栈操作 ---\n");
    for (int i = 1; i <= 5; i++) {
        StackPush(&s, i * 10);
        // 直接通过 s.top->data 获取栈顶(因刚入栈,非空)
        printf("入栈 %d, 栈顶元素: %d, 栈大小: %d\n",
               i * 10, s.top->data, s.size);
    }

    // 出栈测试
    printf("\n--- 出栈操作 ---\n");
    while (!StackEmpty(&s)) {
        int val = StackPop(&s);
        printf("出栈 %d, 剩余大小: %d\n", val, s.size);
        // 注意:出栈后若栈非空,仍可访问 s.top->data
    }

    // 再次测试空栈
    printf("\n栈是否为空: %s\n", StackEmpty(&s) ? "是" : "否");

    // 销毁栈
    StackDestroy(&s);
    printf("\n=== 链栈测试结束 ===\n");

    return 0;
}

运行结果如下:

复制代码
=== 链栈测试开始 ===
栈是否为空: 是
栈大小: 0

--- 入栈操作 ---
入栈 10, 栈顶元素: 10, 栈大小: 1
入栈 20, 栈顶元素: 20, 栈大小: 2
入栈 30, 栈顶元素: 30, 栈大小: 3
入栈 40, 栈顶元素: 40, 栈大小: 4
入栈 50, 栈顶元素: 50, 栈大小: 5

--- 出栈操作 ---
出栈 50, 剩余大小: 4
出栈 40, 剩余大小: 3
出栈 30, 剩余大小: 2
出栈 20, 剩余大小: 1
出栈 10, 剩余大小: 0

栈是否为空: 是

=== 链栈测试结束 ===

关键技巧 :在测试中可直接用 s.top->data 获取栈顶(因已知非空)

4.4)注意事项

  1. 内存管理
    • 每次 Pushmalloc,每次 Popfree
    • 忘记 free → 内存泄漏!
  2. 空栈保护
    • Pop 前必须检查是否为空,否则 s->top->data 会崩溃
  3. 封装性取舍
    • 测试时可直接访问 s.top->data
    • 正式项目建议封装 StackTop() 函数
  4. 性能对比
    • 链栈每次操作涉及内存分配/释放,实际速度可能慢于顺序栈
    • 但胜在空间灵活

5)顺序栈 vs 链栈

对比项 顺序栈(数组) 链栈(链表)
内存分配 静态/需扩容 动态申请
栈满问题 可能溢出 不会溢出(除非内存耗尽)
空间效率 可能浪费(预分配过大) 按需分配
时间复杂度 Push/Pop: O(1) Push/Pop: O(1)
扩容 需要重新分配内存 无需扩容
实现难度 简单 稍复杂(需处理指针)

推荐:一般情况下优先使用顺序栈,除非有特殊需求(如频繁的大规模内存分配限制)。


相关推荐
北顾笙9801 小时前
day41-数据结构力扣
数据结构·算法·leetcode
凯瑟琳.奥古斯特1 小时前
懒加载技巧优化栈增减操作(力扣3629)
开发语言·数据结构·算法
wdfk_prog13 小时前
正常关闭虚拟机时,不要点“关机”,而要点“关闭客户机”
linux·c语言·网络·ide·vscode
流年如夢13 小时前
单链表进阶版 -->双向链表
数据结构·链表
流年如夢14 小时前
单链表 -->增、删、查、改等详细操作
c语言·数据结构
handler0117 小时前
【算法模板】最小生成树:稠密图选 Prim,稀疏图选 Kruskal
c语言·数据结构·c++·算法
此生决int18 小时前
快速复习之数据结构篇——栈和队列
数据结构·c++
昵称小白18 小时前
子串专题部分
数据结构·算法·哈希算法
怀庆同学18 小时前
C语言基础-单链表
c语言·开发语言