[路飞]_常见的五种排序方法

443 阅读5分钟

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

从今天开始,打算记录一下由浅入深的学习算法的整个过程,希望能有助于我对算法的理解,也希望能帮助到和我一样需要从基础开始学起的前端的同学。

今天首先研究一下五种常见的排序算法,在我独自学习的时候遇到了一些问题,和一些思考,很高兴能和大家一起讨论。

选择排序(selectionSort)

选择排序是算法中最简单直观的算法,无论什么数据进行计算时间复杂度都是O(n²),所以在使用的时候,尽量使用在数据规模较小的情况下,所能想到的唯一的好处可能就是不需要占用额外的内存空间。

选择排序的定义我理解的就是每一轮只看最小的那个数,把它放到第一位,第二轮再在剩余数组中找最小的那个,一次找完。

我第一次写的代码是这样的

function selectionSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] > arr[j])[arr[i], arr[j]] = [arr[j], arr[i]]
    }
  }
  console.log(arr);
}

这种情况没有设置中间变量,理论上应该也是选择排序算法吧,不知道合不合理,我每次打印过之后,确实是选择排序定义上的每一个最小的值都排到了第一个,但是数组其他元素的值的位置发生了变化,看过其他大部分人的写法都是先设置一个中间变量,内部循环时只对比不赋值。

function selectionSort(arr) {
  let minIndex;
  for (let i = 0; i < arr.length - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[minIndex] > arr[j]) minIndex = j;
    }
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
  }
  console.log(arr);
}

这样就避免了我第一次那种每次对比都会交换的情况。

插入排序

插入排序也是一种简单直观的排序方法。他就像打扑克的时候整理手牌一样,从左到右依次排序,也就是说每一次循环过后,已经整理过的牌应该是有顺序的。对于未插入的牌,从已整理的牌中,从后向前查找,找到其应该插入的位置。

所以我在进行插入排序的时候,也会进行两次循环,每一次循环会将无序数组中的第一个数插入到有序数组中适当的位置中去,所以插入排序的时间复杂度也应该是O(n²)

这是我的第一次写法

function insertionSort(d) {
  let len = d.length;
  for (let i = 1; i < len; i++) {
    for (let j = i; j > 0; j--) {
      if (d[j] < d[j - 1]) { 
        [d[j], d[j - 1]] = [d[j - 1], d[j]]
      }
    }
  }
  console.log(d);
}

和上面的选择排序一样,我这样的写法导致内部的循环每一次对比一次就会赋值一次。

function insertionSort(arr) {
  const len = arr.length;
  let preIndex, current;
  for (var i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--
    }
    arr[preIndex + 1] = current
  }
  console.log(arr);
}

这样写更符合插入排序的定义,将大于当前值的数向后移动,找到当前值的合适位置时便插入当前值。

希尔排序

希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本。

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

希尔排序的基本思想是,先将整个待排序的记录序列分割成为若干个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

我的第一次写法

function shellSort(arr) {
  let len = arr.length;
  let gap = len;
  while (gap > 1) {
    gap = Math.floor(gap / 2); // 每轮的步长
    for (let n = 0; n < gap; n++) { // 需要进行几组排序
      for (let i = n; i < len; i += gap) { // 插入排序
        for (let j = i; j > 0; j -= gap) {
          if (arr[j] < arr[j - gap]) {
            [arr[j], arr[j - gap]] = [arr[j - gap], arr[j]]
          }
        }
      }
    }
  }
  console.log(arr);
}

直接插入排序和希尔排序进行比较:

  • 直接插入排序是稳定的,而希尔排序是不稳定的(相等数据可能交互位置)
  • 直接插入排序更适合于原始记录基本有序的集合
  • 希尔排序的比较次数和移动次数都要比直接插入排序少,当N越大时,效果越明显
  • 在希尔排序中,增量序列(间隔)gap的取法必须满足:最后一个步长必须是1
  • 直接插入排序也适用于链式存储结构;希尔排序不适用于链式结构

整理思考后的解法

function shellSort(arr) {
  const len = arr.length;
  let gap = Math.floor(len / 2);
  for (gap; gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < len; i++) {
      temp = arr[i];
      for (let j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
        arr[j + gap] = arr[j]
      }
      arr[j + gap] = temp
    }
  }
  console.log(arr);
}

后面的写法,首先进行了步长判断,再从步长第二步开始向前做插入排序。

冒泡排序

冒泡排序指的就是像气泡一样,最大的泡泡一点一点的向上浮,最后浮到最顶端(最后),也就是相邻的两个数值进行比较,前者大于后者则互换,或则向后继续查找,每一次查找都找出未排序数组中最大的那个,置于数组最后。

我第一次的写法是这样的

function bubbleSort(arr) {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
    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]]
    }
  }
  console.log(arr);
}

根据冒泡排序的定义,就是需要每次都对当前元素之后的元素进行排序,所以不需要进行优化。

快速排序

快速排序将大的排序分成若干个小的排序, 首先选定一个基准,然后将左右指针置于数组两侧,先从右向左,查看数组元素与基准的大小,大于基准指针左移,小于基准将数值放于基准位置,再计算左指针与此时基准大小,小于基准指针右移,大于基准放于右指针位置,直到左右指针的位置重合。然后递归,再将基准左右的两个数组再寻找基准,左右分类,直至递归到每个数组只包含一个数时,结束递归。(由于时间和我不会做动画的原因,描述的可能不是很清晰,如果此时有个动画表达的应该更准确)

这是我第一次的写法

function sort(arr) {
  let len = arr.length;
  quick(arr, 0, len - 1)
  console.log(arr);

  function quick(arr, l, r) {
    if (l == r) return;
    let left = l,
      right = r;
    let n = arr[left] // 基准
    while (left < right) {
      while (n < arr[right] && left < right) {
        right--
      }
      [arr[left], arr[right]] = [arr[right], arr[left]];
      while (n > arr[left] && left < right) {
        left++
      }
      [arr[left], arr[right]] = [arr[right], arr[left]];
    }
    arr[right] = n;
    if (l < left) {
      dg(arr, l, left);
    }
    if (r > right) {
      dg(arr, right + 1, r)
    }
  }
}

经过整理,后得到

function sort(arr, left, right) {
  const len = arr.length;
  let partitionIndex;
  left = left >= 0 ? left : 0;
  right = right >= 0 ? right : len - 1;
  if (left < right) {
    partitionIndex = partition(arr, left, right);
    sort(arr, left, partitionIndex - 1);
    sort(arr, partitionIndex + 1, right);
  }
  return arr
}

function partition(arr, left, right) {
  let pivot = left;
  let index = pivot + 1;
  for (let i = index; i <= right; i++) {
    if (arr[i] < arr[pivot]) {
      swap(arr, i, index)
      index++
    }
  }
  swap(arr, pivot, index - 1)
  return index - 1;
}

function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]];
}
console.log(sort([5, 8, 4, 7, 1, 9, 2, 6]));

该方法一开始第4,5行的三段式我没有写≥0,直接写的

left = left ? left : 0;
right = right ? right : len - 1;

由于某些数据递归时会出现left=0,right=0的情况([5, 8, 4, 7, 1, 9, 2, 6],我的测试数据),此时的right=0会被判定为布尔值,导致right= len - 1,从而出现错误;因此我加了≥0。