常用排序算法总结

191 阅读3分钟

参考

1 几种简单的排序

冒泡

  • 时间复杂度: O(n2)
  • 空间复杂度: O(1)
  • 稳定性:稳定。同样的值,左边不可能被移动到右边。
  • 特点:因为冒泡排序必须要在最终位置找到之前不断交换数据项,所以它经常被认为是最低效的排序方法。但它可以在发现列表已排好时立刻结束,因此如果列表只需要几次遍历就可排好,冒泡排序就占有优势。
# 经典冒泡排序
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        while True:
            re = False	# 是否replace
            for i in range(len(nums)-1):	# 对于它右边仍有元素的位置
                if nums[i] > nums[i+1]:
                    nums[i], nums[i+1] = nums[i+1], nums[i]
                    re = True
            if not re:
                break
        return nums

选择

  • 时间复杂度: O(n2)
  • 空间复杂度: O(1)
  • 稳定性:稳定。同样的值,靠左的先被选择。 选择排序提高了冒泡排序的性能,它每遍历一次列表只交换一次数据,一次到位。不过由于每次遍历只关注“最小值”,不能像冒泡排序一样及时发现列表已经排好。

插入

  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)
  • 稳定性:如果将后处理的值排到后边则有序(子表从后向前扫描)。
  • 特点:总是保持一个位置靠前的 已排好的子表,然后每一个新的数据项被 “插入” 到前边的子表里,排好的子表增加一项。
做法1:定位+列表拼接:注意边界条件!
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        for i in range(1, len(nums)):   # 未排序序列
            target = nums[i]
            for j in range(i-1, -1, -1): 
            # 从后往前扫,找到比自己小的就插入在它后面
                if nums[j] <= target:
                    nums[j+1:i+1] = [target] + nums[j+1:i]
                    break
            else: # 如果一直没有找到比自己小的,插入头部
                nums[0:i+1] = [target] + nums[0:i]
        return nums

拼接list这种操作也就在python里写的比较简单,其他语言就悲催了。而且还要特别考虑插入头部的情况。看来这种做法有待改进呀。

更好的做法2:反向冒泡
def insertionSort(alist):
    for i in range(1,len(alist)):	# 从1号元素开始排序
        currentvalue=alist[i]
        position=i
        # 相当于只针对currentvalue一个值的从右向左冒泡。
        # 区别在于,放到合适位置后不走完全程,因为该位置的左边已经有序
        while alist[position-1]>currentvalue and position>0:
        # 只要左边存在且比currentvalue大,左边就向右挪
            alist[position]=alist[position-1]
            position=position-1
        # 翻页到头了,或者不再比currentvalue大了,就填入currentvalue
        alist[position]=currentvalue
    return alist

这种反向冒泡的优势在于,如果数组本来有序,只需要 O(n) 一次扫描即可!

2 分治思想

快排

  • 时间复杂度:O(nlogn).最差情况下 O(n2)
  • 空间复杂度:O(log n) 注意这是栈空间的层数。而非复制了这么多份数组。
  • 稳定性:不稳定。大跨度交换很不靠谱。
  • 特点:当数列近乎有序时,退化为O(n2)的算法。
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def work(left, right):  # 左开右闭的目标排序区间
            if right - left <= 1: return nums
            pivot = left  # 使用第一个数当pivot的位置
            c1 = pivot+1  # 下一个可以放置较小数字的位置
            # c2往前冲,有小的就扔给c1
            for c2 in range(pivot+1, right):
                if nums[c2] < nums[pivot]:
                    nums[c1], nums[c2] = nums[c2], nums[c1]
                    c1 += 1
            # c1-1 = pivot或最后一个较小数字的位置。交换pivot和c1-1。 这种指针题交换的时候要特别注意,必须在边界情况也正常work
            nums[pivot], nums[c1-1] = nums[c1-1], nums[pivot]
            work(left,c1-1)
            work(c1,right)
        
        work(0, len(nums)) 
        return nums

这个代码是单路快排。 除此以外还有:

  • 双路快排(一个往左一个往右)
def quickSort(arr, left, right):    # 闭区间。
    if right-left < 1: return   # 一个元素一下,已经有序
    pivot = left
    cur1,cur2 = left+1,right    # 小于cur1的已经排好(小于pivot),大于cur2的已经排好(大于pivot)
    
    while cur1 <= cur2:  # 这题常见里面写俩小while,一while多步。但我嫌判断越界麻烦,就写成一while一步了
        if arr[cur1] <= arr[pivot]:     # 正常情况
            cur1 += 1
            continue
        if arr[cur2] > arr[pivot]:     # 正常情况
            cur2 -= 1
            continue
        arr[cur1],arr[cur2] = arr[cur2],arr[cur1] ·# 两个都不正常
    # break: cur2 < cur1. cur2的位置是新的中点
    arr[pivot], arr[cur2] = arr[cur2], arr[pivot]
    quickSort(arr, left, cur2-1)
    quickSort(arr, cur2+1, right)
  • 三路快排(荷兰国旗问题)适用于重复元素多的时候
def quickSort(arr, left, right):  # 闭区间。
    if right-left < 1: return   # 一个元素以下,已经有序
    pivot = arr[left]
    cur0,cur1,cur2 = left,left,right
    # 设定:
    # 小于cur0的已经排好(小于pivot)
    # 小于cur1的已经排好(小于等于pivot)
    # 大于cur2的已经排好(大于pivot)
    
    while cur1 <= cur2:  
        if arr[cur1] < pivot: 
            arr[cur0],arr[cur1] = arr[cur1], arr[cur0]
            cur0 += 1
            continue
        if arr[cur1] == pivot:    
            cur1 += 1
            continue
        else:
            arr[cur1],arr[cur2] = arr[cur2],arr[cur1] # 两个都不正常
            cur2 -= 1
            
    quickSort(arr, left, cur0-1)
    quickSort(arr, cur2+1, right)

归并

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n) 需要O(n)的额外空间来临时存放归并结果。归并如果用递归做,也有O(log n)在栈上的的空间复杂度。但O(log n)< O(n)忽略不计。
  • 稳定性:不稳定。举个例子就行了。
    • [3左,3左,3左,3左] + [3左,3左,3左,3左]
    • [3左,3右,3左,3右,3左,3右,3左,3右]

递归肯定是好写的,所以这里复制粘贴一份自底向上的方法叭~

# 自底向上的归并算法
def mergeBU(alist):
    n = len(alist)
    size = 1
    while size <= n:
        for i in range(0, n-size, size+size):	
        # 这样每个归并块一定有两个小块。不够的话说明已经排好序不用管了~
       	    merge(alist, i, i+size-1, min(i+size+size-1, n-1))
        size += size
    return alist

def merge(alist, start, mid, end):
    blist = alist[start:end+1]  # 复制一份. blist是辅助空间。最终的排序结果放进alist中。
    l = start
    k = mid + 1
    pos = start
    
    while pos <= end:
        if (l > mid):
            alist[pos] = blist[k-start]
            k += 1
        elif (k > end):
            alist[pos] = blist[l-start]
            l += 1
        elif blist[l-start] <= blist[k-start]:
            alist[pos] = blist[l-start]
            l += 1
        else:
            alist[pos] = blist[k-start]
            k += 1
        pos += 1

3 利用数据结构

堆排序

  • 时间复杂度:O(nlogn)
  • 空间复杂度:原地操作当然是O(1)啦
  • 稳定性: 肯定不稳定啊~想想所有值都相等的情况
  • 完全二叉树:除最后一层节点外,其他层节点都有两个子节点,并且最后一层节点都要左排列。(满二叉树勾掉最右边几个叶子)在树的顺序存储法中不浪费存储空间,故得名完全二叉树。
菜狗写法
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def heaplify(i):
            # 以i处为堆顶,调整小顶堆
            left,right = 2*i +1, 2*i+2
            if left < len(nums) and nums[left]<nums[i]:   # len(num)可能会发生改变
                nums[left],nums[i] = nums[i],nums[left]
                heaplify(left)
            if right < len(nums) and nums[right]<nums[i]:
                nums[right],nums[i]=nums[i],nums[right]
                heaplify(right)

        for i in range( len(nums)//2 - 1, -1, -1):
            heaplify(i)

        ans = []
        while nums:
            ans.append(nums[0]) 
            nums[0] = nums[-1]
            nums.pop()
            if nums:
                heaplify(0)
        return ans
  • 改进点1:heaplify里直接和最小的交换就可以了。目前的写法可能出现两次交换,增大计算量。
  • 改进点2:可以使用大顶堆 + 从后向前排序。不使用额外空间!
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        def heaplify(i, size): # 更改后的大顶堆
            left,right = 2*i +1, 2*i+2
            j = i
            if left < size and nums[left] >nums[i]:    j = left
            if right < size and nums[right] >nums[j]:  j = right
            if j==i: return
            else: 
                nums[i], nums[j] = nums[j], nums[i]
                heaplify(j, size)	# 顶多只有一个递归分支
        
        for i in range( len(nums)//2 - 1, -1, -1):
            heaplify(i, len(nums))
            
        for i in range(len(nums)-1, 0, -1):   # start, end, step
            #i: 下一个要排序的地方
            nums[i], nums[0] = nums[0], nums[i]
            heaplify(0, i)
            print('{}{}'.format(nums[0:i], nums[i:]))
        
        return nums
        
Inputs:
[0,1,2,3,5,6,7,8,9]
Stdout:
[8, 5, 7, 2, 0, 3, 6, 1][9]
[7, 5, 6, 2, 0, 3, 1][8, 9]
[6, 5, 3, 2, 0, 1][7, 8, 9]
[5, 2, 3, 1, 0][6, 7, 8, 9]
[3, 2, 0, 1][5, 6, 7, 8, 9]
[2, 1, 0][3, 5, 6, 7, 8, 9]
[1, 0][2, 3, 5, 6, 7, 8, 9]
[0][1, 2, 3, 5, 6, 7, 8, 9]

其他资料