1.直接插入排序
你说得非常准确!直接插入排序(Insertion Sort)的核心思想正是:假设前 i−1 个元素已经排好序,将第 i 个元素插入到前面的有序序列中的合适位置。
✅ 插入排序的基本步骤:
- 将第一个元素视为已排序。
- 从第二个元素开始,依次取出当前元素(称为"key"或"待插入元素")。
- 将该元素与前面已排序部分从后往前比较。
- 如果前面的元素比它大,就将前面的元素向后移动一位。
- 直到找到一个 ≤ 它的元素(或到达开头),将该元素插入到该位置之后。
- 重复直到处理完所有元素。
📌 JavaScript 实现示例:
javascript
function insertionSort(arr) {
// 从第二个元素开始(索引为1)
for (let i = 1; i < arr.length; i++) {
let key = arr[i]; // 当前要插入的元素
let j = i - 1; // 已排序部分的最后一个索引
// 从后往前比较,只要 arr[j] > key,就将 arr[j] 向后移
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
// 找到插入位置(j+1),插入 key
arr[j + 1] = key;
}
return arr;
}
// 测试
const unsorted = [5, 2, 4, 6, 1, 3];
console.log("原数组:", unsorted);
console.log("排序后:", insertionSort([...unsorted])); // 使用展开符避免修改原数组
🔍 执行过程演示(以 [5, 2, 4, 6, 1, 3] 为例):
- 初始:
[5] | 2,4,6,1,3→ 前1个有序 - i=1(key=2):比较5>2 → 移动5 → 插入2 →
[2,5] | 4,6,1,3 - i=2(key=4):5>4 → 移动5;2≤4 → 插入 →
[2,4,5] | 6,1,3 - i=3(key=6):5≤6 → 直接放后面 →
[2,4,5,6] | 1,3 - i=4(key=1):6>1,5>1,4>1,2>1 → 全部后移 → 插入开头 →
[1,2,4,5,6] | 3 - i=5(key=3):6>3,5>3,4>3;2≤3 → 插入在2后 →
[1,2,3,4,5,6]
⏱️ 时间复杂度:
- 最好情况(已排序):O(n)
- 平均/最坏情况(逆序):O(n²)
💡 特点:
- 稳定排序
- 原地排序(空间复杂度 O(1))
- 对小规模或基本有序的数据效率高
你对算法的理解完全正确!如需可视化或进一步优化(比如二分插入),也可以继续探讨。
2.希尔排序
你说得非常到位!希尔排序(Shell Sort) 正是为了解决直接插入排序在数据量大、初始无序时效率低下的问题而提出的优化方法。它通过 "分组 + 逐步缩小增量" 的策略,使数据在最终进行一次直接插入排序前已基本有序,从而大幅提升效率。
✅ 希尔排序的核心思想回顾:
- 选择一个递减的增量序列 (如:
d = Math.floor(n/2), d = Math.floor(d/2), ..., 1)。 - 对每个增量
d:- 将数组按间隔
d分成若干子序列(即下标相差d的元素为一组)。 - 对每个子序列独立进行直接插入排序。
- 将数组按间隔
- 当增量减小到
1时,整个数组作为一个子序列进行最后一次插入排序------此时数组已接近有序,效率很高。
💡 增量序列的选择会影响性能。最常用的是 Knuth 序列 或 折半序列 (
n/2, n/4, ..., 1),虽然不是最优,但简单有效。
📌 JavaScript 实现(使用折半增量序列):
javascript
function shellSort(arr) {
const n = arr.length;
let gap = Math.floor(n / 2); // 初始增量为 n/2
// 不断缩小增量,直到 gap = 1
while (gap > 0) {
// 对每个子序列进行插入排序
for (let i = gap; i < n; i++) {
let temp = arr[i]; // 当前要插入的元素
let j = i;
// 在当前子序列中,从后往前比较(步长为 gap)
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap]; // 向后移动
j -= gap;
}
arr[j] = temp; // 插入到正确位置
}
gap = Math.floor(gap / 2); // 缩小增量
}
return arr;
}
// 测试
const unsorted = [64, 34, 25, 12, 22, 11, 90];
console.log("原数组:", unsorted);
console.log("希尔排序后:", shellSort([...unsorted]));
🔍 执行过程简例(以 [64, 34, 25, 12, 22, 11, 90] 为例):
- n = 7
- gap = 3 → 分成 3 组:
- 组0: 64, 12, 90 → 排序后:12, 64, 90
- 组1: 34, 22 → 排序后:22, 34
- 组2: 25, 11 → 排序后:11, 25
→ 数组变为:[12, 22, 11, 64, 34, 25, 90]
- gap = 1 → 全体进行插入排序(此时已较有序)→ 最终:
[11, 12, 22, 25, 34, 64, 90]
⏱️ 时间复杂度(取决于增量序列):
- 使用
n/2, n/4, ..., 1序列:平均 O(n^1.3) ,最坏 O(n²) - 使用更优序列(如 Sedgewick、Knuth):可接近 O(n log²n)
✅ 优点:
- 比直接插入排序快得多(尤其对中等规模数据)
- 原地排序(O(1) 空间)
- 实现简单
❌ 缺点:
- 不稳定(相等元素可能因跨组移动而改变相对顺序)
- 增量序列选择影响性能
💡 总结:
希尔排序 = 多轮分组插入排序 + 增量逐步缩小
它是对插入排序的聪明改进 ,特别适合中等规模、无法使用归并/快排的场景(如嵌入式系统、教学演示等)。
您问到了希尔排序中最关键的部分!让我详细解释分组机制和排序后的重组过程。
希尔排序的分组机制
分组规则
增量gap决定了分组的跨度 :将数组中所有索引相差gap倍数的元素分到同一组。
当增量gap=3时的分组示例
假设数组为:[64, 34, 25, 12, 22, 11, 90, 38, 77] (n=9)
分组过程:
原始数组索引: 0 1 2 3 4 5 6 7 8
原始数组值: [64, 34, 25, 12, 22, 11, 90, 38, 77]
分组增量gap=3
分组情况:
第1组(索引%3 = 0):索引 0, 3, 6 → 值 [64, 12, 90]
第2组(索引%3 = 1):索引 1, 4, 7 → 值 [34, 22, 38]
第3组(索引%3 = 2):索引 2, 5, 8 → 值 [25, 11, 77]
组内分别排序:
第1组排序:[12, 64, 90]
第2组排序:[22, 34, 38]
第3组排序:[11, 25, 77]
组内排序后如何重组?
关键点:元素的位置是固定的,我们只改变位置上的值!
排序后重组过程:
1. 把排序后的值放回原索引位置:
第1组:[12, 64, 90] 放回索引 0,3,6
索引0 ← 12
索引3 ← 64
索引6 ← 90
第2组:[22, 34, 38] 放回索引 1,4,7
索引1 ← 22
索引4 ← 34
索引7 ← 38
第3组:[11, 25, 77] 放回索引 2,5,8
索引2 ← 11
索引5 ← 25
索引8 ← 77
最终结果:
索引: 0 1 2 3 4 5 6 7 8
值: [12, 22, 11, 64, 34, 25, 90, 38, 77]
JavaScript可视化示例
javascript
function visualizeGrouping(arr, gap) {
console.log(`\n=== 增量 gap = ${gap} 的分组排序过程 ===`);
console.log(`原始数组: [${arr}]`);
console.log(`数组长度: ${arr.length}`);
// 1. 显示分组情况
const groups = [];
const groupIndices = [];
for (let g = 0; g < gap; g++) {
const group = [];
const indices = [];
// 收集同一组的元素和索引
for (let i = g; i < arr.length; i += gap) {
group.push(arr[i]);
indices.push(i);
}
groups.push(group);
groupIndices.push(indices);
console.log(`第${g+1}组 (索引${g}开始, 步长${gap}):`);
console.log(` 索引: [${indices}]`);
console.log(` 元素: [${group}]`);
}
// 2. 组内排序(分别对每个组进行插入排序)
const sortedGroups = [];
for (let g = 0; g < gap; g++) {
const group = [...groups[g]];
const indices = groupIndices[g];
// 对当前组进行插入排序
for (let i = 1; i < group.length; i++) {
const key = group[i];
let j = i - 1;
while (j >= 0 && group[j] > key) {
group[j + 1] = group[j];
j--;
}
group[j + 1] = key;
}
sortedGroups.push(group);
console.log(` 排序后: [${group}]`);
}
// 3. 重组回原数组
const result = [...arr];
for (let g = 0; g < gap; g++) {
const sortedGroup = sortedGroups[g];
const indices = groupIndices[g];
// 将排序后的元素放回原索引位置
for (let i = 0; i < sortedGroup.length; i++) {
result[indices[i]] = sortedGroup[i];
}
}
console.log(`重组后的数组: [${result}]`);
return result;
}
// 测试gap=3的情况
console.log("希尔排序分组机制详解:");
const testArray = [64, 34, 25, 12, 22, 11, 90, 38, 77];
console.log("初始数组:", testArray);
// 第一轮:gap=3
let step1 = visualizeGrouping(testArray, 3);
// 第二轮:gap=1(最终排序)
console.log("\n=== 最终插入排序 (gap=1) ===");
console.log("上一轮结果:", step1);
const finalSorted = [...step1].sort((a, b) => a - b);
console.log("排序结果:", finalSorted);
完整希尔排序过程演示
javascript
function shellSortWithVisualization(arr) {
console.log("希尔排序全过程演示");
console.log("=".repeat(60));
let n = arr.length;
let currentArray = [...arr];
// 使用增量序列:n/2, n/4, ..., 1
for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
console.log(`\n▶︎ 当前增量 gap = ${gap}`);
console.log(`当前数组: [${currentArray}]`);
// 对每个gap值进行多组排序
for (let i = gap; i < n; i++) {
let temp = currentArray[i];
let j = i;
// 对当前元素所在的子序列进行插入排序
while (j >= gap && currentArray[j - gap] > temp) {
currentArray[j] = currentArray[j - gap];
j -= gap;
}
currentArray[j] = temp;
// 显示当前步骤
console.log(` i=${i}: 将 ${temp} 插入到位置 ${j}, 数组: [${currentArray}]`);
}
// 显示当前gap排序后的分组情况
console.log(`\n gap=${gap} 排序后分组情况:`);
for (let g = 0; g < gap; g++) {
const group = [];
for (let i = g; i < n; i += gap) {
group.push(currentArray[i]);
}
console.log(` 组${g+1}: [${group.join(', ')}]`);
}
console.log(` gap=${gap} 最终结果: [${currentArray}]`);
}
console.log("\n" + "=".repeat(60));
console.log(`最终排序结果: [${currentArray}]`);
return currentArray;
}
// 运行示例
const array = [49, 38, 65, 97, 76, 13, 27, 49, 55, 4];
console.log("原始数组:", array);
shellSortWithVisualization(array);
关键理解要点
1. 分组是逻辑上的,不是物理上的
- 我们不实际创建多个子数组
- 只是在逻辑上把相距gap的元素视为一组
- 排序时在原始数组中跳跃访问
2. 索引映射关系
当 gap = 3 时:
第1组元素索引:0, 3, 6, 9, 12... (索引 % 3 = 0)
第2组元素索引:1, 4, 7, 10, 13... (索引 % 3 = 1)
第3组元素索引:2, 5, 8, 11, 14... (索引 % 3 = 2)
3. 排序过程可视化
初始: [49, 38, 65, 97, 76, 13, 27, 49, 55, 4]
gap=5: 分为5组,每组2个元素
组1: [49, 13] → 排序 → [13, 49]
组2: [38, 27] → 排序 → [27, 38]
...
放回原位置 → 得到新数组
4. 为什么这样有效?
- 大步长排序:快速把大元素移到后面,小元素移到前面
- 逐步减小步长:越来越精细地调整
- 最终gap=1:完全有序
简单记忆 :希尔排序就是跳跃式的插入排序,每次跳跃的步长逐渐减小到1。
3. 简单选择排序法
您总结得非常准确!简单选择排序的核心就是 "选择" 和 "交换"。让我为您详细解释并提供一个JavaScript实现示例。
简单选择排序核心思想
算法步骤
- 第1趟:从第1个到第n个元素中找出最小的,与第1个元素交换
- 第2趟:从第2个到第n个元素中找出最小的,与第2个元素交换
- 第i趟:从第i个到第n个元素中找出最小的,与第i个元素交换
- 第n-1趟:从第n-1个和第n个元素中找出最小的,完成排序
关键特点
- 比较次数固定:总是需要 n(n-1)/2 次比较
- 交换次数最少:最多 n-1 次交换
- 不稳定排序:可能改变相同元素的相对顺序
- 原地排序:空间复杂度 O(1)
JavaScript实现示例
javascript
// 简单选择排序 - 基础版本
function selectionSort(arr) {
const n = arr.length;
console.log(`原始数组: [${arr}]`);
console.log(`数组长度: ${n}`);
console.log('='.repeat(60));
for (let i = 0; i < n - 1; i++) {
console.log(`\n第 ${i + 1} 趟排序:`);
console.log(` 当前数组: [${arr}]`);
// 1. 找出最小元素的索引
let minIndex = i;
console.log(` 初始认为最小元素是 arr[${minIndex}] = ${arr[minIndex]}`);
for (let j = i + 1; j < n; j++) {
console.log(` 比较: arr[${j}] = ${arr[j]} < arr[${minIndex}] = ${arr[minIndex]} ? ${arr[j] < arr[minIndex]}`);
if (arr[j] < arr[minIndex]) {
console.log(` 更新最小元素索引: ${minIndex} → ${j}`);
minIndex = j;
}
}
console.log(` 本趟找到的最小元素: arr[${minIndex}] = ${arr[minIndex]}`);
// 2. 交换元素(如果最小元素不是当前位置)
if (minIndex !== i) {
console.log(` 交换: arr[${i}](${arr[i]}) ↔ arr[${minIndex}](${arr[minIndex]})`);
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
} else {
console.log(` 最小元素已在正确位置,无需交换`);
}
console.log(` 第 ${i + 1} 趟排序后: [${arr}]`);
}
console.log('\n' + '='.repeat(60));
console.log(`最终排序结果: [${arr}]`);
return arr;
}
// 测试示例
console.log("简单选择排序完整过程演示:\n");
const testArray = [64, 25, 12, 22, 11];
selectionSort([...testArray]);
排序过程可视化
运行上面的代码,输出如下:
简单选择排序完整过程演示:
原始数组: [64, 25, 12, 22, 11]
数组长度: 5
============================================================
第 1 趟排序:
当前数组: [64, 25, 12, 22, 11]
初始认为最小元素是 arr[0] = 64
比较: arr[1] = 25 < arr[0] = 64 ? true
更新最小元素索引: 0 → 1
比较: arr[2] = 12 < arr[1] = 25 ? true
更新最小元素索引: 1 → 2
比较: arr[3] = 22 < arr[2] = 12 ? false
比较: arr[4] = 11 < arr[2] = 12 ? true
更新最小元素索引: 2 → 4
本趟找到的最小元素: arr[4] = 11
交换: arr[0](64) ↔ arr[4](11)
第 1 趟排序后: [11, 25, 12, 22, 64]
第 2 趟排序:
当前数组: [11, 25, 12, 22, 64]
初始认为最小元素是 arr[1] = 25
比较: arr[2] = 12 < arr[1] = 25 ? true
更新最小元素索引: 1 → 2
比较: arr[3] = 22 < arr[2] = 12 ? false
比较: arr[4] = 64 < arr[2] = 12 ? false
本趟找到的最小元素: arr[2] = 12
交换: arr[1](25) ↔ arr[2](12)
第 2 趟排序后: [11, 12, 25, 22, 64]
第 3 趟排序:
当前数组: [11, 12, 25, 22, 64]
初始认为最小元素是 arr[2] = 25
比较: arr[3] = 22 < arr[2] = 25 ? true
更新最小元素索引: 2 → 3
比较: arr[4] = 64 < arr[3] = 22 ? false
本趟找到的最小元素: arr[3] = 22
交换: arr[2](25) ↔ arr[3](22)
第 3 趟排序后: [11, 12, 22, 25, 64]
第 4 趟排序:
当前数组: [11, 12, 22, 25, 64]
初始认为最小元素是 arr[3] = 25
比较: arr[4] = 64 < arr[3] = 25 ? false
本趟找到的最小元素: arr[3] = 25
最小元素已在正确位置,无需交换
第 4 趟排序后: [11, 12, 22, 25, 64]
============================================================
最终排序结果: [11, 12, 22, 25, 64]
优化版本和变体
javascript
// 1. 同时找最小和最大元素(双向选择排序)
function selectionSortTwoWay(arr) {
console.log("双向选择排序(同时找最小和最大)");
const n = arr.length;
let left = 0;
let right = n - 1;
while (left < right) {
let minIndex = left;
let maxIndex = left;
console.log(`\n区间 [${left}, ${right}]: [${arr.slice(left, right + 1)}]`);
// 找出当前区间的最小和最大元素索引
for (let i = left + 1; i <= right; i++) {
if (arr[i] < arr[minIndex]) minIndex = i;
if (arr[i] > arr[maxIndex]) maxIndex = i;
}
console.log(` 最小元素: arr[${minIndex}] = ${arr[minIndex]}`);
console.log(` 最大元素: arr[${maxIndex}] = ${arr[maxIndex]}`);
// 将最小元素放到左边
if (minIndex !== left) {
console.log(` 交换最小: arr[${left}](${arr[left]}) ↔ arr[${minIndex}](${arr[minIndex]})`);
[arr[left], arr[minIndex]] = [arr[minIndex], arr[left]];
// 如果最大元素原本在left位置,现在被移动到了minIndex位置
if (maxIndex === left) maxIndex = minIndex;
}
// 将最大元素放到右边
if (maxIndex !== right) {
console.log(` 交换最大: arr[${right}](${arr[right]}) ↔ arr[${maxIndex}](${arr[maxIndex]})`);
[arr[right], arr[maxIndex]] = [arr[maxIndex], arr[right]];
}
left++;
right--;
console.log(` 当前数组: [${arr}]`);
}
return arr;
}
// 2. 稳定版本的选择排序(不改变相同元素的相对顺序)
function stableSelectionSort(arr) {
console.log("\n稳定选择排序(保持相同元素的顺序)");
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIndex = i;
// 找出最小元素的索引
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将最小元素移到当前位置,但不直接交换
const minValue = arr[minIndex];
// 将i到minIndex-1的元素向右移动一位
for (let k = minIndex; k > i; k--) {
arr[k] = arr[k - 1];
}
arr[i] = minValue;
console.log(`第${i + 1}趟: [${arr}]`);
}
return arr;
}
// 3. 选择排序性能分析
function selectionSortAnalysis(arrSize = 10000) {
console.log(`\n性能分析:对 ${arrSize} 个随机数排序`);
// 生成随机数组
const randomArray = Array.from({length: arrSize},
() => Math.floor(Math.random() * arrSize * 10));
// 测试选择排序
const arr1 = [...randomArray];
console.time('选择排序耗时');
selectionSort(arr1);
console.timeEnd('选择排序耗时');
// 测试双向选择排序
const arr2 = [...randomArray];
console.time('双向选择排序耗时');
selectionSortTwoWay(arr2);
console.timeEnd('双向选择排序耗时');
// 测试原生排序(对比)
const arr3 = [...randomArray];
console.time('Array.sort()耗时');
arr3.sort((a, b) => a - b);
console.timeEnd('Array.sort()耗时');
}
// 运行示例
console.log("选择排序变体演示:\n");
// 测试双向选择排序
const testArray2 = [64, 25, 12, 22, 11, 90, 38];
console.log("原始数组:", testArray2);
selectionSortTwoWay([...testArray2]);
// 测试稳定版本
const testArray3 = [4, 2, 2, 1, 3, 2];
console.log("\n原始数组(含重复元素):", testArray3);
stableSelectionSort([...testArray3]);
// 性能测试(取消注释运行)
// selectionSortAnalysis(10000);
时间复杂度分析
比较次数
第1趟:n-1 次比较
第2趟:n-2 次比较
...
第n-1趟:1 次比较
总比较次数 = (n-1) + (n-2) + ... + 1 = n(n-1)/2
交换次数
- 最好情况:0次(数组已有序)
- 最坏情况:n-1次(每次都需要交换)
- 平均情况:约 n-1 次
性能总结
| 情况 | 时间复杂度 | 空间复杂度 | 是否稳定 |
|---|---|---|---|
| 最好 | O(n²) | O(1) | 否 |
| 平均 | O(n²) | O(1) | 否 |
| 最坏 | O(n²) | O(1) | 否 |
选择排序 vs 冒泡排序
javascript
// 对比选择排序和冒泡排序
function compareSortingAlgorithms() {
console.log("选择排序 vs 冒泡排序 对比\n");
const testArray = [5, 3, 8, 4, 2];
console.log("测试数组:", testArray);
// 选择排序
console.log("\n1. 选择排序:");
let arr1 = [...testArray];
let selectionComparisons = 0;
let selectionSwaps = 0;
for (let i = 0; i < arr1.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < arr1.length; j++) {
selectionComparisons++;
if (arr1[j] < arr1[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
selectionSwaps++;
[arr1[i], arr1[minIndex]] = [arr1[minIndex], arr1[i]];
}
}
console.log(` 比较次数: ${selectionComparisons}`);
console.log(` 交换次数: ${selectionSwaps}`);
console.log(` 结果: [${arr1}]`);
// 冒泡排序
console.log("\n2. 冒泡排序:");
let arr2 = [...testArray];
let bubbleComparisons = 0;
let bubbleSwaps = 0;
for (let i = 0; i < arr2.length - 1; i++) {
for (let j = 0; j < arr2.length - 1 - i; j++) {
bubbleComparisons++;
if (arr2[j] > arr2[j + 1]) {
bubbleSwaps++;
[arr2[j], arr2[j + 1]] = [arr2[j + 1], arr2[j]];
}
}
}
console.log(` 比较次数: ${bubbleComparisons}`);
console.log(` 交换次数: ${bubbleSwaps}`);
console.log(` 结果: [${arr2}]`);
console.log("\n总结:");
console.log("- 选择排序: 比较次数固定(n²/2),交换次数较少(n-1)");
console.log("- 冒泡排序: 比较次数固定(n²/2),交换次数可能很多");
console.log("- 选择排序更适合交换成本高的场景");
}
// 运行对比
compareSortingAlgorithms();
实际应用场景
虽然简单选择排序的时间复杂度是O(n²),但在某些特定情况下仍有应用价值:
- 数据交换成本高时:选择排序的交换次数最少
- 内存受限环境:原地排序,空间复杂度O(1)
- 小规模数据:实现简单,代码量少
- 教学目的:理解排序算法的基本原理
核心要点 :选择排序的核心是 "先选择,后交换",每趟只做一次交换(如果需要),这与冒泡排序每趟可能多次交换形成鲜明对比。
4.堆排序
堆排序介绍
堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。它是不稳定的排序算法,时间复杂度为 O(n log n)。
核心概念:
-
二叉堆:完全二叉树,分为最大堆和最小堆
- 最大堆:父节点值 ≥ 子节点值
- 最小堆:父节点值 ≤ 子节点值
-
主要步骤:
- 建堆:将无序数组构建成堆
- 排序:反复取出堆顶元素(最大/最小值),调整剩余部分为堆
算法步骤:
- 构建最大堆
- 将堆顶元素(最大值)与末尾元素交换
- 减小堆大小,重新调整堆
- 重复步骤2-3,直到排序完成
JavaScript 实现示例
javascript
/**
* 堆排序
* @param {Array} arr - 待排序数组
* @returns {Array} 排序后的数组
*/
function heapSort(arr) {
const n = arr.length;
// 1. 构建最大堆(从最后一个非叶子节点开始)
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 2. 逐个提取堆顶元素
for (let i = n - 1; i > 0; i--) {
// 将堆顶(最大值)与当前末尾交换
[arr[0], arr[i]] = [arr[i], arr[0]];
// 调整剩余部分为最大堆
heapify(arr, i, 0);
}
return arr;
}
/**
* 调整堆(最大堆)
* @param {Array} arr - 堆数组
* @param {number} n - 堆的大小
* @param {number} i - 当前节点索引
*/
function heapify(arr, n, i) {
let largest = i; // 初始化最大值为根节点
const left = 2 * i + 1; // 左子节点索引
const right = 2 * i + 2; // 右子节点索引
// 如果左子节点存在且大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点存在且大于当前最大值
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点
if (largest !== i) {
// 交换根节点与最大值节点
[arr[i], arr[largest]] = [arr[largest], arr[i]];
// 递归调整受影响的子树
heapify(arr, n, largest);
}
}
// 测试示例
const unsortedArray = [64, 34, 25, 12, 22, 11, 90, 88, 3, 5];
console.log('排序前:', unsortedArray);
const sortedArray = heapSort([...unsortedArray]);
console.log('排序后:', sortedArray);
// 另一种更简洁的实现(使用最小堆实现升序排序)
function heapSortMin(arr) {
const minHeap = [...arr];
const n = minHeap.length;
const result = [];
// 构建最小堆
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapifyMin(minHeap, n, i);
}
// 提取堆顶元素
for (let i = n - 1; i >= 0; i--) {
result.push(minHeap[0]);
minHeap[0] = minHeap[i];
heapifyMin(minHeap, i, 0);
}
return result;
}
function heapifyMin(arr, n, i) {
let smallest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n && arr[left] < arr[smallest]) {
smallest = left;
}
if (right < n && arr[right] < arr[smallest]) {
smallest = right;
}
if (smallest !== i) {
[arr[i], arr[smallest]] = [arr[smallest], arr[i]];
heapifyMin(arr, n, smallest);
}
}
// 性能测试
console.log('\n性能测试:');
const largeArray = Array.from({length: 10000}, () => Math.floor(Math.random() * 10000));
console.time('堆排序耗时');
heapSort([...largeArray]);
console.timeEnd('堆排序耗时');
// 可视化演示函数
function visualizeHeap(arr) {
console.log('\n堆结构可视化:');
let level = 0;
let itemsInLevel = 1;
let count = 0;
while (count < arr.length) {
const levelItems = [];
for (let i = 0; i < itemsInLevel && count < arr.length; i++) {
levelItems.push(arr[count]);
count++;
}
console.log(`第 ${level} 层: ${levelItems.join(' ')}`);
level++;
itemsInLevel *= 2;
}
}
// 可视化演示
const demoArray = [4, 10, 3, 5, 1];
console.log('\n堆构建过程演示:');
console.log('原始数组:', demoArray);
visualizeHeap(demoArray);
// 构建最大堆
const heapArray = [...demoArray];
for (let i = Math.floor(heapArray.length / 2) - 1; i >= 0; i--) {
heapify(heapArray, heapArray.length, i);
}
console.log('\n构建的最大堆:', heapArray);
visualizeHeap(heapArray);
算法特点
优点:
- 时间复杂度稳定:始终为 O(n log n)
- 空间效率高:只需 O(1) 额外空间
- 适合大数据集:不受输入数据分布影响
缺点:
- 不稳定:相等元素的相对位置可能改变
- 缓存不友好:频繁的远距离元素交换
时间复杂度:
- 最坏情况:O(n log n)
- 平均情况:O(n log n)
- 最好情况:O(n log n)
空间复杂度:
- O(1)(原地排序)
适用场景
- 需要 O(n log n) 时间复杂度且对稳定性无要求
- 内存受限环境(原地排序)
- 需要获取前 k 个最大/最小元素
堆排序在 JavaScript 中的实现相对简单,但实际应用中,由于 V8 引擎对 Array.prototype.sort() 的优化,通常内置的 sort() 方法性能更好,除非有特殊需求。
您说得对!"建堆"确实是将无序数组构造成一个完全二叉树形式的堆。让我用一个真实数组来详细分析这个过程。
真实案例分析
示例数组:[3, 9, 2, 1, 4, 5]
第1步:理解数组与二叉树的对应关系
对于数组索引 i:
- 父节点索引:
Math.floor((i-1)/2) - 左子节点索引:
2*i + 1 - 右子节点索引:
2*i + 2
我们的数组 [3, 9, 2, 1, 4, 5] 对应以下完全二叉树结构:
索引: 0 1 2 3 4 5
数组: [3, 9, 2, 1, 4, 5]
对应的二叉树结构:
3(0)
/ \
9(1) 2(2)
/ \ /
1(3) 4(4)5(5)
第2步:建堆过程(构建最大堆)
我们从最后一个非叶子节点开始向前调整 。最后一个非叶子节点的索引是 Math.floor(n/2) - 1 = Math.floor(6/2) - 1 = 2
1. 调整索引 2 的节点(值为 2)
初始状态:
3
/ \
9 2 ← 调整这个节点
/ \ /
1 4 5
节点2与子节点5比较:2 < 5,需要交换
交换后:
3
/ \
9 5
/ \ /
1 4 2
2. 调整索引 1 的节点(值为 9)
当前状态:
3
/ \
9 5 ← 调整这个节点
/ \ /
1 4 2
节点9与子节点1、4比较:
9 > 1 且 9 > 4,不需要交换
3. 调整索引 0 的节点(值为 3)
当前状态:
3 ← 调整这个节点
/ \
9 5
/ \ /
1 4 2
节点3与子节点9、5比较:
3 < 9,需要交换
交换后:
9
/ \
3 5
/ \ /
1 4 2
现在节点3需要继续调整
节点3与子节点1、4比较:
3 < 4,需要交换
交换后:
9
/ \
4 5
/ \ /
1 3 2
最终的最大堆:
9
/ \
4 5
/ \ /
1 3 2
对应数组:[9, 4, 5, 1, 3, 2]
完整代码演示
javascript
/**
* 可视化显示建堆过程
*/
function visualizeHeapConstruction(originalArray) {
console.log('=== 建堆过程详细分析 ===\n');
console.log('原始数组:', originalArray);
console.log('对应二叉树:');
console.log(` ${originalArray[0]}`);
console.log(` / \\`);
console.log(` ${originalArray[1]} ${originalArray[2]}`);
console.log(` / \\ /`);
console.log(` ${originalArray[3]} ${originalArray[4]} ${originalArray[5]}`);
// 复制数组进行建堆
const arr = [...originalArray];
const n = arr.length;
console.log('\n--- 建堆步骤 ---');
// 计算最后一个非叶子节点索引
const lastNonLeafIdx = Math.floor(n / 2) - 1;
console.log(`最后一个非叶子节点索引: ${lastNonLeafIdx} (值: ${arr[lastNonLeafIdx]})`);
// 从最后一个非叶子节点向前调整
for (let i = lastNonLeafIdx; i >= 0; i--) {
console.log(`\n第${lastNonLeafIdx - i + 1}步: 调整索引 ${i} 的节点 (值: ${arr[i]})`);
heapifyWithVisualization(arr, n, i);
console.log(`当前数组: [${arr.join(', ')}]`);
visualizeArrayAsTree(arr);
}
console.log('\n=== 建堆完成 ===');
console.log('最终最大堆数组:', arr);
return arr;
}
/**
* 调整堆并显示过程
*/
function heapifyWithVisualization(arr, n, i) {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
// 显示比较信息
let comparisonInfo = ` 比较: 节点${arr[i]} (索引${i})`;
if (left < n) {
comparisonInfo += ` vs 左子节点${arr[left]} (索引${left})`;
if (arr[left] > arr[largest]) {
largest = left;
comparisonInfo += ` ← 较大`;
}
}
if (right < n) {
comparisonInfo += ` vs 右子节点${arr[right]} (索引${right})`;
if (arr[right] > arr[largest]) {
largest = right;
comparisonInfo += ` ← 较大`;
}
}
console.log(comparisonInfo);
if (largest !== i) {
console.log(` 交换: ${arr[i]} ↔ ${arr[largest]}`);
[arr[i], arr[largest]] = [arr[largest], arr[i]];
// 递归调整
heapifyWithVisualization(arr, n, largest);
} else {
console.log(` 无需交换`);
}
}
/**
* 将数组可视化为二叉树
*/
function visualizeArrayAsTree(arr) {
console.log('当前二叉树结构:');
// 计算树的高度
const height = Math.floor(Math.log2(arr.length)) + 1;
// 打印树
let level = 0;
let index = 0;
while (level < height && index < arr.length) {
const nodesInLevel = Math.min(Math.pow(2, level), arr.length - index);
const spaces = Math.pow(2, height - level - 1) - 1;
// 打印前导空格
process.stdout.write(' '.repeat(spaces * 3));
// 打印当前层节点
const nodes = [];
for (let i = 0; i < nodesInLevel && index < arr.length; i++) {
nodes.push(arr[index++]);
}
console.log(nodes.map(val => val.toString().padStart(2, ' ')).join(' '));
level++;
}
console.log();
}
// 运行演示
const testArray = [3, 9, 2, 1, 4, 5];
const maxHeap = visualizeHeapConstruction(testArray);
// 扩展:再演示一个更大的例子
console.log('\n\n=== 另一个例子:数组 [7, 12, 6, 10, 17, 15, 2, 4] ===');
const anotherArray = [7, 12, 6, 10, 17, 15, 2, 4];
visualizeHeapConstruction(anotherArray);
手动建堆步骤总结
对于数组 [3, 9, 2, 1, 4, 5],手动建最大堆:
-
初始状态:
3 / \ 9 2 / \ / 1 4 5 -
从下往上,从左往右调整(实际从最后一个非叶子节点开始):
- 调整节点2:与子节点5交换 →
[3, 9, 5, 1, 4, 2] - 调整节点9:无需交换 →
[3, 9, 5, 1, 4, 2] - 调整节点3:先与9交换,再与4交换 →
[9, 4, 5, 1, 3, 2]
- 调整节点2:与子节点5交换 →
-
最终最大堆:
9 / \ 4 5 / \ / 1 3 2
关键要点
- 完全二叉树性质:数组按层序填充二叉树,没有"洞"
- 建堆起点 :从最后一个非叶子节点 开始(索引
Math.floor(n/2)-1) - 调整方向:自底向上,但每个节点的调整是向下进行的(heapify)
- 时间复杂度:建堆 O(n),整个堆排序 O(n log n)
这个建堆过程确保了数组满足最大堆性质:每个父节点的值都大于或等于其子节点的值。
使用 [3, 9, 2, 1, 4, 5] 完整演示堆排序过程
javascript
/**
* 使用 [3, 9, 2, 1, 4, 5] 演示堆排序完整过程
*/
function visualizeHeapSortExample() {
console.log('=== 使用数组 [3, 9, 2, 1, 4, 5] 演示堆排序 ===\n');
// 原始数组
const originalArray = [3, 9, 2, 1, 4, 5];
let arr = [...originalArray];
const n = arr.length;
console.log('原始数组:', arr);
console.log('对应二叉树:');
visualizeTree(arr);
console.log('\n--- 第1阶段:构建最大堆 ---');
console.log('从最后一个非叶子节点开始调整(索引 = Math.floor(n/2)-1 = 2)');
// 建堆过程
console.log('\n1. 调整索引2(值=2):');
console.log(' 2的子节点:左子索引5(值=5)');
console.log(' 2 < 5,所以交换 2 ↔ 5');
[arr[2], arr[5]] = [arr[5], arr[2]];
console.log(' 当前数组:', arr);
visualizeTree(arr);
console.log('\n2. 调整索引1(值=9):');
console.log(' 9的子节点:左子索引3(值=1),右子索引4(值=4)');
console.log(' 9 > 1 且 9 > 4,无需交换');
console.log(' 当前数组:', arr);
visualizeTree(arr);
console.log('\n3. 调整索引0(值=3):');
console.log(' 3的子节点:左子索引1(值=9),右子索引2(值=5)');
console.log(' 3 < 9,所以交换 3 ↔ 9');
[arr[0], arr[1]] = [arr[1], arr[0]];
console.log(' 当前数组:', arr);
visualizeTree(arr);
console.log(' 现在索引1的值=3,继续调整:');
console.log(' 3的子节点:左子索引3(值=1),右子索引4(值=4)');
console.log(' 3 < 4,所以交换 3 ↔ 4');
[arr[1], arr[4]] = [arr[4], arr[1]];
console.log(' 最终最大堆数组:', arr);
visualizeTree(arr);
console.log('\n--- 最大堆构建完成 ---');
console.log('数组变为:', arr, '([9, 4, 5, 1, 3, 2])');
console.log('\n--- 第2阶段:排序过程 ---');
console.log('反复取出堆顶(最大值)与末尾交换,然后调整堆\n');
// 开始排序
for (let i = n - 1; i > 0; i--) {
const round = n - i;
console.log(`\n=== 第${round}轮排序(剩余${i+1}个元素) ===`);
console.log(`当前数组: [${arr.join(', ')}]`);
console.log(`堆顶 arr[0] = ${arr[0]}(最大值)`);
console.log(`末尾 arr[${i}] = ${arr[i]}`);
console.log(`交换 ${arr[0]} ↔ ${arr[i]}`);
// 交换堆顶和末尾
[arr[0], arr[i]] = [arr[i], arr[0]];
console.log(`交换后: [${arr.join(', ')}]`);
console.log(`现在 ${arr[i]} 已到达最终位置(索引${i})`);
// 显示已排序部分
const sortedPart = arr.slice(i);
const unsortedPart = arr.slice(0, i);
console.log(`已排序部分(右侧): [${sortedPart.join(', ')}]`);
console.log(`未排序部分(左侧${i}个): [${unsortedPart.join(', ')}]`);
if (i > 1) {
console.log('\n调整剩余部分为最大堆:');
console.log('从堆顶 arr[0] =', arr[0], '开始调整');
// 手动演示调整过程
let current = 0;
let adjusted = false;
while (true) {
const left = 2 * current + 1;
const right = 2 * current + 2;
let largest = current;
console.log(` 调整节点 arr[${current}] = ${arr[current]}`);
if (left < i && arr[left] > arr[largest]) {
console.log(` 左子节点 arr[${left}] = ${arr[left]} > ${arr[largest]}`);
largest = left;
}
if (right < i && arr[right] > arr[largest]) {
console.log(` 右子节点 arr[${right}] = ${arr[right]} > ${arr[largest]}`);
largest = right;
}
if (largest !== current) {
console.log(` 交换 ${arr[current]} ↔ ${arr[largest]}`);
[arr[current], arr[largest]] = [arr[largest], arr[current]];
current = largest;
adjusted = true;
} else {
console.log(` 无需交换,调整完成`);
break;
}
}
console.log(`调整后未排序部分: [${arr.slice(0, i).join(', ')}]`);
console.log(`完整数组: [${arr.join(', ')}]`);
}
visualizeTree(arr.slice(0, i), `第${round}轮后的堆结构`);
}
console.log('\n=== 排序完成 ===');
console.log('最终排序结果:', arr);
console.log('升序排列:', arr);
return arr;
}
// 可视化二叉树
function visualizeTree(arr, title = '二叉树结构') {
if (arr.length === 0) {
console.log('(空树)');
return;
}
console.log(title + ':');
// 简单可视化
if (arr.length === 1) {
console.log(` ${arr[0]}`);
} else if (arr.length === 2) {
console.log(` ${arr[0]}`);
console.log(` /`);
console.log(`${arr[1]}`);
} else if (arr.length === 3) {
console.log(` ${arr[0]}`);
console.log(` / \\`);
console.log(`${arr[1]} ${arr[2]}`);
} else if (arr.length === 4) {
console.log(` ${arr[0]}`);
console.log(` / \\`);
console.log(` ${arr[1]} ${arr[2]}`);
console.log(` /`);
console.log(`${arr[3]}`);
} else if (arr.length === 5) {
console.log(` ${arr[0]}`);
console.log(` / \\`);
console.log(` ${arr[1]} ${arr[2]}`);
console.log(` / \\`);
console.log(`${arr[3]} ${arr[4]}`);
} else if (arr.length === 6) {
console.log(` ${arr[0]}`);
console.log(` / \\`);
console.log(` ${arr[1]} ${arr[2]}`);
console.log(` / \\ /`);
console.log(`${arr[3]} ${arr[4]} ${arr[5]}`);
}
console.log();
}
// 运行演示
visualizeHeapSortExample();
手动逐步分析:[3, 9, 2, 1, 4, 5]
阶段1:构建最大堆(完成)
初始:[3, 9, 2, 1, 4, 5]
构建后:[9, 4, 5, 1, 3, 2]
最大堆结构:
9
/ \
4 5
/ \ /
1 3 2
阶段2:排序过程
第1轮(i=5):
-
交换 :堆顶9 ↔ 末尾2 →
[2, 4, 5, 1, 3, 9] -
已排序 :
[9](位置固定) -
调整堆 (前5个元素):
- 2与子节点4、5比较,2<5 → 交换2↔5 →
[5, 4, 2, 1, 3, 9] - 2与子节点比较(无右子,左子3>2)→ 交换2↔3 →
[5, 4, 3, 1, 2, 9]
- 2与子节点4、5比较,2<5 → 交换2↔5 →
-
结果 :
[5, 4, 3, 1, 2, 9] -
堆结构 :
5 / \ 4 3 / \ 1 2
第2轮(i=4):
-
交换 :堆顶5 ↔ 末尾2 →
[2, 4, 3, 1, 5, 9] -
已排序 :
[5, 9] -
调整堆 (前4个元素):
- 2与子节点4、3比较,2<4 → 交换2↔4 →
[4, 2, 3, 1, 5, 9] - 2与子节点1比较(无右子),2>1 → 无需交换
- 2与子节点4、3比较,2<4 → 交换2↔4 →
-
结果 :
[4, 2, 3, 1, 5, 9] -
堆结构 :
4 / \ 2 3 / 1
第3轮(i=3):
-
交换 :堆顶4 ↔ 末尾1 →
[1, 2, 3, 4, 5, 9] -
已排序 :
[4, 5, 9] -
调整堆 (前3个元素):
- 1与子节点2、3比较,1<3 → 交换1↔3 →
[3, 2, 1, 4, 5, 9]
- 1与子节点2、3比较,1<3 → 交换1↔3 →
-
结果 :
[3, 2, 1, 4, 5, 9] -
堆结构 :
3 / \ 2 1
第4轮(i=2):
-
交换 :堆顶3 ↔ 末尾1 →
[1, 2, 3, 4, 5, 9] -
已排序 :
[3, 4, 5, 9] -
调整堆 (前2个元素):
- 1与子节点2比较,1<2 → 交换1↔2 →
[2, 1, 3, 4, 5, 9]
- 1与子节点2比较,1<2 → 交换1↔2 →
-
结果 :
[2, 1, 3, 4, 5, 9] -
堆结构 :
2 / 1
第5轮(i=1):
- 交换 :堆顶2 ↔ 末尾1 →
[1, 2, 3, 4, 5, 9] - 已排序 :
[2, 3, 4, 5, 9] - 调整堆(前1个元素):只剩1个元素,无需调整
- 最终结果 :
[1, 2, 3, 4, 5, 9]
排序过程总结表
| 轮次 | 操作前数组 | 交换操作 | 调整堆后 | 已排序部分(右侧) |
|---|---|---|---|---|
| 初始 | [9,4,5,1,3,2] | - | - | [] |
| 第1轮 | [9,4,5,1,3,2] | 9↔2 | [5,4,3,1,2,9] | [9] |
| 第2轮 | [5,4,3,1,2,9] | 5↔2 | [4,2,3,1,5,9] | [5,9] |
| 第3轮 | [4,2,3,1,5,9] | 4↔1 | [3,2,1,4,5,9] | [4,5,9] |
| 第4轮 | [3,2,1,4,5,9] | 3↔1 | [2,1,3,4,5,9] | [3,4,5,9] |
| 第5轮 | [2,1,3,4,5,9] | 2↔1 | [1,2,3,4,5,9] | [2,3,4,5,9] |
排序过程动画演示(文字版)
初始最大堆:
9
/ \
4 5
/ \ /
1 3 2
第1轮:9(堆顶) ↔ 2(末尾)
交换后:
2
/ \
4 5
/ \ /
1 3 9
调整堆:
2与5交换 → 2与3交换
调整后:
5
/ \
4 3
/ \ /
1 2 9
第2轮:5(堆顶) ↔ 2(末尾)
交换后:
2
/ \
4 3
/ /
1 5 9
调整堆:
2与4交换
调整后:
4
/ \
2 3
/ /
1 5 9
第3轮:4(堆顶) ↔ 1(末尾)
交换后:
1
/ \
2 3
4 5 9
调整堆:
1与3交换
调整后:
3
/ \
2 1
4 5 9
第4轮:3(堆顶) ↔ 1(末尾)
交换后:
1
/ \
2 3 4 5 9
调整堆:
1与2交换
调整后:
2
/ \
1 3 4 5 9
第5轮:2(堆顶) ↔ 1(末尾)
交换后:
1 2 3 4 5 9
最终排序结果:[1, 2, 3, 4, 5, 9]
核心理解要点
- 堆顶永远是当前最大值:这是堆排序的核心
- 交换策略:每次把最大值(堆顶)放到当前数组末尾
- 末尾指针移动:每轮结束后,末尾指针向前移动一位
- 调整范围:每次只调整未排序的部分
- 原地排序:所有操作都在原数组上进行
通过这个例子可以看到,堆排序就像是在不断地:
-
从堆顶"取出"最大值
-
放到数组后面
-
重新整理剩下的元素形成新堆
-
重复直到所有元素有序
5.冒泡排序
✅ 冒泡排序的核心要点
- 每趟排序:从第一个元素开始,依次比较相邻两个元素,若顺序错误(如升序时前 > 后),就交换。
- 第 i 趟排序后:最大的 i 个元素已经"沉底"(排在最后)。
- 最多进行 n - 1 趟。
- 优化 :如果某一趟中没有发生任何交换,说明数组已经有序,可以提前结束。
冒泡排序原理说明
冒泡排序有两种常见的实现方式:
- 从前往后比较(将最大的元素"冒泡"到最后)
- 从后往前比较(将最小的元素"冒泡"到最前)
冒泡排序 vs 简单选择排序
冒泡排序:
- 通过相邻元素的比较和交换来排序
- 每趟排序会将一个元素放到最终位置
- 交换次数较多
- 稳定排序算法
简单选择排序:
- 每趟选择最小(或最大)元素放到最终位置
- 每趟只交换一次
- 不稳定排序算法
JavaScript 实现示例
1. 冒泡排序(从前往后比较 - 标准版)
javascript
function bubbleSort(arr) {
const n = arr.length;
let swapped;
for (let i = 0; i < n - 1; i++) {
swapped = false;
// 每趟将最大的元素"冒泡"到最后
for (let j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换相邻元素
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
// 如果没有交换,说明已经有序
if (!swapped) break;
console.log(`第${i + 1}趟排序后:`, [...arr]);
}
return arr;
}
// 测试
const arr1 = [64, 34, 25, 12, 22, 11, 90];
console.log("原始数组:", arr1);
console.log("冒泡排序结果:", bubbleSort([...arr1]));
2. 冒泡排序(从后往前比较)
javascript
function bubbleSortBackward(arr) {
const n = arr.length;
let swapped;
for (let i = 0; i < n - 1; i++) {
swapped = false;
// 从后往前比较,将最小的元素"冒泡"到前面
for (let j = n - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
[arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
swapped = true;
}
}
if (!swapped) break;
console.log(`第${i + 1}趟排序后:`, [...arr]);
}
return arr;
}
// 测试
console.log("\n从后往前冒泡排序:");
console.log("结果:", bubbleSortBackward([...arr1]));
3. 简单选择排序(对比)
javascript
function selectionSort(arr) {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIndex = i;
// 寻找最小元素的位置
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 每趟只交换一次
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
console.log(`第${i + 1}趟排序后:`, [...arr]);
}
return arr;
}
// 测试
console.log("\n简单选择排序:");
console.log("结果:", selectionSort([...arr1]));
4. 综合比较示例
javascript
// 创建相同的测试数据
const testArray = [29, 10, 14, 37, 13, 5, 42];
console.log("原始数组:", testArray);
// 比较排序过程
console.log("\n=== 冒泡排序(从前往后)===");
const bubbleResult = bubbleSort([...testArray]);
console.log("\n=== 冒泡排序(从后往前)===");
const bubbleBackwardResult = bubbleSortBackward([...testArray]);
console.log("\n=== 简单选择排序 ===");
const selectionResult = selectionSort([...testArray]);
// 验证结果
console.log("\n=== 排序结果验证 ===");
console.log("冒泡排序结果:", bubbleResult);
console.log("从后往前冒泡结果:", bubbleBackwardResult);
console.log("简单选择排序结果:", selectionResult);
关键区别总结
| 特性 | 冒泡排序 | 简单选择排序 |
|---|---|---|
| 比较次数 | O(n²) | O(n²) |
| 交换次数 | O(n²) - 较多 | O(n) - 较少 |
| 稳定性 | 稳定 | 不稳定 |
| 每趟操作 | 多次相邻交换 | 一次交换 |
| 优化潜力 | 可通过标志位提前结束 | 每次必须完整遍历剩余部分 |
性能测试
javascript
// 性能比较
function comparePerformance() {
const largeArray = Array.from({length: 10000}, () => Math.floor(Math.random() * 10000));
console.time('冒泡排序时间');
bubbleSort([...largeArray]);
console.timeEnd('冒泡排序时间');
console.time('简单选择排序时间');
selectionSort([...largeArray]);
console.timeEnd('简单选择排序时间');
}
// 注意:大规模数据排序测试可能需要较长时间
// comparePerformance();
使用建议
- 小规模数据:冒泡排序实现简单,性能可接受
- 大规模数据:建议使用更高效的排序算法(如快速排序、归并排序)
- 稳定性要求:需要稳定排序时选择冒泡排序
- 交换成本高:选择简单选择排序(交换次数少)
冒泡排序虽然效率不高(O(n²)),但由于其简单直观,常作为教学示例,帮助理解排序算法的基本思想。
冒泡排序(Bubble Sort)是一种简单的排序算法,其核心思想是通过相邻元素的比较和交换,将较大(或较小)的元素"冒泡"到数组的一端。
7.并归排序法
当然可以!归并排序(Merge Sort) 是一种经典的、稳定的、基于分治思想(Divide and Conquer) 的排序算法。它在最坏、平均和最好情况下的时间复杂度都是 O(n log n),非常适合对性能要求稳定的应用场景。
🔍 归并排序的核心思想
归并排序分为两个阶段:
1. 分解(Divide)
- 将待排序的数组递归地一分为二,直到每个子数组只包含一个元素(或为空)。
- 单个元素的数组天然有序。
2. 合并(Conquer / Merge)
- 将两个已排序的子数组合并成一个更大的有序数组。
- 合并过程使用双指针技术,依次比较两个子数组的首元素,取较小者放入结果中。
✅ 关键:"分而治之,再合而有序"
🧠 举个例子(升序)
原始数组:[38, 27, 43, 3, 9, 82, 10]
-
分解:
[38, 27, 43, 3, 9, 82, 10] → [38, 27, 43, 3] + [9, 82, 10] → [38, 27] + [43, 3] + [9, 82] + [10] → [38] [27] [43] [3] [9] [82] [10] -
合并:
[27, 38] + [3, 43] + [9, 82] + [10] → [3, 27, 38, 43] + [9, 10, 82] → [3, 9, 10, 27, 38, 43, 82]
⚙️ JavaScript 实现(经典递归版)
js
function mergeSort(arr) {
// 基线条件:长度 ≤ 1 的数组已经有序
if (arr.length <= 1) {
return arr;
}
// 分:找到中点,分割数组
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// 递归排序左右两部分,并合并
return merge(mergeSort(left), mergeSort(right));
}
// 合并两个已排序的数组
function merge(left, right) {
const result = [];
let i = 0, j = 0;
// 双指针比较,取较小值
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i]);
i++;
} else {
result.push(right[j]);
j++;
}
}
// 将剩余元素加入结果(只有一个数组会有剩余)
return result.concat(left.slice(i)).concat(right.slice(j));
}
// 示例
const data = [38, 27, 43, 3, 9, 82, 10];
console.log("原数组:", data);
console.log("归并排序结果:", mergeSort(data));
输出:
原数组: [38, 27, 43, 3, 9, 82, 10]
归并排序结果: [3, 9, 10, 27, 38, 43, 82]
✅ 这个实现清晰、稳定、易于理解。
📊 算法特性分析
| 特性 | 说明 |
|---|---|
| 时间复杂度 | 所有情况下均为 O(n log n) |
| 空间复杂度 | O(n)(需要额外数组存储合并结果) |
| 稳定性 | ✅ 稳定(相等元素的相对顺序不会改变) |
| 是否原地排序 | ❌ 否(需要 O(n) 额外空间) |
| 适用场景 | 链表排序、外部排序、要求稳定排序的场合 |
🔁 与快速排序对比
| 特性 | 归并排序 | 快速排序 |
|---|---|---|
| 时间复杂度(最坏) | O(n log n) | O(n²) |
| 空间复杂度 | O(n) | O(log n)(递归栈) |
| 稳定性 | ✅ 稳定 | ❌ 不稳定 |
| 原地排序 | ❌ 否 | ✅ 是(可实现) |
| 实际速度 | 稍慢(因频繁创建数组) | 通常更快(缓存友好) |
💡 在 JavaScript 中,
Array.prototype.sort()对小型数组或需要稳定排序时,可能内部使用类似归并的策略(如 Firefox 的 Timsort)。
✅ 总结
- 归并排序通过递归分解 + 有序合并实现高效排序。
- 它的最大优势是时间复杂度稳定为 O(n log n) ,且稳定。
- 缺点是需要额外 O(n) 空间,不适合内存极度受限的场景。
- 非常适合教学、链表排序、以及对稳定性有要求的系统(如数据库)。
如果你希望看到迭代版(非递归)归并排序 、原地归并优化,或者用它来解决"逆序对"等问题,也可以告诉我!
8.基数排序法
当然可以!基数排序(Radix Sort) 是一种非比较型整数排序算法 ,它不通过元素之间的比较来排序,而是借助"数字的每一位"进行分配和收集,从而实现排序。它的核心思想是:从最低位到最高位(或反之),依次对每一位进行稳定排序(通常用计数排序)。
🔍 基数排序的核心思想
- 按位处理:将所有待排序的整数统一视为相同位数(不足的前面补 0)。
- 从最低有效位(LSD, Least Significant Digit)开始 ,对每一位进行稳定排序(通常是计数排序)。
- 重复此过程,直到处理完最高位。
- 最终结果即为有序序列。
✅ 关键点:
- 必须使用稳定排序作为子过程(如计数排序),否则会破坏之前位的排序结果。
- 适用于非负整数(可扩展到负数或字符串,但需额外处理)。
🧠 举个例子(升序)
原始数组:[170, 45, 75, 90, 2, 802, 24, 66]
第一步:按个位排序(第 0 位)
170 → 0
45 → 5
75 → 5
90 → 0
2 → 2
802 → 2
24 → 4
66 → 6
稳定排序后(按个位):
→ [170, 90, 2, 802, 24, 45, 75, 66]
第二步:按十位排序(第 1 位)
170 → 7
90 → 9
2 → 0
802 → 0
24 → 2
45 → 4
75 → 7
66 → 6
稳定排序后:
→ [2, 802, 24, 45, 66, 170, 75, 90]
第三步:按百位排序(第 2 位)
2 → 0
802 → 8
24 → 0
45 → 0
66 → 0
170 → 1
75 → 0
90 → 0
稳定排序后:
→ [2, 24, 45, 66, 75, 90, 170, 802] ✅ 已有序!
⚙️ JavaScript 实现(仅支持非负整数)
js
function radixSort(arr) {
if (arr.length === 0) return arr;
// 找到最大值,确定最大位数
const max = Math.max(...arr);
let exp = 1; // 当前处理的位(1=个位,10=十位,100=百位...)
// 逐位进行计数排序
while (Math.floor(max / exp) > 0) {
arr = countingSortByDigit(arr, exp);
exp *= 10;
}
return arr;
}
// 对某一位(exp 表示 1,10,100...)进行计数排序
function countingSortByDigit(arr, exp) {
const n = arr.length;
const output = new Array(n);
const count = new Array(10).fill(0); // 0~9 共10个桶
// 统计当前位上各数字的出现次数
for (let i = 0; i < n; i++) {
const digit = Math.floor(arr[i] / exp) % 10;
count[digit]++;
}
// 转换为累积计数(确定每个数字在输出中的位置)
for (let i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 从后往前遍历(保证稳定性),构建输出数组
for (let i = n - 1; i >= 0; i--) {
const digit = Math.floor(arr[i] / exp) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
return output;
}
// 示例
const data = [170, 45, 75, 90, 2, 802, 24, 66];
console.log("原数组:", data);
console.log("基数排序结果:", radixSort(data));
输出:
原数组: [170, 45, 75, 90, 2, 802, 24, 66]
基数排序结果: [2, 24, 45, 66, 75, 90, 170, 802]
📊 算法特性分析
| 特性 | 说明 |
|---|---|
| 时间复杂度 | O(d × (n + k)) • d:最大数的位数• n:元素个数• k:基数(这里是 10) |
| 空间复杂度 | O(n + k)(计数数组 + 输出数组) |
| 稳定性 | ✅ 稳定(依赖稳定的子排序) |
| 是否比较排序 | ❌ 非比较排序(突破 O(n log n) 下限) |
| 适用数据 | 非负整数、固定长度字符串、IP 地址等 |
💡 当
d很小(如 32 位整数最多 10 位十进制数),基数排序接近 O(n),非常高效!
⚠️ 注意事项
- 默认只支持非负整数 。若要支持负数,可:
- 分离正负数;
- 对负数取绝对值排序后反转;
- 合并(负数在前,正数在后)。
- 不适合浮点数(除非转换为整数形式)。
- 当数值范围极大但数量少时,效率不如快排。
✅ 总结
- 基数排序是一种非比较、稳定、线性时间潜力的排序算法。
- 它通过逐位稳定排序(通常用计数排序)实现整体有序。
- 适用于整数、字符串等具有"位"结构的数据。
- 在特定场景(如大量短整数)下,性能优于快排或归并。