一、递归函数
1. 什么是递归函数?
- 定义:在函数内部直接或间接调用自身的函数,是一种 “自调用” 的编程思想,模拟现实中 “重复嵌套” 的逻辑(如 “从前有座山” 的故事)。
- 核心本质:将复杂问题拆解为 “与原问题结构相同但规模更小” 的子问题,直到子问题可直接解决(即 “出口”),再反向合并结果。
2. 递归的两种调用形式
(1)直接调用:函数内部直接调用自身
// 注意:无出口的“死递归”,会导致栈溢出(浏览器报错)
function fun() {
console.log("递归调用中");
fun(); // 直接调用自身
}
fun();
(2)间接调用:函数 A 调用函数 B,函数 B 再调用函数 A
// 同样是死递归(无出口)
function foo() {
console.log("来自foo");
bar(); // foo调用bar
}
function bar() {
console.log("来自bar");
foo(); // bar调用foo,形成循环
}
foo();
3. 递归的关键:必须有 “出口”
递归的核心是 “有进有出”:“进” 是函数调用自身(规模缩小),“出” 是存在一个 “终止条件”(出口),避免死递归。
示例 1:简单递归实现(打印数字到 1)
// 需求:输入n,依次打印n、n-1、...、1,最后输出18
function age(n) {
console.log(n); // 每次调用打印当前n(“进”的过程)
// 出口:当n=1时,执行操作并终止递归
if (n === 1) {
console.log(18);
return; // 终止递归,不再调用自身
}
age(n - 1); // 调用自身,规模缩小(n-1)
}
age(5); // 执行结果:5 → 4 → 3 → 2 → 1 → 18
示例 2:递归求 “平方和”(1² + 2² + ... + n²)
// 普通循环写法
function sumCycle(n) {
let result = 0;
for (let i = 1; i <= n; i++) {
result += i * i;
}
return result;
}
console.log(sumCycle(5)); // 55(1+4+9+16+25)
// 递归写法(有出口)
function sumRecursive(n) {
// 出口:n=0时,平方和为0
if (n === 0) {
return 0;
}
// 拆解:n² + (1²+2²+...+(n-1)²)
return n * n + sumRecursive(n - 1);
}
console.log(sumRecursive(5)); // 55
4. 递归的优势与注意事项
(1)优势
- 简化代码:将复杂的嵌套逻辑(如多层循环)转化为简洁的自调用(如多层数据渲染)。
- 适合处理 “层级嵌套” 或 “规模递减” 的问题:如树形结构遍历、大型数据查找(减少遍历次数)。
(2)注意事项
- 必须有出口:否则会导致 “栈溢出”(函数调用栈内存耗尽)。
- 内存消耗:每次递归调用会将函数压入 “调用栈”,递归深度过大会占用大量内存(如递归 10 万次可能报错)。
- 性能:递归效率可能低于循环(因栈操作开销),但代码可读性更高。
5. 递归经典案例
案例 1:求 1 到 n 的累加和(循环 vs 递归)
// 循环写法
function sumCycle(n) {
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
console.log(sumCycle(100)); // 5050
// 递归写法
function sumRecursive(n) {
// 出口:n=0时和为0
if (n === 0) {
return 0;
}
// 拆解:n + (1+2+...+(n-1))
return n + sumRecursive(n - 1);
}
console.log(sumRecursive(100)); // 5050
案例 2:小熊掰玉米(反向递归)
- 问题描述:小熊每走 1 米,扔掉手中玉米的 “一半 + 1 个”;走到 10 米时,手中剩 1 个玉米。求开始时(0 米)的玉米数。
- 分析:正向难算,反向推导(10 米→9 米→...→0 米):
设当前米数为length,玉米数为x;下 1 米的玉米数为y,则y = x - (x/2 + 1) = x/2 - 1→ 反向推导:x = 2*(y + 1)。 - 代码实现:
function getTotalCorn(length) {
// 出口:走到10米时,玉米数为1
if (length === 10) {
return 1;
}
// 反向推导:当前玉米数 = 2*(下1米玉米数 + 1)
return 2 * (getTotalCorn(length + 1) + 1);
}
console.log(getTotalCorn(0)); // 1534(开始时的玉米数)
案例 3:递归渲染多层菜单(树形结构)
- 需求:将多层嵌套的菜单数据(如
foodData)渲染为 HTML 的<ul>/<li>结构。 - 代码实现:
// 递归生成多层菜单HTML
function createMenu(data) {
let html = '<ul>';
for (let i = 0; i < data.length; i++) {
html += '<li>';
html += data[i].title; // 渲染当前菜单标题
// 若有子菜单(cont属性),递归调用生成子菜单HTML
if (data[i].cont && data[i].cont.length > 0) {
html += createMenu(data[i].cont);
}
html += '</li>';
}
html += '</ul>';
return html;
}
// 假设foodData是多层菜单数据(示例结构)
const foodData = {
data: [
{ title: '主食', cont: [{ title: '米饭' }, { title: '面条' }] },
{ title: '蔬菜', cont: [{ title: '青菜', cont: [{ title: '菠菜' }, { title: '油麦菜' }] }] }
]
};
// 渲染到页面
const menuHtml = createMenu(foodData.data);
document.write(menuHtml);
6. 递归面试题:变量提升与作用域
var a = 10;
function show() {
console.log(a); // 结果:undefined(函数内var a提升,未赋值)
a = 5; // 给函数内的a赋值(非全局a)
console.log(window.a); // 结果:10(全局a未被修改)
var a = 20; // 函数内a的最终赋值
console.log(a); // 结果:20(函数内a的值)
}
show();
- 关键:函数内的
var a会 “变量提升” 到函数顶部,形成 “函数作用域内的变量”,与全局a无关。
二、常见算法
1. 冒泡排序(Bubble Sort)
(1)原理
- 核心:相邻元素两两比较,若 “前大后小” 则交换位置,每一轮将 “最大元素” 像 “气泡” 一样推到数组末尾。
- 优化:每轮结束后,末尾已排好序的元素无需再比较,因此内层循环次数可递减(
arr.length - 1 - i)。
(2)代码实现
// 原始数组
var exampleArr = [8, 94, 15, 88, 55, 76, 21, 39];
// 冒泡排序函数
function bubbleSort(arr) {
// 外层循环:控制排序轮数(n个元素需n-1轮)
for (let i = 0; i < arr.length - 1; i++) {
// 内层循环:每轮比较相邻元素,未排序部分长度递减(-i)
for (let j = 0; j < arr.length - 1 - i; j++) {
// 前元素 > 后元素,交换位置
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(bubbleSort(exampleArr)); // [8, 15, 21, 39, 55, 76, 88, 94]
(3)复杂度分析
- 时间复杂度:O (n²)(最坏 / 平均情况,两层循环);最好情况 O (n)(数组已有序,需加 “是否交换” 的判断优化)。
- 空间复杂度:O (1)(仅用临时变量
temp,无额外内存消耗)。
2. 选择排序(Selection Sort)
(1)原理
- 核心:每轮找出 “最小元素”,将其与 “当前轮的起始位置元素” 交换,逐步将最小元素排到数组前面。
- 优势:相比冒泡排序,减少了交换次数(每轮仅交换 1 次,冒泡排序每轮可能交换多次)。
(2)代码实现
// 原始数组
var arr = [5, 3, 7, 1, 2, 8];
// 选择排序函数
function selectionSort(arr) {
// 外层循环:控制轮数(n个元素需n-1轮)
for (let i = 0; i < arr.length - 1; i++) {
// 假设当前轮的第一个元素是最小值,记录其下标
let minIndex = i;
let minValue = arr[i];
// 内层循环:找当前轮的最小元素(从i+1开始,避免重复比较)
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < minValue) {
minValue = arr[j]; // 更新最小值
minIndex = j; // 更新最小值下标
}
}
// 交换:将最小元素与当前轮起始位置元素交换
let temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
console.log(selectionSort(arr)); // [1, 2, 3, 5, 7, 8]
(3)复杂度分析
- 时间复杂度:O (n²)(无论数组是否有序,都需两层循环找最小值,无最好情况优化)。
- 空间复杂度:O (1)(仅用临时变量
temp、minIndex等,无额外内存消耗)。
3. 冒泡排序 vs 选择排序
| 对比维度 | 冒泡排序 | 选择排序 |
|---|---|---|
| 核心逻辑 | 相邻元素比较交换,推最大元素到末尾 | 找最小元素,与起始位置交换 |
| 交换次数 | 每轮可能多次交换(最多 n-1 次 / 轮) | 每轮仅 1 次交换 |
| 时间复杂度 | 最坏 O (n²),最好 O (n)(优化后) | 始终 O (n²)(无优化空间) |
| 空间复杂度 | O(1) | O(1) |
| 实际性能 | 数据量大时,性能略差于选择排序 | 数据量大时,因交换少,性能略优 |
性能测试(10 万条随机数据)
// 生成10万条随机数据
var randomArr = [];
for (let i = 0; i < 100000; i++) {
randomArr.push(Math.round(Math.random() * 1000));
}
// 测试时间函数
function testSortTime(sortFn, arr) {
const startTime = new Date().getTime();
sortFn([...arr]); // 拷贝数组,避免原数组被修改
const endTime = new Date().getTime();
console.log(`${sortFn.name} 耗时:${endTime - startTime}ms`);
}
// 测试(结果仅供参考,受环境影响)
testSortTime(bubbleSort, randomArr); // 冒泡排序:约13000ms
testSortTime(selectionSort, randomArr); // 选择排序:约1000ms
4. 二分查找(Binary Search)
(1)原理
- 核心:“折半查找”,仅适用于 “有序数组”(升序 / 降序),每次排除一半数据,大幅减少查找次数。
- 步骤:
- 定义 “左边界”
low(初始 0)和 “右边界”high(初始arr.length-1)。 - 计算中间位置
mid,比较arr[mid]与目标值:- 若
arr[mid] === 目标值:找到,返回mid(下标)。 - 若
arr[mid] > 目标值:目标值在左半部分,更新high = mid - 1。 - 若
arr[mid] < 目标值:目标值在右半部分,更新low = mid + 1。
- 若
- 重复步骤 2,直到
low > high(未找到,返回 - 1)。
- 定义 “左边界”
(2)代码实现
// 有序数组(必须先排序)
var sortedArr = [1, 3, 5, 7, 9, 11, 13, 15];
// 二分查找函数(非递归实现,避免栈溢出)
function binarySearch(target, arr) {
let low = 0; // 左边界
let high = arr.length - 1; // 右边界
while (low <= high) { // 边界条件:左<=右才有查找空间
const mid = Math.floor((low + high) / 2); // 中间下标(避免小数)
if (arr[mid] === target) {
return mid; // 找到,返回下标
} else if (arr[mid] > target) {
high = mid - 1; // 目标在左半部分
} else {
low = mid + 1; // 目标在右半部分
}
}
return -1; // 未找到
}
console.log(binarySearch(7, sortedArr)); // 3(7在数组下标3的位置)
console.log(binarySearch(4, sortedArr)); // -1(4不在数组中)
(3)复杂度分析
- 时间复杂度:O (log₂n)(每轮排除一半数据,查找次数为 log₂n,如 n=16 时仅需 4 轮)。
- 空间复杂度:O (1)(仅用
low、high、mid变量,无额外内存消耗)。
三、总结
- 递归函数:核心是 “自调用 + 出口”,适合处理嵌套 / 规模递减问题,需注意栈溢出风险。
- 排序算法:
- 冒泡排序:相邻交换,易理解但交换频繁。
- 选择排序:找最小交换,交换少,性能略优。
- 两者时间复杂度均为 O (n²),适合小规模数据。
- 二分查找:仅适用于有序数组,时间复杂度 O (log₂n),是高效的查找算法。
562

被折叠的 条评论
为什么被折叠?



