返回

JavaScript 数据结构和算法的瑰宝:归并、快速、希尔和堆排序比较

前端

JavaScript 数据结构与算法的迷人世界:归并、快速、希尔与堆排序大比拼

踏上 JavaScript 旅程的基石:数据结构与算法

在 JavaScript 的世界中遨游,夯实的数据结构与算法基础至关重要。如同武侠高手内功深厚,掌握这些奥秘将助你踏上前端之路的坦途。本文将深入探索归并排序、快速排序、希尔排序和堆排序这四位算法界的巨擘,揭开它们的优劣之谜,助你成为 JavaScript 排序算法的大师。

算法简介

归并排序:分而治之的典范

归并排序遵循“分而治之”的策略,将待排序数组不断分割成更小的子数组,直至每个子数组仅含一个元素。随后,它将这些有序的子数组逐个合并,最终得到一个完全有序的数组。

快速排序:灵活的基准选择

快速排序也采用“分而治之”的原则,但它采取了一种不同的分割策略。它选择一个基准元素,将数组划分为两个子数组:小于基准元素的元素和大于基准元素的元素。然后,它递归地对这两个子数组执行同样的操作。

希尔排序:插入排序的改良版

希尔排序是一种插入排序的改进版本。它将数组划分为若干个子序列,然后对每个子序列进行插入排序。随着间隔的逐渐缩小,它最终将整个数组排序。

堆排序:最大堆的奥秘

堆排序利用堆数据结构的特性对数组进行排序。它将数组构建成一个最大堆,然后依次弹出堆顶元素,得到一个从小到大有序的数组。

比较:谁占鳌头?

时间复杂度:速度比拼

算法 最好情况 最坏情况 平均情况
归并排序 O(n log n) O(n log n) O(n log n)
快速排序 O(n log n) O(n^2) O(n log n)
希尔排序 O(n) O(n^2) O(n^1.3)
堆排序 O(n log n) O(n log n) O(n log n)

从时间复杂度来看,归并排序、快速排序和堆排序在平均情况下都为 O(n log n),而希尔排序在几乎有序数组上的表现优异,复杂度可达 O(n)。

空间复杂度:空间占用

算法 空间复杂度
归并排序 O(n)
快速排序 O(log n)
希尔排序 O(1)
堆排序 O(1)

空间复杂度方面,希尔排序和堆排序表现最为出色,仅需常数级别的额外空间,而归并排序需要 O(n) 的额外空间。

稳定性:保持元素顺序

稳定性是指当两个相等元素在输入数组中出现时,它们在输出数组中的相对顺序是否保持不变。归并排序是稳定的,而快速排序、希尔排序和堆排序是不稳定的。

适用场景:量身定制

归并排序:

  • 稳定的排序需求
  • 输入数组较大时
  • 外部排序场景

快速排序:

  • 对空间复杂度要求较高时
  • 输入数组较小或平均分布较好的场景

希尔排序:

  • 几乎有序数组的排序
  • 小规模数组的排序

堆排序:

  • 对时间复杂度和空间复杂度都有要求时
  • 求解 top k 问题

代码示例

以下是 JavaScript 中这四种排序算法的代码示例:

归并排序:

function mergeSort(arr) {
  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 = [];
  while (left.length && right.length) {
    if (left[0] <= right[0]) {
      result.push(left.shift());
    } else {
      result.push(right.shift());
    }
  }
  return [...result, ...left, ...right];
}

快速排序:

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivot = arr[0];
  const left = [];
  const right = [];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
}

希尔排序:

function shellSort(arr) {
  let gap = Math.floor(arr.length / 2);
  while (gap > 0) {
    for (let i = gap; i < arr.length; i++) {
      let j = i;
      const temp = arr[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;
}

堆排序:

function heapSort(arr) {
  function buildHeap(arr) {
    for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
      heapify(arr, i, arr.length);
    }
  }

  function heapify(arr, i, size) {
    const left = 2 * i + 1;
    const right = 2 * i + 2;
    let max = i;
    if (left < size && arr[left] > arr[max]) {
      max = left;
    }
    if (right < size && arr[right] > arr[max]) {
      max = right;
    }
    if (max !== i) {
      [arr[i], arr[max]] = [arr[max], arr[i]];
      heapify(arr, max, size);
    }
  }

  buildHeap(arr);

  for (let i = arr.length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, 0, i);
  }

  return arr;
}

结语:算法之舞

归并排序、快速排序、希尔排序和堆排序在 JavaScript 排序算法的海洋中各领风骚。了解它们的优劣,犹如欣赏一场算法之舞,在复杂性和效率的交响曲中,谱写代码的优雅与高效。根据你的需求,选择最合适的算法,让你的 JavaScript 代码如虎添翼,在算法的舞台上展现出色的性能。

常见问题解答

1. 我该何时使用归并排序?

当需要稳定的排序,或者输入数组较大时,归并排序是一个不错的选择。

2. 为什么快速排序的时间复杂度最差为 O(n^2)?

在极端情况下,快速排序可能会退化为冒泡排序,导致最差时间复杂度为 O(n^2)。

3. 希尔排序是否总比插入排序快?

在几乎有序的数组上,希尔排序比插入排序快,但在随机数组上,插入排序可能更快。

4. 堆排序在什么情况下优于其他排序算法?

当对时间复杂度和空间复杂度都有要求时,或者需要求解 top k 问题时,堆排序是一个很好的选择。

5. 如何选择最合适的排序算法?

根据你的输入数组特性、对时间和空间复杂度的要求,以及稳定性需求,选择最能满足你特定需求的排序算法。