数据结构:顺序栈与链栈的原理、实现及应用

发布于:2025-09-04 ⋅ 阅读:(23) ⋅ 点赞:(0)

数据结构详解:顺序栈与链栈的原理、实现及应用

1. 引言:栈的核心概念

栈(Stack)是一种重要的线性数据结构,它遵循后进先出(Last In First Out, LIFO)的原则。这意味着最后一个被添加到栈中的元素将是第一个被移除的元素。栈的这种特性使其在多种计算场景中非常有用,例如函数调用、表达式求值和括号匹配等。
栈的基本操作包括:

  • Push(入栈):在栈顶添加一个元素
  • Pop(出栈):移除栈顶元素并返回其值
  • Peek/Top(查看栈顶):返回栈顶元素但不移除它
  • isEmpty(判空):检查栈是否为空
    在这篇技术文章中,我们将详细探讨栈的两种主要实现方式:顺序栈(基于数组)和链栈(基于链表),分析它们的优缺点,并提供实际代码示例和应用场景。

2. 顺序栈:数组实现

2.1 顺序栈的结构与特点

顺序栈使用数组作为底层存储结构,这意味着它需要一块连续的内存空间。顺序栈通常包含两个主要部分:一个用于存储元素的数组和一个用于跟踪栈顶位置的整型变量(通常称为"top"或"栈顶指针")。
顺序栈的主要特点:

  • 内存连续,访问速度快
  • 需要预先指定栈的最大容量
  • 当栈满时无法再添加新元素(除非动态扩容)
  • 操作时间复杂度为O(1)

2.2 顺序栈的实现代码(C语言)

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

#define MAX_SIZE 100 // 定义栈的最大容量

typedef struct {
    int data[MAX_SIZE]; // 存储栈元素的数组
    int top;            // 栈顶指针
} SeqStack;

// 初始化栈
void InitStack(SeqStack *S) {
    S->top = -1; // 初始化为-1表示空栈
}

// 判断栈是否为空
int IsEmpty(SeqStack *S) {
    return (S->top == -1);
}

// 判断栈是否已满
int IsFull(SeqStack *S) {
    return (S->top == MAX_SIZE - 1);
}

// 入栈操作
int Push(SeqStack *S, int value) {
    if (IsFull(S)) {
        printf("Stack is full! Push operation failed.\n");
        return 0;
    }
    S->data[++(S->top)] = value; // 栈顶指针先加1,再将元素放入栈顶位置
    return 1;
}

// 出栈操作
int Pop(SeqStack *S, int *value) {
    if (IsEmpty(S)) {
        printf("Stack is empty! Pop operation failed.\n");
        return 0;
    }
    *value = S->data[(S->top)--]; // 先取出栈顶元素,再将栈顶指针减1
    return 1;
}

// 获取栈顶元素(但不删除)
int GetTop(SeqStack *S, int *value) {
    if (IsEmpty(S)) {
        printf("Stack is empty!\n");
        return 0;
    }
    *value = S->data[S->top];
    return 1;
}

// 打印栈中的所有元素
void PrintStack(SeqStack *S) {
    if (IsEmpty(S)) {
        printf("Stack is empty!\n");
        return;
    }
    
    printf("Stack elements (from bottom to top): ");
    for (int i = 0; i <= S->top; i++) {
        printf("%d ", S->data[i]);
    }
    printf("\n");
}

2.3 顺序栈的优缺点分析

优点:

  1. 访问速度快:由于内存连续,CPU缓存友好,访问效率高
  2. 实现简单:代码逻辑直接,易于理解和实现
  3. 内存开销小:只需要一个数组和一个整型变量,无需额外指针开销
    缺点:
  4. 固定容量:需要预先定义栈的大小,可能导致空间浪费或栈溢出
  5. 扩容困难:当栈满时,扩容需要创建新数组并复制所有元素,时间复杂度为O(n)
  6. 不灵活:难以适应动态变化的数据规模

3. 链栈:链表实现

3.1 链栈的结构与特点

链栈使用链表作为底层存储结构,每个元素包含数据部分和指向下一个节点的指针。链栈通常将链表的头部作为栈顶,这样入栈和出栈操作可以直接在链表头部进行,时间复杂度为O(1)。
链栈的主要特点:

  • 使用离散的内存空间,通过指针连接
  • 无需预先指定容量,可以动态增长
  • 只有当内存耗尽时才会出现"栈满"情况
  • 每个元素需要额外空间存储指针

3.2 链栈的实现代码(C语言)

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

// 定义栈节点结构
typedef struct StackNode {
    int data;               // 数据域
    struct StackNode *next; // 指针域
} StackNode;

// 定义链栈结构
typedef struct {
    StackNode *top; // 栈顶指针
} LinkStack;

// 初始化栈
void InitLinkStack(LinkStack *S) {
    S->top = NULL;
}

// 判断栈是否为空
int IsEmpty(LinkStack *S) {
    return (S->top == NULL);
}

// 入栈操作
void Push(LinkStack *S, int value) {
    // 创建新节点
    StackNode *newNode = (StackNode *)malloc(sizeof(StackNode));
    if (newNode == NULL) {
        printf("Memory allocation failed! Push operation failed.\n");
        return;
    }
    
    newNode->data = value;
    newNode->next = S->top; // 新节点指向原栈顶
    S->top = newNode;       // 更新栈顶指针为新节点
}

// 出栈操作
int Pop(LinkStack *S, int *value) {
    if (IsEmpty(S)) {
        printf("Stack is empty! Pop operation failed.\n");
        return 0;
    }
    
    StackNode *temp = S->top; // 临时保存栈顶节点
    *value = temp->data;
    S->top = S->top->next; // 更新栈顶指针为下一个节点
    free(temp);            // 释放原栈顶节点的内存
    return 1;
}

// 获取栈顶元素(但不删除)
int GetTop(LinkStack *S, int *value) {
    if (IsEmpty(S)) {
        printf("Stack is empty!\n");
        return 0;
    }
    
    *value = S->top->data;
    return 1;
}

// 打印栈中的所有元素
void PrintStack(LinkStack *S) {
    if (IsEmpty(S)) {
        printf("Stack is empty!\n");
        return;
    }
    
    printf("Stack elements (from top to bottom): ");
    StackNode *current = S->top;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

// 销毁栈,释放所有节点内存
void DestroyStack(LinkStack *S) {
    StackNode *current = S->top;
    while (current != NULL) {
        StackNode *temp = current;
        current = current->next;
        free(temp);
    }
    S->top = NULL;
}

3.3 链栈的优缺点分析

优点:

  1. 动态大小:无需预先指定栈的大小,可以随需求动态增长
  2. 内存高效:只分配实际需要的空间,没有容量浪费
  3. 灵活性:适合数据规模动态变化较大的场景
    缺点:
  4. 额外内存开销:每个节点需要额外空间存储指针
  5. 访问速度稍慢:由于内存不连续,CPU缓存不友好
  6. 内存管理复杂:需要手动管理内存分配和释放,容易导致内存泄漏如果处理不当

4. 顺序栈与链栈的对比

为了更清晰地理解两种实现的差异,下表列出了顺序栈和链栈的主要特性对比:

特性 顺序栈 链栈
存储方式 连续的内存空间(数组) 离散的内存空间(链表)
容量 固定,需预先定义 动态增长,受限于可用内存
空间效率 可能有浪费(数组固定大小) 无浪费(按需分配)
时间效率 所有操作 O(1) 所有操作 O(1)
内存管理 简单,系统自动回收 需手动管理节点内存的申请和释放
适用场景 数据规模已知或变化不大的场景 数据规模动态变化较大的场景

选择建议:

  • 如果数据规模已知且变化不大,或者对性能要求极高,优先选择顺序栈
  • 如果数据规模变化较大无法预估最大容量,优先选择链栈

5. 栈的应用实例

例题名称 核心知识点 难度
括号匹配 栈的基本操作、LIFO特性
表达式求值 栈管理运算符优先级、后缀表达式 ⭐⭐
模拟浏览器前进后退 双栈协作、状态管理 ⭐⭐
递归的非递归实现 栈模拟函数调用栈 ⭐⭐
行编辑程序(含退格) 栈处理输入、删除逻辑 ⭐⭐

📝 5.1 括号匹配问题

问题描述:检查一个字符串中的括号是否正确匹配,有效的字符串需满足:左括号必须用相同类型的右括号闭合,且左括号必须以正确的顺序闭合。

解决思路:遍历字符串,遇到左括号就入栈;遇到右括号时,检查栈顶的左括号是否与之匹配。匹配则弹出栈顶,否则无效。最后栈应为空。

代码实现(C语言,顺序栈):

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

#define MAX_SIZE 100

typedef struct {
    char data[MAX_SIZE];
    int top;
} SeqStack;

void initStack(SeqStack *s) { s->top = -1; }
bool isEmpty(SeqStack *s) { return s->top == -1; }
bool isFull(SeqStack *s) { return s->top == MAX_SIZE - 1; }

bool push(SeqStack *s, char c) {
    if (isFull(s)) return false;
    s->data[++(s->top)] = c;
    return true;
}

bool pop(SeqStack *s, char *c) {
    if (isEmpty(s)) return false;
    *c = s->data[(s->top)--];
    return true;
}

bool peek(SeqStack *s, char *c) {
    if (isEmpty(s)) return false;
    *c = s->data[s->top];
    return true;
}

bool isValid(char* s) {
    SeqStack stack;
    initStack(&stack);
    char topChar;

    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
            push(&stack, s[i]);
        } else if (s[i] == ')' || s[i] == ']' || s[i] == '}') {
            if (!peek(&stack, &topChar)) return false; // 栈空,有右括号无左括号匹配

            if ((s[i] == ')' && topChar == '(') ||
                (s[i] == ']' && topChar == '[') ||
                (s[i] == '}' && topChar == '{')) {
                pop(&stack, &topChar);
            } else {
                return false; // 括号类型不匹配
            }
        }
    }
    return isEmpty(&stack); // 检查是否所有左括号都被匹配
}

int main() {
    char expr[] = "({[]})";
    printf("%s is %s\n", expr, isValid(expr) ? "valid" : "invalid");
    return 0;
}

🧮 5.2 表达式求值

问题描述:计算算术表达式的值,例如 4 + 2 * 3 - 10 / (7 - 5)

解决思路:使用两个栈,一个存放操作数,一个存放运算符。根据运算符的优先级决定计算顺序。

代码概要(C语言):

// 此处假设已定义顺序栈结构 SeqStack 及其基本操作

int getPriority(char op) {
    switch(op) {
        case '+': case '-': return 1;
        case '*': case '/': return 2;
        default: return 0;
    }
}

int calculate(int a, int b, char op) {
    switch(op) {
        case '+': return a + b;
        case '-': return a - b;
        case '*': return a * b;
        case '/': return a / b; // 注意除零错误
        default: return 0;
    }
}

int evalExpression(char* expr) {
    SeqStack opnd; // 操作数栈
    SeqStack optr; // 运算符栈
    initStack(&opnd);
    initStack(&optr);
    push(&optr, '#'); // 预设一个起始符

    int i = 0;
    char ch, topOp, arithmeticOp;
    int num, a, b;

    while (expr[i] != '\0' || !isEmpty(&optr)) {
        ch = expr[i];
        if (ch == ' ') { i++; continue; } // 跳过空格

        if (ch >= '0' && ch <= '9') { // 处理数字
            num = 0;
            while (expr[i] >= '0' && expr[i] <= '9') {
                num = num * 10 + (expr[i] - '0');
                i++;
            }
            push(&opnd, num);
        } else if (ch == '(') {
            push(&optr, '(');
            i++;
        } else if (ch == ')') {
            peek(&optr, &topOp);
            while (topOp != '(') {
                pop(&optr, &arithmeticOp);
                pop(&opnd, &b); pop(&opnd, &a);
                push(&opnd, calculate(a, b, arithmeticOp));
                peek(&optr, &topOp);
            }
            pop(&optr, &topOp); // 弹出左括号
            i++;
        } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
            peek(&optr, &topOp);
            while (getPriority(topOp) >= getPriority(ch)) {
                pop(&optr, &arithmeticOp);
                pop(&opnd, &b); pop(&opnd, &a);
                push(&opnd, calculate(a, b, arithmeticOp));
                peek(&optr, &topOp);
            }
            push(&optr, ch);
            i++;
        } else {
            i++; // 处理其他字符或结束
        }
    }

    pop(&opnd, &num); // 最终结果
    return num;
}
// 主函数中测试
int main() {
    char expr[] = "4 + 2 * 3 - 10 / (7 - 5)";
    printf("Result of %s is %d\n", expr, evalExpression(expr));
    return 0;
}

🌐 5.3 模拟浏览器前进后退功能

问题描述:使用两个栈模拟浏览器的前进后退功能。

解决思路:使用两个栈,backStack 存放后退的页面,forwardStack 存放前进的页面。访问新页面时压入 backStack 并清空 forwardStack;后退时从 backStack 弹出并压入 forwardStack;前进时从 forwardStack 弹出并压入 backStack

代码概要(C语言,链栈):

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

typedef struct PageNode {
    char url[100];
    struct PageNode* next;
} PageNode;

typedef struct {
    PageNode* top;
} LinkStack;

void initStack(LinkStack* s) { s->top = NULL; }
bool isEmpty(LinkStack* s) { return s->top == NULL; }

void push(LinkStack* s, const char* url) {
    PageNode* newNode = (PageNode*)malloc(sizeof(PageNode));
    strcpy(newNode->url, url);
    newNode->next = s->top;
    s->top = newNode;
}

bool pop(LinkStack* s, char* url) {
    if (isEmpty(s)) return false;
    PageNode* temp = s->top;
    strcpy(url, temp->url);
    s->top = s->top->next;
    free(temp);
    return true;
}

bool peek(LinkStack* s, char* url) {
    if (isEmpty(s)) return false;
    strcpy(url, s->top->url);
    return true;
}

// 模拟浏览器
LinkStack backStack, forwardStack;
void initBrowser() {
    initStack(&backStack);
    initStack(&forwardStack);
}

void visitUrl(const char* url) {
    push(&backStack, url);
    // 访问新页面时清空前进栈
    char tempUrl[100];
    while (pop(&forwardStack, tempUrl)) { /* 只是清空 */ }
    printf("Visited: %s\n", url);
}

void goBack() {
    char currentUrl[100], backUrl[100];
    if (pop(&backStack, currentUrl) && peek(&backStack, backUrl)) {
        push(&forwardStack, currentUrl);
        printf("Back to: %s\n", backUrl);
    } else {
        printf("Cannot go back.\n");
    }
}

void goForward() {
    char forwardUrl[100];
    if (pop(&forwardStack, forwardUrl)) {
        push(&backStack, forwardUrl);
        printf("Forward to: %s\n", forwardUrl);
    } else {
        printf("Cannot go forward.\n");
    }
}

int main() {
    initBrowser();

    visitUrl("www.homepage.com");
    visitUrl("www.news.com");
    visitUrl("www.mail.com");

    goBack();   // 回到 www.news.com
    goBack();   // 回到 www.homepage.com

    goForward(); // 前进到 www.news.com

    visitUrl("www.search.com"); // 此时前进栈被清空

    return 0;
}

🔄 5.4 递归的非递归实现

问题描述:使用栈模拟递归过程,例如计算阶乘。

解决思路:递归的本质是函数调用栈,我们可以用显式栈来模拟。

代码实现(C语言,顺序栈实现阶乘):

#include <stdio.h>

long factorialIterative(int n) {
    SeqStack stack; // 假设使用之前的顺序栈,存储类型为int
    initStack(&stack);
    long result = 1;

    if (n == 0 || n == 1) return 1;

    while (n > 1) {
        push(&stack, n);
        n--;
    }

    int current;
    while (!isEmpty(&stack)) {
        pop(&stack, &current);
        result *= current;
    }

    return result;
}

int main() {
    int n = 5;
    printf("Iterative %d! = %ld\n", n, factorialIterative(n));
    return 0;
}

⌨️ 5.5 行编辑程序

问题描述:输入若干串字符,遇到换行符 \n 则输出本行当前处理结果。输入以 # 结束。输入 @ 表示回退到本行行首,输入 `` 表示回退一格。

解决思路:使用栈存储当前行的字符。遇到普通字符入栈;遇到 `` 则出栈(相当于退格);遇到 @ 则清空栈(相当于清空当前行)。

代码概要(C语言,顺序栈):

#include <stdio.h>

void lineEditor() {
    SeqStack s;
    initStack(&s);
    char ch, temp;

    printf("Enter your text (end with #, use @ to clear line, use  to backspace):\n");

    while ((ch = getchar()) != '#') {
        if (ch == '\n') {
            // 输出当前行
            printf("\nLine output: ");
            // 逆序输出栈内容较为复杂,可以先用一个临时栈反转
            SeqStack tempStack;
            initStack(&tempStack);
            while (!isEmpty(&s)) {
                pop(&s, &temp);
                push(&tempStack, temp);
            }
            while (!isEmpty(&tempStack)) {
                pop(&tempStack, &temp);
                putchar(temp);
            }
            printf("\n");
            initStack(&s); // 清空栈准备下一行
        } else if (ch == '@') {
            initStack(&s); // 清空栈(回退到行首)
        } else if (ch == '') {
            pop(&s, &temp); // 出栈(回退一格)
        } else {
            if (!isFull(&s)) {
                push(&s, ch); // 普通字符入栈
            } else {
                printf("Stack is full!\n");
            }
        }
    }
}

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

5.1 括号匹配问题

问题描述:给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串,判断字符串中的括号是否匹配正确。
解决方案:使用栈来检查括号匹配

  • 遇到左括号时,将其压入栈中
  • 遇到右括号时,检查栈顶的左括号是否与之匹配
  • 最后检查栈是否为空
    代码实现
int isValid(char* s) {
    SeqStack stack;
    InitStack(&stack);
    int i = 0;
    char ch;

    while (s[i] != '\0') {
        if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
            Push(&stack, s[i]); // 遇到左括号,入栈
        } else {
            if (IsEmpty(&stack)) { // 遇到右括号但栈已空,不匹配
                return 0;
            }
            GetTop(&stack, &ch); // 获取栈顶元素
            if ((s[i] == ')' && ch == '(') ||
                (s[i] == ']' && ch == '[') ||
                (s[i] == '}' && ch == '{')) {
                Pop(&stack, &ch); // 匹配成功,弹出栈顶元素
            } else {
                return 0; // 不匹配
            }
        }
        i++;
    }
    return IsEmpty(&stack); // 最后栈空则有效,非空则无效
}

6. 总结与展望

通过本文的详细讲解,我们了解了:

  1. 栈是一种后进先出(LIFO)的线性数据结构
  2. 栈有两种主要实现方式:顺序栈(基于数组)和链栈(基于链表)
  3. 顺序栈和链栈各有优缺点,适用于不同场景
  4. 栈在计算机科学中有广泛的应用,如括号匹配、表达式求值和函数调用等