在此整理出几种经典的排序算法:
- 插入排序:直接插入排序、折半插入排序、希尔排序
- 交换排序:冒泡排序、快速排序
- 选择排序:简单选择排序
- 归并排序
- 堆排序
一、插入排序
1. 直接插入排序
介绍:将原始数组分成有序区[0, i-1]
和无序区[i, n-1]
两块,每次将无序区的第一个元素nums[i]
和有序区的元素nums[i-1]~nums[0]
从后往前比较,插入到有序区中合适的位置。
复杂度:时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1)
稳定性:不涉及到元素的交换,所以稳定
def InsertSort(self, nums, n):
"""
依次将无序区中第一个元素插入到有序区的对应位置,从后往前比较
时间复杂度n^2, 空间复杂度1
"""
for i in range(1, len(nums)):
if nums[i] < nums[i-1]: # 无序区第一个元素小于有序区最后一个元素,需要插入
tmp = nums[i] # 取出无序区第一个元素
j = i - 1
while j >= 0 and nums[j] >= tmp:
nums[j+1] = nums[j] # 后移一个位置
j -= 1
nums[j + 1] = tmp
return nums
2. 折半插入排序
介绍:直接插入排序,将无序区的首元素插入到有序区的合适位置,可以使用二分查找提高查找效率。具体来讲,二分查找是查找是以nums[i]
为target,查找有序区第一个大于target的位置,那么这个位置就是需要插入的位置。
复杂度:时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间复杂度
O
(
1
)
O(1)
O(1)
稳定性:注意二分查找的判断条件,当查找的条件是第一个大于target的元素,此时是稳定的。
def BiInsertSort(self, nums, n):
"""
在直接插入排序的基础上用二分查找来查找无序区元素需要插入的具体位置
二分查找,找第一个大于该无序区元素的位置
时间复杂度nlogn
"""
for i in range(1, len(nums)):
if nums[i] < nums[i-1]:
tmp = nums[i]
# 二分查找左边界:第一个大于target的位置
left, right = 0, i - 1
while left <= right: # 搜索区间闭区间[]
mid = left + (right - left) // 2
if nums[mid] < tmp:
left = mid + 1
elif nums[mid] > tmp:
right = mid - 1
elif nums[mid] == tmp:
left = mid + 1
if left >= n:
pos = -1
pos = left
for j in range(i - 1, pos - 1, -1):
nums[j + 1] = nums[j]
nums[pos] = tmp # 先后移,再填这个位置
return nums
3. 希尔排序
介绍:可以理解为是直接插入排序的一种并行操作,即将原数组按照不同的步长增量分成若干组,如根据下标分成[0, 3, 6]、[1, 4, 7]、[2, 5, 8]
三组,再在每个组内进行直接插入排序。经过第一轮之后组内是有序的,第二轮再减少步长,重新分组[0, 2, 4, 8]、[1, 3, 5, 7]
,组内再次进行直接插入排序。最后一轮步长减少到1,进行最后一次的直接插入排序。希尔排序一般来说,初始步长为
l
e
n
(
n
u
m
s
)
2
\frac{len(nums)}{2}
2len(nums),每次都减少为原来的
1
2
\frac{1}{2}
21,直到为1时停止。
复杂度:时间复杂度
O
(
n
3
2
)
O(n^\frac{3}{2})
O(n23),空间复杂度
O
(
1
)
O(1)
O(1)
稳定性:组内的插入排序是稳定的,但是组间互相独立,所以组间可能造成不稳定的情况。因此是不稳定的。
def HillSort(self, nums, n):
"""
在直接插入排序的基础上,进行对不同增量基于下标的分组,每组之内进行直接插入排序,最后到增量为1时停止
在开始时,分组较多,那么每组之内直接插入排序的复杂度就很低,当增量变小分组变多时,虽然每组的数变多了,但是由于之前
已经变得比较有序,所以移动的次数较少
时间复杂度n^3/2
"""
dist = n // 2 # 初始增量设置为长度的一半
while dist > 0:
for i in range(dist, n): # 从每个分组的第二个元素开始进行直接插入排序
tmp = nums[i] # 无序区第一个元素
j = i - dist # 有序区最后一个元素
while j >= 0 and nums[j] >= tmp: # 在组内从后往前找到第一个小于tmp的位置
nums[j + dist] = nums[j]
j -= dist
nums[j + dist] = tmp
dist //= 2
return nums
二、交换排序
4. 冒泡排序
介绍:将原数组竖向排列,分成上下两部分,为有序区[0, i-1]
和无序区[i, n-1]
。每次都从无序区的最底部j~[n-1, i]
开始,通过交换将无序区的最小元素交换到无序区的首位。
复杂度:时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1)
稳定性:当两个相邻元素相等时,不会发生交换,所以是稳定的。
def BubbleSort(self, nums, n):
"""
通过从下往上比较相邻元素,交换,把无序区的最小元素移到第一个
时间复杂度n^2
"""
for i in range(n): # i代表无序区第一个元素的位置,无序区[i, n-1]
for j in range(n-1, i, -1): # 从下往上
if nums[j] < nums[j - 1]: # 下面的元素小于上面的元素,发生交换
nums[j], nums[j-1] = nums[j-1], nums[j]
return nums
5. 快速排序
介绍:定义一趟划分,根据基准pivot
将数组分成前后两部分,其中前面部分元素都小于pivot
,后面都大于等于pivot
。然后上一趟划分过后的两部分,分别对每个子序列再次进行划分。
复杂度:一趟划分复杂度为
O
(
n
)
O(n)
O(n),递归子树的高度为
l
o
g
n
logn
logn,所以时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn);空间复杂度为
O
(
l
o
g
n
)
O(logn)
O(logn)
稳定性:在一趟划分中,如果pivot=nums[left]
,那么分治法会将右边第一个小于pivot
的元素放到首位,会改变元素的相对顺序。如[5,3,4,3,6,7]
,一趟划分后后面的3
会被移动到首位。所以不稳定。
def QuickSort(self, nums, n):
"""
定义划分partition,根据Pivot划分成两块
对左右两部分递归调用partition
时间复杂度nlogn
不稳定 [5,3,4,3,6,7] 中枢3和5交换,改变了3的相对顺序
"""
def partition(left, right):
pivot = nums[left] # 基准都选为第一个
i, j = left, right
while i < j:
while i < j and nums[j] >= pivot:
j -= 1
nums[i] = nums[j]
while i < j and nums[i] < pivot:
i += 1
nums[j] = nums[i]
nums[i] = pivot
return i
def helper(left, right):
if left < right:
pivot_index = partition(left, right)
helper(left, pivot_index - 1)
helper(pivot_index + 1, right)
helper(0, n-1)
return nums
三、选择排序
6. 简单选择排序
介绍:将原始数组分成有序区[0, i-1]
和无序区[i, n-1]
两块,每次从无序区[i, n-1]
中通过遍历的方式选取出最小的元素,和无序区的首元素交换。
复杂度:时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1)
稳定性:每次都是将无序区的最小元素和无序区的首元素交换,会改变元素的相对顺序,所以不稳定。
def SelectSort(self, nums, n):
"""
每次以遍历的方式找到无序区中最小元素,放到有序区的后面——最小元素和无序区第一个元素交换
时间复杂度n^2
不稳定 [5,2,2]为例
"""
for i in range(n): # i为无序区第一个元素的位置,无序区[i,n-1]
minarg = -1 # 最小元素下标
minn = 0x3f3f3f3f
for j in range(i, n):
if nums[j] < minn:
minarg = j
minn = nums[j]
nums[minarg], nums[i] = nums[i], nums[minarg] # 最小元素移到无序区第一个位置
return nums
四/7、归并排序
介绍:将原数组递归的两两合并成为一个最终有序数组的过程。子问题就是合并两个有序数组
复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间复杂度
O
(
n
)
O(n)
O(n)
稳定性:因为每次都是合并相邻的数组成为一个有序数组,所以是稳定的。
def MergeSort(self, nums, n):
"""
对原始数组进行相邻的两两分组,相邻分组合并起来,依次合并,整个过程是二叉树的倒形态
时间复杂度nlogn(一趟归并n,二路归并一共要logn次)
稳定
"""
def merge(left, right):
"""
合并两个有序数组Left,right,返回合并后的数组
"""
res = []
i, j = 0, 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
res.append(left[i])
i += 1
elif left[i] >= right[j]:
res.append(right[j])
j += 1
if i < len(left):
res += left[i:]
if j < len(right):
res += right[j:]
return res
def helper(ary): # 对ary进行归并排序,得到归并排序后的数组
if len(ary) == 1:
return ary
num = len(ary) // 2
left = helper(ary[:num])
right = helper(ary[num:])
return merge(left, right)
nums = helper(nums)
return nums
五/8、堆排序
介绍:首先根据原数组构建大根堆,此时树的根元素为当前最大元素,然后将根节点和树中最右下的元素交换,就可以删除根节点了,然后再调整使其满足大根堆。重复,直到这个大根堆的长度为1,此时完成排序。
复杂度:每次调整堆
O
(
l
o
g
n
)
O(logn)
O(logn),调整
n
n
n次,所以时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间复杂度为
O
(
1
)
O(1)
O(1)
稳定性:涉及到堆元素的交换和调整,不稳定
def HeapSort(self, nums, n):
"""
先构建一个大根堆。
然后每次将堆顶和堆中最右下的节点交换,这样大的堆顶就被移到了数组后面。
然后调整除最后一个元素外的数组,使其还是大根堆
不稳定
"""
def max_heapify(ary, start, end):
"""
将ary[start:end]调整成为大根堆
"""
root = start # 数组的第一个元素总是堆顶,即root
while True:
child = 2 * root + 1 # 左子节点的序号
# 下面取子节点比较大的那个child序号
if child > end:
break
if child + 1 <= end and ary[child + 1] > ary[child]: # 存在右子节点,且右子节点大于左子节点
child = child + 1
if ary[root] < ary[child]:
ary[root], ary[child] = ary[child], ary[root]
root = child
else: # 当前已是大根堆,无需更新
break
first = n // 2 - 1
for start in range(first, -1, -1):
max_heapify(nums, start, n-1) # 根据原始数组构建大根堆
for end in range(n-1, 0, -1):
nums[0], nums[end] = nums[end], nums[0]
max_heapify(nums, 0, end - 1)
return nums
最后给出一张对比图