【JS】十大排序算法

209 阅读4分钟

参考

代码

/*
 * 配置项:
 * ARRAY_LENGTH: 数组长度
 * ARRAY_MIN: 数组元素最小值
 * ARRAY_MAX: 数组元素最大值
 * SORT_METHODS: 使用的排序方法
 * |-- bubbleSort: 冒泡排序
 * |-- quickSort: 快速排序
 * |-- insertionSort: 插入排序
 * |-- shellSort: 希尔排序
 * |-- selectSort: 选择排序
 * |-- heapSort: 堆排序
 * |-- mergeSort: 归并排序
 * |-- countingSort: 计数排序
 * |-- bucketSort: 桶排序
 * |-- radixSort: 基数排序
 * SHOW_ORIGIN: 是否输出源数组
 * SHOW_RESULTS: 是否输出排序结果
 * SHOW_TIMES: 是否输出排序耗时
 */
const ARRAY_LENGTH = 10000;
const ARRAY_MIN = 0;
const ARRAY_MAX = 10000;
const SORT_METHODS = [
  bubbleSort,
  selectSort,
  insertionSort,
  shellSort,
  mergeSort,
  quickSort,
  heapSort,
  countingSort,
  bucketSort,
  radixSort,
];
const SHOW_ORIGIN = true;
const SHOW_RESULTS = true;
const SHOW_TIMES = true;
/*
 * 主函数:执行各排序并输出其耗时和排序结果
 */
(function main() {
  // 导入配置,并创建供排序的随机数组
  const arr = buildArray(
    ARRAY_LENGTH,
    generateRandInteger(ARRAY_MIN, ARRAY_MAX)
  );
  // 定义rets,用于存放输出结果
  let rets = {
    origin: arr,
    result: null,
    times: [],
  };
  // 输出源数组
  if (SHOW_ORIGIN) {
    console.log("origin:", rets.origin);
  }
  // 执行各排序
  for (let sortMethod of SORT_METHODS) {
    // 执行排序,并计算耗时
    const timeBegin = process.uptime(),
      sortResult = sortMethod([...arr]),
      timeEnd = process.uptime();
    // 存放排序耗时
    rets.times.push({
      method: sortMethod.name,
      time: (timeEnd - timeBegin) * 1000,
    });
    // 存放排序结果
    if (rets.result === null) {
      rets.result = sortResult;
    }
  }
  if (SHOW_RESULTS) {
    console.log("result:", rets.result);
  }
  if (SHOW_TIMES) {
    rets.times.sort((a, b) => {
      return a.time - b.time;
    });
    console.log("<<SortMethod>>", "\t", "<<TimeCost>>");
    for (let time of rets.times) {
      console.log(time.method, "\t", time.time.toFixed(3), "ms");
    }
  }
})();

/*
 * 构建并返回指定长度的数组
 * @params length {Number} 数组长度
 * @params element {Number|Function} 数组元素或生成数组元素的函数
 */
function buildArray(length, element) {
  let arr = [];
  while (arr.length < length) {
    let ele = typeof element === "function" ? element() : element;
    arr.push(ele);
  }
  return arr;
}

/*
 * 生成并返回指定范围内的随机整数
 * @params min {Number} 最小值
 * @params max {Number} 最大值
 */
function generateRandInteger(min, max) {
  return function () {
    return Math.floor((max - min + 1) * Math.random() + min);
  };
}

/*
 * 生成并返回空白数组
 */
function generateBlankArray() {
  return function () {
    return [];
  };
}

// 冒泡排序
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 稳定性:稳定
function bubbleSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 每经过一轮扫描,就能确定一个最大元素,
  // 并将其放置在最右边,所以共需经过len-1轮扫描
  for (let i = 0; i < len - 1; i++) {
    // 从左向右扫描,确定[0,len-i)范围内的最大元素,
    // 并将其放置在下标为len-i-1处,即范围内最右边
    for (let j = 0; j < len - i - 1; j++) {
      // 如果当前元素大于下个元素,则交换两者,
      // 这样一来,较大的元素会被放置在右边
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

// 选择排序
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 稳定性:不稳定
function selectSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 声明最小元素下标
  let minIdx;
  // 每经过一轮扫描,就能确定一个最小元素,
  // 并将其放置在最左边,所以共需经过len-1轮扫描
  for (let i = 0; i < len - 1; i++) {
    // 首先,将minIdx设置为i
    minIdx = i;
    // 从左向右扫描,确定[i,len)范围内的最小元素
    for (let j = i + 1; j < len; j++) {
      // 如果当前元素小于arr[minIdx],则重新设置minIdx
      if (arr[j] < arr[minIdx]) {
        minIdx = j;
      }
    }
    // 扫描结束后,交换arr[i]和arr[minIdx]
    [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
  }
  return arr;
}

// 插入排序
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 稳定性:稳定
function insertionSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 经过第k轮扫描,就能确定[0,k]范围内的元素顺序,
  // 所以共需经过len-1轮扫描
  for (let i = 1; i < len; i++) {
    // 从右向左扫描,比较arr[i]与其之前的元素,
    // 如果arr[i]小于被比较元素,则交换;否则,结束扫描
    for (let j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
      [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
    }
  }
  return arr;
}

// 希尔排序
// 时间复杂度:O(nlogn)
// 空间复杂度:O(1)
// 稳定性:不稳定
function shellSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 定义组距gap为len的一半,
  // 根据gap进行分组,并使用插入排序对各组进行排序,
  // 组距不断折半,直到为0
  for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < len; i++) {
      for (let j = i; j > 0 && arr[j - gap] > arr[j]; j -= gap) {
        [arr[j - gap], arr[j]] = [arr[j], arr[j - gap]];
      }
    }
  }
  return arr;
}

// 归并排序
// 时间复杂度:O(nlogn)
// 空间复杂度:O(n)
// 稳定性:稳定
function mergeSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 如果数组长度小于等于1,则无进行排序的必要,可直接返回arr
  if (len <= 1) return arr;
  // 定义pivot、middle、left、right,
  // pivot为中间下标,middle为中间位置的元素,
  // left和right分别用于存放比middle小和比middle大的元素
  const pivot = Math.floor(len / 2),
    middle = arr[pivot];
  let left = [],
    right = [];
  // 从左向右扫描,将元素们存放到left或right中
  for (let i = 0; i < len; i++) {
    // 如果当前元素小于middle,将其存放到left中;
    // 否则,将其存放到right中
    if (i !== pivot) {
      if (arr[i] < middle) left.push(arr[i]);
      else right.push(arr[i]);
    }
  }
  // 递归进行合并
  return [...mergeSort(left), middle, ...mergeSort(right)];
}

// 快速排序
// 时间复杂度:O(nlogn)
// 空间复杂度:O(logn)
// 稳定性:不稳定
function quickSort(arr) {
  // 获取数组长度
  const len = arr.length;
  // 声明函数subSort,
  // 作用是对[left,right]范围内的元素进行排序
  function subSort(left, right) {
    // 定义left,用于表示基准下标,
    // 其对应的元素为基准元素
    let pivot = left;
    // 当right大于left时,才有进行排序的必要
    if (right > left) {
      // 从左向右扫描,
      // 将比基准元素小的元素放在其左边;
      // 将比基准元素大的元素放在其右边
      for (let i = left + 1; i <= right; i++) {
        // 如果当前元素小于基准元素,则进行交换操作
        if (arr[i] < arr[pivot]) {
          // 称当前元素为较小元素,
          // 交换较小元素和基准元素右边元素
          [arr[i], arr[pivot + 1]] = [arr[pivot + 1], arr[i]];
          // 交换基准元素和较小元素
          [arr[pivot], arr[pivot + 1]] = [arr[pivot + 1], arr[pivot]];
          // 基准下标加1
          pivot++;
          // 一通操作后,较小元素便被放在了基准元素的左边
        }
      }
      // 对[left,pivot-1]和[pivot+1,right]范围内的元素进行排序
      subSort(left, pivot - 1);
      subSort(pivot + 1, right);
    }
  }
  // 对[0,len-1]范围内的元素进行排序
  subSort(0, len - 1);
  return arr;
}

// 堆排序
// 时间复杂度:O(nlogn)
// 空间复杂度:O(1)
// 稳定性:不稳定
function heapSort(arr) {
  // 获取数组长度
  let len = arr.length;
  // 调整堆
  function heapify(i) {
    // 定义largest,初始为i,
    // 用于表示最大元素下标
    let largest = i;
    // 定义left和right,
    // 分别用于表示左、右节点下标
    const left = 2 * i + 1,
      right = 2 * i + 2;
    // 如果左节点下标在[0,len)范围内,
    // 且arr[left]大于arr[largest],
    // 则将larget设置为left
    if (left < len && arr[left] > arr[largest]) {
      largest = left;
    }
    // 同理,对arr[right]进行判断
    if (right < len && arr[right] > arr[largest]) {
      largest = right;
    }
    // 如果largest被重新设置过,
    // 则交换arr[largest]和arr[i],
    // 并对子节点进行调整
    if (largest !== i) {
      [arr[largest], arr[i]] = [arr[i], arr[largest]];
      heapify(largest);
    }
  }
  // 构建大顶堆
  for (let i = Math.floor(len / 2); i >= 0; i--) {
    heapify(i);
  }
  // 交换堆顶元素和末尾元素,使末尾元素最大,并调整堆,
  // 如此反复进行交换、调整,直到大顶堆变为有序序列
  for (let i = len - 1; i > 0; i--) {
    [arr[i], arr[0]] = [arr[0], arr[i]];
    len--;
    heapify(0);
  }
  return arr;
}

// 计数排序
// 时间复杂度:O(n+k)
// 空间复杂度:O(k)
// 稳定性:稳定
function countingSort(arr) {
  // 获取数组长度和最大元素,并初始化桶
  const len = arr.length,
    max = Math.max(...arr),
    bucket = buildArray(max + 1, 0);
  // 从左向右扫描,并计数
  for (let i = 0; i < len; i++) {
    bucket[arr[i]]++;
  }
  // 把桶中的计数取出并“铺开”
  let idx = 0;
  for (let j = 0; j <= max; j++) {
    while (bucket[j] > 0) {
      arr[idx++] = j;
      bucket[j]--;
    }
  }
  return arr;
}

// 桶排序
// 时间复杂度:O(n+k)
// 空间复杂度:O(n+k)
// 稳定性:稳定
function bucketSort(arr) {
  // 获取数组长度、最小元素和最大元素,
  // 根据桶的规模确定桶的数量,并初始化桶
  const len = arr.length,
    min = Math.min(...arr),
    max = Math.max(...arr),
    bucketSize = 10,
    bucketCount = Math.floor((max - min) / bucketSize) + 1,
    buckets = buildArray(bucketCount, generateBlankArray());
  // 从左向右扫描,将元素装进对应的桶中
  for (let i = 0; i < len; i++) {
    const idx = Math.floor((arr[i] - min) / bucketSize);
    buckets[idx].push(arr[i]);
  }
  // 使用插入排序,对各个桶内的元素进行排序
  for (let j = 0; j < bucketCount; j++) {
    insertionSort(buckets[j]);
  }
  // 把桶中的元素取出并“铺开”
  return buckets.flat(2);
}

// 基数排序
// 时间复杂度:O(nk)
// 空间复杂度:O(n+k)
// 稳定性:稳定
function radixSort(arr) {
  // 获取数组长度、最大元素
  const len = arr.length,
    max = Math.max(...arr);
  // 获取最大元素的位数
  let maxDigit = 1;
  while (max / 10 ** maxDigit >= 1) {
    maxDigit += 1;
  }
  // 第1轮给个位数排序,第2轮给十位数排序,以此类推
  for (let digit = 1; digit <= maxDigit; digit++) {
    // 设置余数和除数,并初始化桶
    const mod = 10 ** digit,
      dev = mod / 10,
      buckets = buildArray(10, generateBlankArray());
    // 从左向右扫描,将元素装进对应的桶中
    for (let i = 0; i < len; i++) {
      let idx = Math.floor((arr[i] % mod) / dev);
      buckets[idx].push(arr[i]);
    }
    // 把桶中的元素取出并“铺开”
    arr = buckets.flat(2);
  }
  return arr;
}

测试

新建sorts.js文件,将上一节代码复制到其中。进入文件夹中,执行命令:

node sorts.js

命令行终端中,会输出源数组、排序结果和各类排序耗时:

origin: [
  2818, 8155, 2632, 9331, 7831,  679, 8924,  178, 7742, 9688,
  3590, 4475,  373, 3064, 5473, 3804, 6301, 9160, 5081, 5041,
  4484,  617, 3087, 1000, 4451, 8463, 2317, 8694, 3072, 8010,
   139, 6004, 6066, 4961, 2829, 2040, 8171, 4579, 9054, 2176,
  3168, 4664, 1097, 8535, 3914, 5333, 2002, 3065, 9035, 9758,
  6563, 7281, 1024, 4731, 4337, 3245, 4959, 1442,  468, 4781,
  1964, 3177, 1873, 4566, 8713,  600, 7305, 3951, 8443, 8255,
   525, 6239, 1744, 8518, 8939, 6896, 9102, 8531, 3025,  625,
  1650, 5211, 4507, 3043, 8861, 5749, 5567, 5146, 3070,  210,
  2244, 3156,  401, 9806, 8742, 6600, 9266, 8188, 9764, 2486,
  ... 9900 more items
]
result: [
    1,   3,   4,   5,   6,   7,   7,   7,   8,  10,  11,  11,
   14,  15,  16,  16,  17,  17,  19,  23,  23,  23,  24,  26,
   27,  29,  31,  36,  38,  40,  41,  43,  45,  47,  47,  48,
   49,  50,  50,  52,  53,  54,  55,  56,  56,  57,  57,  57,
   60,  60,  61,  61,  63,  63,  63,  64,  64,  64,  65,  66,
   67,  67,  67,  70,  71,  72,  74,  75,  75,  75,  75,  77,
   81,  83,  83,  85,  85,  86,  87,  89,  89,  94,  97,  97,
   97,  98,  99,  99, 100, 100, 101, 102, 102, 104, 105, 105,
  107, 107, 108, 108,
  ... 9900 more items
]
<<SortMethod>>   <<TimeCost>>
countingSort     4.167 ms
bucketSort       8.095 ms
heapSort         9.147 ms
mergeSort        13.592 ms
radixSort        14.528 ms
shellSort        19.640 ms
quickSort        20.924 ms
insertionSort    48.618 ms
selectSort       56.237 ms
bubbleSort       134.236 ms

总结

根据是否通过比较进行排序,可以将排序算法分为:

  • 比较类排序:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序。其中,选择排序、冒泡排序和插入排序是最基础的排序方法;希尔排序分组进行插入排序,是插入排序的进阶版;归并排序和快速排序都采用了分而治之的思想,不同之处在于,归并排序先分完所有的组,堆子组进行排序后再合并,而快速排序分一次组后便开始排序,然后再分组和排序;堆排序则是利用完全二叉树和大顶堆来进行排序
  • 非比较类排序:计数排序、桶排序、基数排序。它们都利用桶来辅助排序,不同之处在于:计数排序中的每个桶只存放单一数字的个数;桶排序中的每个桶存放某一范围内的数字,并分桶进行排序;基数排序中根据数字某一位数的值来分配桶并进行排序。