文章目录

  • 引言
  • 一、冒泡排序
  • 二、选择排序
  • 三、插入排序
    • 1.为什么插入排序比冒泡排序更受欢迎?
  • 四、希尔排序
  • 五、归并排序
  • 六、快速排序
    • 1.如何在时间复杂度O(n)内查找到第k大元素?
    • 2.如何在时间复杂度O(n)内查找到第k小元素?
    • 3.归并排序与快速排序对比
    • 4. 如何在O(n)时间复杂度内在10亿个数中地找到最大的一个数?
  • 七、桶排序
    • 1. 桶排序的使用场景
  • 八、计数排序
    • 1.计数排序的使用场景
  • 九、基数排序
    • 1.基数排序的使用场景

引言

  排序这部分,会介绍9种基础的排序及其优化

  1. 计数排序
  2. 基数排序
  3. 冒泡排序
  4. 桶排序
  5. 选择排序
  6. 快速排序
  7. 插入排序
  8. 归并排序
  9. 希尔排序

一、冒泡排序

  冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。冒泡排序总结来说,就是相邻对比交换位置,直至无需再交换。其动画演示为:

# 冒泡排序
def bubble_sort(alist):'''冒泡排序'''n = len(alist)count = 0for j in range(0, n - 1):swap = Falseprint(alist)# 经过j,确定了j个元素,后面的不需要再比较for i in range(0, n - 1 - j):count += 1# i指的是下标,从头走到尾,走到n-2,与n-1比较即可if alist[i] > alist[i + 1]:swap = Truealist[i], alist[i + 1] = alist[i + 1], alist[i]# swap = False时,表明无需再交换,直接返回if not swap:breakprint(f"总循环次数{count}")return alistif __name__ == '__main__':li = [3, 4, 2, 1, 5, 6, 7, 8]print('bubble_sort end:', bubble_sort(li))
[3, 4, 2, 1, 5, 6, 7, 8]
[3, 2, 1, 4, 5, 6, 7, 8]
[2, 1, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
总循环次数22
bubble_sort end: [1, 2, 3, 4, 5, 6, 7, 8]

代码优化:

优化思路︰在排序的过程中,数据可以从中间分为两段,一段是无序状态,另一段是有序状态。每一次循环的过程中,记录最后一个交换元素,它便是有序和无序状态的边界
下一次仅循环到边界即可,从而减少循环次数,达到优化。当然如果没有有序部分,则不会减少循环次数。

# 冒泡排序
def bubble_sort(alist):'''冒泡排序'''n = len(alist)count = 0# 记录最后一个交换元素坐标last_change_index = 0# 有序与无序的分界线border = n - 1for j in range(0, n - 1):swap = Falseprint(alist)# 经过j,确定了j个元素,后面的不需要再比较for i in range(0, border):count += 1# i指的是下标,从头走到尾,走到n-2,与n-1比较即可if alist[i] > alist[i + 1]:swap = Truealist[i], alist[i + 1] = alist[i + 1], alist[i]last_change_index = i# swap = False时,表明无需再交换,直接返回if not swap:breakborder = last_change_indexprint(f"总循环次数{count}")return alistif __name__ == '__main__':li = [3, 4, 2, 1, 5, 6, 7, 8]print('bubble_sort end:', bubble_sort(li))
[3, 4, 2, 1, 5, 6, 7, 8]
[3, 2, 1, 4, 5, 6, 7, 8]
[2, 1, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
总循环次数10
bubble_sort end: [1, 2, 3, 4, 5, 6, 7, 8]

时间复杂度:

  • 最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
  • 最坏时间复杂度:O(n^2)(数据一开始就是倒序的,需要经过n-1次冒泡)

空间复杂度:

  • 空间复杂度是O(1),这类算法也可以叫做原地排序算法。

排序算法的稳定性:

  • 稳定性怎么理解呢?
    如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

总结:
冒泡排序虽然使用了数组存储数据但是并没有使用数组随机访问的特性,因此改用链表这种存健结构,使用冒泡排序仍然是可以实现的。

二、选择排序

  选择排序(Selection sort)是一种简单直观的排序算法。其工作原理为:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。通俗点理解就是拿出最大或最小,然后在剩余中拿出最大或最小。其动画演示为:

  选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

# 选择排序策略是拿出最小,然后在剩余中拿出最小
def selection_sort(aList):n = len(aList)count = 0# 需要n-1次遍历寻找for i in range(0, n - 1):print(aList)# 存储最小值下标min_index = ifor j in range(i + 1, n):count += 1if aList[min_index] > aList[j]:min_index = j# 判断min_index与i的关系if min_index != i:# 元素互换aList[i], aList[min_index] = aList[min_index], aList[i]print(f"总循环次数为{count}")return aListif __name__ == '__main__':li = [3, 4, 2, 1, 8, 6, 5, 7]print('select_sort end:', selection_sort(li))
[3, 4, 2, 1, 8, 6, 5, 7]
[1, 4, 2, 3, 8, 6, 5, 7]
[1, 2, 4, 3, 8, 6, 5, 7]
[1, 2, 3, 4, 8, 6, 5, 7]
[1, 2, 3, 4, 8, 6, 5, 7]
[1, 2, 3, 4, 5, 6, 8, 7]
[1, 2, 3, 4, 5, 6, 8, 7]
总循环次数为28
select_sort end: [1, 2, 3, 4, 5, 6, 7, 8]

代码优化版本:
在开头:拿大,然后剩余部分取大;尾部:拿小,然后剩余部分取小,可以对代码进行优化,减少循环次数。优化点是开头末尾同时选择排序,当最小值的索引 + 1 = 尾部有序部分第一个元素索引时,表明已经排好序了。

# 选择排序策略是拿出最小,然后在剩余中拿出最小
def selection_sort(aList):n = len(aList)count = 0# 需要n-1次遍历寻找for i in range(0, n - 1):print(aList)# 存储最小值下标min_index = i# 存储最大值下标max_index = n - i - 1for j in range(i + 1, n - i - 1):count += 1# 更新最小值索引if aList[min_index] > aList[j]:min_index = j# 更新最大值索引if aList[max_index] < aList[j]:max_index = jif max_index == min_index + 1:break# 判断min_index与i的关系if min_index != i:# 元素互换aList[i], aList[min_index] = aList[min_index], aList[i]# 判断max_index与i的关系if max_index != n - i - 1:# 元素互换aList[max_index], aList[n - i - 1] = aList[n - i - 1], aList[max_index]print(f"总循环次数为{count}")return aListif __name__ == '__main__':li = [3, 4, 2, 1, 8, 6, 5, 7]print('select_sort end:', selection_sort(li))
[3, 4, 2, 1, 8, 6, 5, 7]
总循环次数为6
select_sort end: [3, 4, 2, 1, 8, 6, 5, 7]

时间复杂度:

  • 选择排序无论数据初始是何种状态,均需要在未排序元素中选择最小或最大元素与未排序序列中的首尾元素交换,因此它的最好、最坏、平均时间复杂度均为O(n2)O(n^2)O(n2)。

稳定性:

  • 选择排序在未排序区间选择一个最小值,与前面的元素交换,对于值相同的元素,因为交换会破坏他们的相对顺序,因此它是一种不稳定的排序算法。

空间复杂度:

  • 只需要选择一个变量作为交换,因此,空间复杂度为O(1),是一种原地排序算法。

三、插入排序

  插入排序(英语:Insertion Sort)是一种简单直观的排序算法。其工作原理为:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。通俗来讲,就是将未排序的,在已排序中找到位置插入进去。其动画演示为:

def insert_sort(aList):count = 0n = len(aList)# 从第1个位置处开始遍历for i in range(1, n):print(aList)# 中间变量tmp = aList[i]j = i# j从后往前遍历,寻找合适插入位置while j > 0:count += 1if tmp < aList[j - 1]:aList[j] = aList[j - 1]else:breakj -= 1# 循环结束时,j处是合适的插入位置aList[j] = tmpprint(f'总循环次数{count}')return aListif __name__ == '__main__':li = [1, 3, 4, 2, 1, 5, 6, 7, 8, 4]print('insert_sort end:', insert_sort(li))
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 2, 3, 4, 1, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
总循环次数18
insert_sort end: [1, 1, 2, 3, 4, 4, 5, 6, 7, 8]

时间复杂度:

  • 如果数据是倒序的,每次都相当于在数据的第一个位置插入新数据,所以需要移动大量的数据,最坏时间复杂度为O(n2)O(n^2)O(n2)。

空间复杂度:

  • 需要临时变量存储交换的数据与下标,空间复杂度为O(1),是一种原地排序算法。

稳定性:

  • 插入排序算法是一种稳定排序算法,对于值相同的元素,可以选择将后面出现的元素插入到前面出现元素的后面,这样就可以保证原有的前后顺序不变。

优化思路:

当有序区间数据量很大时,查找数据的插入位置就会显得非常耗时,插入排序算法每次都是从有序区间查找插入位置,以此为切入点,我们可以使用二分查找法来快速确认待插入的位置,于是就有了优化版的插入排序算法,也叫二分查找插入算法。

def insert_sort(aList):"""使用二分查找函数,寻找待插入元素在有序数组中的插入位置"""count = 0n = len(aList)# 从第1个位置处开始遍历for i in range(1, n):print(aList)# 等待插入元素wait_insert_data = aList[i]move_index = i# 使用二分查找函数,寻找待插入元素在有序数组中的插入位置insert_index, count1 = binary_search(aList[0:i], wait_insert_data)count += count1# 移动元素直到待插入位置while insert_index < move_index:count += 1# 元素往后移动,直至待插入位置aList[move_index] = aList[move_index - 1]move_index -= 1# 数据插入操作aList[move_index] = wait_insert_dataprint(f'总循环次数{count}')return aListdef binary_search(data_List, data):"""在有序数组中,二分查找插入位置"""count = 0n = len(data_List)low = 0high = n - 1if data > data_List[-1]:return n, 0if data < data_List[0]:return 0, 0insert_index = 0while low < high-1:count += 1mid = (low + high) // 2if data_List[mid] > data:high = midinsert_index = highelse:low = midinsert_index = mid + 1return insert_index, countif __name__ == '__main__':li = [1, 3, 4, 2, 1, 5, 6, 7, 8, 4]print('insert_sort end:', insert_sort(li))
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 2, 3, 4, 1, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
[1, 1, 2, 3, 4, 5, 6, 7, 8, 4]
总循环次数14
insert_sort end: [1, 1, 2, 3, 4, 4, 5, 6, 7, 8]

1.为什么插入排序比冒泡排序更受欢迎?

  虽然它们的时间复杂度均为O(n2)O(n^2)O(n2),不过因为赋值操作比交换操作耗费的时间多,当数据量特别大时,这两种排序就能体现出差异。

四、希尔排序

  希尔排序是一种分组直接插入排序方法,其原理是:先将整个序列分割成若干小的子序列,再分别对子序列进行直接插入排序,使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
  直接插入排序是基于相邻的元素进行排序,如果说直接插入排序为步长为1,那么希尔排序就是先按步长为K来插入排序,然后在步长K排序的基础上再对步长m进行排序,当然K是大于m的,最后对步长1排序。

# 希尔排序实现,假如数据的长度为n我就简单地采取除以2来求步长,最后到1结束,最终也可以达到效果
def shell_sort(alist):'''希尔排序'''n = len(alist)gap = n // 2# gap 变化到0之前插入算法执行的次数while gap > 0:# 希尔算法与普通的插入算法的区别是gap步长for i in range(gap, n):  # j = [gap,gap+1,...n-1]# 待插入的数据tmp = alist[i]index = i# 从已排序区间查找插入位置for j in range(i - gap, -1, -gap):if tmp < alist[j]:alist[j + gap] = alist[j]index = jelse:breakalist[index] = tmpprint(alist)# 缩短gap步长gap //= 2return alistif __name__ == '__main__':li = [1, 3, 4, 2, 1, 5, 6, 7, 8, 4]print('insert_sort end:', shell_sort(li))
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 3, 4, 2, 1, 5, 6, 7, 8, 4]
[1, 2, 4, 3, 1, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 5, 6, 7, 8, 4]
[1, 2, 1, 3, 4, 4, 6, 5, 8, 7]
[1, 2, 1, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 6, 5, 8, 7]
[1, 1, 2, 3, 4, 4, 5, 6, 8, 7]
[1, 1, 2, 3, 4, 4, 5, 6, 8, 7]
[1, 1, 2, 3, 4, 4, 5, 6, 7, 8]
insert_sort end: [1, 1, 2, 3, 4, 4, 5, 6, 7, 8]
  • 时间复杂度:O(n2)O(n^2)O(n2)
  • 稳定性:不稳定

如果还不理解是怎么运行的,就debug一下,一下子就看明白了!!!几乎任何排序工作,在最开始的时候都可以尝试一下希尔排序,它一般比较稳定。

五、归并排序

  归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。基本思路就是将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。其动画演示为:

# 归并排序
def merge_sort(alist):'''归并排序'''n = len(alist)if n <= 1:return alistelse:mid = n // 2# left 表示采用归并排序后形成的有序的新的列表left_li = merge_sort(alist[:mid])# right 表示采用归并排序后形成的有序的新的列表right_li = merge_sort(alist[mid:])# 将两个有序的子序列合并成一个新的整体# merge(left,right)left_pointer,right_pointer = 0,0result = []while left_pointer < len(left_li) and right_pointer < len(right_li):if left_li[left_pointer] <= right_li[right_pointer]:result.append(left_li[left_pointer])left_pointer += 1else:result.append(right_li[right_pointer])right_pointer += 1result += left_li[left_pointer:]result += right_li[right_pointer:]return resultif __name__ == '__main__':alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]print(alist)sorted_alist = merge_sort(alist)print(sorted_alist)
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

归并排序的代码执行流程

# 归并排序,代码执行流程-缩进代码层级
merge_sort([54, 26, 93, 17, 77, 31, 44, 55, 20])left_li = merge_sort([54, 26, 93, 17,])lefi_li = merge_sort([54,26])lefi_li = merge_sort([54])return [54]right_li = merge_sort([26])return [26]# 比较结果return [26,54]right_li = merge_sort([93,17])lefi_li = merge_sort([93])return [93]right_li = merge_sort([17])return [17]# 比较结果return [17,93]#比较结果return [17,26,54,93]right_li = merge_sort([77, 31, 44, 55, 20])# 比较原理同上,可得比较结果为return [20,31,44,55,77]#比较结果return [17, 20, 26, 31, 44, 54, 55, 77, 93]
  • 时间复杂度为O(nlogn)

  • 空间复杂度为O(n)
    在任意时刻,CPU只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过n个数据的大小,所以空间复杂度是O(n)。

  • 稳定算法
    我们对数组分成左右两部分,对于两边相同的值,我们可以选择将右部分的值归并后放在左边相同值的后面,因此它是稳定的排序算法。

优化:
这里提供一种优化思路,使用哨兵,就判断与哨兵是否相同。比较大小的话,会对二进制的每一位进行比较;判断是否等于,只要二进制位有一位不等于,最后就是不等于。

六、快速排序

  快速排序是用来解决如何在时间复杂度O(n)内查找第k大元素。快速排序的基本思路是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序与归并排序比较:
归并排序是自下而上,先求下面的子问题,然后在逐层归并,最后全部有序
快速排序是子上而下,下面的子问题解决后,数据就全部有序

快速排序算法运作如下:

  • 从数列中挑出一个元素,称为基准(pivot),重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
  • 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。


代码实现:

# 快速排序-递归
def quick_sort(alist, begin, end):# 递归的终止条件是begin >= last,即数组大小为1或0# 递归终止时,数组已经排好序了if begin >= end:returnelse:# 以开头的值作为基准值,然后以基准值为界将数组分区,将分区后的左右两部分继续调用快速排序函数mid_value = alist[begin]low = beginhigh = end# 分别从右往左寻找小于基准值的值,从左往右寻找大于基准值的值while low < high:# 从右往左寻找小于基准值的值while low < high and alist[high] >= mid_value:high -= 1alist[low] = alist[high]# 从左往右寻找大于基准值的值while low < high and alist[low] < mid_value:low += 1alist[high] = alist[low]# 循环结束时,low == high,这个位置正是基准点的位置alist[low] = mid_value# 对low左边的元素执行快速排序quick_sort(alist, begin, low - 1)quick_sort(alist, low + 1, end)if __name__ == '__main__':alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]print(alist)quick_sort(alist, 0, len(alist) - 1)print(alist)
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

时间复杂度:

  • 时间复杂度为O(n2)O(n^2)O(n2)
    快速排序平均时间复杂度为O(nlogn)O(nlogn)O(nlogn),但在极端情况下,时间复杂度会退化为O(n2)O(n^2)O(n2)比如在数据已经是有序的情况时,会出现分区极度不平衡的情况,因此这种情况下时间复杂度为O(n2)O(n^2)O(n2)。

空间复杂度:

  • 空间复杂度为O(1)O(1)O(1),是一种原地排序算法,不需要借助额外的存储空间

稳定性:

  • 快速排序算法是一种不稳定算法,会打乱原有的数字的相对次序

1.如何在时间复杂度O(n)内查找到第k大元素?

# 分区
def partition(alist, begin, end):mid_value = alist[begin]low = beginhigh = end# 分别从右往左寻找小于基准值的值,从左往右寻找大于基准值的值while low < high:# 从右往左寻找小于基准值的值while low < high and alist[high] >= mid_value:high -= 1alist[low] = alist[high]# 从左往右寻找大于基准值的值while low < high and alist[low] < mid_value:low += 1alist[high] = alist[low]# 循环结束时,low == high,这个位置正是基准点的位置alist[low] = mid_valuereturn low# 在O(n)内查找第k大元素
def find_k_top(alist, k):n = len(alist)begin = 0end = n - 1# 基准位置的索引index = partition(alist, begin, end)while index != n - k:# 在当前基准位置的左侧,对这一侧继续分区if index > n - k:end = index - 1index = partition(alist, begin, end)# 否则,在当前基准位置的右侧,对这一侧继续分区else:begin = index + 1index = partition(alist, begin, end)# 当循环结束时,index == n-k,此时基准位置为该数组第k大元素# 返回第k大元素return alist[index]if __name__ == '__main__':alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]for i in [1, 2, 3, 4]:print(f"第{i}大元素是{find_k_top(alist, i)}")
第1大元素是93
第2大元素是77
第3大元素是55
第4大元素是54

2.如何在时间复杂度O(n)内查找到第k小元素?

# 分区
def partition(alist, begin, end):mid_value = alist[begin]low = beginhigh = end# 分别从右往左寻找小于基准值的值,从左往右寻找大于基准值的值while low < high:# 从右往左寻找小于基准值的值while low < high and alist[high] >= mid_value:high -= 1alist[low] = alist[high]# 从左往右寻找大于基准值的值while low < high and alist[low] < mid_value:low += 1alist[high] = alist[low]# 循环结束时,low == high,这个位置正是基准点的位置alist[low] = mid_valuereturn low# 在O(n)内查找第k大元素
def find_k_top(alist, k):n = len(alist)begin = 0end = n - 1# 基准位置的索引index = partition(alist, begin, end)while index+1 != k:# 在当前基准位置的左侧,对这一侧继续分区if index+1 > k:end = index - 1index = partition(alist, begin, end)# 否则,在当前基准位置的右侧,对这一侧继续分区else:begin = index + 1index = partition(alist, begin, end)# 当循环结束时,index + 1 == k(+1是因为索引从0开始),此时基准位置为该数组第k小元素# 返回第k大元素return alist[index]if __name__ == '__main__':alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]for i in [1, 2, 3, 4]:print(f"第{i}小元素是{find_k_top(alist, i)}")
第1小元素是17
第2小元素是20
第3小元素是26
第4小元素是31

3.归并排序与快速排序对比

  • 归并排序
    优点:在任何情况下,时间复杂度稳定在O(nlogn)O(nlogn)O(nlogn)
    缺点:它不是原地排序算法,空间复杂度为O(n)
  • 快速排序
    优点:它是一种原地排序算法
    缺点:快速排序平均时间复杂度为O(nlogn)O(nlogn)O(nlogn),但在极端情况下,时间复杂度会退化为O(n2)O(n^2)O(n2)比如在数据已经是有序的情况时,会出现分区极度不平衡的情况,因此这种情况下时间复杂度为O(n2)O(n^2)O(n2)。

    在python中sort函数的时间复杂度为O(nlogn),sort函数使用的就是快速排序。

4. 如何在O(n)时间复杂度内在10亿个数中地找到最大的一个数?

  这里就要引出桶排序、计数排序、基数排序这一类时间复杂度为O(n)的线性排序算法,这一类排序算法对排序的数据比较苛刻,学习这一类算法应该重点掌握其适用场景!

七、桶排序

  桶排序(Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序适用于每个区间分布比较均匀的情况,若分布不均,极端情况下,分布到一个区间,那么时间复杂度将为O(nlogn)O(nlogn)O(nlogn)。

时间复杂度计算:
假设有n个数据,m个桶,每个桶数据为nm\frac{n}{m}mn​,
则,m∗nm∗log(nm)=n∗log(nm)m*\frac{n}{m}*log(\frac{n}{m})=n*log(\frac{n}{m})m∗mn​∗log(mn​)=n∗log(mn​),当m接近于n时,则log(nm)log(\frac{n}{m})log(mn​)为常数
因此,桶排序的时间复杂度为O(n)O(n)O(n)。

桶排序实现思路:

  1. 初始化桶的大小为K
  2. 获取n个数据中的最大值max,最小值min
  3. 将数据放入到nK+1\frac{n}{K}+1Kn​+1个桶中,a[i]a[i]a[i]放入哪个桶的规则为a[i]−minK\frac{a[i]-min}{K}Ka[i]−min​
  4. 对nK\frac{n}{K}Kn​个桶分别进行快速排序并输出

代码实现:

import random# 快速排序函数
# 快速排序-递归
def quick_sort(alist, begin, end):# 递归的终止条件是begin >= last,即数组大小为1或0# 递归终止时,数组已经排好序了if begin >= end:returnelse:# 以开头的值作为基准值,然后以基准值为界将数组分区,将分区后的左右两部分继续调用快速排序函数mid_value = alist[begin]low = beginhigh = end# 分别从右往左寻找小于基准值的值,从左往右寻找大于基准值的值while low < high:# 从右往左寻找小于基准值的值while low < high and alist[high] >= mid_value:high -= 1alist[low] = alist[high]# 从左往右寻找大于基准值的值while low < high and alist[low] < mid_value:low += 1alist[high] = alist[low]# 循环结束时,low == high,这个位置正是基准点的位置alist[low] = mid_value# 对low左边的元素执行快速排序quick_sort(alist, begin, low - 1)quick_sort(alist, low + 1, end)# 桶排序函数
# 初始化桶的大小(长度)
DEFAULT_BUCKET_SIZE = 5def bucket_sort(alist, bucket_size=DEFAULT_BUCKET_SIZE):n = len(alist)# 初始化最小值与最大值min = max = alist[0]# 寻找最小值与最大值for i in range(0, n):if alist[i] < min:min = alist[i]if alist[i] > max:max = alist[i]# 定义多个桶并且初始化# 计算桶的个数nums_of_buckets = (max - min) // bucket_size + 1buckets = []for i in range(nums_of_buckets):buckets.append([])# 将数据放入桶中for i in range(0, n):buckets[(alist[i] - min) // bucket_size].append(alist[i])# 一次对桶内数据进行快排alist.clear()for i in range(nums_of_buckets):print(f"第{i}个桶排序前的内容是{buckets[i]}")begin = 0end = len(buckets[i]) - 1quick_sort(buckets[i], begin, end)for data in buckets[i]:alist.append(data)if __name__ == '__main__':alist = [random.randint(0, 15) for _ in range(15)]bucket_sort(alist, 5)print(alist)
第0个桶排序前的内容是[1, 5, 1, 1, 5, 3, 5]
第1个桶排序前的内容是[9]
第2个桶排序前的内容是[11, 12, 14, 14, 14, 15, 11]
[1, 1, 1, 3, 5, 5, 5, 9, 11, 11, 12, 14, 14, 14, 15]

桶个数计算为(14−1)//5+1=3(14-1)//5+1=3(14−1)//5+1=3

if __name__ == '__main__':alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]bucket_sort(alist, 5)print(alist)
第0个桶排序前的内容是[17, 20]
第1个桶排序前的内容是[26]
第2个桶排序前的内容是[31]
第3个桶排序前的内容是[]
第4个桶排序前的内容是[]
第5个桶排序前的内容是[44]
第6个桶排序前的内容是[]
第7个桶排序前的内容是[54, 55]
第8个桶排序前的内容是[]
第9个桶排序前的内容是[]
第10个桶排序前的内容是[]
第11个桶排序前的内容是[]
第12个桶排序前的内容是[77]
第13个桶排序前的内容是[]
第14个桶排序前的内容是[]
第15个桶排序前的内容是[93]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

桶个数计算为(93−17)//5+1=16(93-17)//5+1=16(93−17)//5+1=16

1. 桶排序的使用场景

  桶排序适合外部排序,外部排序就是数据在内存之外,比如磁盘上,数据量比较大,无法一次性读入内存。举个例子,假如老板给你一份10GB 大小的文件,是订单的交易明细数据,要求你按订单金额从大到小排序,而你的内存内有4GB,实际可用内存只有2GB,那么此时就是桶排序发挥作用的时候了。

  1. 将文件逐行读入内存,扫描并记录最小值,最大值,假如最小值为1元,最大值为10万元,且都为整数,不是整数也没关系,可以先乘以100换成整数,排序后再除以100还原。
  2. 分桶,个数为100,如果数据分布比较均匀的话,则每一个小文件的大小为100M,小文件进行快速排序后,最后再把各个小文件的排序结果写入到磁盘。

这里举个例子:

  比如说我们有 10GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10GB 的数据加载到内存中。这个时候怎么办呢?
  现在我来讲一下,如何借助桶排序的处理思想来解决这个问题。
  我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到,订单金额最小是 1 元,最大是 10 万元。我们将所有订单根据金额划分到 100 个桶里,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 10001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。
  理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀分到 100 个文件中,每个小文件中存储大约 100MB 的订单数据,我们就可以将 100 个小文件一次放到内存中,用快速排序来排序。等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。
  不过,你可能发现了,订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的,所以 10GB 订单数据是无法均匀地被划分到 100 个文件中。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。这又该怎么办呢?
  针对这些划分之后还是比较大的文件,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们可以继续划分,比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 200 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次读入内存,那就继续再划分,知道所有的文件都能读入内存位置。

八、计数排序

  计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第iii个元素是待排序数组A中值等于iii的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。

# 计数排序实现
def counting_sort(data_list):length = len(data_list)# 定义桶bucket = []max = data_list[0]for d in data_list:if d > max:max = d# 初始化for i in range(max + 1):bucket.append(0)# 计数for i in range(length):bucket[data_list[i]] = bucket[data_list[i]] + 1# 累加for i in range(1, max + 1):bucket[i] = bucket[i] + bucket[i - 1]# 计数排序,定义结果数组并初始化result = []for i in range(length):result.append(0)# 从尾至头查找分数在result的插入位置,如果从头到尾的话就不是稳定的排序算for i in range(length - 1, -1, -1):result[bucket[data_list[i]] - 1] = data_list[i]# 重置个数统计bucket[data_list[i]] = bucket[data_list[i]] - 1# 将结果复制到原来的数组中,达到修改传入数组的效果for i in range(length):data_list[i] = result[i]if __name__ == '__main__':alist = [2, 5, 3, 0, 2, 3, 0, 3]print(alist)counting_sort(alist)print(alist)
[2, 5, 3, 0, 2, 3, 0, 3]
[0, 0, 2, 2, 3, 3, 3, 5]

1.计数排序的使用场景

  计数排序适用于高考分数排名的场景。计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

九、基数排序

  基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
举个例子:

  1. 先排个位数,根据个位数的值,将数据放到桶里面

  1. 将桶中数据依次取出
  2. 然后在排十位数,并将排序结果从桶中取出
  3. 然后再依次排百位数,并将结果依次取出
  4. 个位数,十位数,百位数,依次排序后得到的结果就是最终的结果!

代码实现:

#!usr/bin/env python
# -*- coding:utf-8 -*-
"""
@author: admin
@file: 基数排序.py
@time: 2021/05/15
@desc:
"""
import random# 基数排序实现
class phone_num(object):def __init__(self, num=None):self.num = num# 定义获取数组某一位的方法def get_bit(self, bit):return int(self.num[bit - 1:bit])def __str__(self):return self.numdef _repr__(self):return self.numdef radix_sort(data_list):radix = 11# 借助稳定排序算法从尾至头排序radix次,从后往前for i in range(radix, 0, -1):data_list = counting_sort(data_list, i)# 改写的计数排序,方便基数排序调用, radix指示是待排序数据的那一位
def counting_sort(data_list, radix):length = len(data_list)# 定义桶bucket = []max = data_list[0].get_bit(radix)for i in range(length):if data_list[i].get_bit(radix) > max:max = data_list[i].get_bit(radix)# 初始化for i in range(max + 1):bucket.append(0)# 计数for i in range(length):bucket[data_list[i].get_bit(radix)] = bucket[data_list[i].get_bit(radix)] + 1# 累加for i in range(1, max + 1):bucket[i] = bucket[i] + bucket[i - 1]# 计数排序,定义结果数组并初始化result = []for i in range(length):result.append(0)# 从尾至头查找分数在result的插入位置,如果从头到尾的话就不是稳定的排序算法。for i in range(length - 1, -1, -1):result[bucket[data_list[i].get_bit(radix)] - 1] = data_list[i]bucket[data_list[i].get_bit(radix)] = bucket[data_list[i].get_bit(radix)] - 1for i in range(length):data_list[i] = result[i]return data_listdef create_phone_num():prelist = ["130", "131", "132", "133", "134", "135", "136", "137", "138","153", "155", "156", "157", "158", "159", "186", "187", "188"]randomPre = random.choice(prelist)Number = "".join(random.choice("0123456789") for _ in range(8))phoneNum = randomPre + Numberreturn phoneNumif __name__ == '__main__':data_list = [phone_num(create_phone_num()) for _ in range(10)]print("排序前")for i in data_list:print(i)radix_sort(data_list)print("排序后")for i in data_list:print(i)
排序前
15309746441
15722796875
15518578212
15910809576
13594224748
18815865054
13642866930
15502042257
13661002342
13333439217
排序后
13333439217
13594224748
13642866930
13661002342
15309746441
15502042257
15518578212
15722796875
15910809576
18815865054

1.基数排序的使用场景

  基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果a数据的高位比 b数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到O(n)了。
  在实际应用中,字符串之间排序就可以使用基数排序,如果待排序的字符串位数不一致,可以通过在字符串尾部补0来使他们位数一致,这与小数转整数后再排序的道理是一致的。


如果对您有帮助,麻烦点赞关注,这真的对我很重要!!!如果需要互关,请评论或者私信!


数据结构与算法python—14.排序之九种排序详解相关推荐

  1. 运用python的方式_对Python使用mfcc的两种方式详解

    1.Librosa import librosa filepath = "/Users/birenjianmo/Desktop/learn/librosa/mp3/in.wav" ...

  2. python截图黑屏_对Python获取屏幕截图的4种方法详解

    Python获取电脑截图有多种方式,具体如下: PIL中的ImageGrab模块 windows API PyQt pyautogui PIL中的ImageGrab模块 import time imp ...

  3. UML的九种图例详解

    UML图中类之间的关系:依赖,泛化,关联,聚合,组合,实现 类与类图 1) 类(Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性.操作.关系的对象集合的总称. 2) 在系统中, ...

  4. 【转】UML的九种图例详解

    UML图中类之间的关系:依赖,泛化,关联,聚合,组合,实现 类与类图 1) 类(Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性.操作.关系的对象集合的总称. 2) 在系统中, ...

  5. python连接mysql三种方式_用 Python 连接 MySQL 的几种方式详解

    每个学 Python 的都有必要学好一门数据库,不管你是做数据分析,还是网络爬虫,Web 开发.亦或是机器学习,你都离不开要和数据库打交道,而 MySQL 又是最流行的一种数据库,这篇文章介绍 Pyt ...

  6. 【数据结构和算法笔记】:稀疏矩阵的存储结构详解

    稀疏矩阵定义: 稀疏矩阵和特殊矩阵的不同点: 特殊矩阵的特殊元素分布(值相同元素,常量元素)有规律 稀疏矩阵的特殊元素(非0元素)分布没有规律 三元组储存 例: 所以,该稀疏矩阵的三元组线性表为 稀疏 ...

  7. python杨辉三角两种写法详解

    一般写杨辉三角都是用二维数组,通过二维数组的下标,可以非常容易的计算出下一行结果. [1] [1, 1] [1, 2, 1] [1, 3, 3, 1] [1, 4, 6, 4, 1] [1, 5, 1 ...

  8. UML九种标准图详解

    在 UML 中元素以不同的方式,表达了不同的图表,我们通过不同类型的图片或者图表可以很直观的了解任何复杂的系统,这种方法以不同的形式被广泛应用到不同的行业中. 一个单一的图涵盖所有方面的制度是不够的, ...

  9. python 句柄窗口指定位置截图_对Python获取屏幕截图的4种方法详解

    Python获取电脑截图有多种方式,具体如下: PIL中的ImageGrab模块 windows API PyQt pyautogui PIL中的ImageGrab模块 import time imp ...

  10. Python爬虫初级(十一)—— Selenium 详解

    欢迎关注公众号K的笔记阅读博主更多优质学习内容 上一篇内容:Python爬虫初级(九)-- ajax 详解 Selenium 库的安装 Selenium 的安装比起其他 python 库的安装稍显复杂 ...

最新文章

  1. 微信小程序点击图片实现长按预览、保存、识别带参数二维码、转发等功能
  2. hibernate 一对多、多对多的配置
  3. powershell设置了权限依旧无法运行脚本_没用的知识汇总+1 Windows 权限维持汇总...
  4. 【LeetCode从零单排】No28 Implement strStr()
  5. python函数列表永久修改_python 禁止函数修改列表的实现方法
  6. 研究生期间应该如何充实度过
  7. 文治者必有武备不然长大了挨欺负_【博古斋·六月春拍】人文事者必有武备
  8. java字节码指令简介(仅了解)
  9. 参数pyinstaller_Python用PyInstaller打包笔记
  10. 春节档总票房已破50亿 电影票一票难求
  11. npm run build 出错 npm: 6.5.0-next.0 should be = 3.0.0
  12. 水性油墨在纺织品印花中的应用
  13. undefined reference to '__android_log_print'解决方案
  14. MySQL优化详解(二)——数据库架构和使用优化
  15. require和include的区别(PHP)
  16. Ubuntu 下的PDF阅读器
  17. python中的statistics_详解python statistics模块及函数用法
  18. python调用有道翻译_python调用有道云翻译api
  19. 中国魔芋胶行业研究与投资前景预测报告(2022版)
  20. 最好用的录音软件是哪个?

热门文章

  1. 计算机语言--python
  2. J-Link在SWD模式与MCU能连接成功但不能读写
  3. 【BZOJ 4007】[JLOI2015]战争调度 DP+搜索+状压
  4. 网站移植到linux上后常犯的错误
  5. self = [super init]的解释
  6. 设置仿真器H-JTAG ARM仿真器和MDK 联调设置
  7. 【windows核心编程】第二章 字符和字符串处理
  8. Linux下的iftop命令介绍
  9. 软件测试的知识点总结
  10. 手动写一个上传图片的组件,不适用插件,包括限制图片大小,格式