小段QGraphicsView代码引入递归,超详细完整讲解(从概念、组成、执行流程、栈、优缺点、场景全覆盖)

目录

QGraphicsView实现的简单思维导图demo。(完整代码参考其他章节文章)

[delete node点击后会执行下面这段代码,那么执行顺序会是什么样的呢?](#delete node点击后会执行下面这段代码,那么执行顺序会是什么样的呢?)

一、递归的基础定义

通俗生活化类比

[例子 1:翻多层套娃](#例子 1:翻多层套娃)

[例子 2:查文件夹所有文件](#例子 2:查文件夹所有文件)

二、递归必须具备两大核心条件(缺一不可)

[1. 基线条件(终止条件 / 出口)](#1. 基线条件(终止条件 / 出口))

[2. 递归条件(递推式)](#2. 递归条件(递推式))

标准递归模板伪代码

[三、递归完整两步执行流程:递推 + 回溯](#三、递归完整两步执行流程:递推 + 回溯)

[阶段 1:递推(往下分解,压栈)](#阶段 1:递推(往下分解,压栈))

[阶段 2:回溯(向上合并,弹栈)](#阶段 2:回溯(向上合并,弹栈))

[实例演示:阶乘 fact (3)](#实例演示:阶乘 fact (3))

1)递推压栈过程

2)回溯弹栈计算

栈帧是什么?

四、两大递归分类

[1. 普通递归(头递归)](#1. 普通递归(头递归))

[2. 尾递归(优化递归)](#2. 尾递归(优化递归))

[五、两种实现方式对比:递归 vs 循环(迭代)](#五、两种实现方式对比:递归 vs 循环(迭代))

递归

循环迭代

适用场景选择

[六、经典递归应用场景(结合 Qt 开发)](#六、经典递归应用场景(结合 Qt 开发))

[七、递归最常见 4 种错误](#七、递归最常见 4 种错误)

[1. 缺失基线条件](#1. 缺失基线条件)

[2. 递归调用没有缩小问题规模](#2. 递归调用没有缩小问题规模)

[3. 递归执行顺序颠倒(树形删除高频踩坑)](#3. 递归执行顺序颠倒(树形删除高频踩坑))

[4. 递归深度过大,栈溢出](#4. 递归深度过大,栈溢出)

八、递归核心思想总结


QGraphicsView实现的简单思维导图demo。(完整代码参考其他章节文章)

delete node点击后会执行下面这****段代码,那么执行顺序会是什么样的呢?

复制代码
void MindMapScene::deleteNode(Node *node) 
{
        // 递归删除所有子节点
        for (Node *child : node->children) {
            deleteNode(child);
        }

        // 从父节点的子节点列表中移除当前节点
        if (node->parentNode) {
            node->parentNode->children.removeOne(node);
        }

        // 从场景中移除节点
        removeItem(node);
        nodes.removeOne(node);
        delete node;

        // 更新连接线
        updateConnections();
}

探讨前再预习下递归吧,先看一段简单的代码!(代码的执行顺序和结果会是什么样的呢?)

复制代码
​​#include <QApplication>
#include <qdebug.h>

// 递归求n的阶乘
int factorial(int n)
{
    qDebug()<<"hello11";
    // 基线条件:n为0/1直接返回1
    if(n == 0 || n == 1)
    {
         qDebug()<<"hello22"<<"n:"<<n;
        return 1;
    }
    // 递归条件:缩小问题,调用自身
    int m = 0;
     m = n * factorial(n - 1);
     qDebug()<<"hello"<<"n:"<<n<<"m:"<<m;
     return m;
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    int num = factorial(6);
    return a.exec();
}

输出结果如下:

一、递归的基础定义

递归(Recursion):一个函数,在函数内部调用自身,用来把复杂大问题拆解成结构完全相同、规模更小的子问题,直到分解到最简单可直接求解的最小问题,再反向合并结果。

通俗生活化类比

例子 1:翻多层套娃

你想拆开一整套套娃:

  1. 当前手里的套娃里面还有更小的套娃 → 先拆开里面小套娃(调用自己,处理子问题)
  2. 拿到最小、里面空的套娃 → 不用再拆(终止条件)
  3. 从小套娃开始,一层层装回,得到完整结果(回溯合并)
例子 2:查文件夹所有文件

打开文件夹,里面如果还有子文件夹,就重复 "打开文件夹" 这个动作;直到文件夹里只有文件、没有子文件夹为止。 QTreeWidget 树形删除、遍历文件夹,都是标准递归场景。


二、递归必须具备两大核心条件(缺一不可)

1. 基线条件(终止条件 / 出口)

最小、无法再拆分、可以直接给出答案的情况。 作用:停止递归,防止无限循环、栈溢出崩溃

  • 阶乘:0! = 1,1! = 1
  • 树遍历:节点没有子节点
  • 遍历文件夹:目录无下级子目录

2. 递归条件(递推式)

将当前问题缩小,调用函数自身去处理更小一级的子问题。 要求:每一次递归调用,问题规模必须严格变小 ,无限逼近基线条件。 错误示例:f(n){ f(n); } 规模不变,死递归。 正确示例:fact(n) = n * fact(n-1),n 不断减小,最终到 1。

标准递归模板伪代码

plaintext

复制代码
函数 func(参数)
    if 满足基线条件:
        return 基础结果   // 停止递归
    else:
        缩小参数,调用 func(更小参数)  // 递归条件
        处理、合并子问题返回的结果
        return 最终结果

三、递归完整两步执行流程:递推 + 回溯

阶段 1:递推(往下分解,压栈)

从原始大问题出发,不断调用自身,把问题拆小,每一次函数调用都会在程序调用栈中新建一块独立栈帧保存当前状态;持续压栈,直到命中基线条件,停止向下分解。

阶段 2:回溯(向上合并,弹栈)

触发基线后,不再产生新调用;从最深一层开始逐层返回,栈帧依次弹出,每一层利用下层返回的子结果,计算自身这一层的答案,一路合并,最终得到原始大问题的解。

实例演示:阶乘 fact (3)

公式:

  • 基线:fact(1) = 1
  • 递归:fact(n) = n * fact(n-1)
1)递推压栈过程

plaintext

复制代码
fact(3) → 3 * fact(2)  【栈帧1入栈】
    fact(2) → 2 * fact(1) 【栈帧2入栈】
        fact(1) → return 1 【栈帧3入栈,命中基线,停止递归】

此时调用栈:栈 1 (3)、栈 2 (2)、栈 3 (1)

2)回溯弹栈计算

plaintext

复制代码
栈3弹出,返回1
栈2收到1,计算 2*1=2,弹出栈2,返回2
栈1收到2,计算 3*2=6,弹出栈1,返回6
栈清空,最终结果6

栈帧是什么?

每次函数调用,操作系统在线程栈开辟一块内存(栈帧),存储:

  1. 函数传入的参数
  2. 局部变量(循环变量、临时值)
  3. 返回地址:递归结束后回到上层哪一行继续执行
  4. 寄存器临时数据

递归深度越大,栈帧越多;线程栈容量有限,深度过大会栈溢出崩溃。


四、两大递归分类

1. 普通递归(头递归)

递归调用写在函数中间 / 前面,回溯阶段才做计算,需要保存每一层栈帧状态。 阶乘、二叉树遍历、树形控件删除都属于普通递归。

cpp

运行

复制代码
int fact(int n)
{
    if(n == 1) return 1;
    return n * fact(n-1); // 递归后还要乘法运算
}

2. 尾递归(优化递归)

递归调用是函数最后一行执行语句,没有后续运算;编译器开启优化后,会复用同一个栈帧,栈深度永远固定,不会溢出。

cpp

运行

复制代码
// acc:累加器,提前计算乘积
int factTail(int n, int acc = 1)
{
    if(n == 0) return acc;
    return factTail(n-1, n * acc); // 最后只有递归调用,无后续计算
}

缺点:C++ 默认不开启优化时,尾递归和普通递归性能一致。


五、两种实现方式对比:递归 vs 循环(迭代)

递归

优点:

  1. 树形、分治、嵌套层级结构代码极度简洁,逻辑贴合问题本身(树、文件夹、迷宫)
  2. 天然保留每层中间状态,回溯算法(八皇后、路径查找)不用手动保存数据 缺点:
  3. 函数频繁创建销毁栈帧,有性能开销
  4. 层级过深会触发线程栈溢出崩溃

循环迭代

优点:

  1. 无函数调用开销,执行速度更快
  2. 可手动用堆容器(QStack、vector)模拟栈,无深度上限 缺点:
  3. 多层嵌套结构代码冗长,逻辑绕,可读性差
  4. 回溯场景需要手动存储每一层状态,代码量大

适用场景选择

  • 树、多级目录、分治算法、回溯搜索 → 优先递归
  • 简单数值循环、计算次数上万、层级极深 → 优先迭代

六、经典递归应用场景(结合 Qt 开发)

  1. 树形控件 QTreeWidget 递归遍历 / 删除所有嵌套子节点,你之前问的树形删除就是典型应用。
  2. 文件系统遍历 递归读取目录下所有子文件夹、文件。
  3. 图形绘制 QPainterPath / 分形图案 雪花、分形树等重复嵌套图形,用递归绘制。
  4. 数据结构算法 二叉树前 / 中 / 后序遍历、链表反转、快速排序、归并排序。
  5. 数学计算 阶乘、斐波那契数列、组合数、最大公约数。
  6. 回溯类算法 迷宫寻路、八皇后、全排列、拼图求解。

七、递归最常见 4 种错误

1. 缺失基线条件

无限递归,栈帧无限压入,程序栈溢出崩溃。

cpp

运行

复制代码
void bad(int n)
{
    bad(n); // 没有出口,死递归
}

2. 递归调用没有缩小问题规模

参数不变,永远无法触达基线,死递归。

cpp

运行

复制代码
int f(int n)
{
    if(n == 0) return 0;
    return f(n); // n没有变化,永远到不了0
}

3. 递归执行顺序颠倒(树形删除高频踩坑)

先删除当前节点,再递归访问子节点,访问野指针直接崩溃。 正确逻辑:先递归处理所有子节点,再操作自身

4. 递归深度过大,栈溢出

比如一万层嵌套树形节点,线程栈容量不足以存储上万栈帧,程序直接 crash。 解决:改用迭代 + 容器模拟栈。


八、递归核心思想总结

  1. 自相似:子问题和原问题结构一模一样,只是规模更小;
  2. 分解思想:大事化小,小事解决后反向合并;
  3. 栈驱动:底层依靠程序调用栈保存每层状态,递推压栈、回溯弹栈;
  4. 两个底线:必须有终止出口、每次递归必须缩小问题。