每次找工作前都要复习一遍排序算法,索性按照自己的理解总结一份,方便后续复习。
冒泡排序相对简单,从名字就可以看出来,就是遍历数组n-1次,每次遍历都找出最大的,然后放到最后(冒泡),当然可以按降序排,每次找最大的数时只用在前部分乱序的数组中找,这样遍历完所有的数组即可排好序,代码如下:
def bubbleSort(data):
for i in range(len(data)-1)
for j in range(len(data)-i-1): # -i-1=-(i+1), 表示已有i+1个数被排好,每次找最大数时不需要找后面i+1个数
if data[j] > dayta[j+1]:
temp = data[j]
data[j] = data[j+1]
data[j+1] = temp
return data
总结:时间复杂度O(n2), 空间复杂度O(1)
可以有一种改进方式,当某次寻找最大数时,数组元素位置为发生变化,即if条件未进入,说明数组已经有序,则可直接退出并返回数组。这种条件下,当传入数组为有序,则时间复杂度为O(n)。
选择排序和冒泡排序有点像,而选择排序是每次遍历时选择最小值,然后放到最左边,在升序的情况下,刚开始默认最左边为最小值(i=0),则在data[i+1:](即剩下数字)中找最小,遍历所有数组后即完成,整个过程无论初始数组是否有序,都需要完整的执行完全过程,其时间复杂度不受初始数组的影响。
def selectSort(data):
for i in range(len(data)-1):
min_index = i
for j in range(i+1, len(data)-1):
if data[j] < data[min_index]:
min_index = j
temp = data[i]
data[i] = data[min_index]
data[min_index] = temp
return data
总结:时间复杂度O(n2), 空间复杂度O(1)
插入排序,也和名字一样,将数组分为两部分,有序的一部分,乱序的一部分,最开始时默认第一个元素为一部分,因只有一个元素则为有序,然后在乱序的部分中找出一个数对比有序部分的元素,插入相应的位置。
举例:
数组:[4, 2, 3, 6, 1, 5]
第一步:认为[4]为有序部分,[ 2, 3, 6, 1, 5]为乱序部分
第二步:选出乱序的一个数:2
第三步:与有序部分相比,应插入4的前面:[2, 4]
第四步:继续找乱序的一个数:3
第五步:与有序部分相比,比4小,将4向后移,比2大,则插入至原4的位置
……
第n步:完成排列[1, 2, 3, 4, 5, 6]
def insertSort(data):
for i in range(len(data)-1):
current_num = data[i+1]
sorted_index = i # i及之前的数都为有序
# 将在乱序部分取出的current_num逐一与有序的的元素对比
while sorted_index >= 0 and current_num < data[sorted_index]:
data[sorted_index+1] = data[sorted_index] # 后移
sorted_index -= 1
# 退出while循环,表示找到位置,因多减一次,此时sorted_index指向的是需要插入位置的前一个位置
data[sorted_index+1] = current_num
return data
总结:时间复杂度O(n2),空间复杂度O(1)
插入排序是数据越有序效率越高。
希尔排序是针对插入排序的进化,插入排序在核心在于越有序效率越高,因此希尔排序的思想是优先将距离远的数据排到一起,否则需要一个一个比较很耗时,远距离的比较完最后在逐个比较,其中设置间隔(gap),先比较相隔gap的数据,gap不断减小至1,也就是回到最初的插入排序,最终完成排序。
def shellSort(data):
gap = 1
while gap < len(data) // 3: # 设置gap值,也可指定,并传入,3是随便给的,表示把原数据分成3段,保证gap不小于某一段的长度,举例见下
gap = gap * 3 + 1
while gap > 0:
for i in range(gap, len(data)-1):
current_num = data[i]
sorted_index = i - gap
while sorted_index >= 0 and current_num < data[sorted_index]:
data[sorted_index+gap] = data[sorted_index]
sorted_index -= gap
data[sorted_index+gap] = current_num
gap //= 3
return data
举例:
数组:[4, 2, 3, 6, 1, 5], len(data) = 6, gap = gap * 3 +1 = 4
gap = 4:数据为黑体和斜体部分:[4, 2, 3, 6, 1, 5],也就是[4, 1]一组,[2, 5]一组,优先分别对这两组数据排序,时间复杂度为O(2+1+2),第一个2表示遍历到“4”和“1”两个数,1表示把“1”往前移一个gap,第二个2表示遍历“2”和“5”;
gap = 1:回归插入排序,此时数据经过第一个gap的排序,变成[1, 2, 3, 6, 4, 5], 此时再使用插入排序,前三个已经是升序,只用排后三个,时间复杂度为O(6+3);
针对本例,总体时间复杂度为O(2+1+2+6+3),若仅使用插入排序时间复杂度为O(6+1+1+4+1),此例不足以说明希尔排序的有点,主要针对顺序较乱,效果越明显。
但是希尔排序有个缺点就是不具备稳定性,例子可参考
总结:时间复杂度:不好评估,与gap大小强相关,空间复杂度O(1)
归并排序即是分而治之的思想,将数组拆分为多个子数组,对子数组两两有序合并。
def mergeSort(nums):
# 归并过程
def merge(left, right): # 负责合并
result = [] # 保存归并后的结果
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result = result + left[i:] + right[j:] # 剩余的元素直接添加到末尾
return result
# 递归过程
if len(nums) <= 1:
return nums
mid = len(nums) // 2
# 分组拆分
left = mergeSort(nums[:mid])
right = mergeSort(nums[mid:])
return merge(left, right)
总结:时间复杂度始终O(nlogn),空间复杂度O(n)
快速排序也是分而治之思想。它是处理大数据最快的排序算法之一,虽然 Worst Case 的时间复杂度达到了 O(n²),但是在大多数情况下都比平均时间复杂度为 O(n log n) 的排序算法表现要更好,因为 O(n log n) 记号中隐含的常数因子很小,而且快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快,比复杂度稳定等于 O(n log n) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。参考
def quickSort(nums): # 这种写法的平均空间复杂度为 O(nlogn),即每个数字都要被存储logn次
if len(nums) <= 1:
return nums
pivot = nums[0] # 基准值
left = [nums[i] for i in range(1, len(nums)) if nums[i] < pivot]
right = [nums[i] for i in range(1, len(nums)) if nums[i] >= pivot]
return quickSort(left) + [pivot] + quickSort(right)
'''
@param nums: 待排序数组
@param left: 数组上界
@param right: 数组下界
'''
def quickSort2(nums, left, right): # 这种写法的平均空间复杂度为 O(logn),即只用存储每次分开的pivot
# 分区操作
def partition(nums, left, right):
pivot = nums[left] # 基准值
while left < right:
while left < right and nums[right] >= pivot:
right -= 1
nums[left] = nums[right] # 比基准小的交换到前面
while left < right and nums[left] <= pivot:
left += 1
nums[right] = nums[left] # 比基准大交换到后面
nums[left] = pivot # 基准值的正确位置,也可以为 nums[right] = pivot
return left # 返回基准值的索引,也可以为 return right
# 递归操作
if left < right:
pivotIndex = partition(nums, left, right)
quickSort2(nums, left, pivotIndex - 1) # 左序列
quickSort2(nums, pivotIndex + 1, right) # 右序列
return nums
总结:时间复杂度O(nlogn),空间复杂度见代码