Date: 2019-08-16
在面试中,排序算法是一个经常被问到的一个知识点,它的常用排序算法是:快速排序算法、归并排序算法、冒泡排序算法、插入排序算法、直接选择排序算法、希尔排序算法、堆排序和基数排序算法。其中前两种算法经常被要求现场撕代码实现,后面也容易被问到,同时也会经常被问到分析他们各自的时间复杂度、空间复杂度以及各自使用的场景!
1. 快速排序算法:
个人的理解是,根据基数(key,一般选择每次递归调用时最左边的元素作为基数),对整个list进行划分,使得右半部分的元素都大于等于基数(j位置元素大于等于基数时,直接向前移动,不满足时进行left和right交换),左半部分的元素都小于等于基数(i位置元素小于等于基数时,直接向后移动,不满足时进行right和left的交换),最后right(j)位置上的值换成key;然后在递归调用前半部分(low,left-1)和后半部分(left+1,high). 【先j向前移动】
def quick_sort(list, left, right):
if left >= right: # 如果当前已经满足i>=j时,直接返回list结果,快排结束
return list
key = list[left] # 记录左边的为基数
low = left # 记录当前次递归的最左端元素
high = right # 记录当前次递归的最右端元素
while left < right: # 当 i<j时,执行以下比较交换,使得基数左边的元素都小于等于基数,右边的元素都大于等于基数
while left < right and list[right] >= key: # 当i<j且j位置的元素大于等于基数,则不交换,j向前移动一步
right -= 1
list[left] = list[right] # 直到不满足时,j位置的元素小于基数时,和left进行交换
while left < right and list[left] <= key: # 当i<j且i位置的元素小于等于基数,则不交换,i向后移动一步
left += 1
list[right] = list[left] # 直到不满足时,i的位置元素大于基数时,和right进行交换
list[right] = key # j位置进行基数的替换
quick_sort(list, low, left-1) ### 然后递归调用左半部分
quick_sort(list, left+1, high) ### 然后递归调用右半部分
return list
if __name__ == "__main__":
list = [3, 4, 2, 8, 9, 5, 1]
print("排序前:")
for i in list:
print(i,)
print("排序后:")
for i in (quick_sort(list, 0, len(list)-1)):
print(i,)
分析:快速排序法是一种在整个序列顺序非基本有序时,排序效果更好的一种算法!
快速排序是一种不稳定的排序算法,
最好时间复杂度:O(nlogn) ,每次划分区间的结果都是基数左右两边的序列长度相等或者相差为1的情况,即基数为中间值时。
平均时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2) ,每次划分构成某一边一个,而剩余的元素都在另一边中,(对于基本有序的序列会发生此类情况,当序列基本有序时,不推荐快速排序)
空间复杂度:快排的过程中需要一个栈空间来实现递归。空间复杂度平均为:O(logn)
基数key的选取:一般是最左边,还是有其他基数的选取方法!
2. 归并排序算法:
个人理解是:从程序的正面递归角度理解:将list序列不断进行左右两部分的减半,直到子序列的长度小于等1时,进行合并,然后往回/向上递归不断进行相应的左右两部分的合并!所以在此过程中,主函数sort(list)是进行不断减半的操作,直到长度小于等于1,然后在return部分调用merge_sort(left,right)进行左右两子部分的合并。
merge_sort(left,right)函数的功能是:进行左右两部分的合并。首先初始化左右两部分的下标索引和结果list;然后当两部分都非空时,进行逐一比较,将更小的元素放入结果中,相应累加;当某一字部分为空时,跳出while语句直接增添另一部分的元素!
def sort(left, right): # 用于比较进行合并的函数,递归到长度为1时,然后向上合并
i, j =0, 0 # 初始化下标索引i j
result = [] # 初始化合并后的结果list
while i < len(left) and j < len(right): # 当前后两个部分都还有元素时,进行依次比较
if left[i] <= right[j]: # 左边的第i个元素更小,则增添左边的第i个元素,并i累加1
result.append(left[i])
i += 1
else:
result.append(right[j]) # 否则增添右边的第j个元素,并j累加1
j += 1
# 当上面不成立,即有一部分已经合并结束之后,执行下方代码
result += left[i:]
result += right[j:]
return result
def merge_sort(list):
if len(list) <= 1: # 当递归减半后的元素长度为1时,返回list,然后进行合并。再向上递归
return list
num = len(list)//2 # 否则一直递归减半list
left = merge_sort(list[:num])
right = merge_sort(list[num:])
return sort(left, right) # 合并左右两部分
if __name__ == "__main__":
list = [2,3,4,7,6,5,1,8,9]
print("排序前:")
for i in list:
print(i,)
print("排序后:")
for i in merge_sort(list):
print(i,)
归并排序是利用递归与分治技术将数据序列划分为越来越小的半子表,再对半子表排序,最后再用递归步骤将排好序的半子表合并成越来越大的有序序列!其中的"归"代表的是递归的意思,即递归地将数组折半分离为单个数组,例如【2,6,1,0】先递归折半成【2,6】和【1,0】,然后再折半成【2】,【6】 【1】,【0】。“并”就是将分开的数据按照从小到大或者从大到小的顺序再放到一个数组中,【2】,【6】并成【2,6】,【1】,【0】并成【0,1】,然后再合并成【0,1,2,6】.
step1: 划分字表 merge_sort中主要部分,递归进行划分成半子表
step2: 合并半子表,sort中依次对两个半子表进行比较排序!
归并算法是一种稳定的排序算法,
最坏、平均、最好的时间复杂度都是O(nlogn);
空间复杂度是O(1)。
3. 冒泡排序算法
冒泡排序的整个过程就像是冒泡一样上升,单向冒泡排序的基本思想是(假设进行从小到大的排序):对于给定的n个记录,从第一个记录开始依次对相邻的两个记录进行比较,当前面记录大于后面记录时,交换位置,进行下一轮比较和交换后,n个记录中的最大记录将位于第n位;然后对前n-1个记录进行第二轮的比较;重复该过程直到进行比较记录只剩下一个时。
def bubble_sort(list):
for i in range(len(list)-1): # 外层循环计算需要进行len(list)-1趟排序
for j in range(len(list)-i-1): # 内层循环,在最后几个元素已经排好序的情况下,进行前面未排序的元素比较交换
if list[j] > list[j+1]:
list[j], list[j+1] = list[j+1], list[j] # j 和 j+1的元素进行比较
return list
if __name__ == "__main__":
list = [2,3,5,6,4,7,1,9,8]
print("排序前:")
for i in list:
print(i)
print("排序后:")
for i in bubble_sort(list):
print(i,)
"""
刚开始想的是:第二个循环采用for j in range(i+1,len(list)-1),但是在写交换比较时,会用到当层固定的list[i],不合理
这里采用到的是,在第i次循环下,比较j 和j+1的元素大小,进行比较交换,排序。
"""
冒泡排序是一种稳定的排序算法, 【改进的冒泡排序算法】
最好时间复杂度:O(n),当序列基本有序时,则适用于基本有序的序列进行排序。
平均时间复杂度:O(n^2)
最坏时间复杂度:O(n^2)
空间复杂度:O(1)
4. 插入排序算法
对一组给定的记录,初始时假设时假设第一个记录自成一个序列,其余的记录为无序序列;接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到之前的有序序列中,直至最后一个记录插入到有序序列中为止。
即:首先记录list的长度,从第2个元素到最后一个元素开始进行遍历:假设前面已经是有序序列,获取当前元素值key,并标记前面序列长度(前面需要进行比较的次数),依次往前走,遇到比key大的元素,该元素后移,插入key,直到达到第一个元素位置!
def insert_sort(list):
count = len(list)
for i in range(1,count):
key = list[i]
j = i-1
while j >=0:
if list[j] > key:
list[j+1] = list[j]
list[j] = key
j -= 1
return list
if __name__ == "__main__":
list = [5,4,6]
print('排序前:')
for i in list:
print(i)
print("排序后:")
for i in insert_sort(list):
print(i)
插入排序算法是一种稳定的排序算法
最好的时间复杂度是:O(n),当序列是基本有序时。
平均的时间复杂度是:O(n^2)
最坏的时间复杂度是:O(n^2)
空间复杂度是:O(1)
5.选择排序算法
选择排序算法是一种很直观的排序算法,基本原理:对于给定的一组记录,经过第一轮比较后得到最小的记录,然后将该记录与第一个记录进行交换;接着对不包括第一个记录之外的其他记录进行第二轮的比较,得到最小的记录并于第二个记录进行交换;重复该过程,直到进行比较的记录只有一个时为止。
def select_sort(list):
count = len(list) # 计算list的长度,便于约束后面的循环次数
for i in range(count):
min = i # 在第i次循环时,假设当前值为最小值
for j in range(i+1, count): # 将当前的值min与后面(i+1,count)元素进行逐一比较,更新min
if list[min] > list[j]: # 如果j的值更小,更新min=j
min = j
list[i],list[min] = list[min], list[i] # 将i元素和后面最小的元素进行交换
return list
if __name__ == "__main__":
list = [3,2,4,6,5,1,8,7,9]
print("排序前:")
for i in list:
print(i)
print('排序后:')
for i in select_sort(list):
print(i)
直接选择排序算法是一种不稳定的排序算法,、
最好、平均、最坏的平均时间复杂度是:O(n^2)
空间复杂度是:O(1)
6. 堆排序
堆是一种特殊的树形数据结构,其每个节点都有一个值,通常提到的堆都是指的一个完全二叉树,树根节点的值大于或者小于两个子节点的值,同时根节点的两个子树也分别是一个堆。
堆一般分为大顶堆和小顶堆两种不同的类型。对于给定n个记录的序列(r(1),r(2),r(3),……,r(n)),当且仅当满足条件(r(i)>=r(2i),i=1,2,……,n)时被称为大顶堆,此时堆顶元素必然是最大值。于给定n个记录的序列(r(1),r(2),r(3),……,r(n)),当且仅当满足条件(r(i)<=r(2i+1),i=1,2,……,n)时被称为小顶堆,此时堆顶元素必然是最小值。
堆排序的思想是:对于给定的n个记录,初始时把这些记录看成一棵顺序存储的二叉树,然后将其调整为一个大顶堆,然后将堆的最后一个元素与堆顶元素进行交换后,堆的最后一个元素就是最大记录;接着对前n-1个元素重新调整为一个大顶堆,再将堆顶元素与当前堆的最后一个元素进行交换后得到次大的记录;重复该过程直到调整的堆中只剩下一个元素时为止,该元素就是最小记录,因此这样就可以得到一个有序序列!
堆排序的过程主要有两个:一是构建堆,二是交换堆顶元素与最后一个元素进行交换!
def adjust_heap(list,i,size): # 调整堆的函数
lchild = 2*i +1
rchild = 2*i+2
maxs = i
if i < size/2:
if lchild < size and list[lchild] >list[maxs]:
maxs = lchild
if rchild < size and list[rchild] >list[maxs]:
maxs = rchild
if maxs != i:
list[maxs],list[i] = list[i],list[maxs]
adjust_heap(list,maxs,size)
def bulid_heap(list,size): # 创建堆的函数
for i in range(0,(size//2))[::-1]:
adjust_heap(list,i,size)
def heap_sort(list): # 堆排序的主函数
size = len(list)
bulid_heap(list,size) # 首选根据序列的长度,进行堆的初始建立,并调整成最大堆
for i in range(0,size)[::-1]:
list[0],list[i] = list[i],list[0] # 初始堆已经是最大堆,则进行交换!
adjust_heap(list,0,i) # 然后再调整成最大堆,再交换,再调整……
if __name__ == "__main__":
list = [2,3,6,5,4]
print('排序前:')
for i in list:
print(i)
print("排序后:")
heap_sort(list)
for i in list:
print(i)
7. 基数排序
基数排序(radix_sort)属于分配式排序,又称为桶子法,排序的过程就是将最低位优先用于单关键字的情况,对于【73,22,93,43,55,14,28,65,39,81】作为例子进行剖分:
1)根据个位数把这些数字分配到0-9的桶中
2)然后将这些桶中的数值串起来,成为以下的数列:【81,22,73,93,43,14,55,65,28,39】
3)接下来再按照十位数字进行分配桶,然后将这些桶中的数值串起来:【14,22,28,39,43,55,65,73,81,93】OK
注意:循环的次数是这些数字中的最大值的位数.
import math
def radix_sort(list,radix=10):
k = int(math.ceil(math.log(max(list),radix)))
bucket = [[] for i in range(radix)]
for i in range(1, k+1):
for j in list:
bucket[int(j/(radix**(i-1)) % (radix**i))].append(j)
del list[:]
for z in bucket:
list += z
del z[:]
return list
if __name__ == "__main__":
list = [255,35,43,11,67,83,73,56,95]
print("排序前:")
for i in list:
print(i)
print('排序后:')
for i in radix_sort(list):
print(i)
8.希尔排序 (缩小增量排序法)
总结:
1. 稳定的排序算法: 冒归基插(冒泡+归并+基数+插入) ;不稳定的排序算法: 快选堆希(快速+选择+堆+希尔排序)
2. 排序算法的复杂度与初始序列的顺序状态无关的排序算法有:一堆(堆)乌龟(归并)选(选择)基(基数)友
3.元素总比较次数与初始状态无关的有:选择+基数
4.元素总移动次数与初始状态无关的有:归并+基数
5.在初始序列基本有序时,推荐使用冒泡排序和插入排序,但快速排序强烈不建议(快排在基本有序时,时间复杂度为O(n^2))
6.堆排序常用于仅仅查找前面第k个元素
7.相对而言,归并排序占用的空间(空间复杂度)最高
8.只有有向无环图才可以进行拓扑排序,其可以被作为检测是否有环。
9.改进版本:改进的冒泡排序算法(当进行到后面不需要再进行交换元素时,表明后面已经有序了,当前次循环终止交换);折半插入排序:在寻找待插入的位置时,可选择二分查找查找新位置,而不是逐一向前进行比较!
10.堆排序的时间复杂度是O(nlogn),但初始建堆的时间复杂度是O(n),一次重建的时间复杂度是O(logn),则整体的堆重建时间复杂度是O(nlogn)。
11. 希尔排序也是直接插入排序的一种改进算法。