交换排序 === 冒泡排序,快速排序
插入排序 ===直接插入排序,希尔排序
选择排序 === 简单选择排序,堆排序
归并排序
基数排序
冒泡排序
要点
冒泡排序是一种交换排序。
什么是交换排序呢?
交换排序:两两比较待排序的关键字,并交换不满足次序要求的那对数,直到整个表都满足次序要求为止。
原理:比较两个相邻的元素,将值大的元素交换到前面。
算法思想
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,故命名冒泡。
假设有一个大小为 N 的无序序列。冒泡排序就是要每趟排序过程中通过两两比较,找到第 i 个小(大)的元素,将其往上排。
以上图为例,演示一下冒泡排序的实际流程:
假设有一个无序序列 { 4. 3. 1. 2, 5 }
-
第一趟排序:通过两两比较,找到第一小的数值 1 ,将其放在序列的第一位。
-
第二趟排序:通过两两比较,找到第二小的数值 2 ,将其放在序列的第二位。
-
第三趟排序:通过两两比较,找到第三小的数值 3 ,将其放在序列的第三位。
至此,所有元素已经有序,排序结束。
要将以上流程转化为代码,我们需要像机器一样去思考,不然编译器可看不懂。
假设要对一个大小为 N 的无序序列进行升序排序(即从小到大)。
-
每趟排序过程中需要通过比较找到第 i 个小的元素。
-
所以,我们需要一个外部循环,从数组首端(下标 0) 开始,一直扫描到倒数第二个元素(即下标 N - 2) ,剩下最后一个元素,必然为最大。
假设是第 i 趟排序,可知,前 i-1 个元素已经有序。现在要找第 i 个元素,只需从数组末端开始,扫描到第 i 个元素,将它们两两比较即可。
-
所以,需要一个内部循环,从数组末端开始(下标 N - 1),扫描到 (下标 i + 1)。
1 def bubbleSort(list): 2 # 外层循环 3 for j in range(len(list) - 1): 4 count = 0 5 # 内层循环 6 for i in range(0, len(list)-1-j): 7 if list[i] > list[i + 1]: 8 list[i], list[i + 1] = list[i + 1], list[i] 9 count += 1 10 if 0 == count: 11 break 12 13 14 if __name__ == '__main__': 15 list = [32, 12, 66, 17, 80, 58, 46, 25, 74] 16 print(list) 17 bubbleSort(list) 18 print(list)
算法分析
冒泡排序算法的性能
时间复杂度
由上面的例子可知 总的遍历比较次数为 4 + 3 + 2 + 1 = 10次
对于n位的数列则有比较次数为 (n-1) + (n-2) + ... + 1 = n * (n - 1) / 2,这就得到了最大的比较次数
而O(N^2)表示的是复杂度的数量级。举个例子来说,如果n
= 10000,那么 n(n-1)/2 = (n^2 - n) / 2 = (100000000 - 10000) /
2,相对10^8来说,10000小的可以忽略不计了,所以总计算次数约为0.5 * N^2。用O(N^2)就表示了其数量级(忽略前面系数0.5)。
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数 C 和记录移动次数 M 均达到最小值:Cmin = N - 1, Mmin = 0。所以,冒泡排序最好时间复杂度为 O(N)。
若初始文件是反序的,需要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
Cmax = N(N-1)/2 = O(N2)
Mmax = 3N(N-1)/2 = O(N2)
冒泡排序的最坏时间复杂度为 O(N2)。因此,冒泡排序的平均时间复杂度为 O(N2)。
总结起来,其实就是一句话:当数据越接近正序时,冒泡排序性能越好。
算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。
所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
优化
对冒泡排序常见的改进方法是加入标志性变量swapped ,用于标志某一趟排序过程中是否有数据交换。
如果进行某一趟排序时并没有进行数据交换,则说明所有数据已经有序,可立即结束排序,避免不必要的比较过程。
1 def bubbleSort2(alist): 2 list_len = len(alist) 3 for i in range(list_len): 4 # 定义一个swapped 5 # 如果有元素交换过就置为True 6 # 如果没有元素交换过就退出循环 7 8 swapped = False 9 for j in range(list_len-1-i): 10 if alist[j] > alist[j+1]: 11 alist[j],alist[j+1] = alist[j+1],alist[j] 12 swapped = True 13 # 搅拌排序 14 # 从后面往前面检索,如果前面比后面的大,就交换 15 if swapped: 16 swapped = False 17 # 由于上面已经有一个元素在最后排好序了,所以这时要减2 18 for j in range(list_len - 2 - i,0,-1): 19 if alist[j] < alist[j - 1]: 20 alist[j], alist[j - 1] = alist[j - 1], alist[j] 21 swapped = True 22 # 如果没有发生元素交换,就说明列表已经是有序的了 23 # 这时可以直接退出循环 24 if not swapped: 25 return alist 26 27 28 def main(): 29 print(bubbleSort2([22, 3, 1, 6, 7, 8, 2, 5])) 30 31 if __name__ == '__main__': 32 main()
快速排序
要点
快速排序是一种交换排序。
快速排序由 C. A. R. Hoare 在 1962 年提出。
算法思想
它的基本思想是:
通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。方式不唯一
详细的图解往往比大堆的文字更有说明力,所以直接上图:
上图中,演示了快速排序的处理过程:
-
初始状态为一组无序的数组:2、4、5、1、3。
-
经过以上操作步骤后,完成了第一次的排序,得到新的数组:1、2、5、4、3。
-
新的数组中,以 2 为分割点,左边都是比 2 小的数,右边都是比 2 大的数。
-
因为 2 已经在数组中找到了合适的位置,所以不用再动。
-
2 左边的数组只有一个元素 1,所以显然不用再排序,位置也被确定。(注:这种情况时,left 指针和 right 指针显然是重合的。因此在代码中,我们可以通过设置判定条件遍历 left 必须小于 right,如果不满足,则不用排序了)。
-
而对于 2 右边的数组 5、4、3,设置 left 指向 5,right 指向 3,开始继续重复图中的一、二、三、四步骤,对新的数组进行排序。
分解和合并迭代实现方式:
在数列之中,选择一个元素作为"基准"(pivot),或者叫比较值。
数列中所有元素都和这个基准值进行比较,如果比基准值小就移到基准值的左边,如果比基准值大就移到基准值的右边
以基准值左右两边的子列作为新数列,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
quick_sort = lambda array: array if len(array) <= 1 else quick_sort([item for item in array[1:] if item <= array[0]]) + [array[0]] + quick_sort([item for item in array[1:] if item > array[0]])
简洁快排匿名函数生成器
代码实现:
1 def quick_sort(arr): 2 """快速排序""" 3 if len(arr) < 2: 4 return arr 5 # 选取基准,随便选哪个都可以,选中间的便于理解 6 base = arr[len(arr) // 2] 7 # 定义基准值左右两个空数列用于存放排序后的 8 left, right = [], [] 9 # 从原始数组中移除基准值 10 arr.remove(base) 11 for i in arr: 12 # 大于基准值放右边 13 if i >= base: 14 right.append(i) 15 else: 16 # 小于基准值放左边 17 left.append(i) 18 # 使用迭代进行比较 19 return quick_sort(left) + [base] + quick_sort(right)
算法分析
快速排序算法的性能
时间复杂度
当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。
而当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。
所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差。
空间复杂度
快速排序在每次分割的过程中,需要 1 个空间存储基准值。而快速排序的大概需要 Nlog2N 次的分割处理,所以占用空间也是 Nlog2N 个。
算法稳定性
在快速排序中,相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。
快速排序优化
分治思想的排序在处理大数据集量时效果比较好,小数据集性能差些。
快速排序有一个缺点就是对于小规模的数据集性能不是很好。可能有人认为可以忽略这个缺点不计,因为大多数排序都只要考虑大规模的适应性就行了。但是快速排序算法使用了分治技术,最终来说大的数据集都要分为小的数据集来进行处理,所以快排分解到最后几层性能不是很好,所以我们就可以使用扬长避短的策略去优化快排:
- 先使用快排对数据集进行排序,此时的数据集已经达到了基本有序的状态
- 然后当分区的规模达到一定小时,便停止快速排序算法,而是改用插入排序,插入排序在对基本有序的数据集排序有着接近线性的复杂度,性能比较好。
插入排序
要点
直接插入排序是一种最简单的插入排序。
插入排序:每一趟将一个待排序的记录,按照其关键字的大小插入到有序队列的合适位置里,直到全部插入完成。
算法思想
在讲解直接插入排序之前,先让我们脑补一下我们打牌的过程。
-
先拿一张 5 在手里,
-
再摸到一张 4,比 5 小,插到 5 前面,
-
摸到一张 6,嗯,比 5 大,插到 5 后面,
-
摸到一张 8,比 6 大,插到 6 后面,
-
。。。
-
最后一看,我靠,凑到的居然是同花顺,这下牛逼大了。
以上的过程,其实就是典型的直接插入排序,每次将一个新数据插入到有序队列中的合适位置里。
很简单吧,接下来,我们要将这个算法转化为编程语言。
假设有一组无序序列 R0, R1, … , RN-1。
-
我们先将这个序列中下标为 0 的元素视为元素个数为 1 的有序序列。
-
然后,我们要依次把 R1, R2, … , RN-1 插入到这个有序序列中。所以,我们需要一个外部循环,从下标 1 扫描到 N-1 。
-
接下来描述插入过程。假设这是要将 Ri 插入到前面有序的序列中。由前面所述,我们可知,插入 Ri 时,前 i-1 个数肯定已经是有序了。
所以我们需要将 Ri 和 R0 ~ Ri-1 进行比较,确定要插入的合适位置。这就需要一个内部循环,我们一般是从后往前比较,即从下标 i-1 开始向 0 进行扫描。
核心代码
1 def insertionSort(arr): 2 3 for i in range(1, len(arr)): 4 5 key = arr[i] 6 7 j = i-1 8 while j >=0 and key < arr[j] : 9 arr[j+1] = arr[j] 10 j -= 1 11 arr[j+1] = key 12 13 14 arr = [12, 11, 13, 5, 6] 15 insertionSort(arr) 16 print ("排序后的数组:") 17 for i in range(len(arr)): 18 print ("%d" %arr[i]) 19 20 ------------------------ 21 22 arr = [1,12,2, 11, 13, 5, 6,18,4,9,-5,3,11] 23 def insertionSort(arr): 24 #从要排序的列表第二个元素开始比较 25 for i in range(1,len(arr)): 26 j = i 27 #从大到小比较,直到比较到第一个元素 28 while j > 0: 29 if arr[j] < arr[j-1]: 30 arr[j-1],arr[j] = arr[j],arr[j-1] 31 j -= 1 32 return arr 33 print(insertionSort(arr)) 34 35 ---------------------------------- 36 37 # 一次往数组添加多个数字 38 def AppendNumbers(array): 39 num = input('Numbers:(split by spaces) ').split() 40 for i in num: 41 array.append(int(i)) 42 print('排序前数组:{}.'.format(array)) 43 44 def InsertionSort(array): 45 AppendNumbers(array) # 添加 46 47 list = [] 48 while True: 49 for i in array: 50 minimum = min(array) 51 if i == minimum: 52 list.append(i) 53 array.remove(i) # 删去最小值 54 55 if array == []: 56 break 57 58 print('排序后数组:{}.'.format(list)) 59 60 array = [6, 4, 45, -2, -1, 2, 4, 0, 1, 2, 3, 4, 5, 6, -4, -6, 7, 8, 8, 34, 0] 61 InsertionSort(array)
算法分析
直接插入排序的算法性能
时间复杂度
当数据正序时,执行效率最好,每次插入都不用移动前面的元素,时间复杂度为 O(N)。
当数据反序时,执行效率最差,每次插入都要前面的元素后移,时间复杂度为 O(N2)。
所以,数据越接近正序,直接插入排序的算法性能越好。
空间复杂度
由直接插入排序算法可知,我们在排序过程中,需要一个临时变量存储交换的数据和下标,不需要额外的存储空间,所以空间复杂度为 1 。
算法稳定性
直接插入排序的过程中,不需要改变相等数值元素的位置,所以它是稳定的算法。
插入排序优化
当有序区间数据量很大时,查找数据的插入位置就会显得非常耗时,插入排序算法每次都是从有序区间查找插入位置,以此为切入点,我们可以使用二分查找法来快速确认待插入的位置,于是就有了优化版的插入排序算法,也叫二分查找插入算法。
1 def insert_sort(data_list): 2 ''' 3 无优化版 4 ''' 5 count=0 #统计循环次数 6 length = len(data_list) 7 for i in range(1,length ): #默认第一个位置的元素是已排序区间,因此下标从 1 开始 8 tmp = data_list[i] #待插入的数据 9 j = i 10 while j > 0: #从已排序区间查找插入位置 11 count +=1 12 if tmp < data_list[j-1]: 13 data_list[j] = data_list[j-1] #元素向后移动,腾出插入位置 14 else: 15 break 16 j -= 1 17 data_list[j] = tmp #插入操作 18 print(data_list) 19 print(f"总循环次数为 {count}") 20 return data_list 21 22 if __name__ == "__main__": 23 unsort = [1,3,4,2,1,5,6,7,8,4] 24 print(*insert_sort(unsort)) 25 ------------------------------------------------------------ 26 上述代码中的 count 只是为了统计循环次数,目的是和优化版的进行对比,当然您也可以对时间复杂度进行分析来对比性能的差异。 print(data_list) 是为了打印出每一次插入后数据列的结果,您可以对比结果来理解插入排序算法。 27 28 ---------------------------------------------------------------- 29 def insert_sort2(data_list): 30 ''' 31 使用二分查找函数确定待插入元素在有序区间的插入位置 32 ''' 33 count=0 #统计循环次数 34 length = len(data_list) 35 for i in range(1,length ): #默认第一个位置的元素是已排序区间,因此下标从 1 开始 36 print(data_list) 37 wait_insert_data = data_list[i] ##等待插入元素 38 move_index = i 39 insert_index,count1 = binary_search(data_list[0:i],wait_insert_data) #寻找插入位置 40 count+=count1 #统计循环次数需要加上二分查找的循环次数 41 while move_index > insert_index: #移动元素,直到待插入位置处 42 count+=1 43 data_list[move_index] = data_list[move_index - 1] 44 move_index -= 1 45 data_list[insert_index] = wait_insert_data #插入操作 46 print(data_list) 47 print(f"总循环次数为 {count}") 48 return data_list 49 50 51 def binary_search(data_list,data): 52 """ 53 输入:有序列表,和待查找的数据data 54 输出:data 应该在该有序列表的插入位置 55 count 变量纯粹是为了统计循环次数而使用的,实际应用时可去除。 56 """ 57 count = 0 58 length = len(data_list) 59 low = 0 60 high = length-1 61 ##如果给定元素大于等于最后一个元素,则插入最后元素位置的后面 62 ##如果小于第一个元素,则插入位置0 63 if data >= data_list [length -1]: return length,0 64 elif data < data_list [0]: return 0,0 65 insert_index = 0 66 while low < high-1: 67 count +=1 68 mid = (low + high)//2 #python中的除法结果默认为浮点数取整数部分时使用 // 69 if data_list[mid] > data: 70 high = mid 71 insert_index = high 72 else: 73 low = mid 74 insert_index = low+1 #如果值相同或者值大于mid的值,那么插入位置位于其后面 75 return insert_index,count 76 77 if __name__ == "__main__": 78 unsort = [1,3,4,2,1,5,6,7,8,4] 79 print(*insert_sort2(unsort))
希尔排序-分治思想的插入排序
要点
希尔(Shell)排序又称为缩小增量排序,它是一种插入排序。它是直接插入排序算法的一种威力加强版。
该方法因 DL.Shell 于 1959 年提出而得名。
算法思想
希尔排序的基本思想是:
把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。
随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。
我们来通过演示图,更深入的理解一下这个过程。
在上面这幅图中:
初始时,有一个大小为 10 的无序序列。
在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。
-
接下来,按照直接插入排序的方法对每个组进行排序。
在** 第二趟排序中**,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。
-
按照直接插入排序的方法对每个组进行排序。
在第三趟排序中,再次把 gap 缩小一半,即 gap3 = gap2 / 2 = 1。这样相隔距离为 1 的元素组成一组,即只有一组。
-
按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
需要注意一下的是,图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。
所以,希尔排序是不稳定的算法。
核心代码
1 def shell_sort(data_list): 2 ''' 3 思想:分治策略 4 使用 for 循环 5 ''' 6 length = len(data_list) 7 space = length//2 8 while space > 0: 9 for i in range(space,length ): #默认第一个位置的元素是已排序区间,因此下标从 1 开始 10 tmp = data_list[i] #待插入的数据 11 for j in range(i-space,-1,-space): #从已排序区间查找插入位置 12 if tmp < data_list[j]: 13 data_list[j+space] = data_list[j] #元素向后移动,腾出插入位置 14 i = j #最后的j即为插入的位置 15 else: 16 break 17 data_list[i] = tmp #插入操作 18 print(data_list) 19 space = space // 2 20 return data_list 21 22 unsort = [9,8,7,6,5,4,3,2,1] 23 print(*shell_sort(unsort)) 24 25 ---------------------------- 26 27 def shell_sort2(data_list): 28 ''' 29 思想:分治策略 30 使用 while 循环 31 ''' 32 length = len(data_list) 33 space = length//2 34 while space > 0: 35 i = space 36 while i < length: #默认第一个位置的元素是已排序区间,因此下标从 1 开始 37 tmp = data_list[i] #待插入的数据 38 j = i 39 while j >= space and data_list[j - space] > tmp: #从已排序区间查找插入位置 40 data_list[j] = data_list[j-space] #元素向后移动,腾出插入位置 41 j -= space 42 data_list[j] = tmp #插入操作 43 print(data_list) 44 i +=1 45 space = space // 2 46 return data_list 47 48 49 unsort = [9,8,7,6,5,4,3,2,1] 50 print(*shell_sort2(unsort))
算法分析
希尔排序的算法性能
时间复杂度
步长的选择是希尔排序的重要部分。只要最终步长为 1 任何步长序列都可以工作。
算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为 1 进行排序。当步长为 1 时,算法变为插入排序,这就保证了数据一定会被排序。
Donald Shell 最初建议步长选择为 N/2 并且对步长取半直到步长达到 1。虽然这样取可以比 O(N2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。
比如,如果一个数列以步长 5 进行了排序然后再以步长 3 进行排序,那么该数列不仅是以步长 3 有序,而且是以步长 5 有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。
已知的最好步长序列是由 Sedgewick 提出的(1, 5, 19, 41, 109,…),该序列的项来自这两个算式。
这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
算法稳定性
由上文的希尔排序算法演示图即可知,希尔排序中相等数据可能会交换位置,所以希尔排序是不稳定的算法。
直接插入排序和希尔排序的比较
-
直接插入排序是稳定的;而希尔排序是不稳定的。
-
直接插入排序更适合于原始记录基本有序的集合。
-
希尔排序的比较次数和移动次数都要比直接插入排序少,当 N 越大时,效果越明显。
-
在希尔排序中,增量序列 gap 的取法必须满足:**最后一个步长必须是 1 。**
-
直接插入排序也适用于链式存储结构;希尔排序不适用于链式结构。
1 def shell_sort(alist): 2 n = len(alist) 3 gap = n // 2 4 while gap >= 1: 5 for j in range(gap, n): 6 i = j 7 while i > 0: 8 if alist[i] < alist[i-gap]: 9 alist[i], alist[i-1] = alist[i-1], alist[i] 10 i -= gap 11 else: 12 break 13 gap //= 2 14 15 if __name__ == "__main__": 16 li = [54,26,93,17,77,31,44,55,20] 17 print(li) 18 shell_sort(li) 19 print(li)
简单选择排序
要点
简单选择排序是一种选择排序。
选择排序:每趟从待排序的记录中选出关键字最小的记录,顺序放在已排序的记录序列末尾,直到全部排序结束为止。
算法思想
-
从待排序序列中,找到关键字最小的元素;
-
如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
-
从余下的 N - 1 个元素中,找出关键字最小的元素,重复 1、2 步,直到排序结束。
如图所示,每趟排序中,将当前**第 i 小的元素放在位置 i **上。
算法分析
简单选择排序算法的性能
时间复杂度
简单选择排序的比较次数与序列的初始排序无关。假设待排序的序列有 N 个元素,则**比较次数总是 N (N - 1) / 2 **。
而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0。
当序列反序时,移动次数最多,为 3N (N - 1) / 2。
所以,综合以上,简单排序的时间复杂度为 O(N2)。
空间复杂度
简单选择排序需要占用一个临时空间,在交换数值时使用。
示例代码
1 def SelectSort(input_list): 2 l = len(input_list) 3 if l == 0: 4 return [] 5 sorted_list = input_list 6 for i in range(l): 7 #默认第i个元素是每次的最小值的索引 8 min_index = i 9 #找到后面元素最小的索引 10 for j in range(i+1,l): 11 if sorted_list[min_index] >sorted_list[j]: 12 min_index = j 13 #将找到的最小元素放入前面已经有序序列的末尾 14 temp = sorted_list[i] 15 sorted_list[i] = sorted_list[min_index] 16 sorted_list[min_index] = temp 17 print("%dth"%(i+1)) 18 print(sorted_list) 19 return sorted_list 20 21 if __name__ == '__main__': 22 input_list = [50,123,543,187,49,30,0,2,11,100] 23 print("input_list:") 24 print(input_list) 25 sorted_list = SelectSort(input_list) 26 print("sorted_list:") 27 print(input_list) 28 29 30 input_list: 31 [50, 123, 543, 187, 49, 30, 0, 2, 11, 100] 32 1th 33 [0, 123, 543, 187, 49, 30, 50, 2, 11, 100] 34 2th 35 [0, 2, 543, 187, 49, 30, 50, 123, 11, 100] 36 3th 37 [0, 2, 11, 187, 49, 30, 50, 123, 543, 100] 38 4th 39 [0, 2, 11, 30, 49, 187, 50, 123, 543, 100] 40 5th 41 [0, 2, 11, 30, 49, 187, 50, 123, 543, 100] 42 6th 43 [0, 2, 11, 30, 49, 50, 187, 123, 543, 100] 44 7th 45 [0, 2, 11, 30, 49, 50, 100, 123, 543, 187] 46 8th 47 [0, 2, 11, 30, 49, 50, 100, 123, 543, 187] 48 9th 49 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543] 50 10th 51 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543] 52 sorted_list: 53 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
堆排序
要点
在介绍堆排序之前,首先需要说明一下,堆是个什么玩意儿。
堆是一棵顺序存储的完全二叉树。
其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆。每个非叶子结点的值都要小于或者等于其左右孩子结点的值。
其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆。每个非叶子结点的值都要大于或者等于其左右孩子结点的值。
举例来说,对于 n 个元素的序列 {R0, R1, … , Rn} 当且仅当满足下列关系之一时,称之为堆:
-
Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
-
Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中 i=1,2,…,n/2 向下取整;
如上图所示,序列 R{3, 8,15, 31, 25} 是一个典型的小根堆。
堆中有两个父结点,元素 3 和元素 8。
元素 3 在数组中以 R[0] 表示,它的左孩子结点是 R[1],右孩子结点是 R[2]。
元素 8 在数组中以 R[1] 表示,它的左孩子结点是 R[3],右孩子结点是 R[4],它的父结点是 R[0]。可以看出,它们满足以下规律:
设当前元素在数组中以 R[i] 表示,那么,
-
它的左孩子结点是:R[2*i+1];
-
它的右孩子结点是:R[2*i+2];
-
它的父结点是:R[(i-1)/2];
-
R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。
算法思想
构建一个完全二叉树(序列 -> 完全二叉树 -> 大顶堆 -> 排序 - > 大顶堆 -> 排序 ...)
-
首先,按堆的定义将数组 R[0..n]调整为堆(这个过程称为创建初始堆),交换 R[0]和 R[n];
-
然后,将 R[0..n-1]调整为堆,交换 R[0]和 R[n-1];
-
如此反复,直到交换了 R[0]和 R[1]为止。
以上思想可归纳为两个操作:
-
根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。
-
每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下元素重新调整为大根堆。
当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
先通过详细的实例图来看一下,如何构建初始堆。
设有一个无序序列 { 1, 3,4, 5, 2, 6, 9, 7, 8, 0 }。
构造了初始堆后,我们来看一下完整的堆排序处理:
还是针对前面提到的无序序列 { 1,3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。
相信,通过以上两幅图,应该能很直观的演示堆排序的操作处理。
核心代码
1 # 将列表打印成树(堆排序辅助函数) 2 import math 3 4 def printTree(lst): 5 treeLen = len(lst) 6 treeLay = math.ceil(math.log2(treeLen + 1)) # 树层数 7 index = 0 8 treeWidth = 2 ** treeLay - 1 # 树宽度 9 for i in range(treeLay): 10 for j in range(2**i): 11 print('{:^{}}'.format(lst[index], treeWidth), end=' ') 12 index += 1 13 if index >= treeLen: 14 break 15 treeWidth = treeWidth//2 16 print() 17 18 lst = [1, 2, 3, 4, 5, 6, 7, 8, 9] 19 printTree(lst) 20 21 # 调整当前节点 22 def heap_adjust(n, i, array:list): # array:list 表示array变量的类型是list 23 ''' 24 调整当前节点核心算放 25 :param n: 待比较数字的个数 26 :param i: 当前节点的下标 27 :param array: 待排序数据 28 :return:None 29 ''' 30 while 2 * i <= n: # 性质5,=n只有左子树 31 # 孩子结点判断2i为左孩子,2i+1为右孩子 32 lChild_index = 2 * i 33 maxChild_index = lChild_index # n=2i 34 if n > lChild_index and array[lChild_index + 1] > array[lChild_index]: # n>2i说明还有右孩子 35 maxChild_index = lChild_index + 1 # n=2i+1 36 # 和子树的根节点比较 37 if array[maxChild_index] > array[i]: 38 array[i], array[maxChild_index] = array[maxChild_index], array[i] # 交换 39 i = maxChild_index # 被交换后,需要判断是否还需要调整 40 else: 41 break 42 printTree(array) 43 print('--------------------------') 44 45 # 构建大顶堆 46 def maxHeap(total, array:list): 47 for i in range(total//2, 0, -1): 48 heap_adjust(total, i, array) # 调整当前结点 49 return array 50 51 total = 9 52 origin = [0,30, 20, 80, 40, 50, 10, 60, 70, 90] 53 print('maxHeap is ') 54 printTree(maxHeap(total, origin)) 55 56 # 结论:第一层是最大值,第二层一定有个次大值
1 def HeapSort(input_list): 2 3 #调整parent结点为大根堆 4 def HeapAdjust(input_list,parent,length): 5 6 temp = input_list[parent] 7 child = 2*parent+1 8 9 while child < length: 10 if child+1 <length and input_list[child] < input_list[child+1]: 11 child +=1 12 13 if temp > input_list[child]: 14 break 15 input_list[parent] = input_list[child] 16 parent = child 17 child = 2*child+1 18 input_list[parent] = temp 19 20 if input_list == []: 21 return [] 22 sorted_list = input_list 23 length = len(sorted_list) 24 #最后一个结点的下标为length//2-1 25 #建立初始大根堆 26 for i in range(0,length // 2 )[::-1]: 27 HeapAdjust(sorted_list,i,length) 28 29 for j in range(1,length)[::-1]: 30 #把堆顶元素即第一大的元素与最后一个元素互换位置 31 temp = sorted_list[j] 32 sorted_list[j] = sorted_list[0] 33 sorted_list[0] = temp 34 #换完位置之后将剩余的元素重新调整成大根堆 35 HeapAdjust(sorted_list,0,j) 36 print('%dth' % (length - j)) 37 print(sorted_list) 38 return sorted_list 39 40 41 if __name__ == '__main__': 42 input_list = [50,123,543,187,49,30,0,2,11,100] 43 print("input_list:") 44 print(input_list) 45 sorted_list = HeapSort(input_list) 46 print("sorted_list:") 47 print(input_list) 48 49 50 51 input_list: 52 [50, 123, 543, 187, 49, 30, 0, 2, 11, 100] 53 1th 54 [187, 123, 50, 49, 100, 30, 0, 2, 11, 543] 55 2th 56 [123, 100, 50, 49, 11, 30, 0, 2, 187, 543] 57 3th 58 [100, 49, 50, 2, 11, 30, 0, 123, 187, 543] 59 4th 60 [50, 49, 30, 2, 11, 0, 100, 123, 187, 543] 61 5th 62 [49, 11, 30, 2, 0, 50, 100, 123, 187, 543] 63 6th 64 [30, 11, 0, 2, 49, 50, 100, 123, 187, 543] 65 7th 66 [11, 2, 0, 30, 49, 50, 100, 123, 187, 543] 67 8th 68 [2, 0, 11, 30, 49, 50, 100, 123, 187, 543] 69 9th 70 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543] 71 sorted_list: 72 [0, 2, 11, 30, 49, 50, 100, 123, 187, 543]
算法分析
堆排序算法的总体情况
时间复杂度
堆的存储表示是顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。
当想得到一个序列中第 k 个最小的元素之前的部分排序序列,最好采用堆排序。
因为堆排序的时间复杂度是 O(n+klog2n),若 k ≤ n/log2n,则可得到的时间复杂度为 O(n)。
算法稳定性
堆排序是一种不稳定的排序方法。
因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。
归并排序
要点
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
算法思想
将待排序序列 R[0…n-1] 看成是 n 个长度为 1 的有序序列,将相邻的有序表成对归并,得到 n/2 个长度为 2 的有序表;将这些有序序列再次归并,得到 n/4 个长度为 4 的有序序列;如此反复进行下去,最后得到一个长度为 n 的有序序列。
综上可知:
归并排序其实要做两件事:
-
“分解”——将序列每次折半划分。
-
“合并”——将划分后的序列段两两合并后排序。
我们先来考虑第二步,如何合并?
在每次合并过程中,都是对两个有序的序列段进行合并,然后排序。
这两个有序序列段分别为 R[low, mid] 和 R[mid+1, high]。
先将他们合并到一个局部的暂存数组R2 中,带合并完成后再将 R2 复制回 R 中。
为了方便描述,我们称 R[low, mid] 第一段,R[mid+1, high] 为第二段。
每次从两个段中取出一个记录进行关键字的比较,将较小者放入 R2 中。最后将各段中余下的部分直接复制到 R2 中。
经过这样的过程,R2 已经是一个有序的序列,再将其复制回 R 中,一次合并排序就完成了。
假如我们有一个n个数的数列,下标从0到n-1
首先是分开的过程
1 我们按照 n//2 把这个数列分成两个小的数列
2 把两个小数列 再按照新长度的一半 把每个小数列都分成两个更小的,一直这样重复,一直到每一个数分开了
比如: 6 5 4 3 2 1
第一次 n=6 n//2=3 分成 6 5 4 3 2 1
第二次 n=3 n//2=1 分成 6 5 4 3 2 1
第三次 n=1的部分不分了
n=2 n//2=1 分成 5 4 2 1
之后是合并排序的过程:
3 分开之后我们按照最后分开的两个数比较大小形成正确顺序后组合绑定
刚刚举得例子 最后一行最后分开的数排序后绑定 变成 4 5 1 2
排序后倒数第二行相当于把最新分开的数排序之后变成 6 4 5 3 12
4 对每组数据按照上次分开的结果,进行排序后绑定
6 和 4 5(两个数绑定了) 进行排序
3 和 1 2(两个数绑定了) 进行排序
排完后 上述例子第一行待排序的 4 5 6 1 2 3 两组数据
5 对上次分开的两组进行排序
拿着 4 5 6 1 2 3两个数组,进行排序,每次拿出每个数列中第一个(最小的数)比较,把较小的数放入结果数组。再进行下一次排序。
每个数组拿出第一个数,小的那个拿出来放在第一位 1 拿出来了, 变成4 5 6 2 3
每个数组拿出第一个书比较小的那个放在下一个位置 1 2被拿出来, 待排序 4 5 6 2
每个数组拿出第一个书比较小的那个放在下一个位置 1 2 3 被拿出来, 待排序 4 5 6
如果一个数组空了,说明另一个数组一定比排好序的数组最后一个大 追加就可以结果 1 2 3 4 5 6
相当于我们每次拿到两个有序的列表进行合并,分别从两个列表第一个元素比较,把小的拿出来,在拿新的第一个元素比较,把小的拿出来
这样一直到两个列表空了 就按顺序合并了两个列表
掌握了合并的方法,接下来,让我们来了解如何分解。
在某趟归并中,设各子表的长度为 gap,则归并前 R[0…n-1] 中共有 n/gap 个有序的子表:R[0…gap-1], R[gap…2gap-1], … , R[(n/gap)gap … n-1]。
调用 Merge 将相邻的子表归并时,必须对表的特殊情况进行特殊处理。
若子表个数为奇数,则最后一个子表无须和其他子表归并(即本趟处理轮空):若子表个数为偶数,则要注意到最后一对子表中后一个子表区间的上限为 n-1。
核心代码
1 def merge_sort( li ): 2 #不断递归调用自己一直到拆分成成单个元素的时候就返回这个元素,不再拆分了 3 if len(li) == 1: 4 return li 5 6 #取拆分的中间位置 7 mid = len(li) // 2 8 #拆分过后左右两侧子串 9 left = li[:mid] 10 right = li[mid:] 11 12 #对拆分过后的左右再拆分 一直到只有一个元素为止 13 #最后一次递归时候ll和lr都会接到一个元素的列表 14 # 最后一次递归之前的ll和rl会接收到排好序的子序列 15 ll = merge_sort( left ) 16 rl =merge_sort( right ) 17 18 # 我们对返回的两个拆分结果进行排序后合并再返回正确顺序的子列表 19 # 这里我们调用拎一个函数帮助我们按顺序合并ll和lr 20 return merge(ll , rl) 21 22 #这里接收两个列表 23 def merge( left , right ): 24 # 从两个有顺序的列表里边依次取数据比较后放入result 25 # 每次我们分别拿出两个列表中最小的数比较,把较小的放入result 26 result = [] 27 while len(left)>0 and len(right)>0 : 28 #为了保持稳定性,当遇到相等的时候优先把左侧的数放进结果列表,因为left本来也是大数列中比较靠左的 29 if left[0] <= right[0]: 30 result.append( left.pop(0) ) 31 else: 32 result.append( right.pop(0) ) 33 #while循环出来之后 说明其中一个数组没有数据了,我们把另一个数组添加到结果数组后面 34 result += left 35 result += right 36 return result 37 38 if __name__ == '__main__': 39 li = [5,4 ,3 ,2 ,1] 40 li2 = merge_sort(li) 41 print(li2)
算法分析
归并排序算法的性能
时间复杂度
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是 O(n*log2n)。
空间复杂度
由前面的算法说明可知,算法处理过程中,需要一个大小为 n 的临时存储空间用以保存合并序列。
算法稳定性
在归并排序中,相等的元素的顺序不会改变,所以它是稳定的算法。
归并排序和堆排序、快速排序的比较
-
若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。
-
若从稳定性来考虑,应选取归并排序,因为堆排序和快速排序都是不稳定的。
-
若从平均情况下的排序速度考虑,应该选择快速排序。
示例代码
基数排序、桶排序、计数排序
基数排序一般用于长度相同的元素组成的数组。首先按照最低有效数字进行排序,然后由低位向高位进行。基数排序可以看做是进行多趟桶排序。每个有效数字都在0-9之间,很适合桶排序,建10个桶很方便
[0,1)和[1,+∞)的桶排序是一种非常快速的排序,留置一个数组S,里面含有M个桶,初始化为0。然后遍历数组A,读入Ai时,S[Ai]增一。所有输入被读进后,扫描数组S得出排好序的表。该算法时间花费O(M+N),空间上不能原地排序。
计数排序假设n个输入元素中每一个都是介于0到k之间的整数,此处k为某个整数。当k=O(n)时,计数排序的运行时间为Θ(n)。
对每一个数的元素x,确定出小于x的元素个数。有了这一信息就可以把x直接放到最终输出数组中的位置上。
要点
基数排序与本系列前面讲解的七种排序方法都不同,它不需要比较关键字的大小。
它是根据关键字中各位的值,通过对排序的 N 个元素进行若干趟“分配”与“收集”来实现排序的。
不妨通过一个具体的实例来展示一下,基数排序是如何进行的。
设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0 , 2 , 11, 100}。
我们知道,任何一个阿拉伯数,它的各个位数上的基数都是以 0~9 来表示的。
所以我们不妨把 0~9 视为 10 个桶。
我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。例如:R[0] = 50,个位数上是 0,将这个数存入编号为 0 的桶中。
分类后,我们在从各个桶中,将这些数按照从编号 0 到编号 9 的顺序依次将所有数取出来。
这时,得到的序列就是个位数上呈递增趋势的序列。
按照个位数排序:{50, 30, 0, 100, 11, 2, 123, 543 , 187, 49}。
接下来,可以对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。
算法分析
基数排序的性能
时间复杂度
通过上文可知,假设在基数排序中,r 为基数,d 为位数。则基数排序的时间复杂度为 O(d(n+r))。
我们可以看出,基数排序的效率和初始序列是否有序没有关联。
空间复杂度
在基数排序过程中,对于任何位数上的基数进行“装桶”操作时,都需要 n+r 个临时空间。
算法稳定性
在基数排序过程中,每次都是将当前位数上相同数值的元素统一“装桶”,并不需要交换位置。所以基数排序是稳定的算法。
示例代码
1 class bucketSort(object): 2 def insertSort(self,a): 3 n=len(a) 4 if n<=1: 5 pass 6 for i in range(1,n): 7 key=a[i] 8 j=i-1 9 while key<a[j] and j>=0: 10 a[j+1]=a[j] 11 j-=1 12 a[j+1]=key 13 def sort(self,a): 14 n=len(a) 15 s=[[] for i in xrange(n)] 16 for i in a: 17 s[int(i*n)].append(i) 18 for i in s: 19 self.insertSort(i) 20 return [i for j in s for i in j] 21 def __call__(self,a): 22 return self.sort(a) 23 24 if __name__=='__main__': 25 from random import random 26 from timeit import Timer 27 a=[random() for i in xrange(10000)] 28 def test_bucket_sort(): 29 bucketSort()(a) 30 def test_builtin_sort(): 31 sorted(a) 32 tests=[test_bucket_sort,test_builtin_sort] 33 for test in tests: 34 name=test.__name__ 35 t=Timer(name+'()','from __main__ import '+name) 36 print t.timeit(1)
1 import random 2 class bucketSort(object): 3 def _max(self,oldlist): 4 _max=oldlist[0] 5 for i in oldlist: 6 if i>_max: 7 _max=i 8 return _max 9 def _min(self,oldlist): 10 _min=oldlist[0] 11 for i in oldlist: 12 if i<_min: 13 _min=i 14 return _min 15 def sort(self,oldlist): 16 _max=self._max(oldlist) 17 _min=self._min(oldlist) 18 s=[0 for i in xrange(_min,_max+1)] 19 for i in oldlist: 20 s[i-_min]+=1 21 current=_min 22 n=0 23 for i in s: 24 while i>0: 25 oldlist[n]=current 26 i-=1 27 n+=1 28 current+=1 29 def __call__(self,oldlist): 30 self.sort(oldlist) 31 return oldlist 32 if __name__=='__main__': 33 a=[random.randint(0,100) for i in xrange(10)] 34 bucketSort()(a) 35 print a
1 import random 2 def radixSort(): 3 A=[random.randint(1,9999) for i in xrange(10000)] 4 for k in xrange(4): #4轮排序 5 s=[[] for i in xrange(10)] 6 for i in A: 7 s[i/(10**k)%10].append(i) 8 A=[a for b in s for a in b] 9 return A
1 def countingSort(alist,k): 2 n=len(alist) 3 b=[0 for i in xrange(n)] 4 c=[0 for i in xrange(k+1)] 5 for i in alist: 6 c[i]+=1 7 for i in xrange(1,len(c)): 8 c[i]=c[i-1]+c[i] 9 for i in alist: 10 b[c[i]-1]=i 11 c[i]-=1 12 return b 13 if __name__=='__main__': 14 a=[random.randint(0,100) for i in xrange(100)] 15 print countingSort(a,100)