1、什么是快速排序算法?
快速排序是由东尼·霍尔所发展的一种排序算法,速度快,效率高,也是实际中最常用的一种算法,被称为20世纪对世界影响最大的算法之一。
基本思想:
1): 从序列中挑出一个元素作为"基准"元素,一般是该序列的第一个元素或者是最后一个元素。
2): 把序列分成2个部分,其数值大于"基准"元素的元素放在"基准"元素的左边,否在放在"基准"元
素的右边,此时"基准"元素所在的位置就是正确的排序位置,这个过程被称为 partition(分区)。
3):递归将"基准"元素左边的序列和"基准"元素右边的序列进行partition操作。
2、算法的演示
这个就是待排序的数组序列,第一个元素作为"基准"元素
给"基准"元素找到合适的位置,将比"基准"元素小的元素放在其左边,否则放在其右边
至此这个序列就成了这样了,这个过程成为partition
下面来看看partition的具体实现过程:
将"基准"元素用v表示,使用i作为遍历序列的索引值,j的位置表示>v部分和<v部分的分界位置(也就是最后一个小于v的元素所在位置)。
如果此时i指向的元素大于v,这个好处理,直接将i++即可,也就表示大于v的元素多了一个
如果此时i指向的元素小于v,那么需要将i指向的元素与大于v序列的第一个元素交换位置,即swap(arr[i], arr[j+1]),然后再将i++,再将j++即可,表示小于v的元素多了一个。如下图所示
进行swap(arr[i], arr[j+1])
j++
i++
由此可知,当遍历完成之后,就会出现这样的效果,然后我们只需将元素v与j指向的元素交换位置即可
此时就出现了小于"基准"元素的元素在其左边,大于"基准"元素的元素在其右边的分布情况。
def _partition(arr, l, r):
tag = arr[l]
j = l + 1
for i in range(l+1, r+1):
if arr[i] < tag:
arr[i], arr[j] = arr[j], arr[i]
j += 1
arr[j-1], arr[l] = arr[l], arr[j-1]
return j - 1
def _quick_sort(arr, l, r):
if l < r:
p = _partition(arr, l, r)
_quick_sort(arr, l, p-1)
_quick_sort(arr, p+1, r)
def quick_sort(arr, nums):
l, r = 0, nums-1
_quick_sort(arr, l, r)
3. 普通单路快排特点:
1)普通快速排序最差时间复杂度为o(n^2)
2)期望时间复杂度为o(nlgn)
3)在o(nlgn)中蕴含的常量比较小
4)就地排序,不需要辅助数组空间
改进一,随机单路快排:
那什么时候普通快速排序算法的最差时间复杂度会下降为o(n^2)呢?
我们可以想象一种情况,当待排序的数组近乎有序时,因为我们选择第一个元素作为基准,这时导致比基准元素小的元素基本为0,导致元素全部在基准一边。这样就导致我们递归算法的深度由期望的log(n),变为n。因此算法时间复杂度退化为o(n^2)级别。
那么这种情况的解决办法就是: 尽可能的别让第一个元素成为"基准"元素,而最好使用中间位置的元素成为
"基准"元素,那如何做到这点呢?解决办法就是"基准"元素随机产生,而不指定。请看下面的代码(只用修改_partition()):
def _partition_random(arr, l, r):
ind = random.randint(l, r)
arr[l], arr[ind] = arr[ind], arr[l]
tag = arr[l]
j = l + 1
for i in range(l+1, r+1):
if arr[i] < tag:
arr[i], arr[j] = arr[j], arr[i]
j += 1
arr[j-1], arr[l] = arr[l], arr[j-1]
return j - 1
改进二,双路快排:
之前讲的,当我们排序的是一个近乎有序的序列时,快速排序会退化到一个O(n^2)级别的排序算法,
而对此的改进就是引入了随机化快速排序算法;但是当我们排序的是一个数值重复率非常高的序列时,
此时随机化快速排序算法就不再起作用了,而将会再次退化为一个O(n^2)级别的排序算法,那为什么
会出现这种情况呢?且听下面的分析:
如上图所示就是之前分析的快速排序算法的partition的操作原理,我们通过判断此时i索引指向的数组
元素e>v还是<v,将他放在橙色或者是紫色两个不同的位置,然后将整个数组分成两个部分递归下去;
但是这里其实我们是没有考虑=v的情况,其实隐含的意思就是下面的两种情况:
其实从这里就可以看出来了,不管是>=v还是<=v,当我们的序列中存在大量重复的元素时,
排序完成之后就会将整个数组序列分成两个极度不平衡的部分,所以又退化到了O(n^2)级别
的时间复杂度,这是因为对于每一个"基准"元素来说,重复的元素太多了,如果我们选的"基准"
元素稍微有一点的不平衡,那么就会导致两部分的差距非常大;即时我们的"基准"元素选在了
一个平衡的位置,但是由于等于"基准"元素的元素也非常多,也会使得序列被分成两个及其不平
衡的部分,那么在这种情况下快速排序就又会退化成O(n^2)级别的排序算法。如何解决呢?
这就要用到今天讲的双路快速排序算法的原理了。
双路快速排序算法的原理
之前说的快速排序算法是将>v和<v两个部分元素都放在索引值i所指向的位置的左边部分,而我们
的双路快速排序算法则不同,他使用两个索引值(i、j)用来遍历我们的序列,将<v的元素放在索
引i所指向位置的左边,而将>v的元素放在索引j所指向位置的右边,这也正是双路排序算法的
partition原理:
基本思想:
首先从左边的i索引往右边遍历,如果i指向的元素<v,那直接将i++移动到下一个位置,直道i指向的元素>=v则停止
然后使用j索引从右边开始往左边遍历,如果j指向的元素>v,那直接将j--移动到下一个位置,直道j指向的元素<=v则停止
此时i之前的元素都已经归并为<v的部分了,而j之后的元素也都已经归并为>v的部分了,此时只需要将arr[i]和arr[j]交换位置即可
这样就可以避免出现=v的元素全部集中在某一个部分,这正是双路排序算法的一个核心
将i++,j--开始遍历后后面的元素
代码 :
def insert_sort(arr, l, r):
for i in range(l+1, r+1):
j = i - 1
temp = arr[i]
if arr[i] < arr[j]:
while j >= 0 and arr[j] > temp:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = temp
def _partition_doubule(arr, l, r):
ind = random.randint(l, r)
arr[l], arr[ind] = arr[ind], arr[l]
stand = arr[l]
i, j = l+1, r
while True:
while i <= r and arr[i] < stand: #不能改为arr[i] <= stand, 原因下文有讲解
i += 1
while j >= l+1 and arr[j] > stand: #不能改为arr[j] >= stand.
j -= 1
if i > j:
break
else:
arr[i], arr[j] = arr[j], arr[i]
i += 1
j -= 1
arr[j], arr[l] = arr[l], arr[j]
return j
def _quick_sort(arr, l, r):
if (r - l) < 15: #当待排序元素个数小于15时改为插入排序,可提高程序运行速度
insert_sort(arr, l, r)
return
p = _partition_doubule(arr, l, r)
_quick_sort(arr, l, p-1)
_quick_sort(arr, p+1, r)
def quick_sort(arr, nums):
_quick_sort(arr, 0, nums-1)
tips:
讨论:
比如数组 1,0,0, ..., 0, 0
a. 对于arr[i]<stand和arr[j]>stand的方式,第一次partition得到的分点是数组中间;
b. 对于arr[i]<=stand和arr[j]>=stand的方式,第一次partition得到的分点是数组的倒数第二个。
这是因为对于连续出现相等的情况,a方式会交换i和j的值;而b方式则会将连续出现的这些值归为其中一方,使得两棵子树不平衡