0.算法概述
(1)分类
常见的经典排序算法有10种,可以分为两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
(2)时间复杂度
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(nlogn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n^2) | O(n) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
注:
- 稳定:如果a=b,且a原本在b前面,排序之后a仍然在b的前面。
- 不稳定:如果a=b,且a原本在b前面,排序之后b在a的前面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
1.插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1.1算法步骤
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
1.2动图描述
1.3代码实现
# 直接插入排序
def Direct_Insertion_Sort(ary):
for i in range(1, len(ary)): # 第一个元素认为已排序,从第二个元素开始遍历
if ary[i] < ary[i-1]: # 取出已经排好序的元素的下一个元素,与排好序的最后一个元素对比
tmp = ary[i] # 如果小于最后一个元素,则将待排序元素的值赋给临时变量tmp
index = i # 下标赋给index,待插入下标
for j in range(i-1, -1, -1): # 从排好序的元素的最后一个元素开始向前遍历,循环到0
if ary[j] > tmp: # 若之前有序的元素大于待排序元素
ary[j+1] = ary[j] # 将该元素后移
index = j # 待插入位置为j
else: # 若大于之前排好序的元素则终止循环
break
ary[index] = tmp
return ary
if __name__ == '__main__':
ary = [2, 15, 5, 9, 7, 6, 4, 12, 5, 4, 2, 64, 5, 6, 4, 2, 3, 54, 45, 4, 44] # 待排序数组
correct_ary = [2, 2, 2, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 9, 12, 15, 44, 45, 54, 64] # 排好序数组
print("正确排序后的结果:")
print(correct_ary)
# 直接插入排序结果
insert_sort_result = Direct_Insertion_Sort(ary)
print("直接插入排序后的结果:")
print(insert_sort_result)
原始待排序数组:
[2, 15, 5, 9, 7, 6, 4, 12, 5, 4, 2, 64, 5, 6, 4, 2, 3, 54, 45, 4, 44]
正确排序后的结果:
[2, 2, 2, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 9, 12, 15, 44, 45, 54, 64]
直接插入排序后的结果:
[2, 2, 2, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 9, 12, 15, 44, 45, 54, 64]
1.4算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
1.5扩展——折半插入排序
1.5.1算法描述
折半插入排序是在一组有序数列中插入新的元素,找到插入位置是高半区还是低半区。折半插入排序所需附加存储空间和直接插入排序相同,从时间上比较,折半插入排序减少了关键字间的比较次数,而记录的移动次数不变。
区别:在插入到已排序的数据时采用来折半查找(二分查找),取已经排好序的数组的中间元素,与插入的数据进行比较,如果比插入的数据大,那么插入的数据肯定属于前半部分,否则属于后半部分,依次不断缩小范围,确定要插入的位置。
1.5.2算法步骤
- 分别指向数列的第一位和末位,下标为low和high
- m = (low + high)/2
- 如果要插入的数小于m位置的数,说明要在低半区查找,high = m - 1
- 如果要插入的数大于m位置的数,说明要在高半区查找,low = m + 1
- 如果要插入的数等于m位置的数,直接退出,high=m
- 当low > high时,停止查找
- 插入的位置为high+1
1.5.3代码实现
# 折半插入排序
def Binary_Insertion_Sort(ary):
for i in range(1, len(ary)): # 从数组第二个元素开始
if ary[i] < ary[i - 1]: # 如果小于前一个元素
x = ary[i] # 值赋给临时变量x
low = 0
high = i - 1
while low <= high:
m = int((low + high) / 2)
if x < ary[m]: # 与排好序的中间值比较,找到需要插入的位置
high = m - 1
else:
low = m + 1
for j in range(i - 1, high, -1):
ary[j + 1] = ary[j] # 统一移动该移动元素
ary[high + 1] = x # 插入到正确位置
return ary
1.5.4算法分析
先折半查找元素的应该插入的位置,然后统一移动应该移动的元素,再将这个元素插入到正确的位置。
优点 : 稳定,相对于直接插入排序元素减少了比较次数;
缺点 : 相对于直接插入排序元素的移动次数不变;
时间复杂度:可以看出,折半插入排序减少了比较元素的次数,约为O(nlogn),比较的次数取决于表的元素个数n。因此,折半插入排序的时间复杂度仍然为O(n²),但它的效果还是比直接插入排序要好。
空间复杂度:排序只需要一个位置来暂存元素,因此空间复杂度为O(1)。
2.冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2.1算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
2.2动图描述
2.3代码实现
某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了。用一个标记记录这个状态即可。
# 冒泡排序
def Bubble_Sort(ary):
for i in range(len(ary) - 1, 0, -1):
flag = 1 # 用状态判断本次循环是否改变,flag=1表示没有改变
for j in range(0, i):
if ary