排序算法
- 什么是排序算法
- 能将一串数据一找特定顺序进行排列的算法
- 排序算法的稳定性
- 稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
冒泡排序
- 什么是冒泡排序
- 是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端
- 冒泡排序的运作方式
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
- 算法实现
def bubble_sort(li):
n = len(li)
# 共冒泡n-1趟
for j in range(n-1):
# 一趟冒泡,把最大的一个数冒到最右边
# 数据两两比较,从0位置到n-1位置
flag = False # 设置一个标志,用来后面来设置列表已经是排序状态的标志
for i in range(n-1-j):
if li [i] > li [i + 1]:
# 交换
li [i], li [i + 1] = li [i + 1], li [i]
flag = True
if flag is False:
break
if __name__ == '__main__':
l = [1,1,3,4,5]
# [5,1,2,3,6]
bubble_sort(l)
print(l)
- 时间复杂度
- 最优时间复杂度:O(n)
- 最坏时间复杂度:O(n2)
- 稳定性:稳定
选择排序
- 什么是选择排序
- 是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的后面一个位置上。以此类推,直到所有元素均排序完毕。
- 选择排序的优点
- 选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
- 选择排序的实现
def select_sort(li):
n = len(li)
# 共n次选择,从0到n-1
for j in range(n-1):
# 第一趟选择,记录0位置,把0位置与后面的数据进行比较
# 记录一个比较出来的最小数的下标
temp = j
for i in range(j+1, n):
if li[i] < li[temp]:
temp = i
# 循环结束时,temp指向的是最小的数
li[temp], li[j] = li[j], li[temp]
if __name__ == '__main__':
l = [1, 2, 4, 3]
select_sort(l)
print(l)
- 时间复杂度
- 最优时间复杂度:O(n2)
- 最坏时间复杂度:O(n2)
- 稳定性:不稳定
插入排序
- 什么是插入排序
- 是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
- 插入排序的实现
def insert_sort(li):
n = len(li)
# 依次让后面的数据插入到前面的有序序列中,从第二个数开始到最后一个数据
for j in range(1, n):
# 默认第一个数据是有序的,从第二个数据依次插入到前面的有序序列中
for i in range(j, 0, -1):
if li[i - 1] > li[i]:
li[i - 1], li[i] = li[i], li[i - 1]
else:
break
if __name__ == '__main__':
l = [1, 2, 5, 4, 3]
insert_sort(l)
print(l)
- 时间复杂度
- 最优时间复杂度:O(n)
- 最坏时间复杂度:O(n2)
- 稳定性:稳定
希尔排序
- 什么是希尔排序
- 是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。
- 希尔排序是非稳定排序算法。 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
- 希尔排序的过程
- 将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了。
- 希尔排序的实现
def shell_sort(li):
n = len(li)
# 定义初始步长
gap = n//2
# 让步长从大到小,最后必须以1步长排序
while gap >= 1:
for i in range(gap, n):
# 插入排序
while (i-gap) >= 0:
if li[i] < li[i-gap]:
li[i], li[i - gap] = li[i-gap], li[i]
i -= gap
gap //= 2
if __name__ == '__main__':
l = [7, 6, 5, 4, 3, 2]
shell_sort(l)
print(l)
- 时间复杂度
- 最优时间复杂度:根据步长序列的不同而不同(步长的确定也有其算法)
- 最坏时间复杂度:O(n2)
- 稳定性:不稳定
- 希尔排序比插入排序高效的原因
- 希尔排序同步对步长的限制,可以让相隔较远的元素进行移动,这样就使得,减少了很多中间移动过程,大大的减少了排序过程中的移动,减少了排序由于移动所浪费的时间
- 备注:
- 实际使用希尔排序的时候,步长是通过算法确定的,我们这里只是先将步长设置为一半,一直到1,来进行演示;
归并排序
- 归并排序概念
- 归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组
- 归并排序过程
- 将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
- 代码实现
def merge_sort(li):
# 当列表的长度为1时,不需要再拆分
if len(li) == 1:
return li
# 取一个中间位置,把数据拆分成左右两半
mid = len(li)//2
left = li[:mid]
right = li[mid:]
# 递归继续拆分
left_res = merge_sort(left)
right_res = merge_sort(right)
# 合并下层方法返回的结果,并且进行排序
res = merge(left_res,right_res)
return res
def merge(left,right):
"""把两个有序的序列组成有序序列"""
# 定义两个下标,分别指向两个序列的0位置
left_index = 0
right_index = 0
# 创建临时列表
res = []
while left_index < len(left) and right_index < len(right):
if left[left_index] <= right[right_index]:
res.append(left[left_index])
left_index += 1
else:
res.append(right[right_index])
right_index += 1
res = res + left[left_index:]
res = res + right[right_index:]
return res
if __name__ == '__main__':
l = [6,5,4,3,2,1]
print(merge_sort(l))
- 时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(nlogn)
- 稳定性:稳定
- 缺点
- 归并排序虽然失效复杂度较低,但是每一层进行归并的时候,都需要创建一个临时空间,使用的临时空间很多,所以归并排序的空间复杂度比较高,所以在使用的时候,需要考虑对空间占用的情况
快速排序
- 快速排序概念
- 快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 快速排序步骤
- 从数列中挑出一个元素,称为"基准"(pivot),一般挑选第一个元素。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 代码实现
def quick_sort(li,start,end):
# 当start=end时,证明只对一个数排序,不需要排序
# 当start>end时,证明没有需要排序的数据
if start >= end:
return
# 定义两个下标,一个是头,一个是尾
left = start
right = end
# 把第一个数当作中间值
mid = li[left]
# 不停的让右边下标往左移动,左边下标往右移动,直到左右下标相等
while left < right:
# 右边下标往左移动,找到一个小于mid的数据
while left < right and li[right] >= mid:
right -= 1
# 循环结束时,right指向的数据小于mid,把数据放到左边下标位置
li[left] = li[right]
# 左边下标往右移动,找到一个大于mid的数据
while left < right and li[left] <= mid:
left += 1
# 循环结束时,left指向的数据大于mid,把数据放到右边下标位置
li[right] = li[left]
# 循环结束时,left=right,把mid放到此位置
li[left] = mid
# 递归让左边的数据继续快排
quick_sort(li,start,left-1)
# 递归让右边的数据继续快排
quick_sort(li,left+1,end)
if __name__ == '__main__':
l = [7,6,5,4,3,2]
quick_sort(l,0,len(l)-1)
print(l)
- 时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(n2)
- 稳定性:不稳定
- 时间复杂度说明
- 在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。
常见排序算法的比较