数据结构之排序算法 - 实践

一.冒泡排序

对于一个数组,从前往后依次比较相邻的两个元素,将较大的元素放在较小的元素的后面。

假设数组的长度为n,第一次比较需要比较n-1次,第二次在剩下的n-1个元素里面找最大值,需要比较n-2次......直到比较到还剩下一个元素,这时剩下的这个元素就一定是最小的,从小到大的排序任务就完成了。这个过程中一共要遍历数组n-1次(只剩下一个元素时这种情况不用比较,也就不用遍历,所以遍历数组的次数是n-1),而且每次遍历数组时要遍历的长度都在变小,第一次需要全部遍历,第一次遍历完后已经找到了n个元素中的最大值并把它放到了数组的末尾,第二次就不需要比较这个最大的元素了,只需要在剩下的n-1个元素里找次大的,所以第二次遍历数组时不用全部都遍历,只需要遍历n-1个长度,即从第1个元素遍历到倒数第二个元素(第n-1个);然后依次类推

def bubble_sort(arr):
    n = len(arr)
    for i in range(n-1):
        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
def bubble_sort_short(arr):
    """短冒泡排序"""
    n = len(arr)
    for i in range(n-1):
        swapped = False  # 优化:提前终止标志 值为False说明该轮比较中没有元素交换
        for j in range(1, n-i):
            if arr[j] < arr[j-1]:
                arr[j], arr[j-1] = arr[j-1], arr[j]
                swapped = True
        if not swapped: # 如果本轮比较没有交换,则说明数组已经有序,直接退出循环
            break
if __name__ == "__main__":
    result = bubble_sort([2,3,1,0,6,-4,9,-6])
    print(result)
    a_list = [4,2,-1,9,45,76,-99,3]
    bubble_sort_short(a_list) # 进行原地排序
    print(a_list)

输出:

[-6, -4, 0, 1, 2, 3, 6, 9]
[-99, -1, 2, 3, 4, 9, 45, 76]

时间复杂度分析:

最好情况下只需比较1次,时间复杂度为o(1)

最坏情况下要遍历n-1次,比较次数依次为:n-1, n-2, n-3, ... , 3, 2, 1 。共要比较\frac{n(n-1)}{2}次,所以时间复杂度为o(n^2)

平均情况下要比较\frac{n(n-1)}{2} 除以 (n-1)也就是\frac{n}{2}次,所以时间复杂度为o(n)

二.选择排序

选择排序是在冒泡排序的基础上进行了一些改进。改进的地方在于每次遍历数组时只交换一次

要实现这一点,就要在每次遍历数组时找出数组的最大值,然后在该次遍历完之后把这个最大值放在合适的位置上

假设数组元素个数为n,只需遍历n-1次,找出n-1个较大值,剩下的那个元素自然就有序了

def selected_sort_min(arr):
    """找最小值,把最小值放到数组的第一个位置"""
    n = len(arr)
    for i in range(n-1):
        min_idx = i
        for j in range(i+1,n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr
def selected_sort_max(arr):
    """找最大值,把最大值放到数组的末尾"""
    n = len(arr)
    for i in range(n-1,0,-1): # 序列为(n-1, n-2, n-3, ..., 2, 1)
        max_idx = i
        for j in range(i):
            if arr[j] > arr[max_idx]:
                max_idx = j
        if max_idx != i: # 只有位置不同时才交换,位置相同说明该次遍历中没有元素比该元素大
            arr[i], arr[max_idx] = arr[max_idx], arr[i]
    return arr
if __name__ == "__main__":
    result = selected_sort_min([1,5,6,2,-6,7,-9,4])
    print(result)
    result = selected_sort_max([3,1,9,0,4,-5,5])
    print(result)

输出:

[-9, -6, 1, 2, 4, 5, 6, 7]
[-5, 0, 1, 3, 4, 5, 9]

时间复杂度分析:

最坏情况下:同冒泡排序一样,为o(n^2)

三.插入排序

基本思想:

  • 将数组分为“已排序”和“未排序”两部分
  • 每次从数组中的“未排序”部分取出一个元素
  • 将取出的这个元素插入到“已排序部分”的正确位置
  • 重复这个过程直到“未排序”部分的所有元素都被插入到“已排序”部分中
def insertion_sort(arr):
    n = len(arr)
    for i in range(1,n):
        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
def insertion_sort_swap(arr):
    n = len(arr)
    for i in range(1,n):
        j = i
        while j > 0 and arr[j] < arr[j-1]:
            arr[j], arr[j-1] = arr[j-1], arr[j]
            j -= 1
    return arr
def binary_insertion_sort(arr):
    """二分插入排序 - 减少比较次数"""
    n = len(arr)
    for i in range(1, n):
        key = arr[i]
        # 使用二分查找找到插入位置
        left, right = 0, i - 1
        while left <= right:
            mid =(left + right) // 2
            if arr[mid] > key:
                right = mid - 1
            else:
                left = mid + 1
        # 将元素向右移动,为插入腾出空间
        for j in range(i - 1, left - 1, -1):
            arr[j + 1] = arr[j]
        # 插入元素
        arr[left] = key
    return arr
if __name__ == "__main__":
    result = insertion_sort([2,4,1,5,-9,8,3])
    result_1 = insertion_sort_swap([1,4,98,-6,0,3,-9])
    result_2 = binary_insertion_sort([3,5,9,0,7,-5,2,6])
    print(result)
    print(result_1)
    print(result_2)

四.希尔排序

插入排序的改进版本,也称为“缩小增量排序”

核心思想:通过将原始列表分割为多个子序列,分别对这些子序列进行插入排序,从而使得整个列表逐渐趋于“基本有序”,最后再对整个列表进行一次插入排序。

这样做的原理在于插入排序在对“基本有序”的列表进行排序时,效率非常高,接近o(n)

工作原理:

  • 选择增量序列:首先确定一个增量序列。增量是一个间隔,用于将列表划分为子序列。最常用且最简单的增量是初始增量=列表长度/2,然后每次再减半,直到增量为1
  • 按增量分组并排序:根据当前增量,将列表分割成多个子序列。每个子序列由相隔“增量”位置的元素组成,然后对每个子序列分别进行插入排序
  • 缩小增量:减小增量的值(如减半),重复步骤二
  • 最终排序:当增量减小到1时,整个列表被视为一个子序列,进行最后一次插入排序。此时,由于列表已经基本有序,这次插入排序会非常快

举例说明:

对数组[8,3,5,1,4,7,2,6]进行希尔排序

初始数组为[8,3,5,1,4,7,2,6]

长度n=8,选择增量序列gap=n/2, n/4, n/8,....  即4,2,1

第一轮循环:

增量为4

子序列1:[8,4]      --->     [4,8]

子序列2:[3,7]    --->      [3,7]

子序列3:[5,2]    --->      [2,5]

子序列4:[1,6]    --- >      [1,6]

分别对这四个子序列进行插入排序,得到右边的排好序的子序列

在将这些排好序的子序列按照原来的位置放回原始数组

得到:

[4,3,2,1,8,7,5,6]

第二轮循环:

增量为2

子序列分别为[4,2,8,5]  [3,1,7,6]

分别对这两个子序列进行插入排序得到排好序的子序列:[2,4,5,8]  [1,3,6,7]

第二轮循环得到的数组:[2,1,4,3,5,6,8,7]

第三轮循环:

增量为1,此时整个数组就是一个子序列

再对其进行插入排序,得到最终的排好序的数组:[1,2,3,4,5,6,7,8]

def shell_sort(arr):
    n = len(arr)
    # 初始增量
    gap = n // 2
    # 循环直到gap缩小到0
    while gap > 0:
        # 从gap开始,对每个子序列进行插入排序
        for i in range(gap,n):
            temp = arr[i] # 当前要插入的元素
            j = i
            # 对子序列进行插入排序
            # j >= gap 确保不越界
            # arr[j - gap] > temp 是插入排序的比较条件
            while j >= gap and arr[j - gap] > temp:
                arr[j] = arr[j - gap] # 向后移动元素
                j -= gap
            arr[j] = temp # 插入到正确位置
        # 缩小增量
        gap //= 2
    return arr
if __name__ == "__main__":
    print(shell_sort([2,7,4,-7,9,6,-9,5]))

稳定性:希尔排序不稳定,在排序过程中,相等的原始可能会因为分属不同的子序列而改变相对次序

最坏情况下的时间复杂度:o(n^2)

最好情况下的时间复杂度: o(nlogn)

六.归并排序

核心思想:分而治之

把一个复杂的大问题,分解成若干个简单的小问题,然后逐个解决,最后将解决的小问题合并起来,就得到了大问题的答案

具体到排序上,分为三个步骤:

  • 分:将待排序的数组递归地“分割”成两个子数组,直到每个子数组只剩下一个元素(一个元素的数组本身就是有序的)
  • 治:将两个已经有序子数组“合并”成一个新的有序数组
  • 合:不断地进行合并操作,直到最终合并成一个完整的有序数组

分布详解:

假设要排序的数组为[38, 27, 43, 3, 9, 82, 10]

第一步:分(Split)

不断地从中间将数组拆分成两半,直到每个子数组只有一个元素

初始数组:[38, 27, 43, 3, 9, 82, 10]

第一次分:[38, 27, 43]和[3,9,82,10]

第二次分: [38] [27, 43] 和 [3, 9] [82, 10]

第三次分:[38] [27] [43] 和 [3] [9] [82] [10]

现在,我们得到了7个只有一个元素的子数组:[38] [27] [43]  [3] [9] [82] [10]

关键点:一个元素的数组天然就是有序的。所以现在我们有7个已经“排好序”的小数组

第二步:治(Merge)- 核心操作

现在开始“合并”,合并的过程就是排序的过程。如何将两个有序的数组合并成一个新的有序数组呢?

合并两个有序数组的算法:

1.创建一个临时数组,用来存放合并后的结果

2.设定两个指针,分别指向两个待合并数组的起始位置

3.比较两个指针所指的元素,将较小的那个元素放入临时数组,并将该指针向右移动一位

4.重复步骤3,直到其中一个数组的所有元素都被取完

5.将另一个数组的剩余所有元素直接追加到临时数组的末尾

6.将临时数组拷贝回原数组的对应位置

第三步:合(Combine)- 逐层合并

我们从最底层的一个元素数组开始合并,凉凉合并

第一层合并(合并单个元素)

合并[38]和[27] --> 比较38和27,27小,所以结果是[27, 38]

合并[43]和[3] --> 比较43和3,3小,所以结果是[3, 43]

合并[9]和[82] --> [9, 82]

[10]暂时没有可合并的,留到下一轮

此时数组状态:[27, 38], [3, 43], [9, 82], [10]

第二层合并

合并[27, 38]和[3, 43]  -->  比较27和3,取3;比较27和43,取27;比较38和43,取38;剩余[43],直接追加  --> 结果是:[3,27,38,43]

合并[9, 82]和[10]   -->  比较9和10,取9;比较82和10,取10;剩余[82],直接追加到临时数组中  -->  结果是:[9, 10, 82]

此时数组状态:[3, 27, 38, 43] [9, 10, 82] 

第三次合并(最终合并):

合并[3, 27, 38, 43] 和 [9, 10, 82]  -->  比较3和9,取3;比较27和9,取9;比较27和10,取10;比较27和82,取27;比较38和82,取38;比较43和82,取43;剩余[82],直接追加   -->  最终结果:[3, 9, 10, 27, 38, 43, 82]

def merge_sort(arr):
    """
    归并排序主函数
    """
    # 递归终止函数:数组长度为1或0时,已经是有序的
    if len(arr) <= 1:
        return arr
    # 1.分:找到中间点,分割数组
    mid = len(arr) // 2
    left_arr = arr[:mid] # 左半部分
    right_arr = arr[mid:] # 右半部分
    # 递归地对左右两部分进行排序
    left_sorted = merge_sort(left_arr)
    right_sorted = merge_sort(right_arr)
    # 2.治:合并两个有序数组
    return merge(left_sorted,right_sorted)
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
    # 将剩余元素添加到结果数组中
    # 以下两个while循环只会执行其中一个
    while i < len(left):
        result.append(left[i])
        i += 1
    while j < len(right):
        result.append(right[j])
        j += 1
    return result
# 测试示例
if __name__ == "__main__":
    # 测试用例
    test_arr = [38, 27, 43, 3, 9, 82, 10]
    print("原始数组:", test_arr)
    sorted_arr = merge_sort(test_arr)
    print("排序结果:", sorted_arr)
    # 更多测试
    test_cases = [
        [5, 2, 4, 6, 1, 3],
        [1],
        [],
        [3, 1, 2],
        [9, 8, 7, 6, 5, 4, 3, 2, 1]
    ]
    for i, case in enumerate(test_cases):
        print(f"测试用例 {i+1}: {case} -> {merge_sort(case)}")
#### 上面的版本会创建很多新数组,下面是一个更优化的原地排序版本:
def merge_sort_inplace(arr):
    """
    原地归并排序版本(修改原数组)
    """
    if len(arr) <= 1:
        return arr
    # 使用辅助函数进行递归
    _merge_sort_helper(arr,0,len(arr)-1)
    return arr
def _merge_sort_helper(arr,left,right):
    """
    递归辅助函数
    """
    if left < right:
        mid = (left + right) // 2
        # 递归排序左右两部分
        _merge_sort_helper(arr,left,mid)
        _merge_sort_helper(arr,mid+1,right)
        # 合并已排序的两部分
        _merge_inplace(arr,left,mid,right)
def _merge_inplace(arr,left,mid,right):
    """
    原地合并两个有序子数组arr[left:mid]和arr[mid+1:right]
    """
    # 创建临时数组存放要合并的数据
    temp = []
    i,j = left,mid+1
    # 比较并合并
    while i <= mid and j <= right:
        if arr[i] <= arr[j]:
            temp.append(arr[i])
            i += 1
        else:
            temp.append(arr[j])
            j += 1
    # 添加剩余元素
    while i <= mid:
        temp.append(arr[i])
        i += 1
    while j <= right:
        temp.append(arr[j])
        j += 1
    # 将临时数组的内容复制回原数组
    for k in range(len(temp)):
        arr[left+k] = temp[k]
# 测试原地排序版本
if __name__ == "__main__":
    print("\n=== 原地排序版本测试 ===")
    test_arr = [38, 27, 43, 3, 9, 82, 10]
    print("原始数组:", test_arr)
    # 注意:这个版本会修改原数组
    original = test_arr.copy()
    merge_sort_inplace(test_arr)
    print("排序结果:", test_arr)
    print("原数组已被修改:", original != test_arr)

posted @ 2025-10-22 10:32  wzzkaifa  阅读(6)  评论(0)    收藏  举报