排序算法整理

背景

  每次找工作前都要复习一遍排序算法,索性按照自己的理解总结一份,方便后续复习。

冒泡排序

  冒泡排序相对简单,从名字就可以看出来,就是遍历数组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),空间复杂度见代码

你可能感兴趣的:(python)