数据结构:栈和队列

📌目录


📚 一,栈和队列的定义与特点

栈和队列是两种特殊的线性表,它们在操作上具有严格的限制,广泛应用于算法设计、系统开发等领域。

📥 (一)栈的定义与特点

栈(Stack) 是一种限定仅在表尾进行插入和删除操作的线性表,其核心特点可概括为**"后进先出"(Last In First Out,LIFO)**。

  • 关键术语

    • 允许操作的一端称为栈顶(Top)
    • 另一端称为栈底(Bottom)
    • 插入元素的操作称为入栈(Push)
    • 删除元素的操作称为出栈(Pop)
    • 不含元素的栈称为空栈
  • 示例:叠放的盘子,只能从最上方取放,最后放入的盘子最先被取出。

  • 数学描述:设栈S=(a₁,a₂,...,aₙ),则a₁为栈底元素,aₙ为栈顶元素,操作顺序满足:入栈a₁→a₂→...→aₙ,出栈aₙ→...→a₂→a₁。

📤 (二)队列的定义与特点

队列(Queue) 是一种限定仅在表尾插入元素、表头删除元素的线性表,其核心特点为**"先进先出"(First In First Out,FIFO)**。

  • 关键术语

    • 表尾(插入端)称为队尾(Rear)
    • 表头(删除端)称为队头(Front)
    • 插入元素的操作称为入队(Enqueue)
    • 删除元素的操作称为出队(Dequeue)
    • 不含元素的队列称为空队列
  • 示例:排队购票,先到的人先购票离开,后到的人依次排队等待。

  • 数学描述:设队列Q=(a₁,a₂,...,aₙ),则a₁为队头元素,aₙ为队尾元素,操作顺序满足:入队a₁→a₂→...→aₙ,出队a₁→a₂→...→aₙ。

🌰 二,案例引入

场景1:栈的应用------浏览器历史记录

当你浏览网页时:

  • 点击"前进"按钮,会跳转到上一次关闭的页面(对应栈的入栈);
  • 点击"后退"按钮,会返回之前浏览的页面(对应栈的出栈)。
    浏览器通过栈结构记录访问顺序,确保"最后访问的页面最先被回溯"。

场景2:队列的应用------打印机任务调度

多台电脑向同一台打印机发送打印任务时:

  • 任务按提交顺序进入队列(入队);
  • 打印机按顺序处理任务(出队),先提交的任务先打印。
    队列保证了任务处理的公平性,避免"后提交的任务插队"。

这两个案例揭示了栈和队列的核心价值------通过限制操作顺序,简化特定场景下的问题处理逻辑。

📚 三,栈的表示与操作的实现

(一)栈的类型定义

栈的抽象数据类型(ADT)定义如下:

复制代码
ADT Stack {
    数据:
        栈中元素具有相同类型,相邻元素具有前驱和后继关系,仅允许在栈顶操作。
    
    操作:
        1. InitStack(&S):初始化栈S为空栈。
        2. StackEmpty(S):判断栈S是否为空,若空返回TRUE,否则返回FALSE。
        3. Push(&S, e):将元素e插入栈S的栈顶。
        4. Pop(&S, &e):删除栈S的栈顶元素,并用e返回其值。
        5. GetTop(S, &e):获取栈S的栈顶元素,并用e返回其值(不删除)。
        6. DestroyStack(&S):销毁栈S,释放占用的内存空间。
}

(二)顺序栈的表示与实现

顺序栈是栈的顺序存储结构,采用数组存储元素,用指针标记栈顶位置。

  • 存储表示(C语言):

    c 复制代码
    #define MAXSIZE 100  // 栈的最大容量
    typedef int ElemType;
    typedef struct {
        ElemType data[MAXSIZE];  // 存储栈元素的数组
        int top;                 // 栈顶指针(-1表示空栈,0~MAXSIZE-1表示栈顶位置)
    } SqStack;
  • 核心操作实现

    1. 初始化

      c 复制代码
      void InitStack(SqStack *S) {
          S->top = -1;  // 空栈标志
      }

      时间复杂度:O(1)

    2. 入栈

      c 复制代码
      int Push(SqStack *S, ElemType e) {
          if (S->top == MAXSIZE - 1)  // 栈满
              return 0;
          S->data[++S->top] = e;  // 栈顶指针先加1,再存入元素
          return 1;
      }

      时间复杂度:O(1)

    3. 出栈

      c 复制代码
      int Pop(SqStack *S, ElemType *e) {
          if (S->top == -1)  // 栈空
              return 0;
          *e = S->data[S->top--];  // 先取出栈顶元素,再将栈顶指针减1
          return 1;
      }

      时间复杂度:O(1)

    4. 取栈顶元素

      c 复制代码
      int GetTop(SqStack S, ElemType *e) {
          if (S.top == -1)  // 栈空
              return 0;
          *e = S.data[S.top];  // 仅读取,不修改栈顶指针
          return 1;
      }

      时间复杂度:O(1)

(三)链栈的表示与实现

链栈是栈的链式存储结构,采用单链表存储元素,头节点作为栈顶(便于插入和删除)。

  • 存储表示(C语言):

    c 复制代码
    typedef int ElemType;
    typedef struct StackNode {
        ElemType data;               // 数据域
        struct StackNode *next;      // 指针域(指向后继节点,即栈底方向)
    } StackNode, *LinkStack;

    注:链栈无栈满限制(除非内存不足),空栈标志为top == NULL

  • 核心操作实现

    1. 初始化

      c 复制代码
      void InitStack(LinkStack *top) {
          *top = NULL;  // 空栈,栈顶指针为NULL
      }

      时间复杂度:O(1)

    2. 入栈

      c 复制代码
      int Push(LinkStack *top, ElemType e) {
          StackNode *s = (StackNode*)malloc(sizeof(StackNode));  // 创建新节点
          if (!s) return 0;  // 内存分配失败
          s->data = e;
          s->next = *top;    // 新节点指向原栈顶
          *top = s;          // 栈顶指针更新为新节点
          return 1;
      }

      时间复杂度:O(1)

    3. 出栈

      c 复制代码
      int Pop(LinkStack *top, ElemType *e) {
          if (*top == NULL) return 0;  // 空栈
          StackNode *p = *top;         // 暂存栈顶节点
          *e = p->data;                // 取出数据
          *top = p->next;              // 栈顶指针下移
          free(p);                     // 释放原栈顶节点
          return 1;
      }

      时间复杂度:O(1)

  • 顺序栈 vs 链栈

    • 顺序栈适合已知最大容量的场景(如固定大小的缓存),实现简单、访问高效;
    • 链栈适合容量不确定的场景(如动态表达式求值),内存利用率更高。

🔄 四,栈与递归

(一)采用递归算法解决的问题

递归是指函数直接或间接调用自身的编程技巧,栈是递归实现的底层支撑。适合用递归解决的问题具有**"自相似性"**,即问题可分解为规模更小的同类子问题,例如:

  • 数学问题:阶乘计算(n! = n × (n-1)!)、斐波那契数列(F(n) = F(n-1) + F(n-2));
  • 数据结构问题:二叉树遍历(先序遍历 = 根节点 + 左子树先序 + 右子树先序)、汉诺塔问题。

(二)递归过程与递归工作栈

递归函数的执行过程依赖递归工作栈存储每次调用的状态,包括:

  1. 参数:函数的输入值(如阶乘中的n);
  2. 局部变量:函数内部定义的变量;
  3. 返回地址:函数执行完毕后需返回的调用处地址。

示例:计算3!的递归过程

  • 调用fact(3)时,将参数3、返回地址存入栈;
  • fact(3)调用fact(2),将参数2、返回地址存入栈;
  • fact(2)调用fact(1),将参数1、返回地址存入栈;
  • fact(1)调用fact(0),将参数0、返回地址存入栈;
  • fact(0)返回1,弹出栈顶(0的状态),计算1×1=1
  • 依次弹出栈中状态,最终计算3×2×1×1=6

栈的"后进先出"特性完美匹配递归的"最后调用先返回"逻辑。

(三)递归算法的效率分析

递归的优势是代码简洁,但存在潜在问题:

  • 时间效率:多次重复计算(如斐波那契数列的递归实现,时间复杂度O(2ⁿ));
  • 空间效率:递归深度过大会导致栈溢出(如计算10000!的递归调用可能超出栈容量)。

优化方向

  • 记忆化搜索(缓存子问题结果)减少重复计算;
  • 栈模拟递归(非递归实现)避免栈溢出。

(四)利用栈将递归转换为非递归的方法

核心思路:用人工定义的栈模拟递归工作栈,存储调用状态,步骤如下:

  1. 初始化栈,将初始参数入栈;
  2. 循环:弹出栈顶状态,处理当前逻辑;
  3. 若需继续递归,将子问题参数入栈;
  4. 直至栈空,完成所有计算。

示例:阶乘的非递归实现(栈模拟)

c 复制代码
int Fact(int n) {
    SqStack S;
    InitStack(&S);
    int result = 1;
    // 将所有参数入栈(从n到1)
    while (n > 0) {
        Push(&S, n);
        n--;
    }
    // 出栈计算乘积
    while (!StackEmpty(S)) {
        int e;
        Pop(&S, &e);
        result *= e;
    }
    return result;
}

📋 五,队列的表示和操作的实现

(一)队列的类型定义

队列的抽象数据类型(ADT)定义如下:

复制代码
ADT Queue {
    数据:
        队列中元素具有相同类型,相邻元素具有前驱和后继关系,仅允许在队尾插入、队头删除。
    
    操作:
        1. InitQueue(&Q):初始化队列Q为空队列。
        2. QueueEmpty(Q):判断队列Q是否为空,若空返回TRUE,否则返回FALSE。
        3. EnQueue(&Q, e):将元素e插入队列Q的队尾。
        4. DeQueue(&Q, &e):删除队列Q的队头元素,并用e返回其值。
        5. GetHead(Q, &e):获取队列Q的队头元素,并用e返回其值(不删除)。
        6. DestroyQueue(&Q):销毁队列Q,释放占用的内存空间。
}

(二)循环队列------队列的顺序表示与实现

顺序队列 用数组存储元素,队头指针front指向队头元素,队尾指针rear指向队尾元素的下一个位置。但普通顺序队列存在"假溢出"问题(数组未满但rear已达最大下标),循环队列通过将数组视为环形解决该问题。

  • 存储表示(C语言):

    c 复制代码
    #define MAXSIZE 100
    typedef int ElemType;
    typedef struct {
        ElemType data[MAXSIZE];  // 存储队列元素
        int front;               // 队头指针(初始0)
        int rear;                // 队尾指针(初始0)
    } SqQueue;
    • 队空标志:Q.front == Q.rear
    • 队满标志:(Q.rear + 1) % MAXSIZE == Q.front(牺牲一个位置区分空满)。
  • 核心操作实现

    1. 入队

      c 复制代码
      int EnQueue(SqQueue *Q, ElemType e) {
          if ((Q->rear + 1) % MAXSIZE == Q->front)
              return 0;  // 队满
          Q->data[Q->rear] = e;
          Q->rear = (Q->rear + 1) % MAXSIZE;  // 队尾指针循环后移
          return 1;
      }

      时间复杂度:O(1)

    2. 出队

      c 复制代码
      int DeQueue(SqQueue *Q, ElemType *e) {
          if (Q->front == Q->rear)
              return 0;  // 队空
          *e = Q->data[Q->front];
          Q->front = (Q->front + 1) % MAXSIZE;  // 队头指针循环后移
          return 1;
      }

      时间复杂度:O(1)

(三)链队------队列的链式表示与实现

链队用单链表存储元素,队头指针指向头节点,队尾指针指向尾节点(便于入队操作)。

  • 存储表示(C语言):

    c 复制代码
    typedef int ElemType;
    typedef struct QNode {
        ElemType data;
        struct QNode *next;
    } QNode, *QueuePtr;
    
    typedef struct {
        QueuePtr front;  // 队头指针(指向头节点)
        QueuePtr rear;   // 队尾指针(指向最后一个节点)
    } LinkQueue;
    • 队空标志:Q.front == Q.rear(均指向头节点)。
  • 核心操作实现

    1. 初始化

      c 复制代码
      void InitQueue(LinkQueue *Q) {
          Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));  // 创建头节点
          Q->front->next = NULL;
      }

      时间复杂度:O(1)

    2. 入队

      c 复制代码
      int EnQueue(LinkQueue *Q, ElemType e) {
          QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
          if (!s) return 0;  // 内存分配失败
          s->data = e;
          s->next = NULL;
          Q->rear->next = s;  // 尾节点后接新节点
          Q->rear = s;        // 队尾指针更新为新节点
          return 1;
      }

      时间复杂度:O(1)

    3. 出队

      c 复制代码
      int DeQueue(LinkQueue *Q, ElemType *e) {
          if (Q->front == Q->rear) return 0;  // 队空
          QueuePtr p = Q->front->next;        // 暂存队头元素节点
          *e = p->data;
          Q->front->next = p->next;           // 头节点指向新队头
          if (Q->rear == p) Q->rear = Q->front;  // 若删除的是尾节点,更新队尾指针
          free(p);
          return 1;
      }

      时间复杂度:O(1)

🛠️ 六,案例分析与实现

案例1:括号匹配问题(栈的应用)

问题描述 :判断一个字符串中的括号(()[]{})是否匹配,例如"([)]"不匹配,"()[]{}"匹配。

解题思路

  • 遍历字符串,遇到左括号(([{)则入栈;
  • 遇到右括号时,若栈空(无匹配的左括号)则不匹配;否则弹出栈顶元素,检查是否为对应的左括号(如)对应();
  • 遍历结束后,若栈为空则完全匹配,否则存在未匹配的左括号。

代码实现(顺序栈)

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

#define MAXSIZE 100
typedef char ElemType;
typedef struct {
    ElemType data[MAXSIZE];
    int top;
} SqStack;

void InitStack(SqStack *S) { S->top = -1; }
int Push(SqStack *S, ElemType e) {
    if (S->top == MAXSIZE - 1) return 0;
    S->data[++S->top] = e;
    return 1;
}
int Pop(SqStack *S, ElemType *e) {
    if (S->top == -1) return 0;
    *e = S->data[S->top--];
    return 1;
}
int StackEmpty(SqStack S) { return S.top == -1; }

int isMatch(char *str) {
    SqStack S;
    InitStack(&S);
    int len = strlen(str);
    for (int i = 0; i < len; i++) {
        if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
            Push(&S, str[i]); // 左括号入栈
        } else {
            if (StackEmpty(S)) return 0; // 右括号无匹配的左括号
            char top;
            Pop(&S, &top);
            if ((str[i] == ')' && top != '(') || 
                (str[i] == ']' && top != '[') || 
                (str[i] == '}' && top != '{')) {
                return 0; // 括号类型不匹配
            }
        }
    }
    return StackEmpty(S); // 栈空则完全匹配
}

int main() {
    char str[] = "({[]})";
    printf("%s\n", isMatch(str) ? "匹配" : "不匹配"); // 输出:匹配
    return 0;
}

案例2:银行排队模拟(队列的应用)

问题描述:模拟银行窗口服务过程,客户按到达顺序排队,窗口依次为客户服务,计算平均等待时间。

解题思路

  • 用队列存储客户信息(到达时间、服务时长);
  • 记录当前时间,依次出队客户:
    • 若客户到达时间晚于当前时间,更新当前时间为到达时间;
    • 计算等待时间(当前时间 - 到达时间),累加总等待时间;
    • 更新当前时间(加上服务时长);
  • 平均等待时间 = 总等待时间 / 客户数量。

代码实现(链队)

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

typedef struct {
    int arriveTime; // 到达时间(分钟)
    int serviceTime; // 服务时长(分钟)
} Customer;

typedef struct QNode {
    Customer data;
    struct QNode *next;
} QNode, *QueuePtr;

typedef struct {
    QueuePtr front;
    QueuePtr rear;
} LinkQueue;

void InitQueue(LinkQueue *Q) {
    Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));
    Q->front->next = NULL;
}

void EnQueue(LinkQueue *Q, Customer c) {
    QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
    s->data = c;
    s->next = NULL;
    Q->rear->next = s;
    Q->rear = s;
}

int DeQueue(LinkQueue *Q, Customer *c) {
    if (Q->front == Q->rear) return 0;
    QueuePtr p = Q->front->next;
    *c = p->data;
    Q->front->next = p->next;
    if (Q->rear == p) Q->rear = Q->front;
    free(p);
    return 1;
}

int main() {
    LinkQueue Q;
    InitQueue(&Q);
    // 模拟5个客户:(到达时间, 服务时长)
    Customer customers[] = {{10, 5}, {15, 10}, {18, 3}, {20, 7}, {25, 4}};
    int n = 5;
    for (int i = 0; i < n; i++) {
        EnQueue(&Q, customers[i]);
    }

    int currentTime = 0;
    int totalWait = 0;
    Customer c;
    while (DeQueue(&Q, &c)) {
        if (c.arriveTime > currentTime) {
            currentTime = c.arriveTime;
        }
        totalWait += currentTime - c.arriveTime;
        currentTime += c.serviceTime;
    }

    printf("平均等待时间:%.1f分钟\n", (float)totalWait / n); // 输出:2.4分钟
    return 0;
}

📝 章结

栈和队列作为两种受限的线性表,通过约束操作顺序(LIFO/FIFO),在解决特定问题时展现出独特优势:

  • 的核心价值在于"回溯"与"嵌套"场景,如表达式求值、递归实现、浏览器历史记录等,其"后进先出"特性完美匹配"最后处理的内容最先收尾"的逻辑。

  • 队列的核心价值在于"公平调度"与"顺序处理"场景,如任务队列、消息缓冲、排队系统等,其"先进先出"特性确保了操作的有序性与公平性。

从实现角度看:

  • 顺序存储(顺序栈、循环队列)适合已知规模、追求效率的场景;
  • 链式存储(链栈、链队)适合动态规模、避免空间浪费的场景。

理解栈和队列,不仅是掌握两种数据结构,更是学会通过"限制操作"简化问题------在复杂系统中,合理的约束往往比无限制的灵活性更有价值。

相关推荐
秋说1 小时前
【PTA数据结构 | C语言版】前序遍历二叉树
c语言·数据结构·算法
会唱歌的小黄李2 小时前
【算法】贪心算法:最大数C++
c++·算法·贪心算法
NuyoahC2 小时前
笔试——Day8
c++·算法·笔试
hy.z_7772 小时前
【数据结构】反射、枚举 和 lambda表达式
android·java·数据结构
墨染点香2 小时前
LeetCode Hot100 【1.两数之和、2.两数相加、3.无重复字符的最长子串】
算法·leetcode·职场和发展
秋说3 小时前
【PTA数据结构 | C语言版】二叉树层序序列化
c语言·数据结构·算法
erdongchen3 小时前
分支和循环语句 (1 / 2)
c语言
xiaofann_3 小时前
【数据结构】单链表练习(有环)
数据结构
NuyoahC3 小时前
笔试——Day9
数据结构·c++·笔试
地平线开发者3 小时前
开发者说|Aux-Think:为什么测试时推理反而让机器人「误入歧途」?
算法·自动驾驶