Typescript和JavaScript实现十大排序算法

439 阅读12分钟

本文将使用ts与js分别实现在计算机科学中著名的十大排序算法,冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、计数排序、桶排序、基数排序、堆排序。

1. 冒泡排序

冒泡排序(英语:Bubble Sort),是一种简单的、稳定的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。时间复杂度O(n2)O(n^2),空间复杂度O(1)O(1)

1.1 静图演示

image.png

1.2 动图演示

bubble.gif

1.3 ts代码实现

/**
 * 冒泡排序
 * @param arr 
 * @param compareFn 比较函数: a > b 升序,b > a 降序
 * @returns 
 */
function bubbleSort<T = unknown>(
  arr: T[],
  compareFn: (a: T, b: T) => boolean
) {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  for (let i = 0; i < length - 1; i++) {
    // 每进行一次内循环,都可以找出一个极值
    for (let j = 0; j < length - 1 - i; j++) {
      if (compareFn(arr[j], arr[j + 1])) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }

  return arr;
}

1.4 js代码实现

/**
 * 冒泡排序,改变原数组
 * @param {any[]} arr
 * @param {(a, b) => boolean} compareFn
 * @returns
 */
const bubbleSort = (arr, compareFn) => {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  for (let i = 0; i < length - 1; i++) {
    // 每进行一次内循环,都可以找出一个极值
    for (let j = 0; j < length - 1 - i; j++) {
      if (compareFn(arr[j], arr[j + 1])) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }

  return arr;
};

2. 选择排序

选择排序(英语:Selection sort)是一种简单直观的、不稳定的原址比较排序算法,大致思路是找到数据结构中的最小(大)值并将其放置在第一位,接着找到第二小(大)的值并将其放在第二位,以此类推。时间复杂度O(n2)O(n^2),空间复杂度O(1)O(1)

2.1 静图演示

image.png

2.2 动图演示

Sorting_selection_sort_anim.gif

2.3 ts代码实现

function selectionSort<T = unknown>(
  arr: T[],
  compareFn: (a: T, b: T) => boolean
) {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  for (let i = 0; i < length - 1; i++) {
    let idx = i;
    for (let j = i + 1; j < length; j++) {
      if (compareFn(arr[idx], arr[j])) {
        idx = j;
      }
    }

    if (i !== idx) {
      [arr[i], arr[idx]] = [arr[idx], arr[i]];
    }
  }

  return arr;
}

2.4 js代码实现

/**
 * 选择排序,改变原数组
 * @param {any[]} arr
 * @param {(a, b) => boolean} compareFn
 * @returns
 */
const selectionSort = (arr, compareFn) => {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  for (let i = 0; i < length - 1; i++) {
    let idx = i;
    // 寻找最大(小)值索引,最大还是最小取决于你的比较函数
    for (let j = i + 1; j < length; j++) {
      if (compareFn(arr[idx], arr[j])) {
        idx = j;
      }
    }

    if (i !== idx) {
      [arr[i], arr[idx]] = [arr[idx], arr[i]];
    }
  }

  return arr;
};

3. 插入排序

插入排序(英语:Insertion Sort)是一种稳定的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。时间复杂度O(n2)O(n^2),空间复杂度O(1)O(1)

3.1 静图演示

image.png

3.2 动图演示

Sorting_insertion_sort_anim.gif

3.3 ts代码实现

/**
 * 插入排序,改变原数组
 * @param arr 
 * @param compareFn 
 * @returns 
 */
function insertionSort<T = unknown>(
  arr: T[],
  compareFn: (a: T, b: T) => boolean
) {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  // 将数组的每一项与其前面的每一个元素比较
  for (let i = 1; i < length; i++) {
    let prevIdx = i - 1;
    const currVal = arr[i];

    while(prevIdx >= 0 && compareFn(arr[prevIdx], currVal)) {
      arr[prevIdx + 1] = arr[prevIdx];
      prevIdx--;
    }

    prevIdx++;
    arr[prevIdx] = currVal;
  }

  return arr;
}

3.4 js代码实现

/**
 * 插入排序,改变原数组
 * @param {any[]} arr
 * @param {(a, b) => boolean} compareFn
 * @returns
 */
const insertionSort = (arr, compareFn) => {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  // 将数组的每一项与其前面的每一个元素比较
  for (let i = 1; i < length; i++) {
    let prevIdx = i - 1;
    const currVal = arr[i];

    while (prevIdx >= 0 && compareFn(arr[prevIdx], currVal)) {
      arr[prevIdx + 1] = arr[prevIdx];
      prevIdx--;
    }

    prevIdx++;
    arr[prevIdx] = currVal;
  }

  return arr;
};

4. 希尔排序

希尔排序(英语:Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。时间复杂度O(nlog2n)O(nlog^2n),空间复杂度O(1)O(1)

4.1 动图演示

Sorting_shellsort_anim.gif

4.2 ts代码实现

function shellSort<T = unknown>(
  arr: T[],
  compare: (a: T, b: T) => boolean = (a, b) => a > b
) {
  const len = arr.length;
  if (len <= 1) return arr;

  // 动态定义间隔序列,gap等于1时,就是普通的插入排序
  let gap = 1;
  while (gap < len / 3) {
    gap = gap * 3 + 1;
  }

  do {
    // 从第 gap 个元素开始,依次插入到前面已排序的数组中(假设 gap 索引前的元素已排序)
    for (let i = gap; i < len; i++) {
      const temp = arr[i];
      let prev = i - gap;

      // 从后往前遍历,找到比 temp 小的元素,将其后移
      while (prev >= 0 && compare(arr[prev], temp)) {
        arr[prev + gap] = arr[prev];
        prev -= gap;
      }

      // 不管是否有元素后移,都将 temp 插入到正确的位置
      arr[prev + gap] = temp;
    }

    // 重新计算间隔序列
    gap = (gap - 1) / 3;
  } while (gap >= 1);

  return arr;
}

4.3 js代码实现

function shellSort(
  arr,
  compare = (a, b) => a > b
) {
  const len = arr.length;
  if (len <= 1) return arr;

  // 动态定义间隔序列,gap等于1时,就是普通的插入排序
  let gap = 1;
  while (gap < len / 3) {
    gap = gap * 3 + 1;
  }

  do {
    // 从第 gap 个元素开始,依次插入到前面已排序的数组中(假设 gap 索引前的元素已排序)
    for (let i = gap; i < len; i++) {
      const temp = arr[i];
      let prev = i - gap;

      // 从后往前遍历,找到比 temp 小的元素,将其后移
      while (prev >= 0 && compare(arr[prev], temp)) {
        arr[prev + gap] = arr[prev];
        prev -= gap;
      }

      // 不管是否有元素后移,都将 temp 插入到正确的位置
      arr[prev + gap] = temp;
    }

    // 重新计算间隔序列
    gap = (gap - 1) / 3;
  } while (gap >= 1);

  return arr;
}

5. 归并排序

归并排序(英语:Merge sort),一种分而治之算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。时间复杂度O(nlogn)O(nlogn),空间复杂度O(n)O(n)

5.1 静图演示

image.png

5.2 动图演示

plot-mergesort-1.gif

5.3 ts代码实现

/**
 * 归并排序,不改变原数组,返回新数组
 * @param arr 
 * @param compareFn 
 * @returns 
 */
function mergeSort<T = unknown>(
  arr: T[],
  compareFn: (a: T, b: T) => boolean
): T[] {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  const middle = Math.floor(length / 2);
  const left = mergeSort(arr.slice(0, middle), compareFn);
  const right = mergeSort(arr.slice(middle, length), compareFn);

  const merge = (
    leftArr: T[],
    rightArr: T[],
    compareFn: (a: T, b: T) => boolean
  ) => {
    const result: T[] = [];
    if (compareFn(leftArr[0], rightArr[rightArr.length - 1])) {
      return result.concat(rightArr, leftArr);
    }
    if (compareFn(rightArr[0], leftArr[leftArr.length - 1])) {
      return result.concat(leftArr, rightArr);
    }

    let i = 0;
    let j = 0;
    do {
      if (compareFn(leftArr[i], rightArr[j])) {
        result.push(rightArr[j++]);
      } else {
        result.push(leftArr[i++]);
      }
    } while(i < leftArr.length && j < rightArr.length);

    return result.concat(
      i >= leftArr.length ? rightArr.slice(j) : leftArr.slice(i)
    );
  };

  return merge(left, right, compareFn);
}

5.4 js代码实现

/**
 * 归并排序,不改变原数组
 * @param {any[]} arr
 * @param {(a, b) => boolean} compareFn
 * @returns {any[]}
 */
const mergeSort = (arr, compareFn) => {
  const length = arr.length;
  if (length < 2) {
    return arr;
  }

  // 分割数组
  const middle = Math.floor(length / 2);
  const left = mergeSort(arr.slice(0, middle), compareFn);
  const right = mergeSort(arr.slice(middle, length), compareFn);

  const merge = (leftArr, rightArr, compareFn) => {
    const result = [];
    // 因为leftArr和rightArr都是排好序的,所以只要leftArr的第一个值大于(小于)rightArr的最后一个值
    // 则leftArr所有元素都大于(小于)rightArr的所有元素
    if (compareFn(leftArr[0], rightArr[rightArr.length - 1])) {
      return result.concat(rightArr, leftArr);
    }
    if (compareFn(rightArr[0], leftArr[leftArr.length - 1])) {
      return result.concat(leftArr, rightArr);
    }

    // 将两个排好序的数组,按顺序插入新数组
    let i = 0;
    let j = 0;
    do {
      if (compareFn(leftArr[i], rightArr[j])) {
        result.push(rightArr[j++]);
      } else {
        result.push(leftArr[i++]);
      }
    } while (i < leftArr.length && j < rightArr.length);

    return result.concat(
      i >= leftArr.length ? rightArr.slice(j) : leftArr.slice(i)
    );
  };

  return merge(left, right, compareFn);
};

6. 快速排序

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,是对冒泡排序算法的一种改进,最早由东尼·霍尔提出。算法原理,从数组选出一个基准值,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边,这个过程称为一趟快速排序,对子序列重复快速排序过程。时间复杂度O(nlogn)O(nlogn),空间复杂度视实现方法而定。

6.1 动图演示

Sorting_quicksort_anim.gif

6.2 ts代码实现

/**
 * 快速排序,不改变原数组
 * @param arr 
 * @param compare 
 * @returns 
 */
function quickSort<T = number>(arr: T[], compare = (a: T, b: T) => a > b): T[] {
  if (arr.length < 2) return arr;

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

  return [...quickSort(left, compare), pivot, ...quickSort(right, compare)];
}

/**
 * 快速排序,改变原数组
 * @param arr 
 * @param compare 
 * @returns 
 */
function quickSortPlus<T = number>(arr: T[], compare = (a: T, b: T) => a > b): T[] {
  const quick = (arr: T[], left: number, right: number) => {
    if (left >= right) {
      return;
    }

    const pivot = arr[left];
    let i = left;
    let j = right;
    while( i < j) {
      while (i < j && compare(arr[j], pivot)) {
        j--;
      }
      arr[i] = arr[j];

      while (i < j && compare(pivot, arr[i])) {
        i++;
      }
      arr[j] = arr[i];
    }

    arr[i] = pivot;
    quick(arr, left, i - 1);
    quick(arr, i + 1, right);
  };

  quick(arr, 0, arr.length - 1);
  return arr;
}

6.3 js代码实现

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

  return [...quickSort(left, compare), pivot, ...quickSort(right, compare)];
}

/**
 * 快速排序,改变原数组,空间复杂度更优
 * @param {any[]} arr 
 * @param {(a: number, b: number) => boolean} compare 
 * @returns {any[]}
 */
function quickSortPlus(arr, compare = (a, b) => a > b) {
  const quick = (arr, left, right) => {
    if (left >= right) return;

    const pivot = arr[left];
    let i = left;
    let j = right;

    // 一次排序,将比 pivot 小的放在一边,比 pivot 大的放在另一边
    while (i < j) {
      while (i < j && compare(arr[j], pivot)) {
        j--;
      }
      arr[i] = arr[j];

      while (i < j && compare(pivot, arr[i])) {
        i++;
      }
      arr[j] = arr[i];
    }

    arr[i] = pivot;
    quick(arr, left, i - 1);
    quick(arr, i + 1, right);
  };

  quick(arr, 0, arr.length - 1);
  return arr;
}

7. 计数排序

计数排序(英语:Counting sort)是一种分布式排序算法,一种稳定的线性时间整数排序算法。使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可以迭代以构建排序后的结果数组。时间复杂度O(n+k)O(n + k),空间复杂度O(n+k)O(n + k)

7.1 静图演示

image.png

7.2 动图演示

R-C.gif

7.3 ts代码实现

/**
 * 计数排序,不改变原数组,只支持整数
 * @param arr 
 * @returns 
 */
function countingSort(arr: number[]) {
  if (arr.length < 2) {
    return arr;
  }

  const counts: {positive: number, negative: number}[] = [];
  arr.forEach((val) => {
    if (!Number.isInteger(val)) {
      return;
    }

    if (!counts[Math.abs(val)]) {
      counts[Math.abs(val)] = {positive: 0, negative: 0};
    }

    if (val >= 0) {
      counts[val].positive++;
    } else {
      counts[Math.abs(val)].negative++;
    }
  });

  const sorts: number[] = [];
  counts.forEach((value, i) => {
    while(value.positive > 0 || value.negative > 0) {
      if (value.positive > 0) {
        sorts.push(i);
        value.positive--;
      }

      if (value.negative > 0) {
        sorts.unshift(-i);
        value.negative--;
      }
    }
  });
  return sorts;
}

7.4 js代码实现

/**
 * 计数排序,不改变原数组,只支持整数
 * @param {any[]} arr
 * @returns
 */
const countingSort = (arr) => {
  if (arr.length < 2) {
    return arr;
  }

  const counts = [];
  arr.forEach((val) => {
    if (!Number.isInteger(val)) {
      return;
    }

    if (!counts[Math.abs(val)]) {
      counts[Math.abs(val)] = { positive: 0, negative: 0 };
    }

    if (val >= 0) {
      counts[val].positive++;
    } else {
      counts[Math.abs(val)].negative++;
    }
  });

  const sorts = [];
  counts.forEach((value, i) => {
    while (value.positive > 0 || value.negative > 0) {
      if (value.positive > 0) {
        sorts.push(i);
        value.positive--;
      }

      if (value.negative > 0) {
        sorts.unshift(-i);
        value.negative--;
      }
    }
  });
  return sorts;
};

8. 桶排序

桶排序(英语:Bucket sort) 或所谓的箱排序,是一种分布式排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。时间复杂度O(n+k)O(n + k),空间复杂度O(n+k)O(n + k)

8.1 动图演示

bucketsort.gif

8.2 ts代码实现

/**
 * 桶排序,不改变原数组
 * @param arr 
 * @param isAsc 默认升序
 * @returns 
 */
function bucketSort(arr: number[], isAsc = true) {
  const len = arr.length;
  if (len < 2) return arr;

  const min = Math.min(...arr);
  const max = Math.max(...arr);
  if (min === max) return arr;

  const bucketSize = Math.ceil((max - min) / 5);
  const buckets: number[][] = [];
  for (let i = 0; i < len; i++) {
    const index = Math.floor((arr[i] - min) / bucketSize);
    if (!buckets[index]) {
      buckets[index] = [];
    }
    buckets[index].push(arr[i]);
  }

  const result: number[] = [];
  for (let i = 0; i < buckets.length; i++) {
    if (!buckets[i]) {
      continue;
    }
    const sorts = bucketSort(buckets[i], isAsc);
    isAsc ? result.push(...sorts) : result.unshift(...sorts);
  }
  return result;
}

8.3 js代码实现

/**
 * 桶排序,不改变原数组
 * @param {number[]} arr
 * @param {boolean} [isAsc] 是否升序,默认升序
 * @return {number[]}
 */
function bucketSort(arr, isAsc = true) {
  const len = arr.length;
  if (len < 2) return arr;

  const min = Math.min(...arr);
  const max = Math.max(...arr);
  if (min === max) return arr;

  const bucketSize = Math.ceil((max - min) / 5);
  const buckets = [];
  for (let i = 0; i < len; i++) {
    const index = Math.floor((arr[i] - min) / bucketSize);
    if (buckets[index]) {
      buckets[index].push(arr[i]);
    } else {
      buckets[index] = [arr[i]];
    }
  }

  for (let i = 0; i < buckets.length; i++) {
    if (!buckets[i]) {
      continue;
    }
    buckets[i] = bucketSort(buckets[i], isAsc);
  }

  return buckets.reduce((acc, cur) => {
    if (isAsc) {
      return acc.concat(cur);
    }
    return cur.concat(acc);
  }, []);
}

9. 基数排序

基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。时间复杂度O(kn)O(k * n),空间复杂度O(k+n)O(k + n)

9.1 动图演示

radix.gif

9.2 ts代码实现

function radixSort(arr: number[], isAsc = true) {
  if (arr.length < 2) return arr;

  let max = Math.max(...arr);
  if (max < 0) {
    max = Math.abs(Math.min(...arr));
  }
  const radix = 10;
  let exp = 1;

  while(max / exp > 0) {
    const buckets: number[][] = [];
    arr.forEach((item) => {
      let index = Math.floor(Math.abs(item) / exp) % radix;
      if (item < 0) {
        index += radix;
      }

      if (buckets[index]) {
        buckets[index].push(item);
      } else {
        buckets[index] = [item];
      }
    });

    arr = [];
    buckets.forEach((v, i) => {
      if (!v) return;

      if (isAsc) {
        if (i < 10) {
          arr.push(...v);
          return;
        }
        arr.unshift(...v);
        return;
      }

      if (i < 10) {
        arr.unshift(...v);
        return;
      }
      arr.push(...v);
    });
    exp *= radix;
  }
  return arr;
}

9.3 js代码实现

function radixSort(arr, isAsc = true) {
  if (arr.length < 2) return arr;

  let max = Math.max(...arr);
  if (max < 0) {
    max = Math.abs(Math.min(...arr));
  }
  const radix = 10;
  let exp = 1;

  while (max / exp > 0) {
    const buckets = [];
    arr.forEach((item) => {
      let index = Math.floor(Math.abs(item) / exp) % radix;
      if (item < 0) {
        index += radix;
      }
      if (!buckets[index]) {
        buckets[index] = [];
      }
      buckets[index].push(item);
    });

    arr = [];
    buckets.forEach((val, i) => {
      if (!val) return;

      if (isAsc) {
        if (i < 10) {
          arr.push(...val);
          return;
        }

        arr.unshift(...val);
        return;
      }

      if (i < 10) {
        arr.unshift(...val);
        return;
      }

      arr.push(...val);
    });

    exp *= radix;
  }

  return arr;
}

10. 堆排序

堆排序(Heap Sort)是一种基于比较的排序算法,它的基本思想是:将待排序的序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n1n-1 个序列重新构造成一个堆,这样就会得到 nn 个元素中的次大值。如此反复执行,便能得到一个有序序列了。时间复杂度为 O(nlogn)O(nlogn),空间复杂度为 O(1)O(1)

10.1 静图演示

image.png

10.2 动图演示

heapsort.gif

10.3 ts代码实现

/**
 * 堆排序,改变原数组
 * @param arr 
 * @param compare 默认升序
 * @returns 
 */
function heapSort<T = number>(arr: T[], compare = (a: T, b: T) => a > b) {
  const heapify = (arr: T[], len: number) => {
    const count = Math.floor((len - 1) / 2);

    for (let i = count; i >= 0; i--) {
      const left = i * 2 + 1;
      const right = i * 2 + 2;
      let extremum = i;

      if (left < len && compare(arr[left], arr[extremum])) {
        extremum = left;
      }
      if (right < len && compare(arr[right], arr[extremum])) {
        extremum = right;
      }

      if (extremum !== i) {
        [arr[i], arr[extremum]] = [arr[extremum], arr[i]];
      }
    }
  };

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

  return arr;
}

10.4 js代码实现

/**
 * 堆排序,改变原数组
 * @param {any[]} arr 
 * @param {(a, b) => boolean} [compare] 默认升序
 * @returns 
 */
function heapSort(arr, compare = (a, b) => a > b) {
  const heapify = (arr, len) => {
    const count = Math.floor((len - 1) / 2);
    for (let j = count; j >= 0; j--) {
      const left = 2 * j + 1;
      const right = 2 * j + 2;
      let max = j;

      if (left < len && compare(arr[left], arr[max])) {
        max = left;
      }
      if (right < len && compare(arr[right], arr[max])) {
        max = right;
      }

      if (max !== j) {
        [arr[j], arr[max]] = [arr[max], arr[j]];
      }
    }
  };

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

  return arr;
}

附以上所有最新算法源码,点击跳转。