冒泡排序
1. 算法步骤
- 比较每个元素与其相邻元素的大小,把较大元素挪到较小元素的后面
- 经过一轮比较,该轮中最大的元素将会被置于最后
- 重复上述过程,直至该序列排序完成,其中每一轮过后比较次数-1
2. 算法图解
3. 代码实现
nums = [6,5,3,1,8,7,2,4]
for i in range(1,len(nums)): # 遍历轮次
for j in range(len(nums)-i): # 每一轮比较次数减1
if nums[j] > nums[j+1]: # 比较
nums[j],nums[j+1] = nums[j+1],nums[j] # 交换
# 优化:可以用flag记录有无交换,若一轮次无交换则返回该数组(最优时间复杂度O(n))
4. 复杂度分析
遍历n个轮次,每一轮次比较交换n次,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)
选择排序
1. 算法步骤
- 每轮遍历寻找最小元素的位置,将最小元素与起始位置元素交换(每一轮过后起始位置往后挪一个单位)
- 重复上述操作,直至该序列有序
2. 算法图解
3. 代码实现
nums = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]
for i in range(len(nums)-1): # 遍历轮次
minIndex = i # 每一轮起始位置往后挪一个单位
for j in range(i+1,len(nums)): # 寻找最小值位置
if nums[j]<nums[minIndex]:
minIndex = j
nums[i],nums[minIndex] = nums[minIndex],nums[i] # 交换
4. 复杂度分析
遍历n个轮次,遍历n次寻找最小位置,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)
插入排序
1. 算法步骤
- 将元素抽出,与前面的元素依次进行比较,若小于前面的元素,则前面元素往后移一个单位
- 直到该元素前面的元素大于该元素,则将该元素插入到该位置
- 重复上述过程,直至该序列有序
2. 算法图解
3. 代码实现
nums = [6, 5, 3, 1, 8, 7, 2, 4]
for i in range(1,len(nums)): # 遍历轮次
temp = nums[i] # 临时储值,把需要插入的值抽出来
index = i-1 # 从i-1开始比较
while index>=0 and nums[index]>temp: # 比较temp是否比前面元素小
nums[index+1] = nums[index] # 比较大的元素往后移
index -= 1 # 预插入位置
nums[index+1] = temp # 跳出循环,前面元素已经比temp大了,则将temp插入到预插入位置中
4. 复杂度分析
遍历n轮,每一轮次比较n次,故时间复杂度为O(n2),原地排序只使用了常量空间故空间复杂度为O(1)。该算法相比于冒泡排序少了交换的过程,效率会较高
希尔排序
1. 算法步骤
- 需要定义一个步长序列(每个步长是上一个步长的一半),进行分组排序
- 例如步长序列5,2,1
- 第一次分为5组,每隔5个元素将该元素加入一组直至末尾该组成立,依次对每组元素进行排序。
- 下一次分为2组,每隔2个元素将该元素加入一组直至末尾该组成立,再进行排序(排序手法为插入排序)
- 经过几轮分组排序,整个数组将会变得大概有序,最后在进行插入排序会变得很快
- 最后步长为1,这时相当于插入排序(希尔排序其实就是对于插入排序的一个优化,在数据量比较大时会有较大优势)
- 关于分组的补充:例如数据[8,9,1,7,4,3,5,2,6,0],增量序列5,2,1,第一次分组为隔5个元素为一组,分为[8,3],[9,5],[1,2],[7,6],[4,0]五组。
2. 算法图解
3. 代码实现
nums = [20,63,51,8,15,39,82,3,44,13,24,55,1,62,92,12]
inc_sequence = [5,3,1] # 定义步长序列
for inc in inc_sequence: # 遍历步长
for i in range(inc,len(nums)): # 对每组进行插入排序
temp = nums[i] # 临时储值,把需要插入的值抽出来
index = i - inc # 从i-inc开始比较
while index>=0 and nums[index]>temp: # 比较temp是否比前面元素小
nums[index+inc] = nums[index] # 比较大的元素往后移
index -= inc # 预插入位置
nums[index+inc] = temp # 跳出循环,前面元素已经比temp大了,则将temp插入到预插入位置中
4. 复杂度分析
该排序算法的时间复杂度在于步长的选取,时间复杂度比较多变,若初始增量选取的较差,最坏时间复杂度可以达到O(n2),选取的较好可以达到O(n4/3),由于推导过于复杂,此处不再赘述。空间复杂度为O(1)
此处给出目前最优的步长序列:1, 5, 19, 41, 109,…,详细推导看下行链接
感兴趣可以去wiki查看步长序列的选取,链接:https://en.wikipedia.org/wiki/Shellsort
快速排序
1. 算法步骤
- 选定一个中间元素pivot(pivot值随意取,一开始一般取最左边元素),将大于pivot的元素放到a的右边,把小于pivot的元素放到pivot的左边
- 对pivot左右的元素重复上述操作直至序列有序
2. 算法图解
3. 代码实现
# 使用头尾双指针进行比较交换,其中每一轮完成后会分成左右两个数组(两个子问题),再对左右两个数组进行操作
# 使用分治法,递归求解
def quicksort(nums,start,end):
if start>=end: # 递归结束条件
return
pivot_index = partition(nums,start,end) # 分区,并获取中心元素索引
quicksort(nums,start,pivot_index-1) # 对中心元素左边元素进行排序
quicksort(nums,pivot_index+1,end) # 对中心元素右边元素进行排序
return nums
def partition(nums,start,end): # 分区
pivot = nums[start] # 储存中心元素(取最左边元素),下面进行比较
lo,hi= start+1,end # 定义头尾指针
while lo<hi:# 头尾指针相遇则跳出循环
while lo<hi and nums[lo]<=pivot:# 如果左边的元素已经小于pivot,头指针往后移
lo += 1
while lo<hi and nums[hi]>=pivot:# 如果右边的元素已经小于pivot,尾指针往前移
hi -= 1
swap(nums,lo,hi) # 两个循环均已退出,交换头尾指针的元素,使得pivot两边满足左边小右边大的条件
if nums[lo]>pivot: # 分区完毕,将pivot挪到中间,该判断防止头指针位于较大元素点使得交换后不满足条件(可以优化一下省去该判断,但是我懒ToT,懒得想了,大佬可自行优化)
swap(nums,start,lo-1)
return lo-1
else:
swap(nums,start,lo)
return lo
def swap(nums,lo,hi): # 交换
nums[lo],nums[hi] = nums[hi],nums[lo]
if __name__ == '__main__':
nums = [35,33,42,10,14,19,27,44,26,31]
print(quicksort(nums,0,len(nums)-1))
4. 复杂度分析
最差情况每次都取到最小/最大的元素,此时和冒泡排序没什么区别,时间复杂度为O(n2),最优情况就是每次pivot两边有分区,此时时间复杂度为O(nlogn)。由于递归的存在,空间复杂度会比之前的算法要高,最优情况进行logn次递归调用,空间复杂度为O(logn),最坏情况即为冒泡排序的情况,进行n次递归调用,空间复杂度为O(n)
归并排序
1. 算法步骤
- 分治思想,分:将一个序列重中间拆分,每一次拆分成两个子序列,直至拆分至每个子序列仅有一个元素;治:按照原来的拆分线路进行比较—>合并
- 拆分使用递归,生成一个递归树,左节点储存父节点序列的左半部分,右节点储存父节点序列的右半部分
- 合并使用队列思想,先进先出,拿左右序列的头元素进行比较,比较小的就弹出,并添加到合并序列中
2. 算法图解
3. 代码实现
def mergesort(nums):
if len(nums) == 1: # 递归退出条件,拆分至每个子序列只有一个元素时退出
return nums
mid = len(nums)//2
left, right = mergesort(nums[:mid]), mergesort(nums[mid:]) # 拆分左序列和右序列
return merge(left, right) # 进行归并
def merge(left, right): # 比较->合并两个子序列
merge_nums = [] # 归并后的序列
while left and right: # left和right都不为空时进行比较合并
if left[0] <= right[0]: # 比较left和right的第一个元素,哪个小哪个就弹出,并添加进merge_nums中进行合并
merge_nums.append(left.pop(0))
else:
merge_nums.append(right.pop(0))
merge_nums += left + right # 若left或right还剩余元素,则把他们加入进去
return merge_nums
if __name__ == '__main__':
nums = [6,5,3,1,8,7,2,4]
print(mergesort(nums))
其中python内置heapq模块提供了归并函数heapq.merge(),只需要将分割好的数组作为参数作用到函数中去即可
from heapq import merge
def mergesort(nums):
if len(nums) == 1: # 递归退出条件,拆分至每个子序列只有一个元素时退出
return nums
mid = len(nums)//2
left, right = mergesort(nums[:mid]), mergesort(nums[mid:]) # 拆分左序列和右序列
return merge(left,right) # 进行归并,返回一个可迭代对象
if __name__ == '__main__':
nums = [6,5,3,1,8,7,2,4]
print(list(mergesort(nums)))
4. 复杂度分析
递归树深度log(n),每一层需要n次操作进行合并,故时间复杂度为O(nlogn)。由于需要新开辟空间进行合并,有n个元素就要开辟n个空间,故空间复杂度为O(n)
堆排序
1. 算法步骤
- 用数组模拟大顶堆(类似于二叉树,父节点元素大于子节点),如果子节点的元素大于父节点的元素则将其交换(该部为堆的维护)
- 每次当满足大顶堆的性质时,将堆顶的元素和堆尾进行交换,并使堆的尺寸缩小一个单位,然后继续维护堆,使其满足大顶堆的性质
- 重复上述步骤,直至堆的尺寸缩短为1,则排序完成
- 关于用数组模拟堆,关于父节点和子节点的索引计算:
- 索引为i的节点的父节点:(i-1)/2
- 索引为i的节点的左节点:i*2 + 1
- 索引为i的节点的右节点:i*2 + 2
2. 算法图解
3. 代码实现
def heapsort(nums,n):
# 创建大顶堆
for i in range(n//2-1,-1,-1): # 遍历每一个父节点
heapify(nums,n,i)
# 排序
for i in range(n-1,0,-1): # 堆的尺寸i缩短至1,排序完成
swap(nums,i,0) # 堆顶元素与末尾元素进行交换
heapify(nums,i,0) # 由于交换,堆顶元素变小,需要重新对堆顶元素进行维护(其中i为目前堆的尺寸,每次减一)
return nums
def heapify(heap_arr,n,i): # n为堆的尺寸,i为需要维护的节点
father = i # 父节点索引
lson = i*2 + 1 # 左节点索引
rson = i*2 + 2 #右节点索引
if lson<n and heap_arr[father]<heap_arr[lson]: # 预交换,如果左子节点较大,则让父节点索引变成左子节点索引
father = lson
if rson<n and heap_arr[father]<heap_arr[rson]: # 预交换,如果右子节点较大,则让父节点索引变成右子节点索引
father = rson
if father!=i: # 如果父节点索引改变则交换
swap(heap_arr,father,i) # 交换
heapify(heap_arr,n,father) # 交换后再维护一下堆的性质,保持大顶堆结构
def swap(nums,i,j):
nums[i],nums[j] = nums[j],nums[i]
if __name__ == '__main__':
nums = [6,5,3,1,8,7,2,4]
print(heapsort(nums,len(nums)))
4. 复杂度分析
循环n-1次,堆一共有logn层,从根节点往下遍历需要logn次,故时间复杂度为O(nlogn),堆排序属于原地排序,故空间复杂度为O(n)
计数排序
1. 算法步骤
- 需要知道待排序数组的最大数值maxValue,建立一个大小为maxValue的计数数组
- 遍历待排序数组,使用元素值当作maxValue的索引以来统计每个元素的出现次数
- 将计数数组转化为前缀和数组,映射每个元素的最终位置(保证了计数排序的稳定性)
- 倒叙遍历(保证了计数排序的稳定性)待排序数组,根据元素值寻找前缀和数组中的值,用该值-1既是该元素所在的位置
2. 算法图解
3. 代码实现
def countingsort(nums,maxValue):
count_arr = [0]*(maxValue+1)
for i in nums: # 计数
count_arr[i] += 1
for i in range(1,maxValue+1): # 将计数数组转化为映射数组(前缀和数组)
count_arr[i] += count_arr[i-1]
result= [0]*len(nums)
for i in range(len(nums)-1,-1,-1): # 倒序遍历,将元素映射到结果数组
count_arr[nums[i]] -= 1 # 映射后该元素的索引值
result[count_arr[nums[i]]] = nums[i] # 将该元素添加到结果数组中
return result
if __name__ == '__main__':
nums = [4,0,0,1,0,2,4,5,1]
print(countingsort(nums,5))
- 从代码可以看出,计数排序有明显的缺点:
- 当maxValue非常大时,内存不足以存得下这么大的数据
- 只适用于对数据范围比较集中的数据进行排序
- 只能排序非负整数
4. 复杂度分析
最多只需要遍历n次原数组和k次计数数组,故时间复杂度为O(n+k),额外创建了大小为k的计数数组,额外空间复杂度为O(k)
桶排序
1. 算法步骤
- 创建数个桶(桶的个数按照数据规模自定),每个桶储存一定区间内的数
- 将待排序数组中的元素映射到桶中, 映射公式:bucketIndex = (nums[i]-minValue)/bucketNum
- 对每个桶内的元素进行排序,再依次将桶内元素拿出来
- 其中每个桶的大小为:(maxValue-minValue)/bucketNum
2. 算法图解
3. 代码实现
def bucketsort(nums,bucketNum,maxValue,minValue):
bucket = [[] for i in range(bucketNum)] # 用二维数组模拟桶
for i in range(len(nums)): # 将nums中的元素映射到桶中
bucketIndex = (nums[i]-minValue)//bucketNum
bucket[bucketIndex].append(nums[i])
for i in range(bucketNum): # 对桶中的元素进行排序,排序算法可自选,此处图方便使用内置排序函数
bucket[i].sort()
result = []
for i in range(bucketNum): # 依次将桶内元素拿出来
for j in range(len(bucket[i])):
result.append(bucket[i][j])
return result
if __name__ == '__main__':
nums = [11,9,21,8,17,19,13,1,24,12]
print(bucketsort(nums,5,24,1))
4. 复杂度分析
最多遍历n个元素,k个桶,故时间复杂度为O(n+k),需要额外创建k个桶桶内共塞n个元素,故额外空间复杂度为O(n+k)
基数排序
1. 算法步骤
- 先根据元素的个位进行排序,再根据元素的百位进行排序,再根据元素的千位进行排序…序列中元素最高有多少位就要排序多少次
- 其中每个位的排序需要创建10个队列来存储各个位的大小关系,每遍历一个元素将他压入对应位的队列中去,排序完成将该队列的元素依次弹出,并放回原序列中
2. 算法图解
3. 代码实现
def radixsort(nums,maxDigit):
quene = [[] for i in range(10)] # 创建队列
for i in range(maxDigit): # 有多少位就排序多少次
for num in nums:
quene[num//(10**i)%10].append(num) # 找到该元素的第i位数,并将该元素添加到对应位的队列中
nums = [] # 将数组初始化为空,为下一步做准备
for i in range(len(quene)): # 将队列中元素放回原序列做准备
for j in range(len(quene[i])):
nums.append(quene[i].pop(0))
return nums
if __name__ == '__main__':
nums = [882,3,5,345,254,606,588,808,535,784,715,710]
print(radixsort(nums,3))
4. 复杂度分析
最多遍历n个数,需要排序k次(k为最大位数),故时间复杂度为O(kn),需要创建k个队列队列里面塞n个元素,故空间复杂度为O(n+k)
如文章有错误,望大家指正,共同学习,感谢!