排序算法整理(冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序、计数排序、桶排序、基数排序)

排序算法是计算机科学中用于将数据元素按照特定顺序进行排列的算法,常见的排序算法有以下几类:
比较排序

  1. 冒泡排序:通过重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
  2. 选择排序:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
  3. 插入排序:对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
  4. 希尔排序:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
  5. 归并排序:采用分治法,将一个数组分成两个子数组,对每个子数组进行排序,然后将排序好的子数组合并成一个最终的排序数组。
  6. 快速排序:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
  7. 堆排序:利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

非比较排序

  1. 计数排序:通过统计每个元素在数组中出现的次数,然后根据统计结果将元素按照顺序放入新的数组中,从而实现排序。
  2. 桶排序:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
  3. 基数排序:按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

冒泡排序

基本原理

冒泡排序的基本思想是通过相邻元素之间的比较和交换,将最大(或最小)的元素逐步 "浮" 到数组的末尾(或开头),就像气泡在水中上升一样,每一轮比较都会将当前未排序部分的最大(或最小)元素移动到正确的位置。具体过程如下:

  1. 从数组的第一个元素开始,依次比较相邻的两个元素。
  2. 如果前一个元素大于后一个元素(假设是升序排序),则交换它们的位置。
  3. 对数组中的每一对相邻元素都进行这样的比较和交换操作,直到遍历完整个数组。
  4. 经过第一轮比较和交换后,数组中的最大元素就会被移动到数组的末尾。
  5. 然后对除了最后一个元素之外的其他元素重复上述过程,每一轮都会将当前未排序部分的最大元素移动到正确的位置,直到整个数组都被排序。
    示例代码
python 复制代码
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        # 最后i个元素已经排好序,无需再比较
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort(arr))  

算法复杂度

  • 时间复杂度
    • 最好情况 :数组已经是有序的,只需要进行一次遍历,比较次数为 n − 1 n-1 n−1次,没有交换操作,时间复杂度为 O ( n ) O(n) O(n)。
    • 最坏情况 :数组是逆序的,需要进行 n − 1 n-1 n−1轮比较,每轮比较需要进行 n − i n-i n−i次比较和交换操作,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
    • 平均情况 :时间复杂度也为 O ( n 2 ) O(n^2) O(n2)。
  • 空间复杂度 :冒泡排序是原地排序算法,只需要常数级别的额外空间来进行元素的交换,空间复杂度为 O ( 1 ) O(1) O(1)

算法稳定性

冒泡排序是一种稳定的排序算法。在比较和交换元素时,如果两个相等的元素相邻,且前一个元素在后面元素之前,只有当前一个元素大于后面元素时才会交换位置,相等时不会交换,所以相等元素的相对顺序在排序前后不会改变。

选择排序

基本原理

选择排序(Selection Sort)的核心思想是每一轮从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置(对于升序排序是起始位置,降序排序则是末尾位置),直到全部待排序的数据元素排完。具体步骤如下:

  • 在未排序序列中找到最小(大)元素,记录其索引。
  • 将最小(大)元素与未排序序列的第一个元素交换位置。
  • 此时,已排序序列长度加 1,未排序序列长度减 1。
  • 重复上述步骤,直到未排序序列为空。

算法示例

以下是使用 Python 实现选择排序的代码:

python 复制代码
def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        # 假设当前未排序部分的第一个元素是最小值
        min_index = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_index]:
                min_index = j
        # 将找到的最小值与当前未排序部分的第一个元素交换位置
        arr[i], arr[min_index] = arr[min_index], arr[i]
    return arr

# 测试
arr = [663636]
print(selection_sort(arr))

算法复杂度

  • 时间复杂度 :无论数组初始状态如何,选择排序都需要进行 n − 1 n - 1 n−1 轮选择和交换操作。每一轮都要遍历未排序部分的所有元素,所以比较次数为 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n−1) 次。因此,选择排序的时间复杂度始终为 O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。
  • 空间复杂度 :选择排序是原地排序算法,只需要常数级别的额外空间用于交换元素,空间复杂度为 O ( 1 ) O(1) O(1)。

算法稳定性

选择排序是一种不稳定的排序算法。例如,对于数组 [5, 8, 5, 2],在第一轮选择中,会将最小元素 2 与第一个 5 交换位置,这样两个 5 的相对顺序就发生了改变,所以选择排序不能保证相等元素的相对顺序不变。

插入排序

基本原理

插入排序(Insertion Sort)的工作原理就像我们整理扑克牌一样。它将数组分为已排序和未排序两部分,初始时已排序部分只有一个元素(通常是数组的第一个元素),然后依次从未排序部分取出元素,将其插入到已排序部分的合适位置,使得已排序部分始终保持有序,直到未排序部分为空。具体步骤如下:

  1. 从数组的第二个元素开始,将其视为待插入的元素。
  2. 把这个待插入元素与已排序部分的元素从后往前依次比较。
  3. 如果待插入元素小于比较的元素,则将比较元素后移一位。
  4. 重复步骤 3,直到找到一个不大于待插入元素的位置,将待插入元素插入到该位置之后。
  5. 重复步骤 1 - 4,直到整个数组都被排序。

算法示例

以下是使用 Python 实现插入排序的代码:

python 复制代码
def insertion_sort(arr):
    n = len(arr)
    for i in range(1, n):
        # 取出当前待插入的元素
        key = arr[i]
        j = i - 1
        # 将比 key 大的元素后移
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]
            j = j - 1
        # 插入 key
        arr[j + 1] = key
    return arr

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(insertion_sort(arr))

算法复杂度

  • 时间复杂度
    • 最好情况 :数组已经是有序的,此时每一个待插入元素只需要与已排序部分的最后一个元素比较一次,不需要移动元素,所以时间复杂度为 O ( n ) O(n) O(n),其中 n n n 是数组的长度。
    • 最坏情况 :数组是逆序的,每个待插入元素都要与已排序部分的所有元素进行比较,并将它们依次后移,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
    • 平均情况 :平均时间复杂度也是 O ( n 2 ) O(n^2) O(n2)。
  • 空间复杂度 :插入排序是原地排序算法,只需要常数级别的额外空间来存储待插入元素,空间复杂度为 O ( 1 ) O(1) O(1)。

算法稳定性

插入排序是一种稳定的排序算法。在插入元素时,如果遇到相等的元素,不会将待插入元素插入到相等元素的前面,而是保持它们原来的相对顺序,所以相等元素在排序前后的相对位置不会改变。

希尔排序

基本原理

希尔排序(Shell Sort)是对插入排序的一种改进,也称为缩小增量排序。插入排序在处理基本有序的数据时效率较高,但在处理大规模无序数据时,每次只能将元素移动一位,效率较低。希尔排序通过引入一个"增量"的概念,将原始数据分成多个子序列,对每个子序列分别进行插入排序,随着增量逐渐减小,子序列的长度逐渐增加,整个序列会变得越来越接近有序,最后当增量减至 1 时,整个序列就相当于进行一次普通的插入排序,但此时由于数据已经基本有序,插入排序的效率会大大提高。

具体步骤如下:

  1. 选择一个增量序列 t 1 , t 2 , ⋯   , t k t_1, t_2, \cdots, t_k t1,t2,⋯,tk,其中 t 1 > t 2 > ⋯ > t k = 1 t_1 > t_2 > \cdots > t_k = 1 t1>t2>⋯>tk=1。
  2. 按增量 t i t_i ti 对数组进行分组,每个相隔 t i t_i ti 的元素为一组。
  3. 对每组元素分别进行插入排序。
  4. 重复步骤 2 和 3,直到增量 t k = 1 t_k = 1 tk=1,此时进行最后一次插入排序,整个数组就被排序完成。

算法示例

以下是使用 Python 实现希尔排序的代码:

python 复制代码
def shell_sort(arr):
    n = len(arr)
    # 初始增量为数组长度的一半
    gap = n // 2
    while gap > 0:
        for i in range(gap, n):
            temp = arr[i]
            j = i
            # 对相隔 gap 的元素进行插入排序
            while j >= gap and arr[j - gap] > temp:
                arr[j] = arr[j - gap]
                j -= gap
            arr[j] = temp
        # 缩小增量
        gap //= 2
    return arr

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(shell_sort(arr))

算法复杂度

  • 时间复杂度 :希尔排序的时间复杂度与增量序列的选择有关,不同的增量序列会导致不同的时间复杂度。最坏情况下,时间复杂度为 O ( n 2 ) O(n^2) O(n2),但在实际应用中,通过合理选择增量序列,希尔排序的平均时间复杂度可以接近 O ( n log ⁡ n ) O(n \log n) O(nlogn)。
  • 空间复杂度 :希尔排序是原地排序算法,只需要常数级别的额外空间来进行元素的交换和临时存储,空间复杂度为 O ( 1 ) O(1) O(1)。

算法稳定性

希尔排序是一种不稳定的排序算法。由于在不同的子序列中进行插入排序时,相等元素可能会被交换位置,所以不能保证相等元素的相对顺序在排序前后不变。例如,对于数组 [3, 5, 3, 1],在某个增量下进行排序时,可能会改变两个 3 的相对顺序。

归并排序

基本原理

归并排序(Merge Sort)是采用分治法(Divide and Conquer)的一个非常典型的应用。分治法的核心思想是将一个大问题分解为多个小问题,分别解决这些小问题,然后将小问题的解合并起来得到大问题的解。归并排序具体分为两个步骤:

  • 分解(Divide):将待排序的数组从中间分成两个子数组,递归地对这两个子数组分别进行分解,直到每个子数组只有一个元素或者为空。
  • 合并(Merge):将两个已排序的子数组合并成一个新的有序数组。合并过程是比较两个子数组的元素,依次取出较小(升序排序)的元素放入新数组,直到两个子数组的元素都被取完。

算法示例

以下是使用 Python 实现归并排序的代码:

python 复制代码
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    # 分解数组
    mid = len(arr) // 2
    left_half = arr[:mid]
    right_half = arr[mid:]

    # 递归排序左右子数组
    left_half = merge_sort(left_half)
    right_half = merge_sort(right_half)

    # 合并已排序的子数组
    return merge(left_half, right_half)

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.extend(left[i:])
    # 添加右子数组剩余元素
    result.extend(right[j:])
    return result

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(merge_sort(arr))

算法复杂度

  • 时间复杂度 :归并排序每次将数组分成两部分,递归深度为 log ⁡ n \log n logn( n n n 是数组长度),每一层合并操作的时间复杂度是 O ( n ) O(n) O(n),所以归并排序的时间复杂度稳定为 O ( n log ⁡ n ) O(n \log n) O(nlogn),无论是最好情况、最坏情况还是平均情况。
  • 空间复杂度 :归并排序需要额外的空间来存储合并过程中的临时数组,空间复杂度为 O ( n ) O(n) O(n)。

算法稳定性

归并排序是一种稳定的排序算法。在合并两个子数组时,如果遇到相等的元素,会先将左边子数组的元素放入结果数组,这样就保证了相等元素的相对顺序不变。

快速排序

基本原理

快速排序(Quick Sort)同样采用分治法的思想,它的核心在于选择一个基准元素(pivot),通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比基准元素小,另一部分记录的关键字均比基准元素大,然后分别对这两部分继续进行排序,以达到整个序列有序。具体步骤如下:

  1. 选择基准元素:从数组中选择一个元素作为基准元素,常见的选择方法有选择数组的第一个元素、最后一个元素或者中间元素等。
  2. 分区操作:将数组中的元素重新排列,使得所有小于基准元素的元素都位于基准元素的左边,所有大于基准元素的元素都位于基准元素的右边。这个过程称为分区(partition),分区完成后,基准元素就处于它最终排序后的正确位置。
  3. 递归排序:对基准元素左边和右边的子数组分别递归地应用上述步骤,直到子数组的长度为 0 或 1,此时数组就已经有序。

算法示例

以下是使用 Python 实现快速排序的代码:

python 复制代码
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    else:
        # 选择第一个元素作为基准元素
        pivot = arr[0]
        # 小于基准元素的元素组成的子数组
        left = [x for x in arr[1:] if x <= pivot]
        # 大于基准元素的元素组成的子数组
        right = [x for x in arr[1:] if x > pivot]
        # 递归排序左右子数组并合并结果
        return quick_sort(left) + [pivot] + quick_sort(right)

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(quick_sort(arr))

算法复杂度

  • 时间复杂度
    • 最好情况 :每次选择的基准元素都能将数组均匀地分成两部分,此时递归树的深度为 log ⁡ n \log n logn,每层的分区操作时间复杂度为 O ( n ) O(n) O(n),所以最好情况下的时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)。
    • 最坏情况 :如果数组已经是有序的(升序或降序),并且每次都选择第一个或最后一个元素作为基准元素,那么每次分区只能将数组分成一个长度为 0 的子数组和一个长度为 n − 1 n - 1 n−1 的子数组,递归树退化为单链表,此时时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
    • 平均情况 :在平均情况下,快速排序的时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)。
  • 空间复杂度 :快速排序的空间复杂度主要取决于递归调用的栈深度。在最好和平均情况下,递归树的深度为 log ⁡ n \log n logn,所以空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn);在最坏情况下,递归树退化为单链表,栈深度为 n n n,空间复杂度为 O ( n ) O(n) O(n)。

算法稳定性

快速排序是一种不稳定的排序算法。在分区过程中,相等元素的相对顺序可能会发生改变。例如,在分区时,相等元素可能会被交换到不同的子数组中,从而导致它们的相对顺序发生变化。

堆排序

基本原理

堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并且满足堆积的性质:每个节点的值都大于或等于其子节点的值(大顶堆),或者每个节点的值都小于或等于其子节点的值(小顶堆)。堆排序的基本思想是先将待排序的数组构建成一个堆,然后不断地从堆中取出最大(大顶堆)或最小(小顶堆)的元素,将其放到已排序序列的末尾,同时调整剩余元素,使其仍然满足堆的性质,重复这个过程直到整个数组有序。具体步骤如下:

  1. 构建堆:将待排序的数组构建成一个大顶堆(如果是升序排序)或小顶堆(如果是降序排序)。通常从最后一个非叶子节点开始,依次向上调整每个节点,使其满足堆的性质。
  2. 交换堆顶元素和末尾元素:将堆顶元素(即最大或最小元素)与堆的最后一个元素交换位置,此时堆的最后一个元素就是已排序的元素。
  3. 调整堆:交换后,堆的结构可能被破坏,需要对剩余的元素重新调整,使其再次满足堆的性质。
  4. 重复步骤 2 和 3:不断重复交换和调整堆的操作,直到堆中只剩下一个元素,此时整个数组就已经有序。

算法示例

以下是使用 Python 实现堆排序(升序,使用大顶堆)的代码:

python 复制代码
def heapify(arr, n, i):
    # 初始化根节点为最大值
    largest = i
    left = 2 * i + 1
    right = 2 * i + 2

    # 如果左子节点大于根节点
    if left < n and arr[left] > arr[largest]:
        largest = left

    # 如果右子节点大于当前最大值
    if right < n and arr[right] > arr[largest]:
        largest = right

    # 如果最大值不是根节点
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]
        # 递归调整受影响的子树
        heapify(arr, n, largest)

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

    # 构建大顶堆
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)

    # 一个个交换元素
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]  # 交换堆顶和当前末尾元素
        heapify(arr, i, 0)  # 调整剩余元素为大顶堆

    return arr

# 测试
arr = [64, 34, 25, 12, 22, 11, 90]
print(heap_sort(arr))

算法复杂度

  • 时间复杂度
    • 构建堆 :构建堆的时间复杂度为 O ( n ) O(n) O(n)。因为从最后一个非叶子节点开始调整,每个节点最多需要调整的次数与树的高度有关,而树的高度为 O ( log ⁡ n ) O(\log n) O(logn),节点总数为 n n n,综合起来构建堆的时间复杂度为 O ( n ) O(n) O(n)。
    • 排序过程 :每次交换堆顶元素和末尾元素后,需要对剩余元素进行调整,调整的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),总共需要进行 n − 1 n - 1 n−1 次交换和调整操作,所以排序过程的时间复杂度为 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)。

算法稳定性

堆排序是一种不稳定的排序算法。在交换堆顶元素和末尾元素的过程中,可能会改变相等元素的相对顺序。例如,在调整堆的过程中,相等元素可能会被交换到不同的位置,导致它们的相对顺序发生变化。

计数排序

基本原理

计数排序(Counting Sort)是一种非比较排序算法,其核心思想是通过统计数组中每个元素出现的次数,然后根据统计结果将元素按照顺序输出,从而实现排序。该算法适用于待排序元素的取值范围较小且为整数的情况。具体步骤如下:

  1. 找出待排序数组中的最大值和最小值:确定计数数组的长度,计数数组的长度为最大值减去最小值加 1。
  2. 统计每个元素出现的次数:遍历待排序数组,以元素的值作为计数数组的索引,统计每个元素出现的次数。
  3. 计算累积次数:对计数数组进行累加操作,使得计数数组中的每个元素表示小于等于该索引所对应元素的元素个数。
  4. 输出排序结果:根据计数数组中的累积次数,将待排序数组中的元素依次放入新的数组中,从而得到排序好的数组。

算法示例

以下是使用 Python 实现计数排序的代码:

python 复制代码
def counting_sort(arr):
    if not arr:
        return arr
    # 找出最大值和最小值
    min_val = min(arr)
    max_val = max(arr)
    # 计算计数数组的长度
    range_size = max_val - min_val + 1
    # 初始化计数数组
    count = [0] * range_size
    # 统计每个元素出现的次数
    for num in arr:
        count[num - min_val] += 1
    # 计算累积次数
    for i in range(1, len(count)):
        count[i] += count[i - 1]
    # 初始化结果数组
    result = [0] * len(arr)
    # 输出排序结果
    for num in reversed(arr):
        index = count[num - min_val] - 1
        result[index] = num
        count[num - min_val] -= 1
    return result

# 测试
arr = [4, 2, 2, 8, 3, 3, 1]
print(counting_sort(arr))

算法复杂度

  • 时间复杂度 :计数排序的时间复杂度为 O ( n + k ) O(n + k) O(n+k),其中 n n n 是待排序数组的长度, k k k 是待排序元素的取值范围(即最大值减去最小值加 1)。因为需要遍历待排序数组一次来统计元素出现的次数(时间复杂度为 O ( n ) O(n) O(n)),还需要遍历计数数组一次来计算累积次数(时间复杂度为 O ( k ) O(k) O(k)),最后再遍历待排序数组一次来输出排序结果(时间复杂度为 O ( n ) O(n) O(n)),综合起来时间复杂度为 O ( n + k ) O(n + k) O(n+k)。
  • 空间复杂度 :计数排序需要额外的计数数组和结果数组,空间复杂度为 O ( n + k ) O(n + k) O(n+k)。其中计数数组的长度为 k k k,结果数组的长度为 n n n。

算法稳定性

计数排序是一种稳定的排序算法。在输出排序结果时,通过从后往前遍历待排序数组,保证了相等元素的相对顺序不变。例如,对于数组 [2, 2],在排序过程中,后面的 2 会在前面的 2 之后被放入结果数组,从而保持了它们原来的相对顺序。

桶排序

基本原理

桶排序(Bucket Sort)是一种分布式排序算法,它的核心思想是将待排序的数据分到有限数量的桶里,每个桶再分别进行排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后将各个桶中的数据依次取出,合并成一个有序序列。具体步骤如下:

  1. 确定桶的数量和范围:根据待排序数据的取值范围和数据规模,确定合适的桶数量,并将数据的取值范围划分为若干个区间,每个区间对应一个桶。
  2. 将数据分配到桶中:遍历待排序数组,根据每个数据的值将其放入对应的桶中。
  3. 对每个桶内的数据进行排序:可以使用其他排序算法(如插入排序、快速排序等)对每个桶内的数据进行排序。
  4. 合并各个桶中的数据:按照桶的顺序,依次将各个桶中的数据取出,合并成一个有序的数组。

算法示例

以下是使用 Python 实现桶排序的代码:

python 复制代码
def bucket_sort(arr):
    if not arr:
        return arr
    # 确定桶的数量
    num_buckets = 10
    # 找出最大值和最小值
    min_val = min(arr)
    max_val = max(arr)
    # 计算每个桶的范围
    bucket_range = (max_val - min_val) / num_buckets
    # 初始化桶
    buckets = [[] for _ in range(num_buckets)]
    # 将数据分配到桶中
    for num in arr:
        index = int((num - min_val) // bucket_range)
        if index == num_buckets:
            index = num_buckets - 1
        buckets[index].append(num)
    # 对每个桶内的数据进行排序
    for bucket in buckets:
        bucket.sort()
    # 合并各个桶中的数据
    result = []
    for bucket in buckets:
        result.extend(bucket)
    return result

# 测试
arr = [0.897, 0.565, 0.656, 0.1234, 0.665, 0.3434]
print(bucket_sort(arr))

算法复杂度

  • 时间复杂度
    • 平均情况 :在平均情况下,如果数据均匀分布在各个桶中,每个桶中的元素数量大致相同,那么对每个桶进行排序的时间复杂度为 O ( n k log ⁡ n k ) O(\frac{n}{k} \log \frac{n}{k}) O(knlogkn)(其中 n n n 是数据的数量, k k k 是桶的数量),分配数据到桶中的时间复杂度为 O ( n ) O(n) O(n),合并桶的时间复杂度也为 O ( n ) O(n) O(n),综合起来平均时间复杂度为 O ( n + k + n log ⁡ n k ) O(n + k + n \log \frac{n}{k}) O(n+k+nlogkn)。当 k k k 接近 n n n 时,平均时间复杂度可以接近 O ( n ) O(n) O(n)。
    • 最坏情况 :如果所有数据都集中在一个桶中,那么就相当于对整个数据使用了另一种排序算法(如插入排序的最坏情况为 O ( n 2 ) O(n^2) O(n2)),此时桶排序的时间复杂度退化为 O ( n 2 ) O(n^2) O(n2)。
  • 空间复杂度 :桶排序需要额外的空间来存储桶和桶内的数据,空间复杂度为 O ( n + k ) O(n + k) O(n+k),其中 n n n 是数据的数量, k k k 是桶的数量。

算法稳定性

桶排序的稳定性取决于桶内排序算法的稳定性。如果使用的桶内排序算法是稳定的(如插入排序),那么桶排序就是稳定的;如果使用的桶内排序算法是不稳定的(如快速排序),那么桶排序就是不稳定的。

基数排序

基本原理

基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。它是通过多轮的排序来完成整个排序过程,每一轮只关注数据的某一位数字。具体步骤如下:

  1. 确定最大位数:找出待排序数组中的最大数,确定其位数,这个位数决定了需要进行几轮排序。
  2. 按位排序:从最低位(个位)开始,依次对每一位进行排序。通常使用稳定的排序算法(如计数排序)对当前位进行排序,将数组元素按照当前位的数字大小重新排列。
  3. 重复步骤 2:不断增加位数,重复进行排序操作,直到最高位排序完成,此时整个数组就有序了。

算法示例

以下是使用 Python 实现基数排序的代码,这里使用计数排序作为每一轮的稳定排序算法:

python 复制代码
def counting_sort_for_radix(arr, exp):
    n = len(arr)
    output = [0] * n
    count = [0] * 10

    # 统计当前位上每个数字出现的次数
    for i in range(n):
        index = arr[i] // exp
        count[index % 10] += 1

    # 计算累积次数
    for i in range(1, 10):
        count[i] += count[i - 1]

    # 构建输出数组
    i = n - 1
    while i >= 0:
        index = arr[i] // exp
        output[count[index % 10] - 1] = arr[i]
        count[index % 10] -= 1
        i -= 1

    # 将输出数组复制回原数组
    for i in range(n):
        arr[i] = output[i]

def radix_sort(arr):
    if not arr:
        return arr
    # 找到最大数,确定最大位数
    max_num = max(arr)
    exp = 1
    while max_num // exp > 0:
        # 按当前位进行计数排序
        counting_sort_for_radix(arr, exp)
        exp *= 10
    return arr

# 测试
arr = [170, 45, 75, 90, 802, 24, 2, 66]
print(radix_sort(arr))

算法复杂度

  • 时间复杂度 :基数排序的时间复杂度为 O ( d ( n + k ) ) O(d(n + k)) O(d(n+k)),其中 n n n 是待排序数组的长度, d d d 是最大数的位数, k k k 是每一位的取值范围(通常对于十进制数, k = 10 k = 10 k=10)。因为需要对每一位进行一次计数排序,每次计数排序的时间复杂度为 O ( n + k ) O(n + k) O(n+k),总共需要进行 d d d 轮。
  • 空间复杂度 :主要的额外空间用于计数数组和输出数组,空间复杂度为 O ( n + k ) O(n + k) O(n+k),其中 n n n 是数组长度, k k k 是每一位的取值范围。

算法稳定性

基数排序是一种稳定的排序算法。这是因为每一轮使用的计数排序是稳定的,在排序过程中,相等元素的相对顺序不会改变,所以经过多轮排序后,整个排序过程也是稳定的。

相关推荐
刀客1235 分钟前
python小项目编程-中级(1、图像处理)
开发语言·图像处理·python
信阳农夫18 分钟前
python 3.6.8支持的Django版本是多少?
python·django·sqlite
冷琴199629 分钟前
基于Python+Vue开发的反诈视频宣传管理系统源代码
开发语言·vue.js·python
夏末秋也凉29 分钟前
力扣-回溯-491 非递减子序列
数据结构·算法·leetcode
penguin_bark31 分钟前
三、动规_子数组系列
算法·leetcode
带娃的IT创业者39 分钟前
《Python实战进阶》专栏 No2: Flask 中间件与请求钩子的应用
python·中间件·flask
kyle~40 分钟前
thread---基本使用和常见错误
开发语言·c++·算法
曲奇是块小饼干_1 小时前
leetcode刷题记录(一百零八)——322. 零钱兑换
java·算法·leetcode·职场和发展
坚持就完事了1 小时前
Python之numpy
开发语言·python·numpy
jxf_jxfcsdn1 小时前
python读取pdf文档
开发语言·python·pdf