03 -栈和队列
本文最后更新于93 天前,其中的信息可能已经过时,如有错误请留言

前言:栈和队列是限定插入和删除只能在表的“端点”进行的线性表(是线性表的子集,是插入和删除位置受限的线性表)

定义:限定仅在栈顶进行插入或删除操作的线性表(所以栈与一般线性表区别仅在于运算规则不同)
表尾称为栈顶top,表头又称为栈底
固定的一端称为栈底,变化的一端称为栈顶
栈存入的新元素永远放在栈顶,栈底是栈的起始位置,一旦确定后不会因入栈操作改变

逻辑结构:同线性表相同,仍为一对一关系
存储结构:用顺序栈或链栈存储均可,但以顺序栈更常见

特点后进先出LIFO

栈的抽象数据类型定义:
ADT Stack{
    数据对象: D={ai | ai ∈ ElemSet, i = 1,2 … n n>=0}
    数据关系: R1 = {< ai-1, ai > | ai-1, ai ∈ D}
              约定an端为栈顶, a1端为栈底
    基本操作:
        InitStack(&S); DestroyStack (&S);
        Push (&S, e); Pop (&S, &e);
        ……等等
}ADT Stack

- InitStack(&S):
  ✓ 操作结果: 构造一个空栈S
- DestroyStack (&S)
  ✓ 初始条件: 栈S已经存在
  ✓ 操作结果: 销毁栈S
- ClearStack (&S)
  ✓ 初始条件: 栈S已经存在
  ✓ 操作结果: 将栈S清为空栈

StackEmpty(S):
✓ 初始条件: 栈S已经存在
✓ 操作结果: 若栈S为空栈, 返回TRUE; 否则返回FALSE
StackLength (S)
✓ 初始条件: 栈S已经存在
✓ 操作结果: 返回栈S中的元素个数, 即栈的长度
GetTop (S, &e)
✓ 初始条件: 栈S已经存在且非空
✓ 操作结果: 用e返回栈S的栈顶元素

Push(&S, e):
✓ 初始条件: 栈S已经存在
✓ 操作结果: 插入元素e为新的栈顶元素
Pop (&S, &e)
✓ 初始条件: 栈S已经存在且非空
✓ 操作结果: 删除S的栈顶元素, 并用e返回其值
StackTraverse(S, visit())
✓ 初始条件: 栈S已经存在且非空
✓ 操作结果: 从栈底到栈顶依次对S的每个数据元素调用函数visit(),
  旦visit()失败, 则操作失败。

栈的表示和实现:

顺序栈

typedef struct {
    SElemType *base; //栈底的指针
    SElemType *top;  //栈顶的指针
    int stacksize;
} SqStack;

Top = base表示空栈 
栈满:top-base=stacksize

相应基本操作的实现算法

初始化时栈顶指针被设置为栈底指针位置,随着新增元素,栈顶指针自增
但这点在共享栈中可能会改变,新增元素放在栈顶,栈顶指针自减

Status InitStack (SqStack &S){
    //构造一个空栈
    S.base=(SElemType *)malloc(
            STACK_INIT_SIZE*sizeof (SElemType));
    if (!S.base) exit (OVERFLOW);
    S.top = S.base ;
    S.stacksize = STACK_INIT_SIZE ;
    return OK;
}

Status GetTop (SqStack S, SElemType &e){
    //若栈不空,用e返回S的栈顶元素
    if (S.top == S.base) return ERROR;
    e = *(S.top - 1);
    return OK;
}

Status Push (SqStack &S, SElemType e){
    //插入元素e为新的栈顶元素
    if (S.top - S.base >= S.stacksize) { //栈满,追加存储空间
        S.base = (SElemType *) realloc (S.base,
            (S.stacksize+STACKINCREMENT)*sizeof(SElemType));
        if (!S.base) exit (OVERFLOW);
        S.top = S.base + S.stacksize ;
        S.stacksize += STACKINCREMENT;
    }
    *S.top ++ = e ; //每当插入新的栈顶元素时,指针top增1
    return OK;
}

Status Pop (SqStack &S, SElemType &e){
    //若栈不空,则删除S的栈顶元素,用e返回其值
    if (S.top == S.base) return ERROR;
    e = * -- S.top ;//每当删除栈顶元素时,指针top减1
    return OK;
}

链栈

不带头结点的单链表

链栈中指针不再指向下一个位置,因为方便

链栈的指针的方向和单链表相反

原因

如果我们用一个普通的单链表来实现栈,并把链表的尾部当作栈顶,那么每次 push(入栈)或 pop(出栈)操作都需要遍历整个链表才能到达尾部,而操作都在栈顶。这会导致操作效率极低(O (n))

栈的应用实例

表达式求值:只对四则运算表达式求值

假定输入的表达式没有语法错误,实现算符优先算法,使用两个工作栈
OPTR:寄存运算符
OPND:寄存操作数或运算结果
算法基本思想:
首先置操作数栈为空栈,表达式起始符为运算符栈的栈底元素
依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直到整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为“#”)

// 计算表达式的值并返回结果,返回值类型为OperandType
OperandType EvaluateExpression()
{
    // 初始化运算符栈OPTR,并压入#作为表达式的起始标记
    InitStack(OPTR); 
    Push(OPTR, '#');
    
    // 初始化运算数栈OPND,从输入流读取第一个字符
    InitStack(OPND); 
    c = getchar();
    
    // 循环处理:当输入字符不是#或运算符栈顶不是#时继续
    // 直到表达式处理完毕(#匹配#)
    while (c != '#' || GetTop(OPTR) != '#') {
        // 判断当前字符是否为非运算符(如数字或字母)
        if (!In(c, OP))
        {
            // 非运算符则压入运算数栈,并读取下一个字符
            Push(OPND, c); 
            c = getchar();
        } 
        // 若为运算符,则根据优先级进行处理
        else
            // Precede函数:比较栈顶运算符与当前运算符的优先级
            switch (Precede(GetTop(OPTR), c)) {
                // 栈顶运算符优先级低:当前运算符入栈,读取下一个字符
                case '<': 
                    Push(OPTR, c); 
                    c = getchar(); 
                    break;
                // 优先级相等(如括号匹配或#与#匹配):弹出栈顶运算符,读取下一个字符
                case '=': 
                    Pop(OPTR, x); 
                    c = getchar(); 
                    break;
                // 栈顶运算符优先级高:弹出栈顶运算符进行计算
                case '>': 
                    Pop(OPTR, theta);       // 弹出运算符theta
                    Pop(OPND, b);           // 弹出右操作数b
                    Pop(OPND, a);           // 弹出左操作数a
                    // 计算a theta b的结果并压入运算数栈
                    Push(OPND, Operate(a, theta, b)); 
                    break;
            }
    }
    // 循环结束时,运算数栈顶即为表达式的计算结果
    return GetTop(OPND);
}

栈与递归的实现

递归函数:类似于多个函数的嵌套调用,只是调用函数和被调用函数是同一个函数。

以下三种情况常常用到递归

  • 递归定义的数学函数:如阶乘函数\(Fact(n)=\begin{cases} 1 & 若n = 0 \\ n * Fact(n – 1) & 若n > 0 \end{cases}\)
  • 具有递归特性的数据结构:如二叉树、广义表
  • 可递归求解的问题:如八皇后问题,Hanoi 塔问题

递归问题—用分治法求解
分治法:对于一个较为复杂的问题,能够分解成几个相对简单的且解法相同或类似的子问题来求解

函数调用过程:
调用前,系统完成:

将实参、返回地址等传递给被调用函数
为被调用函数的局部变量分配存储区
将控制转移到被调用函数的入口

调用后,系统完成:

保存被调用函数的计算结果
释放被调用函数的数据区
依照被调用函数保存的返回地址将控制转移到调用函数

遵循后调用的先返回

队列 (Queue)

只能在队尾(rear) 进行插入,在队头(front)进行删除的线性表
先进先出 FIFO(别记混,先进先出,不代表头进尾出,恰恰相反)
常见应用:脱机打印输出:按申请的先后顺序依次输出…

逻辑结构:同线性表
存储结构:顺序队或链队,以循环顺序队列更常见

链队列 – 队列的链式存储

链表表示的队列,为了方便也添加头结点

链队列(带头结点):

  • Q.front:指向头结点(不是队头元素)
  • Q.rear:指向队尾元素结点本身

队列为空Q.front == Q.rear

//链队列的操作-初始化
Status InitQueue (LinkQueue &Q)
{
  Q.front=Q.rear= (QueuePtr) malloc (sizeof (QNode));
  //队列满
  if (! Q.front) exit (OVERFLOW);
  Q.front ->next = NULL;
  return OK;
}

//链队列的操作 - 销毁链队列
//思想:从队头结点开始,依次释放所有节点
Status DestroyQueue (LinkQueue &Q)
{ while (Q.front) {
    Q.rear = Q.front -> next;
    Free (Q.front);
    Q.front = Q.rear;
  }
  return OK;
}

链队列的操作 - 入队列
Status EnQueue (LinkQueue &Q, QElemType e)
{
  //malloc(sizeof(QNode)):向堆内存申请一块大小为 一个 QNode 结点的空间
  p = (QueuePtr) malloc (sizeof (QNode));
  //如果申请失败会返回0,这里就会终止程序
  if (! p) exit (OVERFLOW);
  p -> data = e; 
  //(尾进头出,新进入的结点后无结点)
  p -> next = NULL;
  // 原队尾结点的 next 指向新结点
  Q.rear -> next = p;
  // 尾指针 rear 后移,指向新的队尾结点 p
  Q.rear = p;
  return OK;
}

链队列的操作 - 出队列
Status DeQueue (LinkQueue &Q, QElemType &e)
{ 
  // 队列为空:头指针=尾指针(只有头结点或无元素)
  if (Q.front == Q.rear) return ERROR;
  //front指向头结点,因此其next才为队头结点,p是临时指针变量指向“要删除的结点” 
  p = Q.front -> next;
  // 取出队头元素的数据,保存到 e
  e = p -> data;
  // 头结点跳过 p,指向 p 的后继(把 p 从链中摘掉)
  Q.front ->next = p -> next;
  // 若原来只有一个数据结点,删除后队列变空:尾指针回到头结点
  if (Q.rear == p) Q.rear = Q.front;
  free (p);
  return OK;
}

例题:用链式存储表示队列(无队列长度 n),并设队头指针,问出队入队的时间复杂度:

出队:用队头指针找到头结点,头指针后移一位,不需要遍历
时间复杂度:O(1)
入队:入队要把新结点接到队尾,但你没有队尾指针,需要遍历整个链表,最坏走 n 步。
时间复杂度O(n)

从队头开始一路找“最后一个结点”

循环队列 – 队列的顺序表示

若以最后图像的状态,不可再继续插入新的队尾元素因为数组越界,然而此时不宜如顺序栈那样扩大数组空间,因为队列的实际可用空间被没有占满,一个巧妙的方法是将顺序队列臆造为一个环状的空间,称之为环状队列

循环队列(数组实现)

  • front:指向队头元素的位置(下一次出队从这里取)
  • rear:指向队尾元素的下一个位置(下一次入队放这里)

循环队列-队满和队空无法区分问题?
法1: 另外设一个标志区分队空队满
法2: 队列满的条件:( Q.rear + 1 )%MAXQSIZE = Q.front

队列的长度:(rear−front+MAXQSIZE)%MAXQSIZE

循环队列的基本操作-初始化

Status InitQueue (SqQueue &Q)
{
  //baseQ.base是循环队列底层数组的起始地址,也就是“用来存队列元素的存储区”
  Q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
  if (! Q.base ) exit (OVERFLOW);
  Q.front = Q.rear = 0;
  return OK;
}

循环队列的基本操作 – 入队列

Status EnQueue (SqQueue &Q, QElemType e)
{ 
  if ((Q.rear+1)% MAXQSIZE == Q.front ) return ERROR;
  // 在 rear 指向的“下一个可插入位置”放入新元素 e
  Q.base [ Q.rear ] = e;
  // rear 后移一格(取模实现循环回绕)
  Q.rear = ( Q.rear + 1 ) % MAXQSIZE;
  return OK;
}

循环队列的基本操作 – 出队列

Status DeQueue (SqQueue &Q, QElemType &e)
{
  if ( Q.front == Q.rear ) return ERROR;
  // 取出队头元素:从 front 指向的位置读出数据到 e
  e = Q.base [ Q.front ];
  //让下一个元素成为新头(并对 m 取模回绕)
  Q.front = ( Q.front + 1 ) % MAXQSIZE;
  return OK;
}
对比点循环队列(数组实现)链队列(链表实现,带头结点版本最常见)
存储方式一段固定长度数组,首尾“绕回”使用结点动态链接,内存不要求连续
front 指向通常指向队头元素位置(下一次出队从这取)通常指向头结点(不存数据);队头元素在 front->next
rear 指向通常指向队尾元素的下一个空位(下一次入队放这)通常指向队尾数据结点本身(最后一个元素结点)
判空front == rearfront == rear(此时都指向头结点)
判满有满:常用“牺牲一格”法:(rear+1)%MAX == front通常无“满”:只要内存够就能入队;“满/溢出”表现为 malloc 失败
最大容量固定,若用牺牲一格法:最多存 MAX-1 个元素理论上不固定(受限于内存)

例题:

3.15 假设以顺序存储结构实现一个双向栈,即在一维数组的存储空间中存在着两个栈,它们的栈底分别设在数组的两个端点。试编写实现这个双向栈 tws 的三个操作:初始化 inittack(tws)、入栈 push(tws,i,x) 和出栈 pop(tws,i) 的算法,其中 i 为 0 或 1,用以分别指示设在数组两端的两个栈,并讨论按过程(正/误状态变量可设为零参)或函数设计这些操作算法各有什么有缺点。

1. 标准共享栈(两端向中间生长)

这是最经典的共享栈模型,核心特点:

  • 栈底位置:
    • 栈 1 的栈底在数组起始位置base1 = arr[0])。
    • 栈 2 的栈底在数组末尾位置base2 = arr[size-1])。
  • 生长方向:
    • 栈 1 的栈顶 top1 从栈底(起始位置)向中间移动(入栈时 top1++)。
    • 栈 2 的栈顶 top2 从栈底(末尾位置)向中间移动(入栈时 top2--)。
  • 栈满条件:top1 + 1 == top2(两个栈顶 “相邻”,中间无剩余空间)。

2. 此题的变种(中间起始向两端扩展)

代码里的设计是另一种 “共享思路”:

  • 初始栈顶:两个栈的栈顶都从数组中间开始(top[0] = top[1] = p + m/2)。
  • 生长方向:
    • 栈 0(i=0)向数组起始端扩展(入栈时 top[0]--)。
    • 栈 1(i=1)向数组末尾端扩展(入栈时 top[1]++)。
  • 栈满条件:top[0] == p(栈 0 触达起始端)或 top[1] == p + stacksize - 1(栈 1 触达末尾端)。

所以,之前的解释是共享栈的 “变种实现”,和标准 “两端向中间” 模型不冲突,只是栈顶初始位置和扩展方向相反
栈 0 向左、栈 1 向右,因此它们的 “可用空间” 是从中间向两端逐步占用的。当两个栈顶指针 top[0] 和 top[1] 相遇时(top[0] == top[1]),栈为空;当栈 0 的 top[0] 触达数组起点(p),或栈 1 的 top[1] 触达数组末尾(p + stacksize - 1)时,对应栈满

// 双向栈类定义
class DStack {
    // 两个栈顶指针,top[0]对应一个栈,top[1]对应另一个栈
    ElemType *top[2];
    // 指向数组起始位置的指针
    ElemType *p;
    // 栈的总大小
    int stacksize;
    // 用于指示操作的是哪个栈(0或1)
    int di;
public:
    // 构造函数,初始化双向栈,参数m为数组大小
    DStack(int m) {
        // 为数组分配m个ElemType类型的空间
        p = new ElemType[m];
        // 如果内存分配失败,退出程序
        if (!p) exit(OVERFLOW);
        // 初始化top[0]为数组中间位置,作为其中一个栈的栈顶初始位置
        top[0] = p + m / 2;
        // 初始化top[1]与top[0]相同,作为另一个栈的栈顶初始位置
        top[1] = top[0];
        // 设置栈的总大小
        stacksize = m;
    }
    // 析构函数,释放数组内存
    ~DStack() { delete p; }
    // 入栈操作,i指示栈(0或1),x为入栈元素
    void Push(int i, ElemType x) {
        // 记录要操作的栈
        di = i;
        // 操作第0个栈
        if (di == 0) {
            // 由中间向左扩展,栈顶不断减小。如果栈顶指针大于数组起始指针,说明栈未满,可入栈
            if (top[0] > p) 
                //先赋值再自减
                *top[0]-- = x;
            else 
                cerr << "Stack overflow!";
        } else {
            // 操作第1个栈,由中间向右扩展,栈顶不断自增
            // 如果栈顶指针小于数组末尾指针,说明栈未满,可入栈
            if (top[1] < p + stacksize - 1) 
                *++top[1] = x;
            else 
                cerr << "Stack overflow!";
        }
    }
    // 出栈操作,i指示栈(0或1),返回出栈元素
    ElemType Pop(int i) {
        // 记录要操作的栈
        di = i;
        // 操作第0个栈
        if (di == 0) {
            // 如果两个栈顶指针不重叠,说明栈非空,可出栈
            if (top[0] < top[1]) 
                return *++top[0];
            else 
                cerr << "Stack empty!";
        } else {
            // 操作第1个栈,如果两个栈顶指针不重叠,说明栈非空,可出栈
            if (top[1] > top[0]) 
                return *top[1]--;
            else 
                cerr << "Stack empty!";
        }
        return 0;
    }
};

橙色子索引问题:
假设数组 p 的总大小为 stacksize(即能存储 stacksize 个元素),那么数组的有效索引范围是 0 到 stacksize - 1(从起始位置到末尾位置)。

p + stacksize - 1 指向数组的最后一个有效位置(索引 stacksize - 1 对应的位置)。

p 指向数组的起始地址(索引 0 对应的位置)。

// 计算 Ackerman 函数的递归算法
int akm(int m, int n) {
    // 当 m 为 0 时,直接返回 n + 1
    if (m == 0) {
        return n + 1;
    } 
    // 当 m 不为 0 但 n 为 0 时,递归调用 akm(m - 1, 1)
    else if (n == 0) {
        return akm(m - 1, 1);
    } 
    // 当 m 和 n 都不为 0 时,递归调用 akm(m - 1, akm(m, n - 1))
    else {
        return akm(m - 1, akm(m, n - 1));
    }
}

非递归实现 Ackerman 函数的核心思路是用栈模拟递归调用的过程,因为递归的本质是函数调用栈的层层嵌套,用自定义的栈可以手动管理这些 “调用” 与 “返回” 逻辑。

栈的作用:用栈来存储待计算的 Ackerman 函数参数(m 和 n)。每次处理栈顶的参数,根据 Ackerman 函数的定义,将复杂的递归分解为更简单的子问题,逐步推进计算,直到能直接得到结果。

// 非递归算法:计算 Ackerman 函数
int akm1(int m, int n)
{
    // 定义栈 s,用于模拟递归调用栈
    Stack s;
    // 初始化栈
    InitStack(s);
    // 定义结构体变量 e, e1, d,用于存储 Ackerman 函数的参数 m 和 n
    ElemType e, e1, d;
    // 初始化当前要计算的 Ackerman 函数参数 m 和 n
    e.mval = m;     
    e.nval = n;
    // 将初始参数入栈
    Push(s, e);
    do {
        // 当 m 不为 0 时,进入循环处理
        while (e.mval) {
            // 当 n 不为 0 时,进入循环处理
            while (e.nval) {
                // n 减 1
                e.nval--;
                // 将新的参数入栈
                Push(s, e);
            }
            // m 减 1,n 置为 1
            e.mval--; e.nval = 1;
        }
        // 如果栈中元素个数大于 1,进行回溯计算
        if (StackLength(s) > 1) {
            // 保存当前栈顶元素的 n 值
            e1.nval = e.nval;
            // 弹出栈顶元素
            Pop(s, e);
            // m 减 1
            e.mval--;
            // n 为子问题结果加 1
            e.nval = e1.nval + 1;
        }
    // 直到栈中只剩一个元素且 m 为 0(此时可直接计算结果)
    } while (StackLength(s) != 1 || e.mval != 0);
    // 返回最终结果(结果为 nval + 1,因为 m 为 0 时结果是 n + 1)
    return e.nval + 1;
}

3.19 假设一个算术表达式中可以包含三种括号:圆括号 “(” 和 “)”、方括号 “[” 和 “]” 和花括号 “{” 和 “}”,且这三种括号可按任意的次序嵌套使用(如:…[…{…}…[… ]…]…[… ]…(…)…)。编写判别给定表达式中所含括号是否正确配对出现的算法(已知表达式已存入数据元素为字符的顺序表中)

def is_bracket_matched(expr):
    stack = []
    # 定义括号匹配的字典,右括号为键,左括号为值
    bracket_map = {')': '(', ']': '[', '}': '{'}
    for char in expr:
        if char in '([{':  # 左括号入栈
            stack.append(char)
        elif char in ')]}':  # 右括号判断匹配
            if not stack:  # 栈空,无左括号匹配
                return False
            top = stack.pop()
            if bracket_map[char] != top:  # 左右括号不匹配
                return False
    return len(stack) == 0  # 栈空则全部匹配

# 测试示例
expr1 = "3+[4*(5-2)]"
expr2 = "3+[4*(5-2)"
expr3 = "3+[4*(5-2)}"

print(f"表达式 '{expr1}' 括号匹配:{is_bracket_matched(expr1)}")
print(f"表达式 '{expr2}' 括号匹配:{is_bracket_matched(expr2)}")
print(f"表达式 '{expr3}' 括号匹配:{is_bracket_matched(expr3)}")

3.28 假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾元素结点(注意不设头指针),试编写相应的队列初始化、入队列和出队列的算法。

// 定义元素类型为 int
typedef int ElemType;
// 定义链表结点结构
typedef struct NodeType {
    ElemType data;
    struct NodeType *next;
} QNode, *QPtr;
// 定义队列结构,包含队尾指针和队列大小
typedef struct {
    QPtr rear;
    int size;
} Queue;

// 队列初始化函数
Status InitQueue(Queue &q) {
    // 队尾指针初始化为 NULL
    q.rear = NULL;
    // 队列大小初始化为 0
    q.size = 0;
    return OK;
}

// 入队列函数,将元素 e 加入队列
Status EnQueue(Queue &q, ElemType e) {
    QPtr p;
    // 分配新结点内存
    p = new QNode;
    if (!p) return FALSE;
    // 新结点数据域赋值为 e
    p->data = e;
    // 若队列为空
    if (!q.rear) {
        // 队尾指针指向新结点
        q.rear = p;
        // 新结点的 next 指向自己,形成循环
        p->next = q.rear;
    } else {
        //若队列非空
        // 新结点的 next 指向队头(原队尾的 next)
        p->next = q.rear->next;
        // 原队尾的 next 指向新结点
        q.rear->next = p;
        // 队尾指针指向新结点
        q.rear = p;
    }
    // 队列大小加 1
    q.size++;
    return OK;
}

// 出队列函数,将队头元素出队并保存到 e 中
Status DeQueue(Queue &q, ElemType &e) {
    QPtr p;
    // 若队列为空,出队失败
    if (q.size == 0) return FALSE;
    // 若队列只有一个元素
    if (q.size == 1) {
        // 将指针 p 指向这个唯一的结点,后续对该结点进行数据获取和内存释放操作
        p = q.rear;
        // 取出结点数据
        e = p->data;
        // 队尾指针置为 NULL
        q.rear = NULL;
        // 释放结点内存
        delete p;
    } else {
        // p 指向队头结点(队尾的 next 指向队头)
        p = q.rear->next;
        // 取出队头结点数据
        e = p->data;
        // 队尾的 next 指向新的队头(原队头的 next)
        q.rear->next = p->next;
        // 释放原队头结点内存
        delete p;
    }
    // 队列大小减 1
    q.size--;
    return OK;
}

算法思路

  • 初始化队列(InitQueue:将队尾指针 rear 置为 NULL,表示队列为空,同时将队列大小 size 初始化为 0
  • 入队列(EnQueue:首先创建新结点存储要入队的元素。若队列为空,新结点成为队尾,且自身形成循环(next 指向自己);若队列非空,新结点插入到队尾,调整队尾指针 rear 指向新结点,并维护循环链表的结构,最后队列大小加 1
  • 出队列(DeQueue:先判断队列是否为空,若为空则出队失败。若队列只有一个元素,取出该元素,释放结点并将队尾指针置为 NULL;若队列有多个元素,找到队头元素(通过队尾指针的 next 找到),取出元素,调整链表结构,释放队头结点,最后队列大小减 1

3.30 假设将循环队列定义为:以域变量 rear 和 length 分别指示循环队列中队尾元素的位置和内含元素的个数。试给出此循环队列的队满条件,并写出相应的入队列和出队列的算法(在出队列的算法中要返回队头元素)

// 定义队列的最大容量
#define MaxQSize 4
// 定义队列中元素的类型为 int
typedef int ElemType;
// 定义循环队列的结构体
typedef struct {
    // 指向存储队列元素的数组的指针
    ElemType *base;
    // 指示队尾元素的位置
    int rear;
    // 队列中内含元素的个数
    int length;
} Queue;

// 初始化队列的函数
Status InitQueue(Queue &q) {
    // 为队列分配 MaxQSize 个 ElemType 类型的空间
    //当将数组名赋值给指针时,数组名会隐式转换为指向数组首元素的指针(即首元素的地址)
    q.base = new ElemType[MaxQSize];
    // 如果分配空间失败,返回 FALSE
    if (!q.base) return FALSE;
    // 初始时队尾位置为 0
    q.rear = 0;
    // 初始时队列中元素个数为 0
    q.length = 0;
    // 初始化成功,返回 OK
    return OK;
}

// 入队列的函数,将元素 e 加入队列
Status EnQueue(Queue &q, ElemType e) {
    // 通过下一个队尾的位置” 是否等于 “队头的位置”判断队列是否已满 
    if ((q.rear + 1) % MaxQSize == (q.rear + MaxQSize - q.length) % MaxQSize)
        // 队满,无法入队,返回 FALSE
        return FALSE;
    else {
        // 将元素 e 放入队尾位置
        q.base[q.rear] = e;
        // 队尾位置后移一位,利用取模运算实现循环
        q.rear = (q.rear + 1) % MaxQSize;
        // 队列中元素个数加 1
        q.length++;
    }
    // 入队成功,返回 OK
    return OK;
}

// 出队列的函数,将队头元素出队并保存到 e 中
Status DeQueue(Queue &q, ElemType &e) {
    // 通过通过 “队头位置”是否等于“队尾位置” ,判断队列是否为空
    if ((q.rear + MaxQSize - q.length) % MaxQSize == q.rear)
        // 队空,无法出队,返回 FALSE
        return FALSE;
    else {
        // 计算队头元素的位置,将其值赋给 e
        e = q.base[(q.rear + MaxQSize - q.length) % MaxQSize];
        // 队列中元素个数减 1
        q.length--;
    }
    // 出队成功,返回 OK
    return OK;
}

(q.rear + 1) % MaxQSize:表示下一个队尾的位置。因为rear是当前队尾位置,插入一个新元素后,队尾要后移一位,通过取模运算保证在数组范围内循环。

(q.rear - q.length + MaxQSize) % MaxQSize:表示队头的位置

  • 已知rear是队尾位置,length是元素个数。
  • 队列中元素是连续存储的(循环意义上的连续),所以队头位置 = 队尾位置 – 元素个数 + 最大容量(取模是为了防止负数,保证在数组索引范围内)。

学习笔记如有侵权,请提醒我,我会马上删除
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇