排序算法
排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序。
- 内部排序:数据记录在内存中进行排序。
- 外部排序:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
关于时间复杂度:
- 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序;
- 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
- O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序;
- 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性:
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
- 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
一、冒泡排序
冒泡排序是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
动画演示:
分析:
代码实现:
# 第一次 i 0~n-2 range(0, n-1) i=0 # 第二次 i 0~n-3 range(0, n-1-1) i=1 # 第三次 i 0~n-4 range(0, n-1-2) i=2 # 由此得 range(0, n-1-i) def bubble_sort(array): """冒泡排序""" n = len(array) # 外层循环控制走多少次 for i in range(0, n-1): # 内层循环控制从头走到尾(依次比较) for j in range(0, n-1-i): if array[j] > array[j+1]: array[j], array[j+1] = array[j+1], array[j] array = [5, 3, 4, 1, 7, 2, 8, 9, 6] bubble_sort(array) print(array)
优化(如果array本身是有序的,则时间复杂度为O(n)):
def bubble_sort(array): """冒泡排序""" n = len(array) # i 表示第几趟 for i in range(n-1): # 如果冒泡排序中执行一趟而没有发生交换,则列表已经是有序状态,可以直接结束算法 exchange = False # j 表示指针,从头走到尾 for j in range(n-1-i): if array[j] > array[j+1]: array[j], array[j+1] = array[j+1], array[j] exchange = True if not exchange: return None array = [1, 2, 3, 4, 5, 6] bubble_sort(array) print(array)
二、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是O(n²)的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- 重复第二步,直到所有元素均排序完毕。
动画演示:
分析:
# array = [5, 3, 4, 1, 2] # 0 1 2 3 4 # min_index = 3 # array[0], array[3] = array[3], array[0] # array = [1, 3, 4, 5, 2] # 0 1 2 3 4 # min_index = 4 # array[1], array[4] = array[4], array[1] # array = [1, 2, 4, 5, 3] # 0 1 2 3 4 # min_index = 4 # array[2], array[4] = array[4], array[2] # array = [1, 2, 3, 5, 4] # 0 1 2 3 4 # min_index = 4 # array[3], array[4] = array[4], array[3] # array = [1, 2, 3, 4, 5] # 0 1 2 3 4
得出:对一个长度为n的数组,选择排序需要执行n-1次。
代码实现:
def select_sort(array): """选择排序""" n = len(array) # i 表示第几趟 for i in range(n-1): # 第 i 趟开始时:无序区为[i:n] min_index = i # 记录最小值的索引 for j in range(i+1, n): # i+1 表示自己不用跟自己比 if array[j] < array[min_index]: min_index = j # 更新最小值索引 # i 不是最小值时,将 i 和最小值进行交换 if i != min_index: array[i], array[min_index] = array[min_index], array[i] array = [5, 3, 4, 1, 2] select_sort(array) print(array)
三、插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
算法步骤:
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)。
动画演示:
代码实现:
def insert_sort(array): """插入排序""" n = len(array) for i in range(1, n): # i = [1, 2, 3, ... n-1] # i 相当于摸的那张牌的索引 tmp = array[i] # tmp 用于保存摸到的那张牌 pre_index = i - 1 # 摸的那张牌的前一张牌的索引 while pre_index >= 0 and tmp < array[pre_index]: # 条件一:pre_index >= 0 说明这张牌排到了第一个位置 # 条件二:略 array[pre_index + 1] = array[pre_index] # 将前一张牌往后挪 pre_index -= 1 array[pre_index + 1] = tmp array = [5, 3, 4, 1, 7, 2, 9, 8, 6] insert_sort(array) print(array)
四、快速排序
在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
算法步骤:
- 从数列中挑出一个元素,称为 "基准"(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
动画演示:
代码实现:
def quick_sort(array, left=None, right=None): """快速排序""" left = 0 if not isinstance(left, (int, float)) else left right = len(array)-1 if not isinstance(right, (int, float)) else right if left < right: # 至少两个元素 index = partition(array, left, right) quick_sort(array, left, index-1) quick_sort(array, index+1, right) def partition(array, left, right): """分区""" tmp = array[left] while left < right: # 从右往左找 while left < right and array[right] >= tmp: right -= 1 array[left] = array[right] # 从左往右找 while left < right and array[left] <= tmp: left += 1 array[right] = array[left] # 跳出循环,此时left == right,然后将tmp归位 array[left] = tmp # 归位后,返回此元素的索引 return left array = [5, 3, 4, 1, 7, 2, 9, 8, 6] quick_sort(array) print(array)
五、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列。
算法步骤:
- 建立堆;
- 得到堆顶元素,为最大元素;
- 去掉堆顶,将堆最后一个元素放到堆顶,此时可通过依次调整重新使堆有序;
- 堆顶元素为第二大元素;
- 重复步骤3,直到堆变空。
动画演示:
代码实现:
def heap_sort(array): """堆排序""" # 1. 建堆 n = len(array) # n//2 -1 表示:最后一个非叶子节点 for i in range(n//2 -1, -1, -1): # i 表示建堆时要调整的子树的根的索引 # 倒着依次调整每颗树,因为根节点为array[0],所以范围到-1, sift(array, i, n-1) # high统一定义为n-1也是成立的,而不必定义为每棵树的最后一个节点 # 2. 挨个出数 for j in range(n-1, -1, -1): # j 表示当前high的值,因为顶部根节点依次退休,所以high的位置是变化的 array[j], array[0] = array[0], array[j] # 将退休的元素保存回原来的high位置(不必开辟新空间) # 现在堆的范围:0 ~ j-1 sift(array, 0, j-1) def sift(array, low, high): """调整""" tmp = array[low] i = low j = 2*i + 1 # j 默认是左孩子 while j <= high: # 退出条件2:当前 i 位置是叶子节点,j 位置超过了high # j 指向更大的孩子节点 if j+1 <= high and array[j+1] > array[j]: # 如果右孩子存在并且更大,j 指向右孩子 j += 1 if tmp < array[j]: array[i] = array[j] i = j j = 2*i + 1 else: # 退出条件1:tmp的值大于两个孩子的值 array[i] = tmp break else: array[i] = tmp array = [50, 18, 46, 10, 34, 22, 9, 25] heap_sort(array) print(array) # [9, 10, 18, 22, 25, 34, 46, 50]
六、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
动画演示:
代码实现:
def merge_sort(array): """归并排序""" n = len(array) if n < 2: return array mid = n//2 left = array[0:mid] right = array[mid:] ret1 = merge_sort(left) ret2 = merge_sort(right) return merge(ret1, ret2) def merge(left, right): """合并""" tmp = [] while left and right: if left[0] <= right[0]: tmp.append(left.pop(0)) else: tmp.append(right.pop(0)) while left: tmp.append(left.pop(0)) while right: tmp.append(right.pop(0)) return tmp array = [11, 22, 16, 9] print(merge_sort(array))
七、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
算法步骤:
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj,tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动画演示:
代码实现:
def shell_sort(array): """希尔排序""" n = len(array) gap = n//2 while gap>=1: for i in range(gap, n): index = i while index>0: if array[index] < array[index-gap]: array[index], array[index-gap] = array[index-gap], array[index] index -= gap else: break # 缩短gap步长 gap = gap//2 array = [5, 3, 4, 1, 7, 2, 9, 8, 6] shell_sort(array) print(array)