数据结构之排序方法

1.直接插入排序

你说得非常准确!直接插入排序(Insertion Sort)的核心思想正是:假设前 i−1 个元素已经排好序,将第 i 个元素插入到前面的有序序列中的合适位置

✅ 插入排序的基本步骤:

  1. 将第一个元素视为已排序。
  2. 从第二个元素开始,依次取出当前元素(称为"key"或"待插入元素")。
  3. 将该元素与前面已排序部分从后往前比较。
  4. 如果前面的元素比它大,就将前面的元素向后移动一位。
  5. 直到找到一个 ≤ 它的元素(或到达开头),将该元素插入到该位置之后。
  6. 重复直到处理完所有元素。

📌 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) 正是为了解决直接插入排序在数据量大、初始无序时效率低下的问题而提出的优化方法。它通过 "分组 + 逐步缩小增量" 的策略,使数据在最终进行一次直接插入排序前已基本有序,从而大幅提升效率。


✅ 希尔排序的核心思想回顾:

  1. 选择一个递减的增量序列 (如:d = Math.floor(n/2), d = Math.floor(d/2), ..., 1)。
  2. 对每个增量 d
    • 将数组按间隔 d 分成若干子序列(即下标相差 d 的元素为一组)。
    • 对每个子序列独立进行直接插入排序
  3. 当增量减小到 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趟:从第1个到第n个元素中找出最小的,与第1个元素交换
  2. 第2趟:从第2个到第n个元素中找出最小的,与第2个元素交换
  3. 第i趟:从第i个到第n个元素中找出最小的,与第i个元素交换
  4. 第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²),但在某些特定情况下仍有应用价值:

  1. 数据交换成本高时:选择排序的交换次数最少
  2. 内存受限环境:原地排序,空间复杂度O(1)
  3. 小规模数据:实现简单,代码量少
  4. 教学目的:理解排序算法的基本原理

核心要点 :选择排序的核心是 "先选择,后交换",每趟只做一次交换(如果需要),这与冒泡排序每趟可能多次交换形成鲜明对比。

4.堆排序

堆排序介绍

堆排序(Heap Sort)是一种基于二叉堆数据结构的比较排序算法。它是不稳定的排序算法,时间复杂度为 O(n log n)。

核心概念:

  1. 二叉堆:完全二叉树,分为最大堆和最小堆

    • 最大堆:父节点值 ≥ 子节点值
    • 最小堆:父节点值 ≤ 子节点值
  2. 主要步骤

    • 建堆:将无序数组构建成堆
    • 排序:反复取出堆顶元素(最大/最小值),调整剩余部分为堆

算法步骤:

  1. 构建最大堆
  2. 将堆顶元素(最大值)与末尾元素交换
  3. 减小堆大小,重新调整堆
  4. 重复步骤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);

算法特点

优点:

  1. 时间复杂度稳定:始终为 O(n log n)
  2. 空间效率高:只需 O(1) 额外空间
  3. 适合大数据集:不受输入数据分布影响

缺点:

  1. 不稳定:相等元素的相对位置可能改变
  2. 缓存不友好:频繁的远距离元素交换

时间复杂度:

  • 最坏情况: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],手动建最大堆:

  1. 初始状态

    复制代码
        3
       / \
      9   2
     / \  /
    1  4 5
  2. 从下往上,从左往右调整(实际从最后一个非叶子节点开始):

    • 调整节点2:与子节点5交换 → [3, 9, 5, 1, 4, 2]
    • 调整节点9:无需交换 → [3, 9, 5, 1, 4, 2]
    • 调整节点3:先与9交换,再与4交换 → [9, 4, 5, 1, 3, 2]
  3. 最终最大堆

    复制代码
        9
       / \
      4   5
     / \  /
    1  3 2

关键要点

  1. 完全二叉树性质:数组按层序填充二叉树,没有"洞"
  2. 建堆起点 :从最后一个非叶子节点 开始(索引 Math.floor(n/2)-1
  3. 调整方向:自底向上,但每个节点的调整是向下进行的(heapify)
  4. 时间复杂度:建堆 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个元素):

    1. 2与子节点4、5比较,2<5 → 交换2↔5 → [5, 4, 2, 1, 3, 9]
    2. 2与子节点比较(无右子,左子3>2)→ 交换2↔3 → [5, 4, 3, 1, 2, 9]
  • 结果[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个元素):

    1. 2与子节点4、3比较,2<4 → 交换2↔4 → [4, 2, 3, 1, 5, 9]
    2. 2与子节点1比较(无右子),2>1 → 无需交换
  • 结果[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. 1与子节点2、3比较,1<3 → 交换1↔3 → [3, 2, 1, 4, 5, 9]
  • 结果[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. 1与子节点2比较,1<2 → 交换1↔2 → [2, 1, 3, 4, 5, 9]
  • 结果[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]

核心理解要点

  1. 堆顶永远是当前最大值:这是堆排序的核心
  2. 交换策略:每次把最大值(堆顶)放到当前数组末尾
  3. 末尾指针移动:每轮结束后,末尾指针向前移动一位
  4. 调整范围:每次只调整未排序的部分
  5. 原地排序:所有操作都在原数组上进行

通过这个例子可以看到,堆排序就像是在不断地:

  1. 从堆顶"取出"最大值

  2. 放到数组后面

  3. 重新整理剩下的元素形成新堆

  4. 重复直到所有元素有序

5.冒泡排序

✅ 冒泡排序的核心要点

  • 每趟排序:从第一个元素开始,依次比较相邻两个元素,若顺序错误(如升序时前 > 后),就交换。
  • 第 i 趟排序后:最大的 i 个元素已经"沉底"(排在最后)。
  • 最多进行 n - 1 趟
  • 优化 :如果某一趟中没有发生任何交换,说明数组已经有序,可以提前结束。

冒泡排序原理说明

冒泡排序有两种常见的实现方式:

  1. 从前往后比较(将最大的元素"冒泡"到最后)
  2. 从后往前比较(将最小的元素"冒泡"到最前)

冒泡排序 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]

  1. 分解:

    复制代码
    [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]
  2. 合并:

    复制代码
    [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) 是一种非比较型整数排序算法 ,它不通过元素之间的比较来排序,而是借助"数字的每一位"进行分配和收集,从而实现排序。它的核心思想是:从最低位到最高位(或反之),依次对每一位进行稳定排序(通常用计数排序)


🔍 基数排序的核心思想

  1. 按位处理:将所有待排序的整数统一视为相同位数(不足的前面补 0)。
  2. 从最低有效位(LSD, Least Significant Digit)开始 ,对每一位进行稳定排序(通常是计数排序)。
  3. 重复此过程,直到处理完最高位。
  4. 最终结果即为有序序列。

✅ 关键点:

  • 必须使用稳定排序作为子过程(如计数排序),否则会破坏之前位的排序结果。
  • 适用于非负整数(可扩展到负数或字符串,但需额外处理)。

🧠 举个例子(升序)

原始数组:[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),非常高效!


⚠️ 注意事项

  1. 默认只支持非负整数 。若要支持负数,可:
    • 分离正负数;
    • 对负数取绝对值排序后反转;
    • 合并(负数在前,正数在后)。
  2. 不适合浮点数(除非转换为整数形式)。
  3. 当数值范围极大但数量少时,效率不如快排

✅ 总结

  • 基数排序是一种非比较、稳定、线性时间潜力的排序算法。
  • 它通过逐位稳定排序(通常用计数排序)实现整体有序。
  • 适用于整数、字符串等具有"位"结构的数据
  • 在特定场景(如大量短整数)下,性能优于快排或归并。
相关推荐
TracyCoder1232 小时前
LeetCode Hot100(24/100)——21. 合并两个有序链表
算法·leetcode·链表
数智工坊2 小时前
【数据结构-栈、队列、数组】3.4栈在表达式求值下-递归中的应用
数据结构
power 雀儿2 小时前
前馈网络+层归一化
人工智能·算法
爱吃rabbit的mq2 小时前
第10章:支持向量机:找到最佳边界
算法·机器学习·支持向量机
木非哲2 小时前
AB实验高级必修课(四):逻辑回归的“马甲”、AUC的概率本质与阈值博弈
算法·机器学习·逻辑回归·abtest
兩尛2 小时前
45. 跳跃游戏 II
c++·算法·游戏
执风挽^2 小时前
Python_func_basic
开发语言·python·算法·visual studio code
努力d小白2 小时前
leetcode438.找到字符串中所有字母异位词
java·javascript·算法
tangchao340勤奋的老年?2 小时前
ADS通信 C++ 设置通知方式读取指定变量
开发语言·c++·算法