解码数据结构栈

栈的概念与特性

栈是线性结构的特殊形式,其设计初衷是解决 “数据需按特定顺序存取” 的场景(如函数调用、括号匹配),核心遵循 “后进先出”(LIFO,Last In First Out)原则,是计算机领域中最基础的数据结构之一。
image

关键定义与术语

术语 定义
栈(Stack) 仅允许在一端进行数据插入(入栈)和删除(出栈)的线性表
栈底(Bottom) 栈的封闭端,数据一旦入栈后,只有栈顶元素全部出栈后才能被访问
栈顶(Top) 栈的开放端,所有入栈、出栈操作均在此端进行,栈顶元素始终是 “最新入栈” 的数据
空栈 不包含任何元素的栈,此时栈顶指针(或下标)指向栈底外侧(如顺序栈 Top=-1)

核心操作

栈的基本操作需满足 “仅操作栈顶” 的约束,所有操作的时间复杂度均为O(1)(遍历除外,O (n)),具体如下:

操作名称 功能描述 关键约束
初始化(Init) 创建空栈,并为栈的管理结构(如结构体)分配内存,初始化栈底、栈顶、容量等参数 确保内存分配成功(避免空指针)
入栈(Push) 将数据插入栈顶,更新栈顶指针(或下标) 入栈前需判满(仅顺序栈需判满)
出栈(Pop) 移除栈顶数据,返回该数据,更新栈顶指针(或下标) 出栈前需判空
判空(IsEmpty) 判断栈是否为空(如顺序栈 Top=-1,链式栈栈顶指针为 NULL)
判满(IsFull) 仅顺序栈需实现,判断栈顶是否达到数组容量上限(Top=Size-1) 链式栈无需判满(动态分配)
取栈顶(GetTop) 返回栈顶数据,但不删除该数据 需先判空
遍历(Print) 从栈底到栈顶输出所有元素
销毁(Destroy) 释放栈占用的内存(顺序栈释放数组 + 管理结构体,链式栈释放所有节点 + 栈顶指针) 避免内存泄漏

操作流程示例

以 “Blue→Green→Red” 入栈出栈为例:

image

栈的两种实现方式

栈作为线性表,可基于数组(顺序栈) 或单链表(链式栈) 实现。

顺序栈

  • 核心原理
    • 连续数组存储数据,数组下标 0 为栈底,栈顶随数据插入 / 删除动态移动(Top 初始化为 - 1,代表空栈)。
    • 需用结构体管理关键参数:栈底地址(数组指针)、栈容量(数组大小)、栈顶下标(Top)。
      image
  • 定义
typedef int DataType_t;
// 2. 定义顺序栈管理结构体
typedef struct SequenceStack {
    DataType_t *Bottom;  // 栈底地址(指向数组首元素)
    unsigned int Size;   // 栈容量(数组最大存储元素个数)
    int Top;             // 栈顶下标(空栈时为-1,栈满时为Size-1)
} SeqStack_t;
  • 初始化
SeqStack_t *SeqStack_Init(unsigned int size) {
    // 分配管理结构体内存
    SeqStack_t *Manager = (SeqStack_t *)malloc(sizeof(SeqStack_t));
    if (Manager == NULL) {
        printf("Memory allocation failed for Manager!\n");
        return NULL;
    }
    // 分配栈数组内存
    Manager->Bottom = (DataType_t *)malloc(size * sizeof(DataType_t));
    if (Manager->Bottom == NULL) {
        printf("Memory allocation failed for Stack Array!\n");
        free(Manager);  // 先释放管理结构体,避免内存泄漏
        return NULL;
    }
    // 初始化参数
    Manager->Size = size;
    Manager->Top = -1;  // 空栈标志
    return Manager;
}
  • 判空
bool SeqStack_IsEmpty(SeqStack_t *Manager) {
    return Manager->Top == -1;
}
  • 判满
bool SeqStack_IsFull(SeqStack_t *Manager) {
    return Manager->Top == Manager->Size - 1;
}
  • 入栈
bool SeqStack_Push(SeqStack_t *Manager, DataType_t data) {
    // 先判满
    if (SeqStack_IsFull(Manager)) {
        printf("Stack is full! Push failed.\n");
        return false;
    }
    Manager->Top++;          // 栈顶上移
    Manager->Bottom[Manager->Top] = data;  // 存入数据
    return true;
}
  • 出栈
DataType_t SeqStack_Pop(SeqStack_t *Manager) {
    // 先判空
    if (SeqStack_IsEmpty(Manager)) {
        printf("Stack is empty! Pop failed.\n");
        return 0;  // 此处0为默认值,实际可通过返回bool+输出参数优化
    }
    DataType_t topData = Manager->Bottom[Manager->Top];  // 保存栈顶数据
    Manager->Top--;  // 栈顶下移(逻辑删除,无需真删除数据)
    return topData;
}
  • 取栈顶元素
DataType_t SeqStack_GetTop(SeqStack_t *Manager) {
    if (SeqStack_IsEmpty(Manager)) {
        printf("Stack is empty! GetTop failed.\n");
        return 0;
    }
    return Manager->Bottom[Manager->Top];
}
  • 遍历栈(栈底到栈顶)
void SeqStack_Print(SeqStack_t *Manager) {
    if (SeqStack_IsEmpty(Manager)) {
        printf("Stack is empty!\n");
        return;
    }
    printf("Stack Elements (Bottom -> Top): ");
    for (int i = 0; i <= Manager->Top; i++) {
        printf("%d ", Manager->Bottom[i]);
    }
    printf("\n");
}
  • 销毁
// 二级指针销毁函数 避免野指针
void SeqStack_Destroy(SeqStack_t **Manager) {
    // 先判断二级指针和目标指针是否有效
    if (Manager == NULL || *Manager == NULL) {
        return;  // 避免对空指针解引用
    }

    // 释放内部数组
    if ((*Manager)->Bottom != NULL) {
        free((*Manager)->Bottom);
        (*Manager)->Bottom = NULL;
    }

    // 释放管理结构体
    free(*Manager);
    *Manager = NULL;  // 关键:直接将外部指针置为NULL
}

注:

  • 顺序栈的容量固定,若需动态扩容,可在判满后重新分配更大数组(如原容量 2 倍),拷贝原数据后释放旧数组。
  • 入栈 / 出栈仅操作栈顶,时间复杂度 O (1);遍历需遍历所有元素,时间复杂度 O (n)。

链式栈

  • 核心原理
    • 单链表存储数据,链表头部作为栈顶(头插法入栈、头删法出栈,操作更高效),无需固定容量(动态分配节点)。
    • 仅需一个栈顶指针(指向链表首节点),空栈时栈顶指针为 NULL(无需栈底指针,链表尾节点即为栈底)。
      image
  • 定义
typedef int DataType_t;
typedef struct LinkStackNode {
    DataType_t data;                // 节点数据
    struct LinkStackNode *next;     // 指向下一节点的指针
} LinkStackNode;
  • 初始化
LinkStackNode *LinkStack_Init() {
    return NULL;  // 空栈标志:栈顶指针为NULL
}
  • 判空
bool LinkStack_IsEmpty(LinkStackNode *top) {
    return top == NULL;
}
  • 入栈
// 入栈(头插法:新节点作为新栈顶)
LinkStackNode *LinkStack_Push(LinkStackNode *top, DataType_t data) {
    // 创建新节点
    LinkStackNode *newNode = (LinkStackNode *)malloc(sizeof(LinkStackNode));
    if (newNode == NULL) {
        printf("Memory allocation failed for new node!\n");
        return top;  // 入栈失败,返回原栈顶
    }
     // 新节点指向原栈顶
    newNode->data = data;
    newNode->next = top;
    // 新节点成为新栈顶
    return newNode;
}
  • 出栈
//  出栈(头删法:删除栈顶节点,返回新栈顶)
LinkStackNode *LinkStack_Pop(LinkStackNode *top, DataType_t *outData) {
    // 先判空
    if (LinkStack_IsEmpty(top)) {
        printf("Stack is empty! Pop failed.\n");
        *outData = 0;
        return top;
    }
    // 保存原栈顶节点和数据
    LinkStackNode *oldTop = top;
    *outData = oldTop->data;
    // 新栈顶为原栈顶的下一节点
    top = top->next;
    // 释放原栈顶节点
    free(oldTop);
    oldTop = NULL;
    return top;
}
  • 取栈顶元素
bool LinkStack_GetTop(LinkStackNode *top, DataType_t *outData) {
    if (LinkStack_IsEmpty(top)) {
        printf("Stack is empty! GetTop failed.\n");
        return false;
    }
    *outData = top->data;
    return true;
}
  • 遍历(从栈顶到栈底)
void LinkStack_Print(LinkStackNode *top) {
    if (LinkStack_IsEmpty(top)) {
        printf("Stack is empty!\n");
        return;
    }
    LinkStackNode *p = top;  // 辅助指针
    printf("Stack Elements (Top -> Bottom): ");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}
  • 销毁
LinkStackNode *LinkStack_Destroy(LinkStackNode *top) {
    LinkStackNode *p = top;
    while (p != NULL) {
        LinkStackNode *temp = p;  // 保存当前节点
        p = p->next;              // 移动到下一节点
        free(temp);               // 释放当前节点
        temp = NULL;
    }
    return NULL;  // 销毁后栈顶为NULL
}

顺序栈与链式栈的对比

对比维度 顺序栈(数组) 链式栈(单链表)
存储方式 连续内存空间(数组) 不连续内存空间(节点动态分配)
空间效率 可能浪费空间(数组容量固定,未用满) 无空间浪费(按需分配节点)
时间效率 入栈 / 出栈 O (1),扩容 O (n)(可选) 入栈 / 出栈 O (1)(头插 / 头删)
判满需求 必须判满(数组容量固定) 无需判满(动态扩展)
实现复杂度 较简单(数组操作) 较复杂(节点指针管理,需避免内存泄漏)
适用场景 已知数据规模、需高效存取的场景 数据规模不确定、需灵活扩展的场景

栈的典型应用

应用 1:十进制转十六进制(处理 0 值)

  • 核心逻辑
    • 原理:十进制数反复除以 16,余数作为十六进制位(逆序排列,需用栈存储余数,最后出栈即正序)。
    • 补充:若输入为 0,需单独处理(原 do-while 循环不执行,需手动入栈 '0')。

代码

void SeqStack_Dec2Hex(SeqStack_t *Manager, unsigned int Data) {
    // 处理Data=0的情况(否则do-while不执行,栈为空)
    if (Data == 0) {
        SeqStack_Push(Manager, '0');
    } else {
        int remainder;
        do {
            remainder = Data % 16;
            // 余数映射:0-9→'0'-'9',10-15→'A'-'F'
            if (remainder < 10) {
                SeqStack_Push(Manager, remainder + '0');
            } else {
                SeqStack_Push(Manager, remainder - 10 + 'A');
            }
            Data /= 16;
        } while (Data != 0);
    }

    // 出栈输出(栈中为逆序,出栈后为正序)
    printf("Hex Result: 0x");
    while (!SeqStack_IsEmpty(Manager)) {
        printf("%c", (char)SeqStack_Pop(Manager));
    }
    printf("\n");
}

// 测试:输入0→0x0;输入255→0xFF;输入10→0xA

应用 2:有效括号判断(支持多类型括号)

  • 核心逻辑
    需求:判断字符串中()[]{}是否配对(左括号入栈,右括号与栈顶匹配,最后栈需为空)。

代码

// 有效括号判断(支持()、[]、{})
bool SeqStack_IsValidBracket(SeqStack_t *Manager, const char *str) {
    if (str == NULL) return false;  // 空字符串无效

    const char *p = str;
    while (*p != '\0') {
        // 左括号:入栈
        if (*p == '(' || *p == '[' || *p == '{') {
            SeqStack_Push(Manager, *p);
        }
        // 右括号:判断匹配
        else if (*p == ')' || *p == ']' || *p == '}') {
            // 若栈空,右括号无匹配左括号→无效
            if (SeqStack_IsEmpty(Manager)) {
                return false;
            }
            // 取出栈顶左括号,判断是否匹配
            char topBracket = (char)SeqStack_Pop(Manager);
            if ((*p == ')' && topBracket != '(') ||
                (*p == ']' && topBracket != '[') ||
                (*p == '}' && topBracket != '{')) {
                return false;  // 类型不匹配
            }
        }
        // 非括号字符:忽略(若需求为“仅括号”,可返回false)
        p++;
    }

    // 遍历结束后,栈需为空(所有左括号均闭合)
    return SeqStack_IsEmpty(Manager);
}

// 测试:"()[]{}"→true;"([)]"→false;"({})"→true;"("→false

其他应用场景

  • 函数调用栈:C 语言中,函数调用时会创建 “栈帧”(存储返回地址、局部变量、寄存器值),函数执行完后栈帧出栈,回到调用处(遵循 LIFO)。

  • 表达式求值:将中缀表达式(如3+4*2)转为后缀表达式(3 4 2 * +),用栈处理运算符优先级,再通过栈计算后缀表达式结果。

  • 核心转换规则:

    • 遇到 操作数(如 3、a、123):直接加入后缀表达式(多字符操作数需连续扫描完整后整体加入,如 123 作为一个整体);
    • 遇到 左括号 (:直接压入运算符栈(标记 “括号开始”);
    • 遇到 右括号 ):从运算符栈弹出运算符,加入后缀表达式,直到弹出左括号 (为止(左括号弹出后不加入后缀,只作为 “括号结束” 的标志);
    • 遇到 其他运算符(+、-、×、/):按优先级压栈(乘除优先级 > 加减,优先级低的运算符要等栈里优先级高的弹完再压;若优先级相同或更低,需先弹出栈顶运算符加入后缀,直到栈空、栈顶是 ( 或当前运算符优先级更高,再压入当前运算符);
    • 遇到 表达式扫描结束:将运算符栈中剩余的所有运算符,从栈顶依次弹出并加入后缀表达式。
  • 核心计算规则:

    • 遇到 操作数(如 3、a、123):直接压入操作数栈(多字符操作数需连续扫描完整后整体入栈,如 123 作为一个整体);
    • 遇到 运算符(+、-、×、/):从操作数栈弹出两个操作数(先弹出的是右操作数,后弹出的是左操作数),用该运算符计算后,将结果压回操作数栈;
    • 遇到 表达式扫描结束:操作数栈中剩余的唯一元素即为最终计算结果(若栈中元素数量不为 1,说明表达式非法)。

    中缀转后缀

  • 函数代码

    // 中缀表达式转后缀表达式
    void infixToPostfix(char* infix, char* postfix) {
        int i, j;
        Stack* stack = createStack(strlen(infix));
        
        for (i = 0, j = 0; infix[i]; ++i) {
            // 如果是操作数,直接添加到后缀表达式
            if (isdigit(infix[i]) || infix[i] == '.') {
                postfix[j++] = infix[i];
            }
            // 如果是左括号,入栈
            else if (infix[i] == '(') {
                pushOperator(stack, infix[i]);
            }
            // 如果是右括号,弹出所有运算符直到遇到左括号
            else if (infix[i] == ')') {
                while (!isEmpty(stack) && peekOperator(stack) != '(') {
                    postfix[j++] = ' ';  // 用空格分隔不同的元素
                    postfix[j++] = popOperator(stack);
                }
                popOperator(stack);  // 弹出左括号,不加入后缀表达式
            }
            // 如果是运算符
            else if (isOperator(infix[i])) {
                postfix[j++] = ' ';  // 用空格分隔不同的元素
                // 弹出所有优先级大于等于当前运算符的运算符
                while (!isEmpty(stack) && precedence(peekOperator(stack)) >= precedence(infix[i])) {
                    postfix[j++] = popOperator(stack);
                    postfix[j++] = ' ';
                }
                pushOperator(stack, infix[i]);
            }
        }
        
        // 弹出栈中剩余的所有运算符
        while (!isEmpty(stack)) {
            postfix[j++] = ' ';
            postfix[j++] = popOperator(stack);
        }
        
        postfix[j] = '\0';
        free(stack->operatorItems);
        free(stack->numberItems);
        free(stack);
    }
    
    // 计算后缀表达式
    double evaluatePostfix(char* postfix) {
        Stack* stack = createStack(strlen(postfix));
        double num = 0, decimal = 0, decimalPlace = 0;
        int i;
        
        for (i = 0; postfix[i]; ++i) {
            // 如果是数字或小数点,解析为数字
            if (isdigit(postfix[i]) || postfix[i] == '.') {
                if (postfix[i] == '.') {
                    decimal = 1;
                    decimalPlace = 1;
                    continue;
                }
                
                if (decimal) {
                    num += (postfix[i] - '0') / pow(10, decimalPlace);
                    decimalPlace++;
                } else {
                    num = num * 10 + (postfix[i] - '0');
                }
            }
            // 如果是空格且之前有数字,将数字入栈
            else if (postfix[i] == ' ' && (isdigit(postfix[i-1]) || postfix[i-1] == '.')) {
                pushNumber(stack, num);
                num = 0;
                decimal = 0;
            }
            // 如果是运算符,弹出两个数字进行运算
            else if (isOperator(postfix[i])) {
                double val1 = popNumber(stack);
                double val2 = popNumber(stack);
                double result;
                
                switch (postfix[i]) {
                    case '+':
                        result = val2 + val1;
                        break;
                    case '-':
                        result = val2 - val1;
                        break;
                    case '*':
                        result = val2 * val1;
                        break;
                    case '/':
                        if (val1 == 0) {
                            printf("错误:除数不能为零!\n");
                            return NAN;
                        }
                        result = val2 / val1;
                        break;
                    case '^':
                        result = pow(val2, val1);
                        break;
                    default:
                        result = 0;
                }
                
                pushNumber(stack, result);
            }
        }
        
        double finalResult = popNumber(stack);
        free(stack->operatorItems);
        free(stack->numberItems);
        free(stack);
        return finalResult;
    }
    
  • 浏览器前进后退:用两个栈(forwardStackbackStack),浏览新页面时将当前页面压入backStack;点击后退时将backStack栈顶弹出,压入forwardStack并显示;点击前进时反之。

posted @ 2025-09-27 16:21  YouEmbedded  阅读(11)  评论(0)    收藏  举报