学习笔记
排序算法思维导图,来自:十大经典排序算法
排序算法(英语:Sorting algorithm)是一种能将一串数据依照特定顺序进行排列的一种算法。
排序算法的稳定性
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。
也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以他们的第一个数字来排序。
(4, 1) (3, 1) (3, 7)(5, 6)
在这个状况下,有可能产生两种不同的结果,一个是让相等键值的纪录维持相对的次序,而另外一个则没有:
(3, 1) (3, 7) (4, 1) (5, 6) (维持次序)
(3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)
不稳定排序算法可能会在相等的键值中改变纪录的相对次序,但是稳定排序算法从来不会如此。
其他基本术语说明:
引用:十大经典排序算法
1 冒泡排序
冒泡排序是一种简单的排序算法。
它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。
遍历数列的工作是重复的进行直到没有在需要的交换,也就是说该数列已经排序完成,这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
冒泡排序的分析
冒泡排序的动图演示
时间复杂度
- 最优时间复杂度: O ( n ) O(n) O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:稳定
冒泡排序的实现
def bubble_sort(alist):
for j in range(len(alist)-1,0,-1):
# j表示每次遍历需要比较的次数,是逐渐减小的
for i in range(j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
li = [54,26,93,17,77,31,44,55,20]
print(li)
bubble_sort(li)
print(li)
'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''
冒泡复杂度的优化
# 如果没有交换过就直接退出
# 优化冒泡算法
def bubble_sort(alist):
"""冒泡排序"""
for j in range(len(alist)-1):
count = 0
for i in range(len(alist)-j-1):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
count += 1
if 0 == count:
return alist
2 选择排序
选择排序(Selection sort)的工作原理是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。
选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
选择排序分析
选择排序动图演示
红色表示当前最小值,黄色表示已排序序列,蓝色表示当前位置。
时间复杂度
- 最优时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:不稳定(考虑升序每次选择最大的情况)
选择排序的实现
def selection_sort(alist):
n = len(alist)
# 需要进行n-1次选择操作
for i in range(n-1):
# 记录最小位置
min_index = i
# 从i+1位置到末尾选择出最小数据
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]
alist = [54,226,93,17,77,31,44,55,20]
print(alist)
selection_sort(alist)
print(alist)
'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''
3 插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法。
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序分析
图片来源于捎的饭捎的饭
例如现在拿到一副牌
Input: {5, 3, 8, 4, 2, 6, 1, 7}
-
首先拿起第一张牌, 手上有 {5}
-
拿起第二张牌 3,把 3 与 有序{5}从后往前比较大小 ,insert 到手上的牌 {5},得到 {3 ,5}
-
拿起第三张牌 8,把 8 与 有序{3,5}从后往前比较大小, 此处 insert 最后, 得到 {3 ,5,8}
-
拿起第四张牌 4,把 4 与 有序{3,5,8}从后往前比较大小,此处 insert 到 3,5 之间, 得到 {3,4,5,8}
以此类推。
插入排序动图演示
时间复杂度
- 最优时间复杂度: O ( n ) O(n) O(n) (升序排列,序列已经处于升序状态)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:稳定
插入排序的实现
(1)时间复杂度为 O ( n 2 ) O(n^2) O(n2)
def insert_sort(alist):
for i in range(1,len(alist)):
for j in range(i,0,-1):
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
return alist
alist = [54,226,93,17,77,31,44,55,20]
print(alist)
insert_sort(alist)
print(alist)
'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''
(2)时间复杂度为 O ( n ) O(n) O(n)
def insert_sort(alist):
#插入排序
for i in range(1, len(alist)):
while i >0:
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
return alist
alist = [54,226,93,17,77,31,44,55,20]
print(alist)
insert_sort(alist)
print(alist)
'''
[54, 226, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 31, 44, 54, 55, 77, 93, 226]
'''
4 希尔排序
希尔排序(Shell Sort)是插入排序的一种,也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。
希尔排序是非稳定排序算法。
希尔排序的基本思想是:
把记录按步长 gap 分组,对每组记录采用直接插入排序方法进行排序。
随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。
希尔排序的分析
(1)
(2)
希尔排序动图演示
图片来自:漫画:什么是希尔排序?
时间复杂度
- 最优时间复杂度:根据步长序列的不同而不同
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:不稳定
希尔排序的实现
def shell_sort(alist):
n = len(alist)
# 初始步长
gap = n // 2
# 当 gap = 1 时为简单插入排序
while gap >=1:
# 按步长进行插入排序
for i in range(gap, n):
# j = gap、gap+1、gap+2、...、n-1
j = i
# 插入排序
while j>=gap and alist[j-gap] > alist[j]:
alist[j-gap], alist[j] = alist[j], alist[j-gap]
j -= gap
# 得到新的步长
gap = gap // 2
alist = [54,26,93,17,77,31,44,55,20]
print(alist)
shell_sort(alist)
print(alist)
'''
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
'''
5 快速排序
快速排序,又称划分交换排序,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
快速排序的分析
图片来自:https://cuijiahua.com/blog/2017/12/algorithm_4.html
快速排序动图演示
- 把第一个元素定为基准 key=6
- 两个指针,分别为 low ,初始指向头 6 ;high ,初始指向尾 1
- 首先,比较 high 指向的元素和 key 大小,如果 high 指向的元素大,指针向右移动,如果 high 指向的元素小,将其元素赋给 low ,low 向左一步。这里 1<6 ,把 1 赋给 low
- 只要赋值一次,就交换指针移动权。
- 接下来比较 low 指向的元素和 key 大小,如果 low 指向的元素小,指针向左移动,如果 low 指向的元素大,将其元素赋给 high ,high 向右一步。这里 3<6 ,low 向左一步,7>6,把 7 赋给 high ,high 向右一步
- 当两个指针重合,即为key的位置
- 此时key都小于右边,都大于左边
时间复杂度
- 最优时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:不稳定
快速排序的实现
def quick_sort(alist, start, end):
"""快速排序"""
# 递归的退出条件
if start >= end:
return
# 设定起始元素为要寻找位置的基准元素
mid = alist[start]
# low为序列左边的由左向右移动的游标
low = start
# high为序列右边的由右向左移动的游标
high = end
while low < high:
# 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动
while low < high and alist[high] >= mid:
high -= 1
# 将high指向的元素放到low的位置上
alist[low] = alist[high]
# 如果low与high未重合,low指向的元素比基准元素小,则low向右移动
while low < high and alist[low] < mid:
low += 1
# 将low指向的元素放到high的位置上
alist[high] = alist[low]
# 退出循环后,low与high重合,此时所指位置为基准元素的正确位置
# 将基准元素放到该位置
alist[low] = mid
# 对基准元素左边的子序列进行快速排序
quick_sort(alist, start, low-1)
# 对基准元素右边的子序列进行快速排序
quick_sort(alist, low+1, end)
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]
'''
6 归并排序
归并排序是采用分治法的一个非常典型的应用。
归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
归并排序的分析
引用图片:归并排序
引用图片:图解排序算法(四)之归并排序
治的详细过程
归并排序动图演示
(1)
(2)
时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
- 稳定性:稳定
快速排序的实现
递归操作
def merge_sort(alist):
if len(alist) <= 1:
return alist
# 二分分解
num = len(alist)//2
# left(right)采用归并排序后形成的有序的新的列表
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
result += left[l:]
result += right[r:]
return result
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]
'''
7 堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序
堆是具有以下性质的完全二叉树:
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
来自:图解排序算法(三)之堆排序
堆排序的基本思想是:
(1)将带排序的序列构造成一个大顶堆,根据大顶堆的性质,当前堆的根节点(堆顶)就是序列中最大的元素;
(2)将堆顶元素和最后一个元素交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,然后将剩下的节点重新构造成一个大顶堆;
(3)重复步骤(2),如此反复,从第一次构建大顶堆开始,每一次构建,我们都能获得一个序列的最大值,然后把它放到大顶堆的尾部。
(4)最后,就得到一个有序的序列了。
时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
- 稳定性:不稳定
堆排序动图演示
(1)
(2)
8 计数排序
基本步骤
1.找出待排序的数组中最大和最小的元素;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1.
参考代码:计数排序
def count_sort1(array):
length = len(array)
res = [None] * length
# 首次循环遍历, 每个列表的数都统计
for index in range(length):
# p 表示 a[i] 大于列表其他数 的次数
p = 0
# q 表示 等于 a[i] 的次数
q = 0
# 二次循环遍历, 列表中的每个数都和首次循环的数比较
for two_index in range(length):
if array[index] > array[two_index]:
p += 1
elif array[index] == array[two_index]:
q += 1
for k in range(p, p + q): # q表示相等的次数,就表示, 从p开始索引后, 连续q次,都是同样的数
res[k] = array[index]
return res
dest = [5, 2, 7, 4, 8, 1, 6, 3]
result = count_sort1(dest)
print('最后的结果是:', result)
'''
最后的结果是: [1, 2, 3, 4, 5, 6, 7, 8]
'''
9 桶排序
划分多个范围相同的区间,每个子区间自排序,最后合并。
引用图片:【排序】图解桶排序
10 基数排序
常见排序算法效率比较
至此