数组排序

数组排序

数组排序是编程中的常见操作,以下是常见的排序方法及其分类:

冒泡排序

冒泡排序是一种简单的交换排序算法 ,通过重复比较相邻元素并交换 (如果顺序错误)来排序数组。其核心思想是每一轮排序将当前未排序部分的最大值(或最小值)"冒泡"到正确位置

1. 算法步骤

以升序排序为例:

  1. 比较相邻元素 :从数组的第一个元素开始,依次比较相邻的两个元素(arr[j]arr[j+1])。
  2. 交换(如果逆序) :如果前一个元素比后一个元素大(arr[j] > arr[j+1]),则交换它们的位置。
  3. 重复遍历:每一轮遍历会将当前未排序部分的最大值"冒泡"到数组末尾。
  4. 缩小范围:每一轮排序后,未排序部分的长度减1(因为最大值已归位)。
  5. 终止条件:当某一轮未发生任何交换时,说明数组已完全有序,提前终止。

示例(升序排序):

初始数组:[5, 3, 8, 4, 2]

第1轮

  • 比较 5和3 → 交换 → [3, 5, 8, 4, 2]
  • 比较 5和8 → 不交换 → [3, 5, 8, 4, 2]
  • 比较 8和4 → 交换 → [3, 5, 4, 8, 2]
  • 比较 8和2 → 交换 → [3, 5, 4, 2, 8] (最大值8已归位)

第2轮

  • 比较 3和5 → 不交换 → [3, 5, 4, 2, 8]
  • 比较 5和4 → 交换 → [3, 4, 5, 2, 8]
  • 比较 5和2 → 交换 → [3, 4, 2, 5, 8] (次大值5已归位)

第3轮

  • 比较 3和4 → 不交换 → [3, 4, 2, 5, 8]
  • 比较 4和2 → 交换 → [3, 2, 4, 5, 8] (4已归位)

第4轮

  • 比较 3和2 → 交换 → [2, 3, 4, 5, 8] (3已归位,数组完全有序)

2. JS代码实现

基础版本(未优化)
ini 复制代码
function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    // 每轮将最大的元素放到末尾
    // 缩小范围:每一轮排序后,未排序部分的长度减1(因为最大值已归位)
    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]];
      }
    }
  }
  return arr;
}

// 测试
const array = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSort(array)); // [11, 12, 22, 25, 34, 64, 90]
优化版本(提前终止)
ini 复制代码
function bubbleSortOptimized(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;
  }
  return arr;
}

// 测试
const array = [1, 2, 3, 4, 5]; // 已经有序的数组
console.log(bubbleSortOptimized(array)); // 提前终止,仅需一轮比较

3. 时间复杂度分析

  • 最坏情况 (数组完全逆序):需进行 n(n-1)/2 次比较和交换,时间复杂度为 O(n²)
  • 最好情况 (数组已经有序):只需进行 n-1 次比较(无交换),时间复杂度为 O(n)
  • 平均情况:O(n²)。

4.空间复杂度分析

  • O(1)(原地排序,仅需常数级额外空间用于交换)

5. 特点总结

  • 优点:代码简单,易于实现,适合小规模数据或教学。
  • 缺点:效率低(O(n²)),不适合大规模数据。
  • 稳定性:稳定(相等元素不会交换相对位置)。

选择排序

1. 基本思想

选择排序是一种原地比较排序算法 ,其核心思想是每次从未排序部分选择最小(或最大)元素,放到已排序部分的末尾。通过不断缩小未排序范围,最终完成整个数组的排序。

2. 算法步骤

  1. 外层循环 :控制当前需要填入最小元素的位置(从 0n-2)。
  2. 内层循环:在未排序部分中查找最小元素的索引。
  3. 交换操作:将找到的最小元素与当前外层循环位置的元素交换。

3. 时间复杂度分析

  • 最坏情况 :无论数组是否有序,每次都需要遍历剩余未排序部分,比较次数为 n(n-1)/2,时间复杂度为 O(n²)
  • 最好情况 :即使数组已经有序,仍需完整比较,时间复杂度仍为 O(n²)
  • 平均情况:O(n²)。

4. 空间复杂度

  • O(1)(原地排序,仅需常数级额外空间用于交换)

5. JavaScript 实现

ini 复制代码
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]];
    }
  }
  return arr;
}

// 测试
const array = [64, 25, 12, 22, 11];
console.log(selectionSort(array)); // [11, 12, 22, 25, 64]

6. 过程演示(以 [29, 10, 14, 37, 13] 为例)

  1. 第一轮

    • 未排序部分:[29, 10, 14, 37, 13]
    • 找到最小值 10(索引 1),与 29 交换 → [10, 29, 14, 37, 13]
  2. 第二轮

    • 未排序部分:[29, 14, 37, 13]
    • 找到最小值 13(索引 4),与 29 交换 → [10, 13, 14, 37, 29]
  3. 第三轮

    • 未排序部分:[14, 37, 29]
    • 最小值 14 已在正确位置,无需交换。
  4. 第四轮

    • 未排序部分:[37, 29]
    • 找到最小值 29,与 37 交换 → [10, 13, 14, 29, 37]

7. 特点总结

  • 优点
    • 简单直观,交换次数少(最多 n-1 次交换)。
    • 适合小规模数据或对内存写入敏感的场景(如闪存存储)。
  • 缺点
    • 时间复杂度始终为 O(n²),效率低。
    • 不稳定(例如对 [5, 5, 2] 排序时,第一个 5 会与 2 交换,破坏原有顺序)。

插入排序

1. 基本思想

插入排序是一种简单直观的排序算法,其核心思想是:

  • 将数组分为"已排序"和"未排序"两部分,逐个将未排序部分的元素插入到已排序部分的正确位置。
  • 类似于整理扑克牌时,将新牌插入到手中已排序的牌中的过程。

2. 算法步骤

  1. 初始化
    • 假设第一个元素(arr[0])是已排序部分,其余为未排序部分。
  2. 遍历未排序部分
    • i = 1n-1,依次取出 arr[i] 作为待插入元素。
  3. 插入到正确位置
    • 在已排序部分(arr[0..i-1])中从后向前扫描 ,找到 arr[i] 的正确位置。
    • 若已排序元素大于 arr[i],则将该元素后移一位,直到找到插入点。
  4. 插入元素
    • arr[i] 放入正确位置,保持已排序部分始终有序。

3. 时间复杂度分析

  • 最坏情况 (数组完全逆序):每次插入需移动所有已排序元素,比较次数为 n(n-1)/2,时间复杂度为 O(n²)
  • 最好情况 (数组已有序):每次仅需比较一次,时间复杂度为 O(n)
  • 平均情况:O(n²)。

4. 空间复杂度

  • O(1)(原地排序,仅需常数级额外空间用于临时存储待插入元素)。

5. JavaScript 实现

基础版本(升序)
ini 复制代码
function insertionSort(arr){
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    const current = arr[i]; // 当前待插入元素
    let j = i - 1;          // 从已排序部分的末尾开始比较

    // 在已排序部分中寻找插入位置
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j]; // 元素后移
      j--;
    }
    arr[j + 1] = current;  // 插入到正确位置
  }
  return arr;
}

// 测试
const array = [12, 11, 13, 5, 6];
console.log(insertionSort(array)); // [5, 6, 11, 12, 13]
降序版本
ini 复制代码
function insertionSortDesc(arr){
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    const current = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] < current) { // 仅修改比较符号
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = current;
  }
  return arr;
}

// 测试
console.log(insertionSortDesc([12, 11, 13, 5, 6])); // [13, 12, 11, 6, 5]

6. 过程演示(以 [12, 11, 13, 5, 6] 为例)

  1. 初始状态
    • 已排序部分:[12],未排序部分:[11, 13, 5, 6]
  2. 第一轮(i=1)
    • 取出 11,与 12 比较 → 12 后移 → 插入 11[11, 12, 13, 5, 6]
  3. 第二轮(i=2)
    • 取出 13,比 12 大,无需移动 → [11, 12, 13, 5, 6]
  4. 第三轮(i=3)
    • 取出 5,依次与 131211 比较并后移 → 插入 5[5, 11, 12, 13, 6]
  5. 第四轮(i=4)
    • 取出 6,依次与 131211 比较并后移 → 插入 6[5, 6, 11, 12, 13]

7. 特点总结

  • 优点
    • 简单易实现,适合小规模或基本有序的数据。
    • 稳定排序(相等元素不会交换顺序)。
    • 实际运行效率优于冒泡排序和选择排序(交换次数更少)。
  • 缺点
    • 大规模数据效率低(O(n²))。

希尔排序

1. 基本思想

希尔排序是插入排序的改进版 ,通过将数组分组并对每组进行插入排序,逐步缩小分组间隔(gap),最终实现整体有序。

  • 核心思想
    • 先让数组中距离较远的元素 基本有序,再逐步调整为局部有序,最后用插入排序完成精细化排序。
    • 通过减少元素的移动次数,提升插入排序的效率。

2. 算法步骤

  1. 选择间隔序列(gap sequence)
    • 常见序列:希尔原始序列(gap = n/2, n/4, ..., 1),或更优的序列如 Knuth 序列(gap = (3^k - 1)/2)。
  2. 分组插入排序
    • 对每个 gap,将数组分为 gap 个子序列,分别进行插入排序。
  3. 逐步缩小 gap
    • 重复上述过程,直到 gap = 1(即最后一次为标准的插入排序)。

3. 时间复杂度分析

  • 最坏情况 :取决于间隔序列的选择,一般为 O(n²)(如使用希尔原始序列)。
  • 最优情况 :若数组已部分有序,可接近 O(n log n)(如使用 Sedgewick 序列)。
  • 平均情况 :通常介于 O(n log n)O(n²) 之间。

4. 空间复杂度

  • O(1)(原地排序,仅需常数级额外空间)。

5. JavaScript 实现

基础版本(希尔原始序列:gap = n/2, n/4, ..., 1)
ini 复制代码
function shellSort(arr){
  const n = arr.length;
  let gap = Math.floor(n / 2); // 初始间隔

  while (gap > 0) {
    // 对每个子序列进行插入排序
    for (let i = gap; i < n; i++) {
      const temp = arr[i]; // 当前待插入元素
      let j = i;
      // 在子序列中向前比较并移动元素
      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 array = [12, 34, 54, 2, 3, 9, 8, 7, 1];
console.log(shellSort(array)); // [1, 2, 3, 7, 8, 9, 12, 34, 54]
优化版本(Knuth 序列:gap = (3^k - 1)/2)
ini 复制代码
function shellSortKnuth(arr){
  const n = arr.length;
  let gap = 1;
  // 计算最大初始间隔(Knuth 序列)
  while (gap < n / 3) {
    gap = gap * 3 + 1; // 1, 4, 13, 40, 121, ...
  }

  while (gap > 0) {
    for (let i = gap; i < n; i++) {
      const temp = arr[i];
      let j = i;
      while (j >= gap && arr[j - gap] > temp) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = temp;
    }
    gap = Math.floor((gap - 1) / 3); // 缩小间隔
  }
  return arr;
}

// 测试
console.log(shellSortKnuth([23, 10, 49, 2, 17, 5])); // [2, 5, 10, 17, 23, 49]

6. 过程演示(以 [12, 34, 54, 2, 3] 为例)

  1. 初始 gap = 2
    • 子序列 1(索引 0, 2, 4):[12, 54, 3] → 插入排序后 [3, 12, 54]
    • 子序列 2(索引 1, 3):[34, 2] → 插入排序后 [2, 34]
    • 数组变为 [3, 2, 12, 34, 54]
  2. gap = 1 (标准插入排序):
    • [3, 2, 12, 34, 54] 排序 → [2, 3, 12, 34, 54]

7. 特点总结

  • 优点
    • 比简单插入排序高效(尤其是中等规模数据)。
    • 原地排序,空间复杂度低。
  • 缺点
    • 时间复杂度依赖间隔序列的选择。
    • 不稳定排序(相同元素可能因跨间隔交换而改变顺序)。

归并排序

1. 基本思想

归并排序是一种分治算法(Divide and Conquer),其核心思想是:

  1. 分解:将数组递归地分成两半,直到每个子数组只有一个元素(自然有序)。
  2. 合并:将两个已排序的子数组合并成一个有序数组,直到最终完成整个数组的排序。

2. 算法步骤

  1. 分割阶段
    • 找到数组的中间位置 mid,将数组分为左右两部分 leftright
  2. 递归排序
    • leftright 分别递归调用归并排序。
  3. 合并阶段
    • 创建一个临时数组,按顺序从 leftright 中选取较小的元素放入,直到其中一个子数组被完全合并。
    • 将剩余元素直接拼接到临时数组的末尾。

3. 时间复杂度分析

  • 分割阶段 :每次将数组分成两半,共需 O(log n) 层递归。
  • 合并阶段:每层需要遍历所有元素(O(n))。
  • 总时间复杂度O(n log n)(最优、最坏、平均情况均相同)。

4. 空间复杂度

  • O(n):合并时需要临时数组存储结果(非原地排序)。

5. JavaScript 实现

递归版本(标准实现)
scss 复制代码
function mergeSort(arr){
  // 递归终止条件:数组长度为1时直接返回
  if (arr.length <= 1) return arr;

  // 分割数组
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid)); // 递归排序左半部分
  const right = mergeSort(arr.slice(mid));   // 递归排序右半部分

  // 合并两个有序数组
  return merge(left, 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++]);
    } else {
      result.push(right[j++]);
    }
  }

  // 将剩余元素拼接到结果数组
  return result.concat(left.slice(i)).concat(right.slice(j));
}

// 测试
const array = [38, 27, 43, 3, 9, 82, 10];
console.log(mergeSort(array)); // [3, 9, 10, 27, 38, 43, 82]
优化版本(避免频繁切片)
sql 复制代码
function mergeSortOptimized(arr, start = 0, end = arr.length - 1) {
  if (start >= end) return [arr[start]]; // 单元素数组直接返回

  const mid = Math.floor((start + end) / 2);
  const left = mergeSortOptimized(arr, start, mid);
  const right = mergeSortOptimized(arr, mid + 1, end);
  return merge(left, right);
}

// merge函数同上

6. 过程演示(以 [38, 27, 43, 3, 9, 82, 10] 为例)

  1. 分割
    • 第一次分割:[38, 27, 43, 3][9, 82, 10]
    • 递归分割左半部分:[38, 27][43, 3] → 继续分割为单元素数组。
    • 递归分割右半部分:[9][82, 10][82][10]
  2. 合并
    • 合并 [38][27][27, 38]
    • 合并 [43][3][3, 43]
    • 合并 [27, 38][3, 43][3, 27, 38, 43]
    • 合并 [82][10][10, 82]
    • 合并 [9][10, 82][9, 10, 82]
    • 最终合并 [3, 27, 38, 43][9, 10, 82][3, 9, 10, 27, 38, 43, 82]

7. 特点总结

  • 优点
    • 时间复杂度稳定为 O(n log n),适合大规模数据。
    • 稳定排序(合并时保留相等元素的原始顺序)。
  • 缺点
    • 需要 O(n) 额外空间(非原地排序)。
    • 递归调用可能引发栈溢出(极深递归时)

快速排序

1. 基本思想

快速排序是一种分治算法(Divide and Conquer),其核心思想是:

  1. 选择基准(Pivot):从数组中选择一个元素作为基准值。
  2. 分区(Partition) :将数组分为两部分,使得:
    • 左侧所有元素 ≤ 基准值,
    • 右侧所有元素 ≥ 基准值。
  3. 递归排序:对左右子数组递归调用快速排序。

2. 算法步骤

  1. 基准选择:通常选择第一个、最后一个或随机元素作为基准(以下实现选择最后一个元素)。
  2. 分区操作
    • 使用双指针(ij),i 指向小于基准的区域的末尾。
    • 遍历数组,将小于基准的元素交换到 i 的位置,并移动 i
  3. 放置基准 :将基准值放到 i 的最终位置,此时基准已处于正确位置。
  4. 递归调用:对基准左侧和右侧的子数组重复上述过程。

3. 时间复杂度分析

  • 最优情况 :每次分区均匀,递归树高度为 log n,时间复杂度为 O(n log n)
  • 最坏情况 :每次分区极度不均(如数组已有序且选择首尾为基准),时间复杂度为 O(n²)
  • 平均情况:O(n log n)。

4. 空间复杂度

  • O(log n):递归调用栈的深度(最坏情况下 O(n))。

5. JavaScript 实现

基础版本(Lomuto 分区方案)
scss 复制代码
function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = partition(arr, left, right); // 获取基准位置
    quickSort(arr, left, pivotIndex - 1);  // 递归排序左子数组
    quickSort(arr, pivotIndex + 1, right); // 递归排序右子数组
  }
  return arr;
}

function partition(arr, left, right) {
  const pivot = arr[right]; // 选择最后一个元素作为基准
  let i = left;             // i 指向小于基准的区域的末尾

  for (let j = left; j < right; j++) {
    if (arr[j] < pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]]; // 交换小于基准的元素
      i++;
    }
  }
  [arr[i], arr[right]] = [arr[right], arr[i]]; // 将基准放到正确位置
  return i; // 返回基准索引
}

// 测试
const array = [10, 80, 30, 90, 40, 50, 70];
console.log(quickSort(array)); // [10, 30, 40, 50, 70, 80, 90]
优化版本(Hoare 分区方案 + 随机基准)
scss 复制代码
function quickSortOptimized(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivotIndex = hoarePartition(arr, left, right);
    quickSortOptimized(arr, left, pivotIndex); // 注意边界与 Lomuto 不同
    quickSortOptimized(arr, pivotIndex + 1, right);
  }
  return arr;
}

function hoarePartition(arr, left, right) {
  const randomIndex = Math.floor(Math.random() * (right - left + 1)) + left;
  [arr[left], arr[randomIndex]] = [arr[randomIndex], arr[left]]; // 随机选择基准
  const pivot = arr[left];
  let i = left - 1, j = right + 1;

  while (true) {
    do { i++; } while (arr[i] < pivot);
    do { j--; } while (arr[j] > pivot);
    if (i >= j) return j;
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
}

// 测试
console.log(quickSortOptimized([3, 0, 2, 5, -1, 4, 1])); // [-1, 0, 1, 2, 3, 4, 5]

6. 过程演示(以 [10, 80, 30, 90, 40, 50, 70] 为例)

  1. 初始调用quickSort(arr, 0, 6),基准 pivot = 70(最后一个元素)。
  2. 分区过程
    • i = 0,遍历 j05
      • 10 < 70 → 交换 arr[0] 与自身,i++i = 1
      • 80 > 70 → 跳过
      • 30 < 70 → 交换 arr[1]arr[2][10, 30, 80, 90, 40, 50, 70]i = 2
      • 类似处理剩余元素 → 最终 i = 4
    • 交换 arr[4]pivot[10, 30, 40, 50, 70, 80, 90],返回 pivotIndex = 4
  3. 递归排序
    • 左子数组 [10, 30, 40, 50],右子数组 [80, 90]

7. 特点总结

  • 优点
    • 平均情况下效率极高(O(n log n)),实际运行速度快于归并排序和堆排序。
    • 原地排序(空间复杂度低)。
  • 缺点
    • 不稳定排序(分区时可能改变相等元素的顺序)。
    • 最坏情况下退化为 O(n²)(可通过随机化基准避免)。
相关推荐
-代号95276 小时前
【JavaScript】十二、定时器
开发语言·javascript·ecmascript
灵感__idea7 小时前
JavaScript高级程序设计(第5版):扎实的基本功是唯一捷径
前端·javascript·程序员
摇滚侠7 小时前
Vue3 其它API toRow和markRow
前端·javascript
難釋懷7 小时前
JavaScript基础-history 对象
开发语言·前端·javascript
拉不动的猪7 小时前
刷刷题47(react常规面试题2)
前端·javascript·面试
浪遏7 小时前
场景题:大文件上传 ?| 过总字节一面😱
前端·javascript·面试
计算机毕设定制辅导-无忧学长7 小时前
HTML 与 JavaScript 交互:学习进程中的新跨越(一)
javascript·html·交互
zrhsmile8 小时前
Vue从入门到荒废-单向绑定
javascript·vue.js·ecmascript
百锦再8 小时前
React编程的核心概念:发布-订阅模型、背压与异步非阻塞
前端·javascript·react.js·前端框架·json·ecmascript·html5
冴羽9 小时前
SvelteKit 最新中文文档教程(16)—— Service workers
前端·javascript·svelte