十大排序算法

详细算法过程看注释,结合代码看,更清楚@_@.

1. 选择排序

  • 原理: 每次遍历都找出最小(大)的数字放在起始位置,再从剩余的数组中继续遍历,重复找最小(大)值的过程,直到遍历结束。
  • 时间复杂度: 选择排序的最好、最坏、平均情况下,其时间复杂度都是:O( n 2 n^2 n2)
  • 空间复杂度: O(1)
  • 稳定性分析: 一般的选择排序算法不是稳定性排序算法。这里再解释一下稳定性排序是指:2个相等的元素,在排序前的相对前后位置和排序完成后的,相对前后位置保持一致。
  • 选择排序为啥不是稳定性排序呢,举个例子:数组 5、6、5、1、7,在对其进行第一遍循环的时候,会将第一个位置的5与后面的1进行交换。此时,就已经将两个5的相对前后位置改变了。因此选择排序不是稳定性排序算法。
  • 有没有稳定的选择排序算法呢?
def select_sort(nums):
    n = len(nums)
    # 外循环只用循环n-1次,最后两位数,取出较小值,剩下的就为最大值,不用再比较
    for i in range(n-1):
        # 每次循环最小值索引的位置
        index = i
        # 内循环遍历得到i~n-1(数组索引为0~n-1)的最小值,并记录最小值的索引位置index,0~i-1为已经排好序的数组
        for j in range(i, n):
            if nums[j] < nums[index]:
                index = j
        # 内循环结束
        # 交换最小值和当前值i的位置的值
        nums[i], nums[index] = nums[index], nums[i]
    return nums
  • 通过改变一下写法,就可以将选择排序变为稳定的算法,形式类似于接下来的冒泡算法。
def select_sort1(nums):
    n = len(nums)
    # 外循环只用循环n-1次,最后两位数,取出较小值,剩下的就为最大值,不用再比较
    for i in range(n - 1):
        # 内循环遍历得到i~n-1(数组索引为0~n-1)的最小值,并将最小值的位置的与i的位置互相交换,0~i-1为已经排好序的数组
        for j in range(i, n):
            if nums[i] > nums[j]:
                nums[i], nums[j] = nums[j], nums[i]
        # 内循环结束,0~i为排好序的数组
    return nums

2. 冒泡排序

  • 原理: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。每次遍历完成后,会把最大的元素放到最后。再从剩余的数组中继续遍历,重复冒泡的过程,直到遍历结束。
  • 时间复杂度: 最好情况为O(n),此时为有序数组。最坏、平均情况下,其时间复杂度都是:O( n 2 n^2 n2)
  • 空间复杂度: O(1)
  • 稳定性分析: 稳定
def bubble_sort(nums):
    n = len(nums)
    # 用来判断是否产生了排序动作
    flag = False
    # 外循环只用循环n-1次,最后两位数,相互比较交换后,就已经排好序了,最后一位数不用再比较
    for i in range(n - 1):
        # 内循环,每次循环将从0~n-i-1(减1是因为两两比较,需要多留一位数,防止数组溢出)进行前后比较,将大的数往后移
        # 每次循环结束,0~n-i最大的数冒泡到了最后的位置
        for j in range(n - i - 1):
            # 前后比较,将较大的数往后移
            if nums[j] > nums[j + 1]:
                flag = True
                nums[j], nums[j + 1] = nums[j + 1], nums[j]
        # 内循环结束,此时n-i~n-1为有序数组
        # 若没有产生排序动作,说明此时数组已经有序,可以提前中止。若一开始数组就为有序数组,此时时间复杂度就为o(n)-最好情况
        if not flag:
            break
        flag = False
    return nums

3. 插入排序

  • 原理: 排序的思想就是维护一个有序的部分,将无序部分的数据按照顺序插入到有序部分。
  • 时间复杂度: 最好情况为O(n),此时为有序数组。最坏、平均情况下,其时间复杂度都是:O( n 2 n^2 n2)
  • 空间复杂度: O(1)
  • 稳定性分析: 稳定
def insert_sort(nums):
    n = len(nums)
    # 外循环只用循环n-1次,从第二位数开始,第一位数不用比较
    for i in range(1, n):
        # 内循环,每次循环将从i~0(遍历到0,不取到0,是因为两两比较,需要多留一位数,防止数组溢出)进行前后比较,将较小的数往前移
        for j in range(i, 0, -1):
            # 前后比较,将较小的数往前移
            if nums[j] < nums[j-1]:
                nums[j], nums[j-1] = nums[j-1], nums[j]
            # 说明此时被排序的数已经找到自己的位置,可以提前中止。
            else:
                break
            # 内循环结束,此时0~i为有序数组
    return nums

4. 快速排序

  • 原理: 快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。
    1. 挑选基准值:从数列中挑出一个元素,称为"基准"(pivot);
    2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成;
    3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。
    4. 递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。
  • 时间复杂度: 最坏情况为O( n 2 n^2 n2),此时为刚好为倒序数组。最好、平均情况下,其时间复杂度都是:O( n l o g 2 n nlog_2n nlog2n)
  • 空间复杂度: O(1)
  • 稳定性分析: 不稳定
def quick_sort(nums, low, high):
    # 数组中只有一位或者空数组时,直接返回
    if low >= high:
        return nums
    l_index = low
    r_index = high
    # 取最后一个元素为枢轴量
    pivot = nums[high]
    while l_index < r_index:
        # 交替扫描和交换
        # 从左往右找到第一个比枢轴量大的元素,交换位置
        while l_index < r_index and nums[l_index] <= pivot:
            l_index += 1
        # 如果找到了,进行元素交换
        nums[l_index], nums[r_index] = nums[r_index], nums[l_index]
        # 从右往左找到第一个比枢轴量小的元素,交换位置
        while l_index < r_index and nums[r_index] > pivot:
            r_index -= 1
        nums[l_index], nums[r_index] = nums[r_index], nums[l_index]

    # 至此完成一趟快速排序,枢轴量的位置已经确定好了,就在l_index位置上(l_index和r_index)值相等
    # 递归完成剩下的快排
    # 以l_index为枢轴进行子序列元素的快排
    quick_sort(nums, low, l_index-1)
    quick_sort(nums, l_index+1, high)

5. 归并排序

  • 原理: 归并排序使用了二分法,归根到底的思想还是分而治之。拿到一个长数组,将其不停的分为左边和右边两份,然后以此递归分下去。然后再将两个有序数组合并起来。过程如下图所示:
    在这里插入图片描述

  • 时间复杂度: 最坏、最好、平均情况下,其时间复杂度都是:O( n l o g 2 n nlog_2n nlog2n)

  • 空间复杂度: O(1)

  • 稳定性分析: 稳定

# 将两个有序数组合并
def merge_action(nums1, nums2):
    # 两个数组二分而来,因此任意选择一个数组向另一个数组进行合并
    while nums2:
        # 合并思路:由于两个数组都是有序,因此当nums2开始位置的数大于nums1最后位置的数,说明两个数组直接合并起来就为有序数组
        if nums2[0] < nums1[-1]:
            # 若小于,则将nums[0]的数插入到nums1中--插入排序
            temp = nums2.pop(0)
            for i in range(len(nums1)):
                if temp < nums1[i]:
                    # 找到插入的位置,插入nums[0]
                    nums1.insert(i, temp)
                    break
        # nums2开始位置的数大于nums1最后位置的数,此时不需要再插入
        else:
            break
    # 两个数组直接合并起来就为有序数组
    return nums1 + nums2


def merge_sort(nums):
    n = len(nums)
    # 数组长度为1,返回,此时为最小单位的有序数组
    if n <= 1:
        return nums
    # 二分数组分割点
    divid_index = n // 2
    # 将左边的数组继续分割,分割成最小单位。深度优先遍历
    left = merge_sort(nums[: divid_index])
    # 将右边的数组继续分割,分割成最小单位
    right = merge_sort(nums[divid_index:])
    # 将两个有序数组进行合并成一个有序数组
    merge_list = merge_action(left, right)
    return merge_list

6. 堆排序

  • 原理: 堆排序是利用堆这种数据结构而设计的一种排序算法。将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。可称为有序区,然后将剩余n-1个元素重新构造成一个堆,重复执行,便能得到一个有序序列。
    1. 最大堆:所有的父节点的值都比孩子节点大,叶子节点值最小。root 根节点是第一个节点值最大;
    2. 最小堆:和大顶堆相反,所有父节点值,都小于子节点值,root 根节点是 第一个节点值最小。如下图所示,红色的数字为数组的索引。
      在这里插入图片描述
  • 时间复杂度: 最坏、最好、平均情况下,其时间复杂度都是:O( n l o g 2 n nlog_2n nlog2n)
  • 空间复杂度: O(1)
  • 稳定性分析: 不稳定
# 从索引为index的地方,重新建立最大堆
def heapify(nums, n, index):
    # 最大值的索引,此时为传入的根节点
    largest_index = index
    # 左子节点的索引
    l_index = largest_index * 2 + 1
    # 右子节点的索引
    r_index = largest_index * 2 + 2
    # 若有左子节点,且左子节点大于根节点,则最大值索引记录为左子节点的索引值
    if l_index<n and nums[l_index] > nums[largest_index]:
        largest_index = l_index
    # 若有右子节点,且右子节点大于根节点,则最大值索引记录为右子节点的索引值
    if r_index<n and nums[r_index] > nums[largest_index]:
        largest_index = r_index
    # 若最大值索引不等于根节点,则将根节点与最大值索引进行交换
    if largest_index != index:
        nums[largest_index], nums[index] = nums[index], nums[largest_index]
        # 继续向下遍历
        heapify(nums, n, largest_index)


def heap_sort(nums):
    n = len(nums)
    # 超过(n // 2 + 1),后面不会有子节点,不需要再遍历
    # 从下向上(调整次数最少)构建初始的最大堆
    for i in range(n // 2 + 1, -1, -1):
        heapify(nums, n, i)
    # 每次取出堆顶数据,然后重新构建最大堆
    # 只用循环n-1次,最后两位数,相互比较交换后,就已经排好序了,最后一位数不用再比较
    for i in range(n - 1, 0, -1):
        nums[0], nums[i] = nums[i], nums[0]
        heapify(nums, i, 0)

7. 希尔排序

  • 原理: 希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
    先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
  • 时间复杂度: 平均情况下,其时间复杂度都是:O( n 1.3 n^{1.3} n1.3)
  • 空间复杂度: O(1)
  • 稳定性分析: 不稳定
def shell_sort(nums):
    n = len(nums)
    gap = n // 2
    while gap >= 1:
        # 0~gap-1为起始值,都需要进行一次排序
        for s in range(gap):
            # 这里使用的为插入排序,可以换成其他排序算法
            for i in range(s + gap, n, gap):
                for j in range(i-gap, -1, -gap):
                    if nums[j] > nums[j + gap]:
                        nums[j], nums[j + gap] = nums[j + gap], nums[j]
                    else:
                        break
        gap = gap // 2

8. 计数排序

  • 原理: 计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。对每一个元素x,确定x的元素个数。有了这一信息就可以把x直接放到最终输出数组中的位置上。
    计数排序适合于有较多的重复值,且数值范围不大的情况。
  • 时间复杂度: 最坏、最好、平均情况下,其时间复杂度都是:O( n + k n+k n+k)
  • 空间复杂度: O(k)
  • 稳定性分析: 不稳定
def count_sort(nums):
    # 数组中最大的值,用来构建计数数组的边界
    bound = max(nums)
    # 构建计数数组,0~n,数组个数为bound+1
    count_list = [0] * (bound + 1)
    # 计数数组对应的位置计数值加1
    for num in nums:
        count_list[num] += 1
    # 用来记录插入的边界
    pre_index, cur_index = 0, 0
    # 遍历计数数组
    for i in range(len(count_list)):
        # 若计数值不为0,则在原数组中 插入 计数值个索引值
        if count_list[i] != 0:
            cur_index += count_list[i]
            nums[pre_index:cur_index] = [i] * count_list[i]
            pre_index = cur_index

9. 基数排序

  • 原理: 先排元素的最后一位,再排倒数第二位,直到所有位数都排完。基数排序可以看做是进行多趟桶排序。
  • 时间复杂度: 最坏、最好、平均情况下,其时间复杂度都是:O( d ( k + N ) d(k+N) d(k+N))。
    在基数排序中,需要走访待排序列表中的每一个元素进行分桶,列表长度为 n , 然后将每个桶中的数据取出进行合并,一共有 k 个桶,所以进行一轮基数排序的时间复杂度为T(n)=n+k,再乘分桶和合并的步骤数(常数,不影响大O记法),得出进行一轮基数排序的时间复杂度为 O(n+k) 。当待排序列表中的最大值有 d 位时,需要进行 d 轮基数排序,时间复杂度为 O(d*(n+k)) 。
  • 空间复杂度: O(k)
  • 稳定性分析: 稳定
def radix_sort(nums):
    # 从最低位,个位开始排,0表示当前正在排第一位
    index = 0
    # 最大值
    max_num = max(nums)
    # 记录最大值的位数
    count = len(str(max_num))
    while index < count:
        bucket_list = [[] for _ in range(10)]  # 初始化桶数组
        for x in nums:
            bucket_list[int(x / (10 ** index)) % 10].append(x)  # 找到位置放入桶数组
        print(bucket_list)
        nums.clear()
        for bucket in bucket_list:  # 放回原序列
            for num in bucket:
                nums.append(num)
        index += 1

10. 桶排序

  • 原理: 当输入符合均匀分布时,例如,元素均匀的分布在区间[0,1)上,可以将桶排序与其它排序方法结合使用。
    如果序列的大小为n,就将[0,1)划分成n个相同大小的子区间(桶),然后将n个输入数分布到各个桶中。先对各个桶中的数进行排序,然后按照次序把各桶中的元素列出来即可。
    计数排序和基数排序都属于桶排序。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值