【排序算法】

常见算法

复杂度

算法名称时间复杂度空间复杂度是否稳定排序
冒泡排序O(n^2)O(1)稳定
选择排序O(n^2)O(1)不稳定
插入排序O(n^2)O(1)稳定
快速排序O(nlogn)O(logn)~O(n)不稳定
归并排序O(nlogn)O(n)稳定
堆排序O(nlogn)O(1)不稳定
计数排序O(n+k)O(k)稳定
桶排序O(n+k)O(n)~O(k)稳定
基数排序O(d(n+k))O(n+k)稳定

使用场景

  1. 冒泡排序:适用于小数据量的排序,对于大数据量的排序性能不佳。

  2. 选择排序:适用于小数据量的排序,对于大数据量的排序性能不佳。但相对于冒泡排序,减少了交换次数,其对于比较稳定的数据排序更快。

  3. 插入排序:适用于基本有序的数据排序和小数据量的排序,但对于大数据量的排序性能不佳。

  4. 快速排序:适用于中等数据量和大数据量的排序,是一种高效的排序算法,性能很优秀。但最坏的情况下会退化为n^2,不保证稳定性。

  5. 归并排序:适用于全部数据排序,特别是大数据量的排序,具有稳定性。

  6. 堆排序:适用于全部数据排序,性能不及归并排序,但不需要额外的内存来保存数据,可以原地排序。

  7. 计数排序:适用于元素值在一个给定的范围(比如0到k)且整个数据集比较集中的情况。时间复杂度非常优秀,但需要额外的内存来保存数据。

  8. 桶排序:适用于元素值分布均匀且相差不大的情况。需要额外的内从来保存数据和桶,但时间复杂度较优。

  9. 基数排序:适用于即使在排序之后仍需要保持数据位数时。时间复杂度与数字的位数md相乘,其中m代表数字的最大值,d代表数字长度,因此,最高位有数值的数组排序较为缓慢,但通常具有比较稳定的性能。

冒泡排序

代码实现

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

算法说明
冒泡排序,一种基础的排序算法,它的基本思想是通过相邻元素之间的比较和交换来把小的数交换到数组的前面,把大的数交换到数组的后面。冒泡排序算法的平均时间复杂度为O(n^2)。

测试

def test_bubble_sort():
    assert bubble_sort([3, 2, 1]) == [1, 2, 3]
    assert bubble_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert bubble_sort([]) == []
    assert bubble_sort([5]) == [5]
    assert bubble_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_bubble_sort()

总结
冒泡排序算法虽然比较简单,但是其时间复杂度较高,不适合处理大量数据。在实际应用中,应该选用更高效的排序算法来处理大规模数据的排序。

选择排序

代码实现

def selection_sort(nums):
    for i in range(len(nums)):
        min_idx = i
        for j in range(i+1, len(nums)):
            if nums[j] < nums[min_idx]:
                min_idx = j
        
        nums[i], nums[min_idx] = nums[min_idx], nums[i]
    return nums

算法说明
选择排序是一种简单的排序算法,它的主要思想是每次从未排序的元素中找到最小值,然后将它放到已排序的元素后面。在循环中,我们需要从未排序的元素中找到最小的,并与当前元素交换位置。

测试

def test_selection_sort():
    assert selection_sort([3, 2, 1]) == [1, 2, 3]
    assert selection_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert selection_sort([]) == []
    assert selection_sort([5]) == [5]
    assert selection_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_selection_sort()

总结
选择排序算法是一种简单而直观的排序算法,在实现时只需要使用两个嵌套循环即可。尽管选择排序的时间复杂度为O(n^2),但它在对小数组进行排序时表现良好。

插入排序

代码实现

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr

算法说明

插入排序是一种简单直观的排序算法,它的实现思路是依次将每一个元素插入到已经排序好的序列中。插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),但是当序列已经基本有序时,插入排序的性能会更好。此外,插入排序是一种稳定的排序算法。

算法的核心是将元素逐个插入到已排序序列中。具体实现过程如下:

  1. 从第一个元素开始,认为这个元素已经是有序的。
  2. 取出下一个元素并将它插入到已经排序好的序列中。
  3. 重复第2步,直到所有元素都已经插入到有序序列中。

测试

def test_insertion_sort():
    assert insertion_sort([3, 2, 1]) == [1, 2, 3]
    assert insertion_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert insertion_sort([]) == []
    assert insertion_sort([5]) == [5]
    assert insertion_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_insertion_sort()

总结
插入排序虽然时间复杂度较高,但是它具有简单、稳定的特点,并且对于已经基本有序的序列,它的性能会更好。在实际使用中,我们可以通过对序列的特点进行分析,选择合适的排序算法来提高排序效率。

快速排序

代码实现

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

算法说明
快速排序是一种高效的排序算法,基于分治的思想,在平均情况下的时间复杂度为O(nlogn)。算法基本思路是选取一个基准元素,将数组划分为两个子数组,使得一个数组的所有元素都比基准元素小,另一个数组的所有元素都比基准元素大。之后递归进行子数组排序,最终将子数组合并成一个有序数组。

测试

def test_quick_sort():
    assert quick_sort([3, 2, 1]) == [1, 2, 3]
    assert quick_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert quick_sort([]) == []
    assert quick_sort([5]) == [5]
    assert quick_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_quick_sort()

总结
快速排序算法利用分治的思想达到较好的效率,将需要排序的序列不断划分并合并。然而在最坏情况下时间复杂度会退化为O(n^2),需要合理选取基准元素来避免这种情况的产生。在实际应用中,快速排序仍然是一种高效的排序算法,广泛应用于各个领域。

归并排序

代码实现

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left_half = merge_sort(arr[:mid])
    right_half = merge_sort(arr[mid:])

    i = j = k = 0

    while i < len(left_half) and j < len(right_half):
        if left_half[i] <= right_half[j]:
            arr[k] = left_half[i]
            i += 1
        else:
            arr[k] = right_half[j]
            j += 1
        k += 1

    arr[k:] = left_half[i:] if i < len(left_half) else right_half[j:]

    return arr

算法说明
归并排序是一种分治思想的排序算法。将待排序的序列不断地分成两部分,分别对左右两部分排序,然后将排序好的两部分合并起来即可得到一个有序的序列。

具体来说,先将待排序的序列不断地递归分成左右两半,直到只剩一个或没有元素。然后将两个有序的子序列合并起来,合并时从两个子序列的开头开始比较,将较小的元素放到排序好的序列中,直至两个子序列中的元素都被放到排序好的序列中。

可以证明,归并排序的时间复杂度为 O(nlogn),并且是一种稳定的排序算法。

测试

def test_merge_sort():
    assert merge_sort([3, 2, 1]) == [1, 2, 3]
    assert merge_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert merge_sort([]) == []
    assert merge_sort([5]) == [5]
    assert merge_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_merge_sort()

总结

归并排序是一种时间复杂度为 O(nlogn) 的稳定排序算法,虽然在实际应用中可能存在一定的时间和空间开销,但是它是一种非常重要的排序算法,在实际应用中有着广泛的应用。

堆排序

代码实现

def heapify(arr, heap_len, root_node): # 更新根
    max_node = root_node
    left_node = max_node * 2 + 1
    right_node = max_node * 2 + 2

    if left_node < heap_len and arr[left_node] > arr[max_node]:
        max_node = left_node

    if right_node < heap_len and arr[right_node] > arr[max_node]:
        max_node = right_node

    if max_node != root_node:
        arr[root_node], arr[max_node] = arr[max_node], arr[root_node]
        heapify(arr, heap_len, max_node)


def heap_sort(arr):
    arr_len = len(arr)

	# 初始化堆
    for i in range(arr_len // 2 - 1, -1, -1):
        heapify(arr, arr_len, i)
	
	# 取根后更新堆
    for i in range(arr_len - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)

    return arr

算法说明

堆排序是一种基于树的排序算法,它的特点在于排序过程中的元素之间的比较是在树形结构上进行的,而不是随机访问数组中的元素。这个树形结构就是堆(Heap),堆是一种特殊的树形数据结构,它有以下两个性质:

  • 堆是一棵完全二叉树,也就是说,除了最后一层,其他层的节点都是满的,最后一层上的节点都靠左排列。
  • 堆中每个节点的值都必须大于等于/小于等于其子树中的任意节点的值,这个性质叫做堆特性。

堆排序的基本思想是将待排序序列构造成一个大堆或小堆,此时整个序列的最大值或最小值就是堆顶的根节点。将它移走(其实就是第一个数字和最后一个数字进行交换,用堆顶代表最后一个数字,然后对前面的 n-1 个数据重新构造成一个堆,如此反复,便能得到一个有序序列了。

最好、最坏、平均时间复杂度均为 O(n log n),空间复杂度 O(1),不适用于数据量比较小的场景,但是对于数据规模非常大的场景,堆排序还是比较好用的。

测试

def test_heap_sort():
    assert heap_sort([3, 2, 1]) == [1, 2, 3]
    assert heap_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert heap_sort([]) == []
    assert heap_sort([5]) == [5]
    assert heap_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_heap_sort()

总结

堆排序的实现并不难,但是需要理解堆的概念和性质,才能清楚其过程。虽然堆排序在时间复杂度上优于冒泡、插入等排序算法,但是在实际应用中,它不如快速排序、归并排序等其他排序算法被广泛使用,但是我们还是需要了解堆排序的原理和思路,以备不时之需。

计数排序

代码实现

def counting_sort(arr):
    arr_len = len(arr)
    if arr_len < 2:
        return arr
        
    min_val = min(arr)
    max_val = max(arr)
    count_len = max_val - min_val + 1
    count_list = [0] * count_len

    for item in arr:
        count_list[item - min_val] += 1

    for i in range(1, count_len):
        count_list[i] += count_list[i - 1]

    res = [0] * arr_len
    for num in arr:
        index = count_list[num - min_val] - 1
        res[index] = num
        count_list[num - min_val] -= 1

    return res

算法说明

计数排序是一种较快速的排序算法,其时间复杂度为O(n+k),其中n为待排序的元素个数,k为待排序的元素最大值与最小值之差。这个算法的思想是先找出待排序元素中的最大值和最小值,然后根据最大值和最小值的范围创建一个数组count,用于存储每个元素在数组arr中出现的次数。最后,遍历数组arr,将每个元素根据在count数组中出现的次数,存放到一个新的数组output中,实现排序。

测试

def test_counting_sort():
    assert counting_sort([3, 2, 1]) == [1, 2, 3]
    assert counting_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert counting_sort([]) == []
    assert counting_sort([5]) == [5]
    assert counting_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_counting_sort()

总结

计数排序是一种非常简单、有效的排序算法,在算法分析和应用中都有广泛的应用。实现也非常简单,不需要额外的空间,只需要原理和代码实现即可。需要注意的是,计数排序只适用于元素值范围较小的数组,而且不支持负数与小数排序。

桶排序

代码实现

def bucket_sort(arr, bucket_size=5):
    arr_len = len(arr)
    if arr_len <= 1:
        return arr

    min_val, max_val = min(arr), max(arr)
    if min_val == max_val:
        return arr

    # 创建桶
    bucket_count = (max_val - min_val) // bucket_size + 1
    buckets = [[] for _ in range(bucket_count)]

    # 数据入桶
    for i in range(arr_len):
        bucket_index = (arr[i] - min_val) // bucket_size
        buckets[bucket_index].append(arr[i])

    # 桶排序
    for i in range(bucket_count):
        buckets[i].sort()

    # 有序取出数据
    arr = []
    for bucket in buckets:
        arr += bucket
    return arr

算法说明
桶排序是一个线性排序算法,其核心思想是将待排序的数据分到几个有序的桶中,每个桶内的数据再分别进行排序,最后将所有的桶按顺序依次连接起来即可得到最终的有序序列。桶排序相比较于其他的排序算法,最为关键的是桶的设计,桶的大小要根据待排序数据的分布情况来确定,否则桶排序的效率将受到很大的影响。

测试

def test_bucket_sort():
    assert bucket_sort([3, 2, 1]) == [1, 2, 3]
    assert bucket_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert bucket_sort([]) == []
    assert bucket_sort([5]) == [5]
    assert bucket_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_bucket_sort()

总结
桶排序是一个非常高效的排序算法,其时间复杂度为O(n),但是其适应范围并不广泛,只能用于待排序数据分布在指定区间的场景中,如果待排序数据分布比较均匀的情况下,桶排序的效率将远低于其他的排序算法。在实际的应用场景中,选择合适的排序算法非常重要,需要根据具体的业务场景以及待排序数据的特征来进行选择。

基数排序

代码实现

def radix_sort(arr):
    if len(arr) <= 1:
        return arr

    max_val = max(arr)
    digit = 1
    while max_val // digit > 0:
        buckets = [[] for _ in range(10)]  # 创建桶
        for val in arr:
            radix = val // digit % 10  # 求当前位数字
            buckets[radix].append(val)
        arr.clear()
        for bucket in buckets:
            arr += bucket
        digit *= 10
    return arr

算法说明

采用LSD(从低位到高位)方式,每个位的值按顺序分类到桶进行排序,基数排序算法在优化空间复杂度方面具有优势时间复杂度为 O(d * (n + k)),其中 d 表示位数,n 表示元素个数,k 表示数值范围。

测试

def test_radix_sort():
    assert radix_sort([3, 2, 1]) == [1, 2, 3]
    assert radix_sort([5, 1, 4, 2, 8]) == [1, 2, 4, 5, 8]
    assert radix_sort([]) == []
    assert radix_sort([5]) == [5]
    assert radix_sort([1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]

test_radix_sort()

总结

基数排序算法是一种比较实用的排序方法,尤其适用于数据集中元素具有相同位数的情形,其排序过程通过分组实现,可以有效降低时间复杂度并优化空间复杂度,缺点是容易受到数值范围的限制。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值