深入理解算法核心:从递归思想、数组扁平化到快速排序

在算法与数据结构的学习中,递归(Recursion)往往是初学者面临的第一个分水岭。许多开发者在掌握了基础的 for/while 循环后,面对函数自身的调用往往会感到困惑。然而,循环的本质是线性重复,而递归的价值在于它能更自然地表达嵌套结构。本文将从递归的核心思想出发,通过经典的"数组扁平化"实战,最终推导至高效排序算法------快速排序(Quick Sort),带你建立完整的算法知识体系。

一、 递归与分治:自顶向下的算法哲学

1. 递归的本质

递归并非魔法,它的核心本质就是:把一个大问题,拆解成规模更小的同类子问题

以经典的"求 1 到 n 的和"为例,我们可以清晰地看到递归的三要素:

  • 递归公式sum(n) = sum(n - 1) + n,将当前问题转化为更小的问题。
  • 退出条件(基准情形)if (n === 1) return 1;,防止无限调用导致栈溢出。
  • 问题规模缩小 :每次调用 sum(n-1)n 都在不断逼近基准情形。

与之对应的是迭代法,通过 for 循环累加实现。虽然迭代在性能上通常更优,但递归提供了一种抽象的、树状的思维方式。

2. 递归与分治的区别

在算法设计中,我们常听到"分治策略"。需要明确的是:递归是代码的实现方式,而分治是算法的设计思想

  • 递归:自顶向下,通过函数自身调用来缩小问题规模。
  • 分治(Divide and Conquer) :包含"分、治、合"三个步骤。将大问题切成互相独立的小问题(分),解决小问题(治),最后合并小问题的结果(合)。绝大多数分治算法都是借助递归来实现的。

二、 实战演练:数组扁平化(Array Flatten)

数组扁平化是检验递归与分治思想的绝佳试金石。面对嵌套结构如 [1, [2, [3, 4, [5, 6]]]],我们需要将其转换为一维数组。

1. 现代 API 方案:Array.flat()

在 ES6+ 及现代前端项目中,最优雅的方式是使用原生 API。flat() 方法默认只扁平化一层,若需完全扁平化,可传入 Infinity

javascript

编辑

ini 复制代码
1const arr = [1, [2, [3, 4, [5, 6]]]];
2console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6]

在现代引擎中,flat(Infinity) 已经过高度优化,通常优于手写递归。

2. 手写递归方案

若需兼容旧环境或在扁平化时附加过滤、去重等业务逻辑,手写递归是最佳选择。核心逻辑是:遍历数组,判断元素是否为数组,若是则继续展开,否则直接放入结果集

javascript

编辑

ini 复制代码
1const flatten = (arr) => {
2  let result = [];
3  arr.forEach((item) => {
4    if (Array.isArray(item)) {
5      // 核心:使用 concat 拼接,而不是 push,避免产生嵌套数组
6      result = result.concat(flatten(item));
7    } else {
8      result.push(item);
9    }
10  });
11  return result;
12};

⚠️ 避坑指南 :在递归扁平化时,千万不要写成 result.push(flatten(item)),这会导致结果集中依然包含嵌套数组。必须使用 concat 进行数组合并。

三、 算法进阶:快速排序(Quick Sort)

理解了分治与递归,我们来看其在排序算法中的巅峰应用------快速排序。相较于时间复杂度为 O(n2)O(n2) 的冒泡、选择、插入排序,快排的平均时间复杂度达到了 O(nlog⁡n)O(nlogn) 。

1. 核心思想与流程

快排完美体现了"分而治之":

  1. 选取基准值(Pivot) :通常选数组的第一个元素。一轮排序后,该基准值的最终位置是确定的。
  2. 分区(Partition) :通过双指针交换,将比基准值小的元素放到左边,比基准值大的元素放到右边。
  3. 递归(Recursion) :对基准值左侧和右侧的子数组,分别重复上述过程,直到子数组长度为 0 或 1。

2. 为什么快排"快"且"不稳定"?

  • 快的原因:基准值的确立将问题规模缩小为 O(log⁡n)O(logn) 层,而每一层的双指针原地交换耗时 O(n)O(n) 。
  • 不稳定的原因:在双指针交换的过程中,相等的元素可能会发生相对位置的颠倒。例如,原本在前的相等元素可能被交换到后面,这完美体现了快排作为"不稳定排序"的特性。

3. 核心代码实现

javascript

编辑

scss 复制代码
1// 分区函数:原地排序,返回基准值的最终索引
2function partition(nums, left, right) {
3  let i = left, j = right;
4  while (i < j) {
5    // 从右向左找第一个小于基准值的元素
6    while (i < j && nums[j] >= nums[left]) j--;
7    // 从左向右找第一个大于基准值的元素
8    while (i < j && nums[i] <= nums[left]) i++;
9    // 交换找到的两个元素
10    [nums[i], nums[j]] = [nums[j], nums[i]];
11  }
12  // 将基准值交换到两子数组的分界线
13  [nums[i], nums[left]] = [nums[left], nums[i]];
14  return i; 
15}
16
17// 快速排序主函数
18function quickSort(nums, left, right) {
19  if (left >= right) return; // 递归出口
20  
21  let pivot = partition(nums, left, right);
22  quickSort(nums, left, pivot - 1);  // 递归排序左半部分
23  quickSort(nums, pivot + 1, right); // 递归排序右半部分
24}
25
26const arr = [2, 4, 1, 0, 3, 5];
27quickSort(arr, 0, arr.length - 1);
28console.log(arr); // [0, 1, 2, 3, 4, 5]

四、 总结与复习建议

  • 线性重复优先循环,嵌套结构优先考虑递归
  • 写任何递归前,先问自己三个问题:递归公式是什么?退出条件是什么?问题规模有没有变小?
  • 数组扁平化在现代项目中首选 flat(Infinity),但手写递归能帮你深刻理解 concat 与树状遍历。
  • 快速排序是分治思想的集大成者,掌握 partition 的双指针原地交换逻辑,是理解其 O(nlog⁡n)O(nlogn) 高效性与不稳定性的关键。

深入理解算法核心:从递归思想、数组

相关推荐
得物技术2 小时前
从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理
人工智能·算法·架构
AI小老六5 小时前
SkillOpt 架构拆解:把 Skill 文本当参数,用执行轨迹训练 Agent
后端·算法·ai编程
胡萝卜术6 小时前
从“分数打架”到“排名投票”:为什么你的ChatBI必须用RRF?
算法·设计模式·面试
Asize7 小时前
初识DFS 与 BFS:递归、队列与图遍历
算法
罗西的思考20 小时前
机器人 / 强化学习】HIL-SERL:人类在环驱动的具身智能进化框架
人工智能·算法·机器学习
美团技术团队1 天前
LongCat 开源 VitaBench 2.0:长期动态智能体基准新标杆
人工智能·算法
To_OC2 天前
LC 207 课程表:刚学图论那会儿,我连这是拓扑排序都没看出来
javascript·算法·leetcode
To_OC2 天前
LC 208 实现 Trie 前缀树:曾被名字劝退,写完发现是送分题
javascript·算法·leetcode
BadBadBad__AK2 天前
线段树维护区间 k 次方和
c++·数学·算法·stl