算法学习:递归

news2024/11/16 9:21:21

在这里插入图片描述

🔥 个人主页:空白诗

在这里插入图片描述

文章目录

    • 一、引言
    • 二、什么是递归?
    • 三、两大基本要素
      • 🏁 基线条件(Base Case)
      • 🔁 递归条件(Recursive Case)
      • 📃 代码示例:计算斐波那契数列
      • 💻 代码示例:计算阶乘
    • 四、递归的注意事项
      • 1. 栈溢出风险:监控递归深度
      • 2. 效率考量:平衡计算资源
      • 3. 逻辑清晰性:确保易于理解和维护
    • 五、递归的好处
      • 1. 计算斐波那契数列(While循环实现)
      • 2. 计算阶乘(While循环实现)
    • 六、总结


一、引言

递归,这个在编程世界中既神奇又强大的概念,常常让初学者既着迷又困惑。想象一下,一个函数能够自我调用,层层深入,最终解决复杂问题,就像俄罗斯套娃一样,一层套着一层,直到达到最核心的解答。本文将带你一步步走进递归的世界,用JavaScript这把钥匙,解锁递归之门的秘密!🚪


二、什么是递归?

在这里插入图片描述

递归,简单来说,就是一个函数在其定义中直接或间接地调用自身的过程。它通常用于解决那些可以通过分解为相似子问题的问题,比如计算阶乘、遍历树形结构、寻找斐波那契数列等。


三、两大基本要素

🏁 基线条件(Base Case)

  • 定义: 基线条件是递归过程的停靠点,是递归函数不再调用自身的条件。
  • 作用: 确保递归不会无限进行,是递归函数能够最终返回结果的关键。
  • 特点: 应该是最简单的情况,可以直接给出答案,无需进一步递归。

🔁 递归条件(Recursive Case)

  • 定义: 递归条件描述了如何将原问题分解成较小的子问题,并继续调用自身来解决这些子问题。
  • 作用: 实现问题的逐步简化,直至达到基线条件。
  • 特点: 每次递归调用都应使问题规模减小,趋近于基线条件。

📃 代码示例:计算斐波那契数列

斐波那契数列是递归的经典案例,其中每个数字是前两个数字的和,序列从0和1开始。

斐波那契数列(Fibonacci sequence)是一个非常著名的数列,在数学上有着悠久的历史和广泛的应用。这个数列以其发现者,意大利数学家列昂纳多·斐波那契(Leonardo Fibonacci)的名字命名。斐波那契数列最初是斐波那契在《算盘书》(Liber Abaci)中以兔子繁殖的问题作为例子引入的,因此有时也被称为“兔子数列”。

斐波那契数列的定义是这样的:数列的前两项通常是1(有些定义中第一项为0,第二项为1),之后的每一项都是前两项之和。用数学表达式表示就是:

[ F(0)=0, , F(1)=1, , F(n)=F(n-1)+F(n-2) , \text{对于} , n \geq 2 ]

按照这个规则,数列的前几项是:

[ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, \ldots ]

斐波那契数列在自然界和艺术中都能找到其身影,比如植物的分支模式、花瓣排列、动物的生长序列等,都与斐波那契数列紧密相关。此外,它还在计算机科学、密码学、经济学、音乐理论等领域有着重要应用,并且与黄金分割比例(大约为1.61803398875)有着密切的数学联系。斐波那契数列的连续两项之比,随着项数的增加,趋近于黄金比例。

JavaScript实现

function fibonacci(n) {
  if (n <= 1) {  // 基线条件:n为0或1时,直接返回n
    return n;
  } else {  // 递归条件:n的值由前两个斐波那契数相加得到
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

console.log(fibonacci(10)); // 输出: 55
  • 当调用fibonacci(10)时,它不满足基线条件,因此进入递归条件。
  • fibonacci(10)依赖于fibonacci(9)fibonacci(8)的结果。
  • 这个过程会一直持续,直到递归到fibonacci(0)fibonacci(1),这两个是基线条件,直接返回0和1。
  • 之后,每个递归层开始回溯,计算出各自的结果,最终fibonacci(10)得出55。

💻 代码示例:计算阶乘

阶乘(Factorial),一个在数学领域内同样举足轻重的概念,承载着深厚的理论意义和广泛的实际应用。阶乘源自乘法原理的延伸,其符号表示为n!,读作“n阶乘”。阶乘的提出,虽不如斐波那契数列那样具有传奇色彩的故事背景,却在数学分析、组合数学、概率论乃至计算机科学中占据着不可替代的地位。

阶乘的定义简洁而直观:对于任何非负整数n,

  • 如果n为0,则0! 定义为1,这是阶乘的基础约定,保证了递推关系的一致性。
  • 若n为正整数(n > 0),则n! = n × (n-1) × (n-2) × … × 2 × 1。

换言之,阶乘意味着将一个数与其以下的所有正整数相乘,形成一个累积的乘积。例如,

[ 5! = 5 × 4 × 3 × 2 × 1 = 120 ]

阶乘在数学计算中扮演着多重角色,如计算排列数、组合数时它是核心元素,同时也是多项式展开、级数求和、概率分布等领域不可或缺的工具。在计算机科学中,阶乘算法的实现,尤其是递归方法,常作为教学递归思想的经典案例,同时启发了对算法效率、栈空间管理等深入讨论。

function factorial(n) {
  // 基本情况:0的阶乘是1
  if (n === 0) {
    return 1; // 这就是我们的Base Case
  } else { // 递归情况:n的阶乘是n乘以n-1的阶乘
    return n * factorial(n - 1); // 这里调用了自身,是Recursive Case
  }
}

console.log(factorial(5)); // 输出: 120
  • factorial(5)被调用时,它检查n是否为0(基本情况),不是,则继续执行递归情况,即计算5 * factorial(4)
  • 接着factorial(4)调用自己,变成4 * factorial(3),以此类推,直到factorial(0)
  • 当到达factorial(0)时,这是基本情况,直接返回1,然后每个之前的调用开始返回其计算结果,最终factorial(5)计算完成,输出120。

四、递归的注意事项

递归编程作为算法设计的一项基本技能,其有效运用依赖于对几个核心要素的深刻理解和谨慎操作。以下是递归实践中必须留意的关键点:

1. 栈溢出风险:监控递归深度

问题描述示例:计算一个非常大的数的阶乘时,直接递归可能会导致栈溢出。

原始递归代码(存在栈溢出风险):

function factorial(n) {
    if (n === 0) return 1;
    return n * factorial(n - 1);
}

console.log(factorial(10000)); // 可能导致栈溢出

应对措施示例:设置递归深度限制

// 定义一个带有深度限制的阶乘计算函数
function factorialWithDepthLimit(n, depth = 0, maxDepth = 1000) { // 添加最大递归深度限制参数
    // 检查当前递归深度是否超过了设定的最大深度,如果是,则抛出错误
    if (depth > maxDepth) throw new Error("递归深度超出限制");
    // 递归终止条件:如果n等于0,返回1
    if (n === 0) return 1;
    // 递归调用:n乘以(n-1)的阶乘结果,并增加递归深度计数
    return n * factorialWithDepthLimit(n - 1, depth + 1, maxDepth);
}

// 尝试计算一个大数的阶乘,并捕获可能抛出的错误
try {
    console.log(factorialWithDepthLimit(10000)); // 尝试计算10000的阶乘
} catch (e) {
    // 如果发生错误(例如递归深度超限),打印错误信息
    console.error(e.message);
}

2. 效率考量:平衡计算资源

问题描述示例:计算斐波那契数列时,直接递归会有大量重复计算。

原始递归代码(效率低):

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(30)); // 效率极低

这段代码定义了一个带有深度限制的阶乘计算函数factorialWithDepthLimit,旨在防止因递归调用过深而导致的栈溢出错误。函数接受三个参数:要计算阶乘的数n、当前递归深度depth(默认为0)、以及允许的最大递归深度maxDepth(默认为1000)。通过在递归过程中检查深度是否超过最大值,函数能够提前终止递归并抛出错误,从而保护程序免受栈溢出的影响。最后,通过try-catch结构调用该函数并妥善处理可能发生的错误。

优化策略示例:使用记忆化(缓存)

// 初始化一个Map用于存储已经计算过的斐波那契数,键为n,值为第n项斐波那契数
const memo = new Map();

// 定义一个使用记忆化的斐波那契函数
function fibonacciMemo(n) {
    // 如果之前已经计算过这个值,直接从memo中取出返回,避免重复计算
    if (memo.has(n)) return memo.get(n);
    
    let result;
    // 递归终止条件
    if (n <= 1) {
        result = n;
    } else {
        // 递归计算前两项斐波那契数之和
        result = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
    }
    
    // 计算完成后,将结果存入memo中,供后续可能的使用
    memo.set(n, result);
    return result;
}

// 调用优化后的函数计算第30项斐波那契数,由于使用了记忆化技术,即使n较大也能高效计算
console.log(fibonacciMemo(30)); // 高效计算

这段代码通过引入一个memo(记忆)对象来存储已经计算过的斐波那契数,确保对于每一个n值,函数只会被调用一次,之后再次请求该值时直接从memo中查找而非重新计算,从而大大提高了计算效率,尤其是在计算较大的斐波那契数时效果显著。

3. 逻辑清晰性:确保易于理解和维护

挑战说明示例:复杂的递归逻辑如汉诺塔问题可能难于理解。

汉诺塔(Tower of Hanoi),又称为河内塔,是一个经典的递归问题和益智游戏,源自印度的一个古老传说。游戏由三根杆子(通常称为A、B、C)和一系列不同大小的盘子组成,最初所有盘子按照从大到小的顺序依次叠放在第一根杆子(A杆)上,且任何时候大盘子都不能放在小盘子之上。

游戏的目标是将所有盘子从起始杆(A杆)移动到目标杆(C杆),并且遵循以下规则:

  1. 每次只能移动一个盘子。
  2. 在任何时候,盘子都必须保持大盘在下,小盘在上的顺序。
  3. 只能通过借助第三根杆子(B杆,作为辅助)来完成移动,但最终所有盘子都必须移到C杆上。

汉诺塔问题可以用递归算法来解决,基本思路是将n个盘子的问题分解为两个子问题:先将上面的n-1个盘子借助C杆移动到B杆,然后将最下面的盘子直接移动到C杆,最后将B杆上的n-1个盘子移动到C杆上。这个过程不断地自我重复,直到只剩下一个盘子,直接移动即可。

汉诺塔不仅是一个有趣的智力游戏,也是计算机科学中教授递归思想的经典案例。通过解决汉诺塔问题,可以深入理解递归算法的设计和分析,以及递归如何通过将复杂问题分解为更简单实例来实现问题的解决。

在这里插入图片描述

原始递归代码(稍显复杂):

function hanoiTower(n, fromRod, toRod, auxRod) {
    if (n === 1) {
        console.log(`Move disk 1 from rod ${fromRod} to rod ${toRod}`);
    } else {
        hanoiTower(n - 1, fromRod, auxRod, toRod);
        console.log(`Move disk ${n} from rod ${fromRod} to rod ${toRod}`);
        hanoiTower(n - 1, auxRod, toRod, fromRod);
    }
}

hanoiTower(3, 'A', 'C', 'B');

解决办法示例:通过清晰的注释和模块化

/**
 * 使用auxRod作为辅助,将n个盘子从fromRod移动到toRod。
 * @param {number} n 碟子的数量
 * @param {string} fromRod 起始柱子
 * @param {string} toRod 目标柱子
 * @param {string} auxRod 辅助柱子
 */
function hanoiTower(n, fromRod, toRod, auxRod) {
    // 递归终止条件:如果只有一个盘子,直接从起始柱子移动到目标柱子
    if (n === 1) {
        console.log(`将磁盘1从杆${fromRod}移到杆${toRod}`);
    } else {
        // 将n-1个盘子从起始柱子借助目标柱子移动到辅助柱子
        hanoiTower(n - 1, fromRod, auxRod, toRod);
        
        // 移动最底下的第n个盘子(此时它是起始柱子上最大的盘子)到目标柱子
        console.log(`将磁盘${n}从杆${fromRod}移到杆${toRod}`);
        
        // 最后,将n-1个盘子从辅助柱子移动到目标柱子
        hanoiTower(n - 1, auxRod, toRod, fromRod);
    }
}

// 示例:使用清晰的注释和结构化调用来解决3个盘子的汉诺塔问题
hanoiTower(3, 'A', 'C', 'B'); // A为起始柱,C为目标柱,B为辅助柱

这段代码通过递归调用自身来逐步解决问题,展现了汉诺塔问题的分治策略。首先处理较小规模的问题(即将n-1个盘子借助某一柱子移动),然后处理当前规模的特殊部分(即移动最底下的一个盘子),最后再解决剩余的较小规模问题(将n-1个盘子从辅助柱移动到目标柱)。这样的递归逻辑清晰地反映了汉诺塔问题的解决步骤,易于理解和教学。


五、递归的好处

为了全面展示递归与传统循环(这里以forwhile为例)的区别,我们通过计算斐波那契数列和阶乘两个经典问题来对比递归与循环实现方式。

1. 计算斐波那契数列(While循环实现)

在上文中递归实现直接体现了斐波那契数列的定义,代码简洁。但存在重复计算和高时间复杂度的问题,对于大数容易造成栈溢出。

function fibonacciIterative(n) {
    let a = 0, b = 1, temp;
    while (n-- > 0) {
        temp = a;
        a = b;
        b = temp + b;
    }
    return a;
}

console.log(fibonacciIterative(10)); // 同样输出第10个斐波那契数

循环实现避免了递归的栈溢出风险,性能更高,特别是对于大数计算更为稳定,但可能不如递归直观。

2. 计算阶乘(While循环实现)

在上文中递归直接体现了阶乘的数学定义,代码易于理解,但对于极大数同样面临栈溢出的风险。

function factorialIterative(n) {
    let result = 1;
    while (n > 1) {
        result *= n;
        n--;
    }
    return result;
}

console.log(factorialIterative(5)); // 同样输出5的阶乘

而循环实现避免了递归可能导致的栈溢出问题,对于任何大小的输入都能稳定运行,但相比递归可能在表达问题本质方面显得不够直接。

递归的优点在于代码的简洁性和逻辑的直观性,它自然地符合某些问题的结构,比如树形结构遍历或分治算法。而循环,尤其是while循环,提供了更直接的控制流,对于性能敏感或深度大的情况更适用,能够避免递归带来的栈溢出问题。选择哪种方式取决于具体问题、性能要求以及对代码可读性的考量。

综上所述,递归编程是一把双刃剑,既能优雅地解决诸多问题,也暗含着潜在的挑战。通过合理设计、适时优化和清晰的编码实践,我们可以最大化递归的优势,同时有效规避其带来的风险。


六、总结

递归与循环(包括for循环和while循环)都是编程中实现重复逻辑的重要手段,它们各自有独特的应用场景和优缺点。通过对比斐波那契数列和阶乘问题的递归与循环实现,我们可以总结如下:

递归的优势:

  • 代码简洁: 递归能够以非常直观的方式直接反映问题的数学定义或逻辑结构,使得代码易于理解。
  • 自然表达复杂关系: 对于树形结构遍历、分治算法等问题,递归提供了一种自然且优雅的解决方案。
  • 逻辑清晰: 递归强调问题分解,将复杂任务分解为更小的相似子问题,有助于理解和设计算法。

递归的劣势:

  • 性能问题: 未优化的递归可能导致大量的重复计算和较高的时间复杂度。
  • 栈溢出风险: 深度过大的递归调用会消耗过多的调用栈空间,可能引起栈溢出错误。
  • 资源消耗: 相较于循环,递归可能消耗更多的内存资源。

循环(特别是while循环)的优势:

  • 性能高效: 循环通常提供更好的性能表现,尤其是在处理大量数据或深度循环时,因为它们不涉及额外的函数调用开销。
  • 控制灵活: 循环结构提供了更细粒度的控制能力,可以直接管理迭代变量和终止条件。
  • 栈空间友好: 不会导致栈溢出问题,适用于需要处理大规模数据或深度迭代的场景。

循环的劣势:

  • 逻辑复杂: 对于某些自然递归的问题,使用循环实现可能使得代码逻辑较为繁琐,不易于直观理解。

综上所述,选择递归还是循环应基于具体问题的需求、性能考量、代码可读性以及潜在的规模限制。在追求代码简洁和直观性时倾向于递归,在考虑效率和处理大规模数据时则可能更适合使用循环。此外,实际应用中,也可以结合两者优点,如在递归中引入记忆化技术优化性能,或者利用循环结构模拟递归逻辑,达到最佳的解决方案。


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d75eae6a8d804d4b947a329c1a7e823e.png

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

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

相关文章

Unity曲线插件Dreamteck Splines生成曲线Mesh

一、需求 脱离编辑器&#xff0c;运行时添加点&#xff0c;动态生成管道、线缆等曲线Mesh。 二、Dreamteck Splines简单运用 这方面资料不多&#xff0c;只有官方文档全英参考&#xff0c;而且又介绍得不详细。 2个重要组件介绍&#xff1a; SplineComputer&#xff1a; 最…

GRU模块:nn.GRU层

摘要&#xff1a; 如果需要深入理解GRU的话&#xff0c;内部实现的详细代码和计算公式就比较重要&#xff0c;中间的一些过程及中间变量的意义需要详细关注。只有这样&#xff0c;才能准备把握这个模块的内涵和意义&#xff0c;设计初衷和使用方式等等。所以&#xff0c;仔细研…

药品管理系统的设计与实现

互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对药品信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#xff0…

学习笔记:【QC】Android Q : telephony-phone 模块

一、phone init 流程图 高清的流程图参考&#xff1a;【高清图&#xff0c;保存后可以放大看】 二、phone MO 流程图 高清的流程图参考&#xff1a;【高清图&#xff0c;保存后可以放大看】 三、phone MT 流程图 高清的流程图参考&#xff1a;【高清图&#xff0c;保存后可以…

C 深入指针(3)

目录 一、关于数组名 1 数组名的理解 2 数组名 与 &数组名 的区别 二、使用数组访问指针 三、一维数组传参的本质 四、二级指针 五、指针数组 六、指针数组模拟二维数组 一、关于数组名 1 数组名的理解 //VS2022 x64 #include <stdio.h> int main() {int a…

Cesium 问题:billboard 加载未出来

文章目录 问题分析问题 接上篇 Cesium 展示——图标的依比例和不依比例缩放,使用加载 billboard 时,怀疑是路径的原因导致未加载成功 分析 原先

知名专业定制线缆知名智造品牌品牌推荐-精工电联:如何实现清扫机器人线缆产品的精益求精

在科技日新月异的今天&#xff0c;智能清扫机器人已经融入我们的日常生活。然而&#xff0c;其背后不可或缺的一部分&#xff0c;就是那些被称为机器人血管的精密线缆。精工电联作为高科技智能化产品及自动化设备专用连接线束和连接器配套服务商&#xff0c;致力于通过精益求精…

用户行为分析与内容创新:Kompas.ai的数据驱动策略

在数字化营销的今天&#xff0c;用户行为数据分析已成为内容创新和策略调整的核心。通过深入理解用户的行为模式和偏好&#xff0c;品牌能够创造出更具吸引力和相关性的内容&#xff0c;从而实现精准营销。本文将探讨用户行为数据分析在内容创新和策略调整中的价值&#xff0c;…

Langchain实战

感谢阅读 LangChain介绍百度文心API申请申请百度智能云创建应用 LLMChain demo以及伪幻觉问题多轮对话的实现Sequential ChainsSimpleSequentialChainSequentialChainRouter Chain Documents ChainStuffDocumentsChainRefineDocumentsChainMapReduceDocumentsChainMapRerankDoc…

JavaEE 初阶篇-深入了解 HTTP 协议

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 HTTP 协议概述 2.0 HTTP 请求协议 2.1 请求方式的具体体现 3.0 HTTP 响应协议 3.1 常见的状态码及描述 3.2 常见的响应头 4.0 HTTP 协议解析 4.1 简单实现服务器响…

win11环境下,Idea 快捷键shift+f6重命名无法使用

微软自带输入法导致 单击win弹出搜索框

Java数据结构---栈和队列

目录 栈&#xff08;Stack&#xff09; 队列&#xff08;Queue&#xff09; 循环队列 栈&#xff08;Stack&#xff09; 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除操作元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一…

【启明智显分享】7寸HMI智能屏应用方案:Model4芯片实现展会多媒体互动展示

所谓&#xff1a;“技术是底层逻辑&#xff0c;用户是中心动力”。技术水平不断提升&#xff0c;人们对展会展览的形式和体验不断提出更多的要求&#xff0c;展会展现形式也正在不断地寻求突破。 传统的展示形式往往只能提供静态显示和有限的互动体验&#xff0c;在这个公众注意…

因表别名引用错误导致查询SQL执行时间长未出结果

问题描述&#xff1a; 项目组人员反馈在执行一条提取数据SQL时执行很慢&#xff0c;每次执行一段时间就报超时&#xff0c;要求帮忙提取下。 解决过程&#xff1a; 项目组人员发来SQL后&#xff0c;看了下SQL&#xff0c;没什么问题&#xff0c;就在客户端上执行了下&#xff0…

Antd Table组件,state改变,但是render并不会重新渲染

背景 在table上面&#xff0c;当鼠标放在cell上面的时候&#xff0c;需要去请求接口拉取数据&#xff0c;然后setList(res.result)后&#xff0c;希望render中的traceIds也能够实时更新渲染。 const [traceIds, setTraceIds] useState() // 需要展示在popover上面的数据&…

社媒营销中的截流获客是怎么一回事?

如果你要问&#xff0c;现在做社媒营销是通过哪些方式进行引流的&#xff0c;那么必然有一种是截流&#xff0c;顾名思义也就是分取别人的流量&#xff0c;方法其实很简单&#xff0c;主要分为两种&#xff1a;&#xff08;1&#xff09;抓取别人的粉丝出来进行群发私信&#x…

2024上海国际地下空间工程与技术展览会

2024上海国际地下空间工程与技术展览会 2024 Underground Space Project and Technology Expo 时间&#xff1a;2024年10月30日-11月1日 地点&#xff1a;上海世博展览馆 主办单位&#xff1a;联合国人居署 上海市住房与城乡建设管理委员会 城博会背景 上海国际城市与建筑…

mysql数据库---操作数据库跟表的命令总结

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文着重整理mysql管理库跟表的指令。 不涉及增删查改等指令 其实本篇主要是我做好笔记格式 用的时候直接复制粘贴的 所以排版大多是为了快速找功能来排的 方便大家快速找目标语法 数据库的简介 一个数据库系…

(代码以上传,超级详细)面试必备算法题----Leeecode290单词规律

文章目录 概要题目要求测试and提交结果技术细节 概要 来自Leecode ​ 代码已上传&#xff09;仓库&#xff0c;需要测试实例和其他题型解决&#xff0c;可以去自行浏览 点击这里进入仓库领取代码喔&#xff01;顺便点个star给原子加油吧&#xff01; ​ 题目要求 使用哈希表 …

国内外主流大模型都具备有哪些特点?

文章目录 ⭐ 火爆全网的大模型起点⭐ 国外主流LLM及其特点⭐ 国内主流LLM及其特点⭐ 全球大模型生态的发展 该章节呢&#xff0c;我们主要是看一下关于国内外主流的大语言模型&#xff0c;通过它们都具备哪些特点&#xff0c;来达成对多模型有一个清晰的认知。对于 “多模型” …