冒泡排序
基本思想
- 两两比较相邻的关键字,最大(小)的元素会被交换到最后一位
- 对n个数据依次操作n-1轮,从而完成排序(每轮都找到一个最大/小值)
- 稳定性:元素相同时不做交换,是稳定的排序算法
复杂度
最好时间复杂度
O(n),当输入数组刚好是顺序的时候,只需要挨个比较一遍就行了,不需要做交换操作
最坏时间复杂度
O(n2),当数组刚好是完全逆序的时候,每轮排序都需要挨个比较 n 次,并且重复 n 次
平均时间复杂度
O(n2),当输入数组杂乱无章时,每轮排序都需要挨个比较 n 次,并且重复 n 次
空间复杂度
O(1),使用了交换法,不需要开辟额外的空间
代码
def BubbleSort(a):
len_a = len(a)
if len_a < 2:
return a
# 一共进行n-1轮找最大值
for i in range(len_a - 1):
# 每轮需要比较的数组长度
for j in range(len_a - i - 1):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = BubbleSort(array_test)
print(res)
快速排序
基本思想
- 挑选一个元素,作为基准(pivot)
- 所有比基准值小的放在基准前面,所有比基准大的放在基准后面,基准处于中间位置,这个称为分区操作
- 递归地,把小于基准值和大于基准值的区间里的数进行排序(也就是重复1,2)
- 稳定性:分区涉及到交换操作,所以不稳定
复杂度
最好时间复杂度
O(nlogn),每次都能选中中位数
最坏时间复杂度
O(n2),每次分区都选中了最小值或者最大值,得到不均等的两组,那么需要n次分区操作
平均时间复杂度
O(nlogn),大部分情况下,很难选到极端情况
空间复杂度
O(logn),额外空间开销在于暂存基准值
代码
def Partion(a, l, r):
# 以这个区间的最左边的数作为基准数
pivot = a[l]
# l和r指针分别指向左边大于基准数的数字和右边大于基准数的数字
while(l < r):
# 从右边开始寻找小于基准数的数字
while(l < r and a[r] >= pivot):
r -= 1
# 把这个数字放到基准数位置上
a[l] = a[r]
# 从左边开始寻找大于基准数的数字
while(l < r and a[l] <= pivot):
l += 1
a[r] = a[l]
# 最后l指向的位置是错误的,需要用基准数填上
a[l] = pivot
return l
def QuickSort(a, l, r):
if l < r:
# 找到基准数应该摆放的位置
p = Partion(a, l, r)
QuickSort(a, l, p - 1)
QuickSort(a, p + 1, r)
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
l = 0
r = len(array_test) - 1
res = QuickSort(array_test, l, r)
print(res)
插入排序
基本思想
- 第一个元素可以认为已经有序
- 取下一个元素,在已排好序的元素序列中,从后往前扫描
- 如果该元素(已排序)大于新元素,则将该元素后移
- 重复3直到找到小于或等于新元素的位置,将新元素插入该位置,重复2-4
- 稳定性:元素相同时,插入到已排好序元素的后一位置,所以是稳定的
复杂度
最好时间复杂度
O(n),数组完全顺序,每次只用比较一次就能找到正确插入位置
最坏时间复杂度
O(n2),数组完全逆序,要比较1+2+3+…+n-1次才能找到正确插入位置,
平均时间复杂度
O(n2)
空间复杂度
O(1),额外开销在于每次都要保存要插入的那个元素,否则其他已排好序的元素后移时,要插入元素会发生丢失
代码
def InsertSort(a):
if len(a) < 2:
return a
for i in range(1, len(a)):
target = a[i]
# 这里必须是反向遍历
j = i - 1
while(j >= 0 and target < a[j]):
a[j+1] = a[j]
j -= 1
# 此时j指向小于或者等于target的位置
a[j+1] = target
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = InsertSort(array_test)
print(res)
希尔排序
基本思想
插入排序的改进版,又叫缩小增量排序
- 把整个待排序序列分割为若干个子序列分别进行直接插入排序
- 先做远距离移动使序列基本有序,逐渐缩小间隔重复操作,最后间隔为1时即为简单插入排序
- 稳定性:不稳定
复杂度
最好时间复杂度
O(n)
最坏时间复杂度
O(n2)
平均时间复杂度
O(n1.3)
空间复杂度
O(1)
代码
def ShellSort(a):
# 增量排序的间隔(依次减半)
step = len(a) // 2
while(step > 0):
# 对间隔step的元素序列进行简单插入排序
for i in range(step, len(a)):
while(i >= step and a[i] < a[i-step]):
a[i], a[i-step] = a[i-step], a[i]
i -= step
step //= 2
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = ShellSort(array_test)
print(res)
选择排序
基本思想
- 选择排序包括简单选择排序+堆排序(每次都是从未排序数组中找出最大/小值,将这个数加入到已排序数组中)
- 从整体数组中找到最小值并和0位置的数字交换,下一次循环从[1:len(a)]中寻找次小值,依此类推
- 稳定性:不稳定,因为会把当前数交换到比它更小的位置上去
复杂度
最好时间复杂度
O(n2)
最坏时间复杂度
O(n2)
平均时间复杂度
O(n2)
空间复杂度
O(1)
代码
def SelectSort(a):
if len(a) < 2:
return a
for i in range(len(a)):
min_idx = i
for j in range(i+1, len(a)):
if a[j] < a[min_idx]:
min_idx = j
if min_idx != i:
a[i], a[min_idx] = a[min_idx], a[i]
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = SelectSort(array_test)
print(res)
堆排序
基本思想
-
性质:子节点的键值总是小于/大于它的父节点
-
构造一个大顶堆,取堆顶元素R1与最后一个元素R[n]交换
-
新的无序区R[1]…R[n-1]和新的有序区,再将剩下的数构建一个大顶堆,取堆顶元素与无序区最后一个元素交换
-
重复以上操作,直到取完堆中的数字,最终得到一个从大到小的序列
-
对O(n)个节点进行堆调整操作O(logn),之后每次操作确定一个数的次序,因此总的时间复杂度O(nlogn)
-
额外开销在于根节点下移交换时的一个暂存空间
-
稳定性:不稳定
-
初始构建大顶堆时,所有数按照层序摆放在二叉树中
-
从最后一个非叶子节点(len(a)//2-1)开始,从左到右,从下到上进行调整
-
每一步调整都需要考虑到是否使得子树节点结构混乱,是则需要重新调整
-
最后将堆顶元素和最后一个元素进行互换
复杂度
最好时间复杂度
O(nlogn)
最坏时间复杂度
O(nlogn)
平均时间复杂度
O(nlogn)
空间复杂度
O(1)
代码
# 以i为根节点构建大顶堆
def HeapAdjust(a, i, n):
largest = i
l = 2 * i + 1
r = 2 * i + 2
# 让largest指向最大值的下标
if l < n and a[l] > a[largest]:
largest = l
if r < n and a[r] > a[largest]:
largest = r
if largest != i:
a[i], a[largest] = a[largest], a[i]
# largest指向的子树需要重新调整堆
HeapAdjust(a, largest, n)
# 堆排序
def HeapSort(a):
n = len(a)
if n < 2:
return a
# 初始构建大顶堆
# 从第一个非叶子节点开始,从左到右,从下到上调整
for i in range(len(a)//2-1, -1, -1):
HeapAdjust(a, i, n)
# 一个个的取出最大值放到数组最后
# 从n-1, n-2,..., 1依次确定这些位置上的数字
for i in range(n-1, 0, -1):
# 此时的a[0]是一个大顶堆的最大元素
a[i], a[0] = a[0], a[i]
# 重新调整堆(从这里开始,堆的重排从被换到0位置的数字开始,因为除了它,其他子树已经重排好了)
HeapAdjust(a, 0, i)
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = HeapSort(array_test)
print(res)
归并排序
基本思想
本质上就是分治算法,将数组不断地二分,直到最后每个部分只包含1个数据,然后对每个部分分别进行排序,最后将排序好的相邻部分合并在一起
- 将长度为n的序列分为2个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排好序的子序列合并成一个最终的排序序列
- 是稳定的排序算法,相同元素前后顺序不变
复杂度
最好时间复杂度
O(nlogn)
最坏时间复杂度
O(nlogn)
平均时间复杂度
O(nlogn)
空间复杂度
O(n),每次合并都需要开辟基于数组的临时内存空间
代码
# 合并两个有序数组成一个有序数组
def Merge(a, left, mid, right):
# 2个数组的起始位置
i = left
j = mid + 1
# 开辟一个临时数组空间
temp = []
while(i <= mid and j <= right):
if a[i] < a[j]:
temp.append(a[i])
i += 1
else:
temp.append(a[j])
j += 1
# 循环结束后,剩余数组直接追加到temp后面
if i > mid:
temp.extend(a[j:right+1])
if j > right:
temp.extend(a[i:mid+1])
# 最后把temp直接复制给a
a[left:right+1] = temp[:]
def MergeSort(a, left, right):
# 如果left=right,说明只有一个元素不用排序
if left < right:
mid = (left + right) // 2
MergeSort(a, left, mid)
MergeSort(a, mid+1, right)
Merge(a, left, mid, right)
return a
array_test = [12, 23, 54, 32, 11, 5, 73]
res = MergeSort(array_test, 0, len(array_test)-1)
print(res)
在设计算法时,我们需要考虑到时间复杂度和空间复杂度,一般情况我们认为时间比空间更宝贵所以会牺牲空间来减少运行时间,以上都是基于比较的排序,时间复杂度的下限是O(nlogn),而以下三种排序算法都是不基于比较的排序算法,可以突破O(nlogn)这一下界
计数排序
基本思想
- 根据待排序数组中最大元素和最小元素的差值范围,申请额外空间;
- 遍历待排序数组,将每一个元素出现的次数记录到元素值对应的额外空间内(待排序数组的元素值等于额外空间的索引);
- 遍历额外空间,每个数字出现多少次就输出多少次;
局限性:
- 待排序数组的最大元素和最小元素差距过大时不适合计数排序,比如20个数里面最大值和最小值分别是1和100000,此时申请额外空间会严重浪费空间
- 数组中元素如果存在非整数,则不适合计数排序,因为待排序数组的元素值和辅助数组的索引是一一对应的,显然无法创建辅助数组
复杂度
最好时间复杂度
O(n+k)
最坏时间复杂度
O(n+k)
平均时间复杂度
O(n+k)
空间复杂度
O(k),需要开辟基于数组数组最大值和最小值差值的临时内存空间
代码
def CountSort(a):
max_a = float("-inf")
min_a = float("inf")
# 寻找最大值和最小值
for i in range(len(a)):
if a[i] > max_a:
max_a = a[i]
if a[i] < min_a:
min_a = a[i]
# 建立辅助数组,计数
b = [0 for i in range(max_a - min_a + 1)]
for i in range(len(a)):
b[a[i]-min_a] += 1
# 输出排序的值
a_i = 0
for i in range(len(b)):
while(b[i]):
a[a_i] = i+min_a
a_i += 1
b[i] -= 1
print(a)
array_test = [12, 23, 54, 32, 11, 5, 73, 100]
CountSort(array_test)
桶排序
基本思想
桶排序算法想法类似于散列表。首先要假设待排序的元素输入符合均匀分布,例如数据均匀分布在[0,100]区间上,则可将此区间划分为10个大小一样的小区间,称为桶,对散布到同一个桶中的元素再使用其他排序算法进行排序,最后顺序输出桶中元素即可
桶排序的稳定性取决于桶内排序使用的算法
复杂度
最好时间复杂度
O(n+k)
最坏时间复杂度
O(n2)
平均时间复杂度
O(n+k)
空间复杂度
O(n+k)
代码
def BucketSort(a):
max_a = float("-inf")
min_a = float("inf")
# 寻找最大值和最小值确定桶的范围
for i in range(len(a)):
if a[i] > max_a:
max_a = a[i]
if a[i] < min_a:
min_a = a[i]
# 确定桶的个数
num = 5
# 桶的范围
bucket_range = (max_a - min_a) // num + 1
# print(bucket_range)
# 把原始序列加入桶内
buckets = [[] for i in range(num)]
for i in range(len(a)):
temp = (a[i] - min_a) // bucket_range
# print(temp)
buckets[temp].append(a[i])
# 对桶内元素进行排序
for i in range(len(buckets)):
# 原地修改
buckets[i].sort()
# 输出桶内元素
res = []
for i in range(len(buckets)):
res.extend(buckets[i])
print(res)
array_test = [-12, 23, 54, 32, 11, 200, 73, 100]
BucketSort(array_test)
基数排序
基本思想
基数排序是桶排序的扩展,将整数按位数切割成不同的数字,然后从低位到高位,依次按每位上的数大小排序
复杂度
最好时间复杂度
O(n*k)
最坏时间复杂度
O(n*k)
平均时间复杂度
O(n*k)
空间复杂度
O(n+k)
代码
def RadixSort(a):
# 最大值的位数表示需要进行几次比较
max_a = max(a)
count = 0
while(max_a):
count += 1
max_a //= 10
for i in range(count):
# 按照位数上的大小加入桶中
buckets = [[] for i in range(10)]
# 把待排序元素依次加到桶里
for j in range(len(a)):
# 计算a[j]在当前位i上的数字
radix = a[j] // pow(10, i) % 10
buckets[radix].append(a[j])
# 把桶里的元素依次赋值给a
a = []
for k in range(10):
a.extend(buckets[k])
# print(buckets)
return a
array_test = [12, 23, 154, 32, 11, 234, 73, 1000]
a = RadixSort(array_test)
print(a)