26考研——栈、队列和数组_栈(3)

news2025/4/1 12:52:54

408答疑


文章目录

  • 一、栈
    • 1、栈(Stack)的概念和特点
      • 定义
      • 术语
      • 操作特性
      • 示例
      • 直观理解
      • 栈的基本操作
        • 初始化栈
        • 判断栈是否为空
        • 入栈操作
        • 出栈操作
        • 读取栈顶元素
        • 销毁栈
      • 栈的数学性质
    • 2、栈的顺序存储结构
      • 顺序栈的定义
        • 栈顶指针初始化
        • 注意事项
      • 共享栈
        • 共享栈的操作
        • 共享栈的优势
      • 代码实操
        • 静态顺序栈
          • 结构定义
          • 初始化
          • 判空
          • 入栈
          • 出栈
          • 取栈顶元素
          • 打印栈
        • 动态顺序栈(小概率出现)
          • 结构定义
          • 初始化
          • 入栈
    • 3、栈的链式存储结构
      • 链栈概念
        • 链栈的优点
        • 链栈的实现
        • 链栈的操作
        • 注意事项
      • 代码实操
        • 结点定义
        • 初始化
        • 判空
        • 入栈操作
        • 出栈操作
        • 取栈顶元素
        • 打印链栈
    • 4、栈的应用
      • 栈在表达式转换中的应用
        • 表达式分类
        • 表达式划分规则
        • 表达式手动转换(最基础最重要)
          • 中缀表达式转化为后缀表达式
            • 步骤
            • 示例
        • 表达式借助栈转换
          • 运算符的优先级关系
          • 计算机利用栈将中缀表达式转化为后缀表达式的过程
            • 步骤
            • 示例
          • 计算机利用栈将中缀表达式转化为前缀表达式的过程
            • 步骤
            • 示例
      • 栈在表达式求值中的应用
        • 算术表达式
          • 中缀表达式
          • 括号的必要性
          • 后缀表达式的特点
          • 示例
        • 表达式求值
          • 中缀表达式求值
            • 步骤
            • 示例
          • 后缀表达式求值
            • 定义
            • 步骤
            • 例题
          • 前缀表达式求值
      • 栈的深度分析
      • 栈在括号匹配中的应用
        • 概述
        • 括号序列示例
        • 分析过程
        • 算法思想
        • 代码实操
      • 栈在递归中的应用
        • 递归的定义
        • 递归的特点
        • 斐波那契数列的递归定义
        • 递归模型的条件
        • 递归的精髓
        • 栈在函数调用中的作用和工作原理
        • 递归调用执行过程
        • 递归算法转换为非递归算法
      • 进制转换
        • 代码实操
      • 出栈的顺序判断
        • 代码实操
  • 四、参考资料
    • 鲍鱼科技课件
    • 26王道考研书


一、栈

1、栈(Stack)的概念和特点

定义

栈是一种特殊的线性表,其特点是只允许在一端进行插入或删除操作。

术语

  • 栈顶(Top):允许进行插入和删除操作的一端。
  • 栈底(Bottom):固定的,不允许进行插入和删除操作的另一端。
  • 空栈:不含任何元素的空表。

操作特性

栈的操作特性可以明显地概括为后进先出(Last In First Out,LIFO)。这意味着最后进入栈的元素会最先被移除。

示例

假设某个栈 S = ( a 1 , a 2 , a 3 , a 4 , a 5 ) S = (a_1, a_2, a_3, a_4, a_5) S=(a1,a2,a3,a4,a5),则:

  • a 1 a_1 a1 为栈底元素。
  • a 5 a_5 a5 为栈顶元素。
  • 入栈次序依次为 a 1 , a 2 , a 3 , a 4 , a 5 a_1, a_2, a_3, a_4, a_5 a1,a2,a3,a4,a5
  • 出栈次序为 a 5 , a 4 , a 3 , a 2 , a 1 a_5, a_4, a_3, a_2, a_1 a5,a4,a3,a2,a1

入栈次序 : a 1 → a 2 → a 3 → a 4 → a 5 出栈次序 : a 5 → a 4 → a 3 → a 2 → a 1 \begin{align*} \text{入栈次序} & : a_1 \rightarrow a_2 \rightarrow a_3 \rightarrow a_4 \rightarrow a_5 \\ \text{出栈次序} & : a_5 \rightarrow a_4 \rightarrow a_3 \rightarrow a_2 \rightarrow a_1 \end{align*} 入栈次序出栈次序:a1a2a3a4a5:a5a4a3a2a1

在这里插入图片描述

直观理解

栈的先进后出结构可以用“喝多了吐”来形象地理解:最后喝的酒最先吐出来,即最后进入的元素最先被移除。

栈的基本操作

各种辅导书中给出的基本操作的名称不尽相同,但所表达的意思大致是一样的。这里我们以严蔚敏编写的教材为准给出栈的基本操作,希望读者能熟记下面的基本操作。

初始化栈
  • InitStack(&S):初始化一个空栈 S
判断栈是否为空
  • StackEmpty(S):判断一个栈是否为空,若栈 S 为空则返回 true,否则返回 false
入栈操作
  • Push(&S, x):入栈,若栈 S 未满,则将 x 加入使之成为新栈顶。
出栈操作
  • Pop(&S, &x):出栈,若栈 S 非空,则弹出栈顶元素,并用 x 返回。
读取栈顶元素
  • GetTop(S, &x):读栈顶元素,但不出栈,若栈 S 非空,则用 x 返回栈顶元素。
销毁栈
  • DestroyStack(&S):销毁栈,并释放栈 S 占用的存储空间(“&”表示引用调用)。

在解答算法题时,若题干未做出限制,则也可直接使用这些基本的操作函数。

栈的数学性质

n n n 个不同元素入栈时,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1} C_{2n}^n n+11C2nn。这个公式称为卡特兰数(Catalan)公式,可采用数学归纳法证明,有兴趣的读者可以参考组合数学教材。

2、栈的顺序存储结构

顺序栈的定义

顺序栈是栈的顺序实现,利用顺序存储结构(数组)实现的栈。栈顶指针 top 的初始化有两种形式:-1 和 0,这会影响入栈 push 和取栈顶元素 top 的操作实现。

栈顶指针初始化
  • 初始设置 S.top = -1:栈顶元素:S.data[S.top]
    • 入栈操作:栈不满时,栈顶指针先加 1,再送值到栈顶。
    • 出栈操作:栈非空时,先取栈顶元素,再将栈顶指针减 1。
    • 栈空条件:S.top == -1;栈满条件:S.top == MaxSize - 1;栈长:S.top + 1
  • 初始设置 S.top = 0:入栈时先将值送到栈顶,栈顶指针再加 1;出栈时,栈顶指针先减 1,再取栈顶元素;栈空条件是 S.top == 0;栈满条件是 S.top == MaxSize
注意事项

顺序栈的入栈操作受数组上界的约束,当对栈的最大使用空间估计不足时,有可能发生栈上溢,此时应及时向用户报告消息,以便及时处理,避免出错。

共享栈

利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。

在这里插入图片描述

共享栈的操作
  • 两个栈的栈顶指针都指向栈顶元素,top0 = -1 时 0 号栈为空,top1 = MaxSize - 1 时 1 号栈为空;仅当两个栈顶指针相邻(top1 - top0 = 1)时,判断为栈满。
  • 当 0 号栈入栈时 top0 先加 1 再赋值,1 号栈入栈时 top1 先减 1 再赋值;出栈时则刚好相反。
共享栈的优势

共享栈是为了更有效地利用存储空间,两个栈的空间相互调节,只有在整个存储空间被占满时才发生上溢。其存取数据的时间复杂度均为 O ( 1 ) O(1) O(1),所以对存取效率没有什么影响。

代码实操

静态顺序栈
结构定义
  • 静态顺序栈使用固定大小的数组存储栈元素,栈顶指针 top 用于记录栈的当前状态
typedef struct SeqStack
{
    ElemType data[MAX_SIZE]; // 栈空间,固定大小
    int top;                // 栈顶指针
} SeqStack;
初始化
  • 将栈顶指针初始化为0,表示栈为空。
void initStack(SeqStack &pst)
{
    pst.top = 0;
}
判空
  • 通过判断栈顶指针是否为0来判断栈是否为空。
bool empty(SeqStack &pst)
{
    return pst.top == 0;
}
入栈
  • 将元素压入栈中,同时检查是否栈满。
void pushStack(SeqStack &pst, ElemType x)
{
    if (pst.top >= MAX_SIZE) // 栈满时无法入栈
    {
        printf("空间已满, %d 不能入栈.\n", x);
        return;
    }
    pst.data[pst.top] = x;
    pst.top++;
}
出栈
  • 移除栈顶元素,同时检查栈是否为空。
void popStack(SeqStack &pst)
{
    if (empty(pst))
    {
        printf("栈已空,不能出栈.\n");
        return;
    }
    pst.top--;
}
取栈顶元素
  • 返回栈顶元素,但不移除。
int topStack(SeqStack &pst)
{
    return pst.data[pst.top - 1]; // 返回栈顶元素
}
打印栈
  • 从栈顶到栈底依次打印栈内元素。
void printStack(SeqStack &pst)
{
    for (int i = pst.top - 1; i >= 0; --i)
        printf("%d\n", pst.data[i]);
}

静态顺序栈使用固定大小的数组存储数据,操作简单,但无法动态扩展。适合栈大小已知且固定的应用场景。

动态顺序栈(小概率出现)
结构定义
  • 动态顺序栈使用动态分配的数组存储栈元素,支持动态扩展。
typedef struct SeqStack
{
    int *data;    // 动态分配的栈空间
    int top;      // 栈顶指针
    int maxsize;  // 当前栈的最大容量
} SeqStack;
初始化
  • 动态分配栈空间,并初始化栈顶指针和容量。
void initStack(SeqStack &pst, int size)
{
    pst.data = (int *)malloc(sizeof(int) * size);
    pst.top = 0;
    pst.maxsize = size;
}
入栈
  • 将元素压入栈中,同时检查是否栈满。
void push(struct seqstack *s, int value)
{
    if (s->top < s->maxsize)
    {
        s->data[s->top] = value;
        s->top++;
        printf("Pushed %d onto the stack.\n", value);
    }
    else
    {
        printf("Stack is full. Cannot push %d.\n", value);
    }
}

动态顺序栈通过动态分配内存,可以在运行时调整栈的大小,但需要手动管理内存分配和释放。

3、栈的链式存储结构

链栈概念

链栈是栈的链式实现,利用链式存储结构(链表)进行实现。使用链表实现栈结构,只允许在链表的一头(一般为表头)插入和删除。

链栈的优点
  • 便于多个栈共享存储空间和提高其效率。
  • 不存在栈满上溢的情况。
链栈的实现

通常采用单链表实现链栈,并规定所有操作都是在单链表的表头进行的。这里规定链栈没有头结点,Lhead 指向栈顶元素。

在这里插入图片描述

链栈的操作
  • 入栈和出栈的操作都在链表的表头进行。
  • 对于带头结点和不带头结点的链栈,具体的实现会有所不同。
注意事项

采用链式存储,便于结点的插入与删除。链栈的操作与链表类似,但需要注意的是,对于带头结点和不带头结点的链栈,具体的实现会有所不同。

代码实操

结点定义
  • 链栈使用链表存储栈元素,每个结点存储一个元素和指向下一个结点的指针。
typedef struct LinkStackNode
{
    int data;                    // 栈元素
    struct LinkStackNode *next;  // 指向下一个结点
} LinkStackNode, *LinkStack;
初始化
  • 创建一个头结点,初始化链栈。
LinkStack initStack()
{
    LinkStackNode *s = (LinkStackNode *)malloc(sizeof(LinkStackNode));
    s->next = NULL;
    return s;
}
判空
  • 通过判断头结点的 next 指针是否为空来判断栈是否为空。
bool empty(LinkStack pst)
{
    return pst->next == NULL;
}
入栈操作
  • 将新元素插入到头结点的下一个位置,实现头插法。
void pushStack(LinkStack pst, int x)
{
    LinkStackNode *s = (LinkStackNode *)malloc(sizeof(LinkStackNode));
    s->data = x;
    s->next = pst->next;
    pst->next = s;
}
出栈操作
  • 移除头结点的下一个结点,并释放内存。
void popStack(LinkStack pst)
{
    LinkStackNode *p = pst->next;
    pst->next = p->next;
    free(p);
}
取栈顶元素
  • 返回头结点的下一个结点的数据。
int topStack(LinkStack pst)
{
    return pst->next->data;
}
打印链栈
  • 从头结点的下一个结点开始,依次打印链栈中的元素。
void printStack(LinkStack pst)
{
    LinkStackNode *p = pst->next;
    while (p != NULL)
    {
        printf("%d\n", p->data);
        p = p->next;
    }
}

链栈使用链表实现,支持动态扩展,无需提前分配固定大小的内存。适合栈大小不确定或频繁变化的场景。

4、栈的应用

栈在表达式转换中的应用

表达式分类
  • 前缀表达式
  • 中缀表达式
  • 后缀表达式
表达式划分规则

表达式的分类是按照运算符跟两个运算数的位置进行划分的:

  • 前缀表达式: + a b +ab +ab(运算符在两个运算数的前面)
  • 中缀表达式: a + b a+b a+b(运算符在两个运算数的中间)
  • 后缀表达式: a b + ab+ ab+(运算符在两个运算数的后面)
表达式手动转换(最基础最重要)
中缀表达式转化为后缀表达式
步骤
  1. 按照运算符的运算顺序对所有运算单位加括号。
  2. 将运算符移至对应括号的后面,相当于按“左操作数右操作数运算符”重新组合。
  3. 去除所有括号。
示例

例如,中缀表达式 A + B ∗ ( C − D ) − E / F A+B*(C-D)-E/F A+B(CD)E/F 转后缀表达的过程如下(下标表示运算符的运算顺序):

  1. 加括号: ( ( A + ③ ( B ∗ ② ( C − ① D ) ) ) − ⑤ ( E / ④ F ) ) ((A+③(B*②(C-①D)))-⑤(E/④F)) ((A+(B(CD)))(E/④F))
  2. 运算符后移: ( ( A ( B ( C D ) − ① ) ∗ ② ) + ③ ( E F ) / ④ ) − ⑤ ((A(B(CD)-①)*②)+③(EF)/④)-⑤ ((A(B(CD)))+(EF)/④)
  3. 去除括号后,得到后缀表达式: A B C D − ① ∗ ② + ③ E F / ④ − ⑤ ABCD-①*②+③EF/④-⑤ ABCD+EF/④
表达式借助栈转换
运算符的优先级关系
θ 1 \theta _{1} θ1 \ θ 2 \theta _{2} θ2+-*/()#
+>><<<>>
->><<<>>
*>>>><>>
/>>>><>>
(<<<<<=
)>>>>>>
#<<<<<=

表格隐含了左结合思想,所以是同级别符号中栈顶运算符大于当前运算符

  • θ 1 \theta _{1} θ1为栈顶运算符, θ 2 \theta _{2} θ2为当前运算符
计算机利用栈将中缀表达式转化为后缀表达式的过程
步骤
  1. 手算(检验借助栈的答案正确否)。
  2. 借助一个栈和一个队列:操作符栈、结果栈。
  3. 表达式扫描顺序:从左往右扫描。
  4. 遇到操作数,直接输出到结果栈。
  5. 遇到运算符,则比较优先级:
    • 若其优先级高于栈顶运算符或遇到栈顶为“(”,则直接入栈;
    • 若其优先级低于或等于栈顶运算符,则依次弹出栈中的运算符并输出到结果栈,直到遇到一个优先级低于它的运算符或遇到“(”或栈空为止,之后将当前运算符入栈。
  6. 如果遇到括号,则根据括号的方向进行处理:
    • 若为“(”,则直接入栈;
    • 若为“)”,则不入栈,且依次弹出栈中的运算符并输出到结果栈,直到遇到“(”为止,并直接删除“(”。
  7. 重复上述的3、4、5步骤,直到表达式扫描完毕。
  8. 扫描完成中缀表达式后,结果栈中所保留的数据则为后缀表达式。
示例

表达式: ( a + b ) ∗ c + d − ( e + g ) ∗ h (a+b)*c+d-(e+g)*h (a+b)c+d(e+g)h
手算结果: a b + c ∗ d + e g + h ∗ − ab+c*d+ eg+h*- ab+cd+eg+h

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

计算机利用栈将中缀表达式转化为前缀表达式的过程
步骤
  1. 手算(检验借助栈的答案正确否)。
  2. 借助一个栈和一个队列:操作符栈、结果栈。
  3. 表达式扫描顺序:从右往左扫描。
  4. 遇到操作数,直接输出到结果栈。
  5. 遇到运算符,则比较优先级:
    • 若其优先级高于或等于栈顶运算符或遇到栈顶为“)”,则直接入栈;
    • 若其优先级低于栈顶运算符,则依次弹出栈中的运算符并输出到结果栈,直到遇到一个优先级低于或等于它的运算符或遇到“)”或栈空为止,之后将当前运算符入栈。
  6. 如果遇到括号,则根据括号的方向进行处理:
    • 若为“)”,则直接入栈;
    • 若为“(”,则不入栈,且依次弹出栈中的运算符并输出到结果栈,直到遇到“)”为止,并直接删除“)”。
  7. 重复上述的3、4、5步骤,直到表达式扫描完毕。
  8. 扫描完成中缀表达式后,结果栈中所保留的数据则为前缀表达式。
示例

表达式: ( a + b ) ∗ c + d − ( e + g ) ∗ h (a+b)*c+d-(e+g)*h (a+b)c+d(e+g)h
手算结果: − + ∗ + a b c d ∗ + e g h -+*+abcd *+egh ++abcd+egh

栈在表达式求值中的应用

算术表达式
中缀表达式

中缀表达式(如 3 + 4 3+4 3+4)是人们常用的算术表达式,操作符以中缀形式处于操作数的中间。与前缀表达式(如 + 34 +34 +34)或后缀表达式(如 34 + 34+ 34+)相比,中缀表达式不容易被计算机解析,但仍被许多程序语言使用,因为它更符合人们的思维习惯。

括号的必要性

与前缀表达式或后缀表达式不同的是,中缀表达式中的括号是必需的。计算过程中必须用括号将操作符和对应的操作数括起来,用于指示运算的次序。

后缀表达式的特点

后缀表达式的运算符在操作数后面,后缀表达式中考虑了运算符的优先级,没有括号,只有操作数和运算符。

示例

中缀表达式 A + B ∗ ( C − D ) − E / F A+B*(C-D)-E/F A+B(CD)E/F 对应的后缀表达式为 A B C D − ∗ + E F / − ABCD-*+EF/- ABCD+EF/,将后缀表达式与原表达式对应的表达式树的后序遍历序列进行比较,可发现它们有异曲同工之妙。

在这里插入图片描述

表达式求值
中缀表达式求值
步骤
  1. 手算求值用于检验借助栈的答案是否正确。
  2. 需要借助两个栈结构
    • 操作符栈
    • 数据栈
  3. 表达式扫描顺序:从左往右扫描。
  4. 遇到操作数,将操作数压入数据栈。
  5. 遇到运算符,比较优先级:
    • 如果当前运算符的优先级 > > > 栈顶运算符的优先级(当栈顶是括号时,直接入栈),则将运算符直接入栈。
    • 如果当前运算符的优先级 < < < 栈顶运算符的优先级,则将栈顶运算符出栈,并将数据栈出栈,先出的为右值,后出的为左值,将运算之后的结果重新入到数据栈。
  6. 遇到括号,根据括号的方向进行处理:
    • 如果是左括号,则直接入栈。
    • 如果是右括号,则遇到左括号前将所有的运算符全部出栈,并将数据栈两个数出栈,将运算之后的结果重新入到数据栈,直到遇到左括号为止。
  7. 重复上述的3、4、5步骤,直至整个表达式扫描完成。
示例

中缀表达式求值,例如 ( 3 + 4 ) − 7 × 5 − 6 (3+4)-7\times 5 - 6 (3+4)7×56

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

后缀表达式求值
定义

后缀表达式又称逆波兰表达式,运算符位于操作数之后。

步骤
  1. 手算求值用于检验借助栈的答案是否正确。
  2. 只需借助一个栈:数据栈。
  3. 表达式扫描顺序:从左往右扫描。
  4. 如果遇到操作数,将操作数压入数据栈。
  5. 如果遇到运算符:
    • 弹出栈顶的两个数,先出栈的为右数,后出栈的为左数。
    • 做运算后将结果重新入栈。
  6. 重复步骤 3 和 4,直到表达式扫描完毕,则数据栈中保存的数据则为表达式的结果。
例题

例如, ( 3 + 4 ) − 7 × 5 − 6 (3+4) -7\times 5 - 6 (3+4)7×56 对应的后缀表达式就是 3   4   +   75   × −   6   − 3\ 4\ +\ 75\ \times-\ 6\ - 3 4 + 75 × 6 

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

前缀表达式求值

与后缀表达式求值差不多,扫描方向相反即可

栈的深度分析

所谓栈的深度,是指栈中的元素个数,通常是给出入栈和出栈序列,求最大深度(栈的容量应大于或等于最大深度)。有时会间接给出入栈和出栈序列,例如以中缀表达式和后缀表达式的形式给出入栈和出栈序列。掌握栈的先进后出的特点进行手工模拟是解决这类问题的有效方法。

栈在括号匹配中的应用

概述

假设表达式中允许包含两种括号:圆括号和方括号,其嵌套的顺序任意,即 []()[([][])] 等均为正确的格式,而 [(])([()])(()) 均为不正确的格式。

括号序列示例

考虑下列括号序列:

在这里插入图片描述

分析过程
  1. 计算机接收第 1 个括号 [ 后,期待与之匹配的第 8 个括号 ] 出现。
  2. 获得了第 2 个括号 (,此时第 1 个括号 [ 暂时放在一边,而急迫期待与之匹配的第 7 个括号 ) 出现。
  3. 获得了第 3 个括号 [,此时第 2 个括号 ( 暂时放在一边,而急迫期待与之匹配的第 4 个括号 ] 出现。第 3 个括号的期待得到满足,消解之后,第 2 个括号的期待匹配又成为当前最急迫的任务。
  4. 以此类推,可见该处理过程与栈的思想吻合。
算法思想
  1. 初始设置一个空栈,顺序读入括号。
  2. 若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的栈中所有未消解的期待的急迫性降了一级。
  3. 若是右括号,则或使置于栈顶的最急迫期待得以消解,或是不合法的情况(括号序列不匹配,退出程序)。算法结束时,栈为空,否则括号序列不匹配。
代码实操
  • 给定一个只包括’(‘,’)‘,’{‘,’}‘,’[‘,’]'的字符串 s,判断括号字符串是否有效。
    有效字符串需满足:
    1. 左括号必须用相同类型的右括号闭合。
    2. 左括号必须以正确的顺序闭合。
    3. 每个右括号都有一个对应的相同类型的左括号。
  • 示例:
    • 输入:"()"
      输出:true
    • 输入:"()[]{}"
      输出:true
    • 输入:"(]"
      输出:false
    • 输入:"([)]"
      输出:false
    • 输入:"{[]}"
      输出:true
//判断字符串中的括号是否匹配,利用链栈存储左括号。
bool isValid(char *s)
{
    LinkStack st = initStack();
    while (*s != '\0')
    {
        if (*s == '{' || *s == '[' || *s == '(') // 左括号入栈
            pushStack(st, *s);
        else
        {
            if (empty(st)) // 右括号但栈为空,不匹配
                return false;
            char topval = topStack(st); // 取栈顶元素
            if ((*s == '}' && topval != '{') || (*s == ']' && topval != '[') || (*s == ')' && topval != '('))
                return false;
            popStack(st); // 匹配成功,出栈
        }
        s++;
    }
    return empty(st); // 栈为空则匹配
}

利用栈存储左括号,遇到右括号时检查栈顶元素是否匹配。最终栈为空则表示括号匹配。

栈在递归中的应用

递归的定义

递归是一种重要的程序设计方法。简单来说,若在一个函数、过程或数据结构的定义中又应用了它自身,则这个函数、过程或数据结构称为是递归定义的,简称递归。

递归的特点

递归通常把一个大型的复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。但在通常情况下,它的效率并不是太高。

斐波那契数列的递归定义

以斐波那契数列为例,其定义为:
F ( n ) = { F ( n − 1 ) + F ( n − 2 ) , n > 1 1 , n = 1 0 , n = 0 F(n) = \begin{cases} F(n-1) + F(n-2), & n > 1 \\ 1, & n = 1 \\ 0, & n = 0 \end{cases} F(n)= F(n1)+F(n2),1,0,n>1n=1n=0

递归模型的条件

必须注意递归模型不能是循环定义的,其必须满足下面的两个条件:

  • 递归表达式(递归体)。
  • 边界条件(递归出口)。
递归的精髓

递归的精髓在于能否将原始问题转换为属性相同但规模较小的问题。

栈在函数调用中的作用和工作原理

在递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储,递归次数过多容易造成栈溢出等。而其效率不高的原因是递归调用过程中包含很多重复的计算。

递归调用执行过程

下面以 n = 5 n=5 n=5 为例,列出递归调用执行过程:

在这里插入图片描述

显然,在递归调用的过程中, F ( 3 ) F(3) F(3) 被计算 2 次, F ( 2 ) F(2) F(2) 被计算 3 次。 F ( 1 ) F(1) F(1) 被调用 5 次, F ( 0 ) F(0) F(0) 被调用 3 次。所以,递归的效率低下,但优点是代码简单,容易理解。

递归算法转换为非递归算法

可以将递归算法转换为非递归算法,通常需要借助栈来实现这种转换。

进制转换

代码实操
  • 编写函数实现,将一个十进制整数value,转换为对应的二进制。
//将十进制数转换为二进制数,利用栈存储中间结果。
void Dec2Bin(int value)
{
    int stack[200] = {0};
    int top = 0;
    while (value)
    {
        stack[top++] = value % 2; // 计算余数并入栈
        value /= 2;               // 更新值
    }
    while (top)
        printf("%d", stack[--top]); // 逆序输出栈内容
}

利用栈的后进先出特性,存储每次除法的余数,最后逆序输出即为二进制结果。

出栈的顺序判断

代码实操
  • 给出入栈序列 In = [6,7,8,9,10,11],出栈序列 Out = [9,11,10,8,7,6],判断出栈序列是否是入栈序列的一种出栈可能性。
//判断给定的出栈序列是否合法,利用链栈模拟入栈和出栈过程。
bool isValidStackSeq(ElemType pushed[], ElemType popped[], int n)
{
    LinkStack st = initStack();
    int i = 0, j = 0;
    while (i < n)
    {
        if (pushed[i] != popped[j])
        {
            pushStack(st, pushed[i]); // 不匹配时入栈
            i++;
        }
        else
        {
            i++;
            j++;
            while (!empty(st) && topStack(st) == popped[j]) // 匹配时出栈
            {
                popStack(st);
                j++;
            }
        }
    }
    return empty(st); // 栈为空则表示出栈序列合法
}

通过模拟入栈和出栈操作,判断给定的出栈序列是否与入栈序列匹配。栈为空时说明出栈序列合法。

四、参考资料

鲍鱼科技课件

b站免费王道课后题讲解:
在这里插入图片描述

网课全程班:
在这里插入图片描述

26王道考研书

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322472.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于 mxgraph 实现流程图

mxgraph 可以实现复杂的流程图绘制。mxGraph里的Graph指的是图论(Graph Theory)里的图而不是柱状图、饼图和甘特图等图(chart)&#xff0c;因此想找这些图的读者可以结束阅读了。 作为图论的图&#xff0c;它包含点和边&#xff0c;如下图所示。 交通图 横道图 架构图 mxGrap…

动态路由机制MoE专家库架构在多医疗AI专家协同会诊中的应用探析

随着医疗人工智能技术的飞速进步,AI在医学领域的应用日益增多,尤其是在复杂疾病的诊断和治疗中,AI技术的应用带来了巨大的潜力。特别是动态路由机制混合专家(Mixture of Experts,MoE)架构,因其灵活、高效的特点,正逐渐成为实现多AI专家协同会诊的关键技术。通过将多个不…

双工通信:WebSocket服务

&#xff08;一&#xff09;WebSocket概述 WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c; 并进行双向数据传输 注意;Websocket也只能由客户端先握…

洪水灌溉算法 + 总结

文章目录 floodfill算法图像渲染题解代码 岛屿数量题解代码 岛屿的最大面积题解代码 被围绕的区域题解代码 太平洋大西洋水流问题题解代码 扫雷游戏题解代码 衣橱整理题解代码 总结 floodfill算法 1. 寻找相同性质的联通块&#xff0c;可以使用dfs或者bfs解决&#xff0c;比如…

LangChain4j(1):初识LangChain4j

1 什么是LangChain和LangChain4j LangChain是一个大模型的开发框架&#xff0c;使用LangChain框架&#xff0c;程序员可以更好的利用大模型的能力&#xff0c;大大提高编程效率。如果你是一个lava程序员&#xff0c;那么对LangChain最简单直观的理解就是&#xff0c;LangChain…

Photoshop 2025安装包下载及Photoshop 2025详细图文安装教程

文章目录 前言一、Photoshop 2025安装包下载二、Photoshop 2025安装教程1.解压安装包2.运行程序3.修改安装路径4.设安装目录5.开始安装6.等安装完成7.关闭安装向导8.启动软件9.安装完成 前言 无论你是专业设计师&#xff0c;还是初涉图像处理的小白&#xff0c;Photoshop 2025…

SQL Server安装程序无法启动:系统兼容性检查失败

问题现象&#xff1a; 运行 SQL Server 2022 安装程序时&#xff0c;提示 “硬件或软件不满足最低要求”&#xff0c;安装向导直接退出或无法继续。 快速诊断 操作系统版本检查&#xff1a; # 查看 Windows 版本&#xff08;需 20H2 或更高&#xff09; winver 支持的系统&…

期权合约作废的话,权利金和保证金会退还么?

在期权交易中&#xff0c;权利金是否可以退回&#xff0c;主要取决于期权的交易情况和合约条款。 期权作废的三种情形 一般来说期权作废一共有三种情况&#xff0c;分别是到期没有行权、主动放弃或者是标的退市了。 第一种是到期未行权&#xff0c;一般来说值得都是虚值期权&…

MIPI计算ECC和CRC工具介绍

一、MIPI简介 MIPI联盟&#xff0c;即移动产业处理器接口&#xff08;Mobile Industry Processor Interface 简称MIPI&#xff09;联盟。MIPI&#xff08;移动产业处理器接口&#xff09;是MIPI联盟发起的为移动应用处理器制定的开放标准和一个规范。MIPI官网https://mipi.org/…

医院管理系统(源码)分享

「医院管理系统&#xff08;源码&#xff09; 源码&#xff1a; https://pan.quark.cn/s/b6e21488fce3 第1章 绪论 1.1 项目背景 随着计算机科学的迅猛发展和互联网技术的不断推进&#xff0c;人们的生活方式发生了巨大的变化&#xff0c;同时也推动了整个软件产业的发展。把…

使用Geotools从DEM数据中读取指定位置的高程实战

目录 前言 一、GridCoverage2D对象介绍 1、GridCoverage2D的属性 2、GridCoverage2D核心方法 3、GridCoverage2D中的高级操作 二、指定位置的高程获取 1、存储原理 2、相关属性的获取 3、获取高程的方法 三、总结 前言 在地理信息科学领域&#xff0c;高程数据是至关重…

STM32F103_LL库+寄存器学习笔记05 - GPIO输入模式,捕获上升沿进入中断回调

导言 GPIO设置输入模式后&#xff0c;一般会用轮询的方式去查看GPIO的电平状态。比如&#xff0c;最常用的案例是用于检测按钮的当前状态&#xff08;是按下还是没按下&#xff09;。中断的使用一般用于计算脉冲的频率与计算脉冲的数量。 项目地址&#xff1a;https://github.…

直播预告 | TDgpt 智能体发布 时序数据库 TDengine 3.3.6 发布会即将开启

从海量监控数据&#xff0c;到工业、能源、交通等场景中实时更新的各类传感器数据&#xff0c;时序数据正在以指数级速度增长。而面对如此庞杂的数据&#xff0c;如何快速分析、自动发现问题、精准预测未来&#xff0c;成为企业数字化转型过程中的关键挑战。 TDengine 的答案是…

vscode 通过Remote-ssh远程连接服务器报错 could not establish connection to ubuntu

vscode 通过Remote-ssh插件远程连接服务器报错 could not establish connection to ubuntu&#xff0c;并且出现下面的错误打印&#xff1a; [21:00:57.307] Log Level: 2 [21:00:57.350] SSH Resolver called for "ssh-remoteubuntu", attempt 1 [21:00:57.359] r…

【JavaScript 简明入门教程】为了Screeps服务的纯JS入门教程

0 前言 0-1 Screeps: World 众所不周知&#xff0c;​Screeps: World是一款面向编程爱好者的开源大型多人在线即时战略&#xff08;MMORTS&#xff09;沙盒游戏&#xff0c;其核心机制是通过编写JavaScript代码来控制游戏中的单位&#xff08;称为“Creep”&#xff09;&#…

Prometheus stack命令行接入springboot服务metrics

使用Prometheus Stack监控SpringBoot应用 本文将详细介绍如何使用Prometheus Stack监控SpringBoot应用的metrics。假设你已经安装了Kubernetes集群&#xff0c;并使用Helm安装了Prometheus Stack全家桶。SpringBoot应用已经配置好&#xff0c;暴露了相应的metrics端点。 Sprin…

Git Bash 设置Notepad++作为默认编辑器

网上搜的时候发现别人搞得有点复杂 &#xff08;绝对正确的方法&#xff09;Git Bash 设置Notepad作为默认编辑器_git 通过notpad 编辑器-CSDN博客 最简单的方式就是重新安装git&#xff0c;然后在选择编辑器的时候&#xff0c;勾选notepad即可

Qt 制作验证码

Qt 制作验证码 #include <QRandomGenerator> #include <QPainterPath> #include <QPainter>// 生成随机数 int r(int a,int b0){return b ? QRandomGenerator::global()->bounded(a, b): QRandomGenerator::global()->bounded(a); }// 生成随机多边形…

【数据结构】二叉树 — 经典OJ面试题剖析!!!

目录 二叉树相关oj题 1. 检查两颗树是否相同 2. 另一棵树的子树 3. 翻转二叉树 4. 判断一颗二叉树是否是平衡二叉树 5. 对称二叉树 6. 二叉树的构建及遍历 7. 二叉树的层序遍历 8. 判断一棵树是不是完全二叉树 9. 二叉树的最近公共祖先 10. 根据前序与中序遍历序列构…

【MySQL】用户账户、角色、口令、PAM

目录 查看用户账户设置 连接 1.本地连接 2.远程连接 账户 角色 操作用户账户和角色 配置口令和账户有效期限 手工使口令过期 配置口令有效期限 PAM身份验证插件 客户端连接&#xff1a;使用 PAM 账户登录 在连接到MySQL服务器并执行查询时&#xff0c;会验证你的身…