JavaScript实现五种排序算法

105 阅读6分钟

冒泡排序

规则:

  1. 将第一个元素和第二个元素进行对比,如果第一个元素大于第二个,则将二者进行交换,反之,则不用交换

  2. 当第一步比较完之后,对第二个元素和第三个以同样的规则进行比较,依次类推,最终的结果就是这一轮比较中最大的那个数被移动到了最后的位置,符合我们的预期目的

  3. 进行完第一轮比较,开始第二轮,依然是在第一个元素的位置开始比较,同样的规则,但是这一次只需要比较到倒数第二个元素,因为最后的那个已经被第一轮的最大数给占据着,无需再与它比较。

  4. 经过第二轮,倒数第二大的数也被拿出来了,并且移动到了数组倒数第二的位置,以此类推,再进行多轮直到结束,得到一个从小到大的数组。

/**
 * Bubble Sort
 * Time complexity: Best case O(n), worst case O(n^2)
 * @param arr: unsorted array
 * @returns sorted array
 */
function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j + 1] < arr[j]) {
        // ES6语法
        [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
      }
    }
  }
  return arr;
}

升级版本的冒泡排序,对于无序度低的数组,可以减少计算量

function improvedBubbleSort(arr) {
  let noSwaps;
  for (let i = arr.length; i > 0; i--) {
    noSwaps = true;
    for (let j = 0; j < i - 1; j++) {
      if (arr[j + 1] < arr[j]) {
        [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
        noSwaps = false;
      }
    }
    if (noSwaps) {
      break;
    }
  }
  return arr;
}

插入排序

插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过 构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插 入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从 后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法规则:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5
/**
 * Insertion Sort
 * Time complexity: Best case O(n), worst case O(n^2)
 * @param arr: unsorted array
 * @returns sorted array
 */
function insertionSort(arr) {
  for (let i = 1; i < arr.length; i++) {
    for (let j = i - 1; j > -1; j--) {
      if (arr[j + 1] < arr[j]) {
        [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
      }
    }
  }
  return arr;
}

选择排序

选择排序更加直观。选择排序会首先从待排序序列中选择一个最小的元素放入排序好的序列中,然后依次在从未排序好的序列中选择最小的元素,直到最后需要选择的待排序序列中只有一个元素,只需要将这个元素放在最后位置,就完成了整个排序过程。

/**
 * Selection Sort
 * Time complexity: Best case O(n), worst case O(n^2)
 * @param arr: unsorted array
 * @returns sorted array
 */
function selectionSort(arr) {
  let min;
  for (let i = 0; i < arr.length; i++) {
    min = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[min]) {
        min = j;
      }
    }

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

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。(摘自baidu)

归并排序的核心思想是将两个已经排序的序列合并成一个序列,那如何得到两个已经排序的序列呢?我们知道, 如果一个序列只有一个元素,那该序列是已经排序的,这样我们就可以利用分治的思想,将未排序的序列划分成更小的序列,只到我们可以很方便的对小序列进行排序(比如划分到序列只有一个元素, 或者序列很小可以方便的使用其它排序算法进行排序),然后再将小序列逐次合并,得到最后的排序结果。

/**
 * Merge Sort
 * Time complexity: normal case O(nlogn)
 * @param arr: unsorted array
 * @returns sorted array
 */
function merge(arr1, arr2) {
  // 定义一个空数组,两个指针
  let res = [],
    i = 0,
    j = 0;

  // 循环直到其中一个指针到达数组尽头
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) {
      res.push(arr1[i]);
      i++;
    } else {
      res.push(arr2[j]);
      j++;
    }
    
  }

  // 将指针未遍历完的数组添加到新数组
  while (i < arr1.length) {
    res.push(arr1[i]);
    i++;
  }
  while (j < arr2.length) {
    res.push(arr2[j]);
    j++;
  }
  return res;
}

function mergeSort(arr) {
  // Base case
  if (arr.length <= 1) {
    return arr;
  }

  let mid = Math.floor(arr.length / 2);
  let left = mergeSort(arr.slice(0, mid));
  let right = mergeSort(arr.slice(mid));

  return merge(left, right);
}

快速排序

快排的核心思想就是每次选定一个 pivot ,调整pivot的位置,使得pivot左边的值都小于pivot,pivot右边的值均大于pivot。对于和pivot相等的值,将其归类到pivot左边。

function partition(arr, start, end) {
  let pivot = arr[start];
  let swapIdx = start;

  for (let i = start + 1; i <= end; i++) {
    if (arr[i] < pivot) {
      swapIdx++;
      [arr[i], arr[swapIdx]] = [arr[swapIdx], arr[i]];
    }
  }

  [arr[swapIdx], arr[start]] = [arr[start], arr[swapIdx]];
  return swapIdx;
}

function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    let pivotIndex = partition(arr, left, right);

    quickSort(arr, left, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, right);
  }
  return arr;
}

总结

上述五种经典排序算法,冒泡排序、插入排序、选择排序的平均时间复杂度均为 O(n^2),处理小数据量绰绰有余,对于数据量较大的数列,要采用使用递归方法的归并排序或者快速排序,这两个算法的时间复杂度为 O(nlogn)。

总结了五种算法的时间复杂度,接下对内存消耗和稳定性做分析。

对于归并排序,我们需要将排好序的子序列填充到一个新开辟的数组空间里去,而对于其他四种排序算法,都是先做比较然后在原数组上做换位操作。所以归并排序需要更多的内存消耗。对于除了存储数据本身的空间不需要额外的辅助存储空间的算法,我们称之为原地排序(Sorted in place)的算法,即空间复杂度为 O(1) 的算法。

稳定性这一概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。稳定性在实际工作中有非常重要的意义。因为在实际工作中,我们排序的不是一个数组,通常会是一系列对象,我们需要按照对象的某个key来排序。

比如要给电商交易系统中的订单排序。订单有两个属性,一个是下单时间,一个是订单金额。如果现在有10万条数据,我们希望按照金额从小到大对订单数据排序。对于金额相同的订单,我们希望按照下单时间从早到晚有序。

对于这样一个需求,借助稳定排序算法,可以这么解决:先按照下单时间排序,排序完成后,再用稳定排序算法,按照订单金额重新排序。这样就实现了订单按照下单时间从早到晚有序,同时相同金额的订单仍然保持下单时间从早到晚有序。

最后总结:

  • 冒泡排序:原地、稳定、O(n^2)
  • 插入排序:原地、稳定、O(n^2)
  • 选择排序:原地、不稳定(因为每次都要找剩余最小、相对位置最靠后的元素,并和前面的元素交换位置)、O(n^2)
  • 归并排序:非原地、稳定性要看merge函数(这里的归并排序不是稳定算法,如果判定条件为if (arr1[i] <= arr2[j])则该算法稳定)、O(nlogn)
  • 快速排序:原地、不稳定、O(nlogn)