排序算法的稳定性:一个稳定的排序算法,如果两个等值键R和S在排序前后次序不变,称其是稳定的
(3, 1) (3, 7) (4, 1) (5, 6) 维持次序,稳定
(3, 7) (3, 1) (4, 1) (5, 6) 次序被改变,不稳定


1.冒泡排序
两两相邻记录的关键字,如果反序就交换,直到没有反序的记录为止。

# 直觉的写法,但实际不能算冒泡
def bubble_sort(alist):
for i in range(len(alist)-1): # 从第一个元素到倒数第二个元素
for j in range(i+1, len(alist)): # 如果a[i]比后面的元素更大,交换
if alist[i] > alist[j]:
alist[i], alist[j] = alist[j], aslit[i]
因为实际上每次固定的a[i]会和后面的所有元素都比较一遍,并不是冒泡中的比较两两相邻的关键字。
两两是相邻元素;如果有n个元素那就要比较n-1轮,每一轮都减少一次比较;从下往上两两比较,就像泡泡往上冒一样。
# 升序
def bubble_sort(alist):
n = len(alist)
for i in range(n-1): # 当前轮从a[i]开始,两两相邻比较,最后一轮从a[n-2]开始。a[:i]已经有序
for j in range(n-1, i, -1): # 从下向上冒
if alist[j] < alist[j-1]: # 如果后一个元素比前一个小,就交换
alist[j], alist[j-1] = alist[j-1], alist[j]
做一点优化
def bubble_sort_pro(alist):
n = len(alist)
for i in range(n-1):
count = 0
for j in range(n-1, i, -1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
count += 1
if count == 0: # 如果某一轮没有进行任何交换,说明整个数组已经有序了
return
2.选择排序
通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换。1<=i<=n。

在未排序序列中找到最小(大)元素,放到已排序序列起始位置(一次交换);
然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾(一次交换);
以此类推,直到所有元素均排序完毕
选择排序的优点在于数据移动的操作:如果某个元素位于正确的最终位置上,它就不会被移动。所以至多需要交换 n-1 次(因为第一次就直接认为第一个元素是最小(大))。操作的是无序的那部分。
def selection_sort(alist):
n = len(alist)
for i in range(n-1): # 要找 n-1 轮最值,第一个首先认为是个最小(大)值
min_index = i # 记录当前的最小(大)位置
for j in range(i+1, n): # 遍历剩下无序部分的元素,找出最小的,再交换
if alist[j] < alist[min_index]:
min_index = j
if min_index != i:
alist[i], alist[min_index] = alist[min_index], alist[i]
3.直接插入排序
简单排序中性能最好的。将一个记录插入到一个已经排好序的有序表中。
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。所以过程中需反复把已经排序的元素向后挪。
操作的是有序的那部分。

def insert_sort(alist):
n = len(alist)
for i in range(1, n):
for j in range(i, 0, -1): # 待插入元素为a[i],有序序列为a[:i]
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j] # a[i]从后往前比较,找到插入位置。就相当于一直交换。
优化
def insert_sort_pro(alist):
n = len(alist)
for i in range(1, n):
j = i
while j > 0:
# 寻找插入位置,只要没到位置,一直交换,最终插入
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
j -= 1
else: # 如果找到要插入的位置了就不用往前再看了,避免了多余的循环
break
4.希尔排序
时间复杂度降到O(nlogn),对直接插入排序进行修改。
把记录按下标的一定增量(gap)分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个记录恰被分成一组,算法终止。
例如下图就是第一轮分了5组,第二轮分了2组

# 按gap进行插入排序
def shell_sort(alist):
n = len(alist)
gap = n // 2
while gap > 0:
for i in range(gap, n): # 从 0+gap 开始往后找元素往有序序列中插入,一直找到最后
j = i
while j >= gap and alist[j] < alist[j-gap]: # 每个子序列的j元素的前一个元素不再是j-1而是j-gap,因为要在a[j]属于的分组里做插入排序
alist[j-gap], alist[j] = alist[j], alist[j-gap]
j -= gap
gap //= 2 # 新步长
def shell_sort(alist):
n = len(alist)
gap = n // 2
while gap > 0:
for i in range(gap, n): # 待插元素从a[gap]到最后
for j in range(i, 0, -gap): # a[i]找插入位置,每次不再向前挪1而挪gap即可
if alist[j] < alist[j-gap]:
alist[j], alist[j-gap] = alist[j-gap], alist[j]
gap //= 2
5.堆排序
利用前一趟比较的结果,对选择排序进行改进。堆是具有以下性质的完全二叉树:每个节点的值都大于等于(小于等于)其左右孩子节点的值。
大顶堆 & 小顶堆

1. 将待排序的序列构造成一个大顶堆(或小顶堆)
2. 整个序列的最大值就是堆顶的根节点,将它移走(与堆数组的末尾元素交换,此时末尾元素就是最大值)。
3. 将剩余的n-1个序列重新构造成一个堆,重复上述步骤,获得升序序列。
升序排序,使用大顶堆。

堆具有的性质,如果从堆的根节点开始从1往后编号,则节点的值满足:
,n/2向下取整。
树节点编号从1开始而数组下标从0开始。统一成从0开始计数,父节点 i 的左右孩子节点在2i+1、2i+2,子节点 i 的父节点在 floor((i-1)/2)
def heapify(arr, lst, i):
"""维护最大堆,以i为根节点的子树,最后一个节点lst"""
largest = i
left, right = 2*i+1, 2*i+2
# 找到 i、i的左子树、i的右子树中的最大值
if left <= lst and arr[i] < arr[left]:
largest = left
if right <= lst and arr[largest] < arr[right]:
largest = right
# 如果这三个节点中最大的不是i,则最大节点的值和i的值交换
if largest != i:
arr[largest], arr[i] = arr[i], arr[largest]
heapify(arr, lst, largest) # 继续对largest节点递归维护堆。如果i是最大的元素,则函数结束
def heap_sort(arr):
n = len(arr)
for i in range(int((n-1-1)/2), -1, -1): # 从最后一个节点n-1的父节点开始往顶上,第一个要维护的子树根节点为 floor(((n-1)-1)/2)
heapify(arr, n-1, i)
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 堆顶a[0] 和 当前堆的最后一个元素(如果认为每次都把排好的移除。实际就是arr[i])交换
heapify(arr, i-1, 0) # 交换后要从顶向下维护堆,堆最后一个节点为下标为i-1
a = [3,4,1,2,9,5,6,8]
heap_sort(a)
print(a)
优化,把heapify函数中的尾部递归用迭代实现,效率更高
def heapify(arr, lst, i):
"""维护最大堆,以i为根节点的子树,最后一个节点lst"""
while True:
largest = i
left, right = 2*i+1, 2*i+2
# 找到 i、i的左子树、i的右子树中的最大值
if left <= lst and arr[i] < arr[left]:
largest = left
if right <= lst and arr[largest] < arr[right]:
largest = right
# 如果这三个节点中最大还是i,则函数结束
if largest == i:
break
arr[largest], arr[i] = arr[i], arr[largest] # 否则的把最大值换到当前根节点i上
i = largest # 继续下一轮,对当前的largest为根节点的子树进行维护
6.归并排序
分治思想,先递归分解数组,再合并数组。
将数组分解最小之后,把n个记录看成是n个有序的子序列,每个子序列长度为1。然后两两归并,得到ceil(n/2)个长度为2或者1的有序子序列,再两两归并...,如此重复直到得到长度为n的有序序列为止。
其中两两归并的基本思路是,比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
def merge_sort(alist):
if len(alist) <= 1:
return alist
# 二分分解
num = len(alist)//2
left = merge_sort(alist[:num])
right = merge_sort(alist[num:])
# 合并
return merge(left,right)
def merge(left, right):
'''合并操作,将两个有序数组left[]和right[]合并成一个大的有序数组'''
#left与right的下标指针
l, r = 0, 0
result = []
while l<len(left) and r<len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
if l < len(left):
result += left[l:]
elif r < len(right):
result += right[r:]
return result
迭代实现,直接从下往上。

def merge_sort(alist):
if alist is None or len(alist) <= 1:
return alist
# 每次要合并的两组数组为 a[low, low+i]、a[low+i, low+2*i]
n = len(alist)
tmp = [0]*n # 建立临时数组
i = 1 # 步长,也就是合并后数组元素的一半。第一次合并后数组长度为2,所以i初始化为1
while i < n: # 最后一次合并之前i不超过n。例如考虑n=5的情况,i=1,2,4,最后i=4合并长度为4,1的两组后结束,下一轮i=8,超过n了
# 开始一趟,两两合并所有分组数组,每一趟都从头开始
low = 0
while low < n:
mid = low + i
high = min(low + 2*i, n) # 如果第二组越界了,high等于n即可。数组a[n]会越界,但a[:n]不会,等价于a[:]
if mid < high: # 只有存在第二组才需要merge。而第二组只要有元素,high一定大于等于mid+1
# merge
merge(alist, low, mid, high, tmp)
low += 2*i # 下两组的起始元素在上两组最后元素之后
i *= 2 # 每一趟合并全部结束后,下一趟步长翻倍
def merge(alist, low, mid, high, tmp):
"""这里merge要考虑分组数组长度为1的情况,还有high等于n的情况
所以还是用索引在长度上移动来遍历,比较不容易出错
"""
l, r, k = 0, 0, 0
len_l, len_r = len(alist[low: mid]), len(alist[mid: high]) # 两个数组的长度
while l < len_l and r < len_r: # 只要有一个数组遍历完了,补上没有遍历完的即可
if alist[low + l] < alist[mid + r]:
tmp[k] = alist[low + l]
k += 1
l += 1
else:
tmp[k] = alist[mid + r]
k += 1
r += 1
if l < len_l:
tmp[k: k + mid - (low + l)] = alist[low + l: mid]
alist[low: high] = tmp[: k + mid - (low + l)]
if r < len_r:
tmp[k: k + high - (mid + r)] = alist[mid + r: high]
alist[low: high] = tmp[: k + high - (mid + r)]
alist = [54,26,93,17,77,31,44,55,20]
merge_sort(alist)
print(alist)
7.快速排序
通过一趟排序将要排序的数据分割成独立的两部分(选择一个比较基准),其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。
步骤:
挑一个元素作为基准,重排元素,比基准大的放基准后面,比基准小的放基准前面;
递归的把两个子序列进行用上述方法分别排序

def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if low < high:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
Qsort(arr, low, point-1)
Qsort(arr, point+1, high)
def Partition(arr, low, high):
# 起始元素为要寻找位置的基准元素,先把基准点的值拿出来,序列中始终就有一个多余的空位
point_value = arr[low] # 基准点初始化为起始元素
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low], arr[high] = arr[high], arr[low] # 交换,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high], arr[low] = arr[low], arr[high] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置
return low
快速排序的优化
1. 优化基准点的选取,三数取中法,基准点选取三个数中间大小的那个,避免极端情况,提高性能。
def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if low < high:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
Qsort(arr, low, point-1)
Qsort(arr, point+1, high)
def Partition(arr, low, high):
# 令基准点介于a[low]和a[high]之间
mid = low + (high - low) // 2
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[low]:
arr[low], arr[mid] = arr[mid], arr[low]
point_value = arr[low] # 基准点
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low], arr[high] = arr[high], arr[low] # 交换,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high], arr[low] = arr[low], arr[high] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置
return low
2. 优化不必要的交换,直接改成赋值就行了,因为先把基准元素拿出来了,始终有一个位置是空余的。
def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if low < high:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
Qsort(arr, low, point-1)
Qsort(arr, point+1, high)
def Partition(arr, low, high):
# 令基准点介于a[low]和a[high]之间
mid = low + (high - low) // 2
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[low]:
arr[low], arr[mid] = arr[mid], arr[low]
point_value = arr[low] # 基准点
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low]= arr[high] # 赋值,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high] = arr[low] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置,填回去
return low
3. 优化小数组时的排序方案,数组长度小于7的时候用直接插入排序。
def InsertSort(alist, low, high): # 修改一下插入排序,只排序alist中low到high这一段
for i in range(low+1, high+1):
for j in range(i, low, -1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high, MAX_LENGTH_INSERT_SORT=7):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if high - low > MAX_LENGTH_INSERT_SORT:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
Qsort(arr, low, point-1)
Qsort(arr, point+1, high)
else: # 数组小的话就直接插入
InsertSort(arr, low, high)
def Partition(arr, low, high):
# 令基准点介于a[low]和a[high]之间
mid = low + (high - low) // 2
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[low]:
arr[low], arr[mid] = arr[mid], arr[low]
point_value = arr[low] # 基准点
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low]= arr[high] # 赋值,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high] = arr[low] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置,填回去
return low
4. 优化递归操作,只要有可能,就把递归写成尾递归(函数中的递归形式出现在末尾),能够提高运行效率。尾递归可以比较容易的写成迭代。
def InsertSort(alist, low, high): # 修改一下插入排序,只排序alist中low到high这一段
for i in range(low+1, high+1):
for j in range(i, low, -1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high, MAX_LENGTH_INSERT_SORT=7):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if high - low > MAX_LENGTH_INSERT_SORT:
while low < high:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
Qsort(arr, low, point-1)
# 现在 low 已经没用了,后半部分的递归是point+1到high,如果令low=point+1,则可以在用循环在下一轮实现point+1到high的递归
low = point + 1 # 把尾递归写成while循环
else: # 数组小的话就直接插入
InsertSort(arr, low, high)
def Partition(arr, low, high):
# 令基准点介于a[low]和a[high]之间
mid = low + (high - low) // 2
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[low]:
arr[low], arr[mid] = arr[mid], arr[low]
point_value = arr[low] # 基准点
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low]= arr[high] # 赋值,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high] = arr[low] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置,填回去
return low
进一步地,要充分发挥递归的优势,加一个判断,令长度大的那部分用循环代替尾递归
def InsertSort(alist, low, high): # 修改一下插入排序,只排序alist中low到high这一段
for i in range(low+1, high+1):
for j in range(i, low, -1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
def quick_sort(alist):
Qsort(alist, 0, len(alist)-1)
def Qsort(arr, low, high, MAX_LENGTH_INSERT_SORT=7):
"""待排序数组arr, 起始位置low, 末尾位置high
"""
if high - low > MAX_LENGTH_INSERT_SORT:
while low < high:
point = Partition(arr, low, high) # 根据基准点a[point]处理数组,小的放基准点左边大的放基准点右边
if point - low < high - point: # 后半数组长度大
Qsort(arr, low, point - 1)
# 现在 low 已经没用了,后半部分的递归是point+1到high,如果令low=point+1,则可以在用循环在下一轮实现point+1到high的递归
low = point + 1 # 把尾递归写成while循环
else:
Qsort(arr, point + 1, high) # 现在high已经没用了
high = point - 1
else: # 数组小的话就直接插入
InsertSort(arr, low, high)
def Partition(arr, low, high):
# 令基准点介于a[low]和a[high]之间
mid = low + (high - low) // 2
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[low]:
arr[low], arr[mid] = arr[mid], arr[low]
point_value = arr[low] # 基准点
# 两个游标,low自左向右,high自右向左
while low < high:
while low < high and arr[high] >= point_value: # high游标自右向左找到比基准元素小的元素位置
high -= 1
arr[low]= arr[high] # 赋值,把比基准点小的放前面
while low < high and arr[low] < point_value: # low游标自左向右找到比基准元素大的元素位置
low += 1
arr[high] = arr[low] # 交换,把比基准点大的放后面
alist[low] = point_value # 当low与high位置重合,这就是基准点在最终序列中的位置,填回去
return low