算法学习笔记——排序

292 阅读4分钟

这是我参与更文挑战的第22天,活动详情查看: 更文挑战

什么是排序

排序就是将输入的数字按照从小到大的顺序进行排列。

各种各样的排序算法

由于排序是一个比较基础的问题,所以排序算法的种类也比较多。

冒泡排序

冒泡排序就是重复“从序列右边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置”这一操作的算法。在这个过程中,数字会像泡泡一样,慢慢从右往左“浮”到序列的顶端,所以这个算法才被称为“冒泡排序”。

耗时

在冒泡排序中,第1轮需要比较n-1次,第2轮需要比较n-2次……第n-1轮需要比较1次。因此,总的比较次数为(n-1)+(n-2)+…+1≈n2/2。这个比较次数恒定为该数值,和输入数据的排列顺序无关。

冒泡排序的时间复杂度为O(n2)。

实现

function bubbleSort(list) {
  for (let i = 0; i < list.length - 1; i++) {
    // 数组长度为n,循环 n-1次
    for (let j = list.length - 1; j - 1 - i > 0; j--) {
      // 从右侧往左侧两两对比并交换
      if (list[j - 1] > list[j]) {
        [list[j - 1], list[j]] = [list[j], list[j - 1]];
      }
    }
  }
}
console.log(bubbleSort([1,2,5,9,3,4,6,8,7])) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

上面使用了一个小技巧,使用数组解构来交换值,比借助临时变量要快一些。

选择排序

选择排序就是重复“从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换”这一操作的算法。在序列中寻找最小值时使用的是线性查找。

耗时

选择排序使用了线性查找来寻找最小值,因此在第1轮中需要比较n-1个数字,第2轮需要比较n-2个数字……到第n-1轮的时候就只需比较1个数字了。因此,总的比较次数与冒泡排序的相同,都是(n-1)+(n-2)+…+1≈n2/2次。

选择排序的时间复杂度也和冒泡排序的一样,都为O(n2)。

实现

// 选择排序
function selectSort(list) {
  // 数组长度为n,只需要处理n-1个节点
  for (let i = 0; i < list.length - 1; i++) {
    let minIndex = getMinIndex(list.slice(i), i);
    [list[i], list[minIndex]] = [list[minIndex], list[i]];
  }
  console.log(list);
}

// 获取右侧未处理的数据中最小值的索引
function getMinIndex(list, skip) {
  let min = list[0],
    minIndex = 0;
  for (let i = 1, len = list.length; i < len; i++) {
    if (list[i] < min) {
      min = list[i];
      minIndex = i;
    }
  }
  return minIndex + skip;
}
selectSort([1, 2, 5, 9, 3, 4, 6, 8, 7]);

插入排序

插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。

耗时

O(n2)。

实现

// 插入排序
function insertSort(list) {
  if (list.length < 2) return list;
  for (let i = 1; i < list.length; i++) {
    for (let j = i - 1; j > 0; j--) {
      if (list[j+1] < list[j]) {
        [list[j+1], list[j]] = [list[j], list[j+1]]
      }
    }
  }
  console.log(list);
}

insertSort([1, 2, 5, 9, 3, 4, 6, 8, 7]);

堆排序

堆排序的特点是利用了数据结构中的堆。

耗时

堆排序一开始需要将n个数据存进堆里,所需时间为O(nlogn)。排序过程中,堆从空堆的状态开始,逐渐被数据填满。由于堆的高度小于log2n,所以插入1个数据所需要的时间为O(logn)。

整体来看堆排序的时间复杂度为O(nlogn)。

归并排序

归并排序算法会把序列分成长度相同的两个子序列,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。该操作会一直重复执行,直到所有子序列都归并为一个整体为止。

耗时

O(nlogn)。

快速排序

快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成以下形式。

知识点

快速排序是一种“分治法”。它将原本的问题分成两个子问题(比基准值小的数和比基准值大的数),然后再分别解决这两个问题。子问题,也就是子序列完成排序后,再像一开始说明的那样,把他们合并成一个序列,那么对原始序列的排序也就完成了。不过,解决子问题的时候会再次使用快速排序,甚至在这个快速排序里仍然要使用快速排序。只有在子问题里只剩一个数字的时候,排序才算完成。像这样,在算法内部继续使用该算法的现象被称为“递归”。

耗时

O(nlogn)。