# 快速排序

类似于合并排序,相同点:分治策略

  • 都会先拆分问题
  • 然后分别处理
  • 平均执行直接O(nlgn)

不同点:

  • 快速排序空间复杂度O(1)
  • 快速排序常数时间更少
  • 合并排序更适合并发环境

# 关键子问题-根据中心点拆分数组

选择一个中心点 70 ,把 <= 中心点的值放到中心点左边,大于中心点的值放到右边

静态图片

图1-1

静态图片

图1-2

从左边找到第一个大于 70 的数,在从右边找到第一个小于 70 的数,交换

在从左边找到第二个大于 70 的数,从右边找到第二个小于 70 的数,交换

....

静态图片

图1-3

最终的结果就是小于 70 的(10 50 30 40)在左边,大于 70 的(90 80 70)在右边

静态图片

图1-4

70 在和 左边数第1个大于 70 的值(90)交换

静态图片

图1-5

静态图片

图1-6

静态图片

图1-7

演示过程

i = 0 时候 A[i] = 10 小于 中心70 i++

静态图片

图1-8

i = 1 时候 A[i] = 80 大于中心 70 交换 --j

静态图片

图1-9

8050 交换后,此时A[i] = 50 小于 中心 70 i++

静态图片

图1-10

A[i] = 30 时候,小于 中心 70 i++

静态图片

图1-11

A[i] = 90 大于 70 交换 --j

静态图片

图1-12

A[i] = 40 小于 70 i++

静态图片

图1-13

未处理的值为空的情况,循环终止

静态图片

图1-14

静态图片

图1-15

函数的抽象

function partition(A, lo, hi) {
  - A 需要排序的数组
  - lo 开始位置 (闭区间)
  - hi 结束位置 (开区间)

  返回: 中心所在位置

  副作用:[lo, hi) 区间被中心点分成两个区域
}
1
2
3
4
5
6
7
8
9
function swap (A, i, j) {
  [A[i], A[j]] = [A[j], A[i]]
}

function partition(A, lo, hi) {
  const pivot = A[hi - 1] // 中心点位置
  let i = lo, j = hi - 1

  // 小于中心点返回:[lo, i)
  // 未确认范围:[i, j)
  // 大于中心点范围:[j, hi - 1)

  while (i !== j) {
    if (A[i] <= pivot) {
      i++
    } else {
      swap(A, i, --j)
    }
  }
  // 中心点的交换
  swap(A, j, hi - 1)
  // 返回中心点所在位置
  return j
}

const A = [10, 50, 30, 90, 40, 80, 70]
const B = [10, 50, 30, 90, 40, 80, 70]

const p1 = partition(A, 0, 7)
const p2 = partition(B, 1, 3)
console.log(p1, A) // 4  [10, 50, 30, 40, 70, 90, 80]
console.log(p2, B) // 1  [10, 30, 50, 90, 40, 80, 70]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

快速排序原理:

静态图片

图1-15

函数抽象

function qSort (A) {
  - A 需要排序的数组

  返回: 无
}
1
2
3
4
5
function qSort (A, lo = 0, hi = A.length) {
  if (hi - lo <= 1) {
    return
  }

  const p = partition(A, lo, hi)
  qSort(A, lo, p)
  qSort(A, p + 1, hi)
}
const A = [10, 50, 30, 90, 40, 80, 70]
qSort(A)
console.log(A) // [10, 30, 40, 50, 70, 80, 90]

1
2
3
4
5
6
7
8
9
10
11
12
13

# 快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。

网摘文章: 快速排序原理和实现

原理 高快省的排序算法,既不浪费空间也可以快一点的排序算法。

假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:3 1 2 5 4 6 9 7 10 8

在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。

方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

静态图片

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。

静态图片

静态图片

现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下: 6 1 2 5 9 3 4 7 10 8

静态图片

静态图片

到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下: 6 1 2 5 4 3 9 7 10 8

第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下: 3 1 2 5 4 6 9 7 10 8

静态图片

静态图片

到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。 OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。

左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧 如果你模拟的没有错,调整完毕之后的序列的顺序应该是:

2 1 3 5 4 OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下: 1 2 3 4 5 6 9 7 10 8 对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下 1 2 3 4 5 6 7 8 9 10 到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。

静态图片

这是为什么呢?快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。我们后面还会遇到“二分”思想,到时候再聊

# 如何优化快速排序

让不平衡的树更加平衡, 让拆分更加平均:

  • 随机打乱原数组O(n)
  • 使用中位数做重点O(n)累计
  • 找三个数,取中间数字O(1)累计