排序算法---快速排序,随机快速排序和双路快排(python版)

[原文链接](https://blog.csdn.net/m0_37519490/article/details/80648011)

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指向的元素交换位置即可

此时就出现了小于"基准"元素的元素在其左边,大于"基准"元素的元素在其右边的分布情况。


  
  
  1. def _partition(arr, l, r):
  2. tag = arr[l]
  3. j = l + 1
  4. for i in range(l+ 1, r+ 1):
  5. if arr[i] < tag:
  6. arr[i], arr[j] = arr[j], arr[i]
  7. j += 1
  8. arr[j -1], arr[l] = arr[l], arr[j -1]
  9. return j - 1
  10. def _quick_sort(arr, l, r):
  11. if l < r:
  12. p = _partition(arr, l, r)
  13. _quick_sort(arr, l, p -1)
  14. _quick_sort(arr, p+ 1, r)
  15. def quick_sort(arr, nums):
  16. l, r = 0, nums -1
  17. _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()):


  
  
  1. def _partition_random(arr, l, r):
  2. ind = random.randint(l, r)
  3. arr[l], arr[ind] = arr[ind], arr[l]
  4. tag = arr[l]
  5. j = l + 1
  6. for i in range(l+ 1, r+ 1):
  7. if arr[i] < tag:
  8. arr[i], arr[j] = arr[j], arr[i]
  9. j += 1
  10. arr[j -1], arr[l] = arr[l], arr[j -1]
  11. 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--开始遍历后后面的元素

代码 :


  
  
  1. def insert_sort(arr, l, r):
  2. for i in range(l+ 1, r+ 1):
  3. j = i - 1
  4. temp = arr[i]
  5. if arr[i] < arr[j]:
  6. while j >= 0 and arr[j] > temp:
  7. arr[j+ 1] = arr[j]
  8. j -= 1
  9. arr[j+ 1] = temp
  10. def _partition_doubule(arr, l, r):
  11. ind = random.randint(l, r)
  12. arr[l], arr[ind] = arr[ind], arr[l]
  13. stand = arr[l]
  14. i, j = l+ 1, r
  15. while True:
  16. while i <= r and arr[i] < stand: #不能改为arr[i] <= stand, 原因下文有讲解
  17. i += 1
  18. while j >= l+ 1 and arr[j] > stand: #不能改为arr[j] >= stand.
  19. j -= 1
  20. if i > j:
  21. break
  22. else:
  23. arr[i], arr[j] = arr[j], arr[i]
  24. i += 1
  25. j -= 1
  26. arr[j], arr[l] = arr[l], arr[j]
  27. return j
  28. def _quick_sort(arr, l, r):
  29. if (r - l) < 15: #当待排序元素个数小于15时改为插入排序,可提高程序运行速度
  30. insert_sort(arr, l, r)
  31. return
  32. p = _partition_doubule(arr, l, r)
  33. _quick_sort(arr, l, p -1)
  34. _quick_sort(arr, p+ 1, r)
  35. def quick_sort(arr, nums):
  36. _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方式则会将连续出现的这些值归为其中一方,使得两棵子树不平衡

http://coding.imooc.com/learn/questiondetail/4920.html

[原文链接](https://blog.csdn.net/m0_37519490/article/details/80648011)

1、什么是快速排序算法?

猜你喜欢

转载自blog.csdn.net/qq_28327765/article/details/85321061
今日推荐