TypeScript算法题实战——详解十大经典排序算法(插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序)

目录

  • 一、综合分析
  • 二、冒泡排序
  • 三、选择排序
  • 四、插入排序
  • 五、归并排序
  • 五、快速排序
  • 六、希尔排序
  • 七、堆排序
  • 八、计数排序
  • 九、桶排序
  • 十、基数排序

算法是程序开发中不可或缺的一部分。排序算法作为最基本、最常用的算法之一,在程序开发中起到了至关重要的作用。本文将深入探讨十大经典排序算法,探索这些排序算法的实现原理、时间复杂度及其适用场景并使用TypeScript语言来实现。废话不多说,让我们一同踏上 TypeScript排序算法实战的旅程!

一、综合分析

二、冒泡排序

冒泡排序是一种基本的排序算法,其思路是比较相邻元素的大小,如果逆序则交换,一轮结束后将最大元素“冒泡”到数列的末尾。这个过程像冒泡一样,因此被称为冒泡排序。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

const bubblesort = (arr) => {
  for (let i = 0; i < arr.length; i++){
    for (let j = 0; j < arr.length - i; j++){
      if(arr[j] > arr[j + 1])
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
    }
  }
  return arr;
}

console.log(bubblesort([4, 2, 6, 1, 5, 8, 7, 3]));

时间复杂度:

  • 最好情况:已经有序,只需要进行一次比较,时间复杂度为O(n)。
  • 最坏情况:完全逆序,需要进行n轮比较,时间复杂度为O(n^2)。
  • 平均情况:时间复杂度也为O(n^2)。

三、选择排序

选择排序是一种简单直观的排序算法,其思想是每次从待排序的数列中选择最小(或最大)的元素,放到已排序数列的末尾(或开头),直到整个数列有序。

const selectsort = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    let tmpMin = arr[i];
    let tmpIndex = i;
    for (let j = i; j < arr.length; j++){
      if (tmpMin > arr[j]) {
        tmpMin = arr[j];
        tmpIndex = j;
      }
    }
    [arr[i], arr[tmpIndex]] = [arr[tmpIndex], arr[i]]
  }
  return arr;
}
console.log(selectsort([4, 2, 6, 1, 5, 8, 7, 3]));

时间复杂度:

  • 最好情况:O(n^2)。
  • 最坏情况:O(n^2)。
  • 平均情况:O(n^2)。

四、插入排序

插入排序是一种简单直观的排序算法,其基本思想是将待排序序列分为两部分,一部分已经排好序,另一部分待排序,然后将待排序部分的元素依次插入到已排序部分的适当位置,使得插入后仍然有序。

const insertsort = (arr) => {
  for (let i = 1; i < arr.length; i++){
    let cur = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > cur) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = cur;
  }
  return arr;
}

console.log(insertsort([4, 2, 6, 1, 5, 8, 7, 3]));

时间复杂度:

  • 最好情况:O(n)。
  • 最坏情况:O(n^2)。
  • 平均情况:O(n^2)。

五、归并排序

归并排序是一种经典的分治算法,它的基本思想是将待排序的数组划分为两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组合并成一个有序数组。具体来说,归并排序的步骤如下:

  1. 分解:将待排序的数组不断地二分,直到每个子数组只包含一个元素,即认为这些子数组已经有序。
  2. 合并:将已经有序的子数组两两合并,形成新的有序子数组。
  3. 重复步骤2,直到只剩下一个有序数组,即为最终排序结果。

简易写法:

const quickSort = (arr: number[]): number[] => {
  if (arr.length <= 1) return arr;
  const pivotIndex = Math.floor(arr.length / 2);
  const pivot = arr[pivotIndex];
  const left: number[] = [];
  const right: number[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (i == pivotIndex) continue;
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
};
console.log(quickSort([4, 2, 6, 1, 5, 8, 7, 3]));

归并排序的时间复杂度是O(nlogn),其中n是待排序数组的长度。虽然归并排序的时间复杂度比较稳定,但需要额外的空间来存储辅助数组,因此其空间复杂度是O(n)。

时间复杂度:假设数组长度为 n,需要进行 logn 次归并操作;每次归并操作需要 O(n) 的时间复杂度;

  • 最好情况:O(n)。
  • 最坏情况:O(nlogn)。
  • 平均情况:O(nlogn)。

五、快速排序

const quickSort = (arr: number[]): number[] => {
  if (arr.length <= 1) return arr;
  const pivotIndex = Math.floor(arr.length / 2);
  const pivot = arr[pivotIndex];
  const left: number[] = [];
  const right: number[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (i == pivotIndex) continue;
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
};

六、希尔排序

希尔排序是一种插入排序的变体,也称为缩小增量排序。其基本思想是将待排序的数组分成若干个子序列,对每个子序列进行插入排序,然后逐步缩小增量,直到增量为1。

具体来说,希尔排序的步骤如下:

  1. 选择一个增量序列:将待排序数组按照一定的间隔划分成若干个子序列,对每个子序列进行插入排序。这里的增量序列可以使用Hibbard增量序列,它的公式为 2 k − 1 2^k-1 2k1,其中k为增量的大小,从n/2、n/4、n/8等不断除以2,直到增量等于1为止。
  2. 对子序列进行插入排序:对每个子序列进行插入排序,将子序列中的数依次插入到前面已经排好序的序列中,使得插入后仍然有序。
  3. 逐步缩小增量:对于增量序列,逐步缩小增量,重复步骤2,直到增量为1。
const shellSort = (arr: number[]): number[] => {
  let gap = Math.floor(arr.length / 2);
  while (gap > 0) {
    for (let i = gap; i < arr.length; i++) {
      const cur = arr[i];
      let j = i;
      while (j >= gap && arr[j - gap] > cur) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = cur;
    }
    gap = Math.floor(gap / 2);
  }
  return arr;
};
console.log(shellSort([4, 2, 6, 1, 5, 8, 7, 3]));
  • 最好情况:O(n)。
  • 最坏情况:O( n 2 n^2 n2)。
  • 平均情况:O(nlogn) ~ O( n 2 n^2 n2)。

七、堆排序

堆排序的实现过程主要分为两个步骤:

  1. 构建最大堆:将待排序的数组看作是完全二叉树的形式,从最后一个非叶子节点开始,依次对每个非叶子节点进行堆调整,使其成为最大堆的形式。具体做法是比较节点和它的两个子节点之间的大小,将较大的子节点调整到父节点的位置上,然后递归地对调整过后的子节点进行堆调整。
  2. 堆排序:从最大堆中取出根节点(即数组的第一个元素),将其与最后一个元素交换位置,然后对剩余的节点重新进行堆调整,以确保剩余节点仍然构成最大堆。重复这个过程,直到所有元素都被取出并排序。

堆排序相较于其他排序算法的优势在于,它是一种原地排序算法,只需要一个额外的空间来存放根节点的值,没有浪费的空间。同时,堆排序还具有稳定的时间复杂度和稳定的排序结果。不过,需要注意的是,堆排序对于小规模的数据排序并不是特别高效,适用于大规模数据的排序。

const heapSort = (arr: number[]): number[] => {
  const adjustHeap = (arr: number[], i: number, len: number) => {
    const tmp = arr[i];
    for (let j = 2 * i + 1; j < len; j = 2 * j + 1) {
      if (j + 1 < len && arr[j + 1] > arr[j]) j++;
      if (arr[j] > tmp) {
        arr[i] = arr[j];
        i = j;
      } else {
        break;
      }
    }
    arr[i] = tmp;
  };
  // 先建立大根堆(从最后一个非叶子节点向上调整)
  for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
    adjustHeap(arr, i, arr.length);
  }
  // 每次把堆顶的数与最后一个数交换,并重新调整堆
  for (let i = arr.length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    adjustHeap(arr, 0, i);
  }
  return arr;
};
console.log(heapSort([4, 2, 6, 1, 5, 8, 7, 3]));

八、计数排序

const countSort = (arr: number[]): number[] => {
  if (arr.length <= 1) return arr;
  // 找出最大值和最小值
  let max = arr[0];
  let min = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) max = arr[i];
    if (arr[i] < min) min = arr[i];
  }
  // 统计数列中每个值出现的次数
  const count = new Array(max - min + 1).fill(0);
  for (let i = 0; i < arr.length; i++) {
    count[arr[i] - min]++;
  }
  let index = 0;
  for (let i = 0; i < count.length; i++) {
    while (count[i] > 0) {
      arr[index++] = i + min;
      count[i]--;
    }
  }
  return arr;
};
console.log(countSort([4, 2, 6, 1, 5, 8, 7, 3]));

九、桶排序

const bucketSort = (arr: number[], bucketSize: number): number[] => {
  if (arr.length <= 1) return arr;
  // 插入排序
  function insertionSort(arr: number[]) {
    let i;
    let j;
    let temp;
    for (i = 1; i < arr.length; i++) {
      temp = arr[i];
      j = i - 1;
      while (j >= 0 && arr[j] > temp) {
        arr[j + 1] = arr[j];
        j--;
      }
      arr[j + 1] = temp;
    }
    return arr;
  }
  // 找到数组中的最大值和最小值
  let minValue = arr[0];
  let maxValue = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) minValue = arr[i];
    else if (arr[i] > maxValue) maxValue = arr[i];
  }
  // 计算桶的数量
  const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  const buckets = new Array(bucketCount);
  for (let i = 0; i < buckets.length; i++) {
    buckets[i] = [];
  }
  // 将数组中的元素分配到各个桶中
  for (let i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
  }
  // 对每个桶中的元素进行排序
  arr.length = 0;
  for (let i = 0; i < buckets.length; i++) {
    if (buckets[i].length > 0) {
      insertionSort(buckets[i]);
      for (let j = 0; j < buckets[i].length; j++) {
        arr.push(buckets[i][j]);
      }
    }
  }
  return arr;
};
console.log(bucketSort([4, 2, 6, 1, 5, 8, 7, 3], 5));

十、基数排序

const radixSort = (arr: number[]) => {
  const maxDigit = getMaxDigit(arr);
  // 获取最大位数
  function getMaxDigit(arr: number[]) {
    let max = 0;
    for (let i = 0; i < arr.length; i++) {
      const digit = getDigitCount(arr[i]);
      max = Math.max(max, digit);
    }
    return max;
  } // 获取数字的位数
  function getDigitCount(num: number) {
    if (num === 0) return 1;
    return Math.floor(Math.log10(Math.abs(num))) + 1;
  } // 获取数字的第i位数字
  function getDigit(num: number, i: number) {
    return Math.floor(Math.abs(num) / Math.pow(10, i)) % 10;
  }
  for (let i = 0; i < maxDigit; i++) {
    const buckets: number[][] = []; // 创建桶
    for (let j = 0; j < arr.length; j++) {
      const digit = getDigit(arr[j], i); // 获取数字的第i位数字
      if (!buckets[digit]) {
        buckets[digit] = [];
      }
      buckets[digit].push(arr[j]); // 将数字放入相应的桶中
    }
    const tmp: number[] = [];
    arr = tmp.concat(...buckets); // 将桶中的数字取出来,重新放入arr数组中
  }
  return arr;
};

console.log(radixSort([4, 2, 6, 1, 5, 8, 7, 3]));

你可能感兴趣的:(TypeScript算法题实战——详解十大经典排序算法(插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序))