数据结构与算法——十大排序算法

一、冒泡排序

排序过程:

  • 列表每两个相邻的数,如果前者大于后者,则交换这两个数;遍历列表,完成一趟排序
  • 继续从头遍历,重复上述过程,直到没有发生交换为止
    在这里插入图片描述
def BubbleSort(a):
    if len(a) == 0:
        return None
    
    for i in range(len(a) - 1):
        exchange = False    # 标志位,第i趟如果没有发生交换,则排序已经完成,不需要再进行后面的冒泡
        for j in range(len(a) - i - 1):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                exchange = True
        if exchange is False:    # 没有发生交换,返回
            return

复杂度分析:

  • 最好情况: O ( n ) O(n) O(n)
  • 最坏情况: O ( n 2 ) O(n^2) O(n2)
  • 平均情况: O ( n 2 ) O(n^2) O(n2)

二、选择排序

排序过程:

  • 一趟遍历记录最小的数,放到一个位置
  • 再遍历一趟,记录无需去最小的数,放到有序区的第二个位置
  • 重复以上过程,直到列表结束

在这里插入图片描述

def SelectionSort(a):
    if len(a) == 0:
        return None
    
    for i in range(len(a) - 1):
        min_index = i    # 记录无序区最小数的位置
        for j in range(i, len(a)):
            if a[j] < a[min_index]:
                min_index = j
        if min_index != i:
            a[i], a[min_index] = a[min_index], a[i]

复杂度分析:

  • 最好情况: O ( n 2 ) O(n^2) O(n2)
  • 最坏情况: O ( n 2 ) O(n^2) O(n2)
  • 平均情况: O ( n 2 ) O(n^2) O(n2)

三、插入排序

排序过程:

  • 步骤1:从第一个元素 a [ i ] , i = 0 a[i], i=0 a[i],i=0开始,该元素为有序区
  • 步骤2:取下一个元素 a [ i + 1 ] a[i+1] a[i+1],在有序区的元素序列中从后向前扫描
  • 步骤3:如果有序区元素 a [ j ] a[j] a[j]大于新元素 a [ i + 1 ] a[i+1] a[i+1],将该元素移到下一位置 a [ j − 1 ] a[j-1] a[j1]
  • 步骤4:重复步骤3,直到找到有序区的元素小于或者等于新元素的位置;
  • 步骤5:将新元素 a [ i + 1 ] a[i+1] a[i+1]插入到该位置后;
  • 步骤6:重复步骤2~5

在这里插入图片描述

def InsertionSort(a):
    if len(a) == 0:
        return None
    
    for i in range(1, len(a)):
        temp = a[i]    # 取无序区的第一个数
        j = i - 1      # 有序区的倒数第一个数索引为i-1
        while j >= 0 and temp < a[j]:    # 遍历有序区
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = temp    # 找到temp的位置

复杂度分析:

  • 最好情况: O ( n ) O(n) O(n)
  • 最坏情况: O ( n 2 ) O(n^2) O(n2)
  • 平均情况: O ( n 2 ) O(n^2) O(n2)

四、快速排序

排序过程:

  • 取一个元素(一般为第一个元素)P,使元素归位
  • 归位操作后的列表被P分为两部分,P左边的元素都比P小,右边的都比P大
  • 递归地把小于P的子数列和大于P的子数列排序

在这里插入图片描述

def partition(a, left, right):
	if len(a) == 0:
        return None
        
    temp = a[left]
    while left < right:
        # 从右边找比temp小的数
        while left < right and a[right] >= temp:
            right -= 1
        a[left] = a[right]
        # 从左边找比temp大的数
        while left < right and a[left] <= temp:
            left += 1
        a[right] = a[left]
    # 找到了temp的位置
    a[left] = temp
    # 返回temp1的位置, return right也可以,因为left和right重合
    return left

def QuickSort(a, left, right):
    if left < right:
        # 选取a的第一个元素P,归位
        mid = partition(a, left, right)
        # 递归P的左边
        QuickSort(a, left, mid - 1)
        # 递归P的右边
        QuickSort(a, mid + 1, right)

复杂度分析:

  • 最佳情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 最差情况: O ( n 2 ) O(n^2) O(n2)
  • 平均情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)

五、堆排序

是一种特殊的完全二叉树。大根堆: 完全二叉树的任一节点都比其孩子节点大。小根堆: 完全二叉树的任一节点都比其孩子节点小。

在这里插入图片描述

堆的向下调整: 假设根节点的左右子树都是堆,但根节点不满足堆的性质,可以通过一次向下调整将其变成一个堆。

排序过程:

  • 步骤1:建立大根堆
  • 步骤2:得到对顶元素,为最大元素
  • 步骤3:堆顶元素和堆的最后一个元素交换,交换后可以通过一次调整使堆有序(不包括刚才及之前被换到堆后面的元素)
  • 步骤4:堆顶元素为第二大元素,重复步骤3,直到堆变空

在这里插入图片描述

def adjustHeap(a, low, high):
    i = low        # 指向根节点
    j = 2 * i + 1  # 指向根节点的左孩子
    temp = a[i]
    while j <= high:
        # 如果有右孩子,并且右孩子比左孩子大
        if j + 1 <= high and a[j+1] > a[j]:
            j += 1    # 指向右孩子
        
        # 如果子节点比temp大,交换a[i]和a[j]
        if a[j] > temp:
            a[i] = a[j]
            i = j
            j = 2 * i + 1
        else:
            break
    a[i] = temp

def HeapSort(a):
	if len(a) == 0:
        return None
        
    n = len(a)
    # 从底向上地建立大根堆,从最后一个非叶子节点开始,即(n-2)//2
    for i in range((n - 2) // 2, -1, -1):
        adjustHeap(a, i, n-1)
    # 从最后一个元素开始,与堆顶调换
    for i in range(n-1, -1, -1):
        a[0], a[i] = a[i], a[0]    # 堆顶与a[i]调换
        adjustHeap(a, 0, i-1)

复杂度分析:

  • 最佳情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 最差情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 平均情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)

python里堆的内置模块:heapq

# 堆的内置模块
import heapq

a = list(range(100))
random.shuffle(a)

heapq.heapify(a)    # 建堆
# 依次出数
for i in range(len(a)):
    print(heapq.heappop(a), end=',')

top-k问题: 有n个数,取前k大的数(k<n)

  • 取列表前k个元素建立一个小根堆
  • 从第k+1个开始依次向后遍历原列表,如果小于堆顶,则跳过改元素;如果大于堆顶,则将该元素放到堆顶,进行一次调整
  • 遍历结束后,倒序弹出堆顶
# 调整小根堆
def adjustHeap(a, low, high):
    i = low        # 指向根节点
    j = 2 * i + 1  # 指向根节点的左孩子
    temp = a[i]
    while j <= high:
        # 如果有右孩子,并且右孩子比左孩子小
        if j + 1 <= high and a[j+1] < a[j]:
            j += 1    # 指向右孩子
        
        # 如果子节点比temp小,交换a[i]和a[j]
        if a[j] < temp:
            a[i] = a[j]
            i = j
            j = 2 * i + 1
        else:
            break
    a[i] = temp

def top_k(a, k):
	if len(a) == 0:
        return None
        
    heap = a[:k]
    # 建堆
    for i in range((k - 2) // 2, -1, -1):
        adjustHeap(heap, i, k-1)
    # 遍历
    for i in range(k, len(a) - 1):
        if a[i] > heap[0]:
            heap[0] = a[i]
            adjustHeap(heap, 0, k-1)
    # 出数
    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        adjustHeap(heap, 0, i - 1)
    return heap

六、归并排序

归并: 将两段有序列表合并成一个有序列表

排序过程:

  • 把长度为n的输入列表分为长度为n//2的子序列
  • 对这两个子序列分别采用归并排序
  • 将两个排序好的子序列合并成一个最终的排序序列
    在这里插入图片描述
    在这里插入图片描述
def merge(a, low, mid, high):
    # a的左右半边子序列已为有序
    i, j = low, mid + 1    
    temp = []
    # 依次比较
    while i <= mid and j <= high:    # 只要两个子序列还有数
        if a[i] < a[j]:
            temp.append(a[i])
            i += 1
        else:
            temp.append(a[j])
            j += 1
    # 上面循环结束后至少有一个子序列已被完全遍历
    # 下面将可能剩下的部分添加到temp
    temp.extend(a[i:mid+1])
    temp.extend(a[j:high+1])
    #print(len(a[i:mid+1]), len(a[j:high]), len(a[low:high+1]), len(temp))
    # 替换原数组
    a[low:high+1] = temp

def MergeSort(a, low, high):
	if len(a) == 0:
        return None
        
    if low < high:
        # low为a的最左边,high为a的最右边,mid为左半边子序列的最右边
        mid = (low + high) // 2
        # 归并左半边子序列
        MergeSort(a, low, mid)
        # 归并右半边子序列
        MergeSort(a, mid+1, high)
        # 合并两个有序子序列
        merge(a, low, mid, high)

复杂度分析:

  • 最佳情况: O ( n ) O(n) O(n)
  • 最差情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 平均情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)

七、希尔排序

希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破 O ( n 2 ) O(n^2) O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序。

排序过程:

  • 首先取一个整数 d 1 = n / / 2 d_1= n // 2 d1=n//2,将列表分为 d 1 d_1 d1个组,每组相邻元素之间距离为 d 1 d_1 d1,在每组里进行插入排序
  • 去第二个整数 d 2 = d 1 / / 2 d_2=d_1 // 2 d2=d1//2,重复上述分组排序过程,直到 d i = 1 d_i=1 di=1,即所有元素都在同一组内插入排序
  • 希尔排序每趟使列表整体越来越接近有序
    在这里插入图片描述
def InsertSortWithGap(a, gap):
    if len(a) == 0:
        return None
    
    for i in range(1, len(a)):
        temp = a[i]    # 取无序区的第一个数
        j = i - gap      # 有序区的倒数第一个数索引为i-gap
        while j >= 0 and temp < a[j]:    # 遍历有序区
            a[j + gap] = a[j]
            j -= gap
        a[j + gap] = temp    # 找到temp的位置

def ShellSort(a):
	if len(a) == 0:
        return None
        
    d = len(a) // 2
    while d >= 1:
        InsertSortWithGap(a, d)
        d //= 2

复杂度分析: 希尔排序的复杂度分析是个难题,根据选取的分组整数序列 d 1 , d 2 , ⋯   , d i d_1, d_2, \cdots, d_i d1,d2,,di的不同,其时间复杂度也不同,有的复杂度由于数学上的难题仍然没有定论。对于我们介绍的一直除2的这种方式:

  • 最佳情况: O ( n log ⁡ 2 n ) O(n \log ^2 n) O(nlog2n)
  • 最坏情况: O ( n log ⁡ 2 n ) O(n \log ^2 n) O(nlog2n)
  • 平均情况: O ( n log ⁡ n ) O(n \log n) O(nlogn)

八、计数排序

基数排序要求已知列表中数的范围在0到 k k k之间

排序过程:

  • 步骤1:找出待排序的列表中最大和最小的元素;
  • 步骤2:统计数组中每个值为 i i i的元素出现的次数,存入数组 C C C的第 i i i项;
  • 步骤3:遍历列表,对所有元素的计数累加;
  • 步骤4:填充目标列表:将每个元素 i i i放在新列表的第 C ( i ) C(i) C(i)项,每放一个元素就将 C ( i ) C(i) C(i)减去1。

在这里插入图片描述

def CountingSort(a):
    if len(a) == 0:
        return None
    
    # 找最大最小值
    min_, max_ = a[0], a[0]
    for num in a:
        if num > max_:
            max_ = num
        if num < min_:
            min_ = num
    # 初始化计数数组
    C = [0] * (max_ - min_ + 1)
    # 遍历数组
    for num in a:
        C[num] += 1
    # 填充原数组
    a.clear()
    for i, c in enumerate(C):
        for j in range(c):
            a.append(i)

复杂度分析:

  • 最佳情况: O ( n + k ) O(n+k) O(n+k)
  • 最差情况: O ( n + k ) O(n+k) O(n+k)
  • 平均情况: O ( n + k ) O(n+k) O(n+k)

九、桶排序

假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序

排序过程:

  • 步骤1:人为设置一个bins,即有多少个桶。比如 [ 0 , 1 , ⋯   , 9 , 10 ] [0,1, \cdots, 9, 10] [0,1,,9,10]这个列表,bins=5,那么第一个桶放0、1,第二个桶放2、3,第三个桶放4、5,依次类推;
  • 步骤2:遍历列表,并且把元素一个一个放到对应的桶里去;
  • 步骤3:对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
  • 步骤4:从不是空的桶里把排好序的数据拼接起来。
    在这里插入图片描述
def BucketSort(a, bins):
	if len(a) == 0:
        return None
        
    # 初始化桶
    buckets = [[] for _ in range(bins)]
    # 找到最大值最小值
    min_, max_ = a[0], a[0]
    for num in a:
        if num > max_:
            max_ = num
        if num < min_:
            min_ = num
    # 遍历
    for num in a:
        i = min(num // (max_ // bins), bins-1)    # 第几号桶
        buckets[i].append(num)
        # 保持桶内顺序
        for j in range(len(buckets[i]) - 1, 0,-1):
            if buckets[i][j] < buckets[i][j - 1]:    # 桶内下小上大
                buckets[i][j], buckets[i][j - 1] = buckets[i][j - 1], buckets[i][j]
            else:
                break
    # 从桶内拿出
    sorted_a = []
    for bucket in buckets:
        sorted_a.extend(bucket)
    return sorted_a

复杂度分析:

  • 最佳情况: O ( n + k ) O(n+k) O(n+k)
  • 最差情况: O ( n 2 ) O(n^2) O(n2)
  • 平均情况: O ( n + k ) O(n+k) O(n+k)

十、基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

排序过程:

  • 步骤1:取得数组中的最大数,并取得位数;
  • 步骤2:取列表中元素的最低位,按最低位对元素进行分桶(10个桶,0-9);
  • 步骤3:按桶的顺序还原列表,然后取元素的导数第二位,对元素再次分桶;
  • 步骤4:重复上述分桶、还原的过程,直到最大元素的最高位

在这里插入图片描述

def RadixSort(a):
    if len(a) == 0:
        return None
    
    max_ = max(a)
    it = 0
    while 10 ** it <= max_:
        # 按倒数第it位分桶
        buckets = [[] for _ in range(10)]
        for num in a:
            digit = (num // 10 ** it) % 10    # 取倒数第it位数
            buckets[digit].append(num)
        it += 1
        # 出桶
        a.clear()
        for bucket in buckets:
            a.extend(bucket)

复杂度分析:

  • 最佳情况: O ( n × k ) O(n \times k) O(n×k)
  • 最差情况: O ( n × k ) O(n \times k) O(n×k)
  • 平均情况: O ( n × k ) O(n \times k) O(n×k)

十一、总结

排序算法平均时间复杂度最好情况最坏情况空间复杂度排序方式稳定
冒泡排序 O ( n 2 ) O(n^2) O(n2)O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)in-place稳定
选择排序 O ( n 2 ) O(n^2) O(n2)O(n^2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)in-place不稳定
插入排序 O ( n 2 ) O(n^2) O(n2)O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)in-place稳定
快速排序 O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( log ⁡ n ) O(\log n) O(logn)in-place不稳定
堆排序 O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( 1 ) O(1) O(1)in-place不稳定
归并排序 O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n ) O(n) O(n)out-place稳定
希尔排序 O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n) O ( n log ⁡ 2 n ) O(n \log^2 n) O(nlog2n) O ( 1 ) O(1) O(1)in-place不稳定
计数排序 O ( n + k ) O(n+k) O(n+k)O(n+k) O ( n + k ) O(n+k) O(n+k) O ( k ) O(k) O(k)out-place稳定
桶排序 O ( n + k ) O(n+k) O(n+k)O(n+k) O ( n 2 ) O(n^2) O(n2) O ( n + k ) O(n+k) O(n+k)out-place稳定
基数排序 O ( n × k ) O(n \times k) O(n×k) O ( n × k ) O(n \times k) O(n×k) O ( n × k ) O(n \times k) O(n×k) O ( n + k ) O(n+k) O(n+k)out-place稳定

上表中,in-place表示占用常数内存,不占额外内存(递归内存除外),out-place表示占用额外内存; k k k为桶的个数;稳定表示如果a原本在b前面且a==b,排序后a仍然在b前面,不稳定则是a可能出现在b后面。

参考文献

[1] https://blog.csdn.net/weixin_41190227/article/details/86600821
[2] https://edu.csdn.net/course/detail/24449

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值