冒泡排序

n个数从左至右,编号从0开始到n-1,索引0和索引1的值比较,如果索引0的值大,则交换两者位置,如果索引1大,则不交换;继续比较索引1和2的值,将大值放在右侧,直至n-2和n-1比较完,第一轮比较完成。第二轮从索引0比较到n-2,因为最右侧n-1位置上已经是最大值。依次类推,每轮都会减少最右侧不参与比较,直至剩下最后2个数比较。

# 测试数据
# lst = [1, 2, 3, 4, 5, 6]
# lst = [6, 5, 4, 3, 2, 1]
lst = [20, 12, 100, 3, 82, 6]

swap_count = 0  # 交换次数
count = 0
for i in range(len(lst)):
    count += 1
    for j in range(len(lst) - 1 - i): # (len(lst) - 1)是避免索引越界,再“-i”是因每轮循环后大值都在右边,可以不参考比较
        if lst[j+1] < lst[j]:
            swap_count += 1 # 统计数据交换次数
            tmp = lst[j]  # 使用一个临时变量交换两个数
            lst[j] = lst[j+1]
            lst[j+1] = tmp
            # print(j, lst)
print(lst)
print(swap_count)
print(count)

# 输出
[3, 6, 12, 20, 82, 100]
9
6

如果提供的测试数据为lst = [1, 2, 3, 4, 5, 6],这样的数据刚好是我们需要的目标数据,不需要进行交换,如果使用上边的代码依然需要迭代len(lst)次,如果真有这种刚好符合目标数据的数据,那可以进行如下优化。

lst = [1, 2, 3, 4, 5, 6]
# lst = [6, 5, 4, 3, 2, 1]
# lst = [20, 12, 100, 3, 82, 6]
# lst = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

swap_count = 0  # 交换次数
count = 0

for i in range(len(lst)):
    count += 1
    flag = False
    for j in range(len(lst) - 1 - i):
        if lst[j+1] < lst[j]:
            swap_count += 1
            tmp = lst[j]
            lst[j] = lst[j+1]
            lst[j+1] = tmp
            flag = True  # 只要有数据交换就设置flag为True
            # print(j, lst)
    if not flag:  # 进行了一轮的冒泡比较后发现没有进行数据交换,说明测试数据刚好符合需求,后边的循环测试就可以不做了
        break

print(lst)
print(swap_count)
print(count)
# 输出
[1, 2, 3, 4, 5, 6]
0
1  

降序排列只需要把判断条件进行修改即可,即“if lst[j+1] < lst[j]”修改为“if lst[j+1] > lst[j]”,而两值相等时不用交换数据,可以不考虑。

程序中使用for循环和if做条件判断时都非常快,没有复杂的逻辑就可以不进行优化,避免过度优化,需要衡量投入和产出比。

当初始数据与目标数据完全相反,需要交换数据的次数为n(n-1)/2,当初始数据与目标数据完全相同时,需要交换数据的次数为0

第一层for循环是需要进行n次迭代,无法避免;而第二层for循环可以进行优化适当减少迭代次数,但整体上来以时间复杂度来衡量,那冒泡排序法的时间复杂度应该属于O(n<sup>2</sup>)

快速排序

原理:

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。

  1. 挑选一个基准值,从数列中挑选一个基准值(pivot),一般选择第一个元素或最后一个元素;
  2. 分割,重新排序数列。所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边),在这个分割结束之后,对基准值的排序就已经完成,现在形成了两个子序列;
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

arr = [10, 20, 15, 5, 7, 9]序列为例说明:

  1. 以最后一个元素9为基准值,循环该序列(range(0, len(arr)-1)),如果第一次遇到元素值小于基准值时,就把该元素与索引为0的值进行交换,序列变为[5, 20, 15, 10, 7, 9],如果第二次遇到元素值小于基准值时,就把该元素与索引为1的值进行交换,序列变为[5, 7, 15, 10, 20, 9], 依次类推,所以比基准值小的元素都会放到序列的开头部分;
  2. 当循环结束后,基准值应该与最后一次交换数据时索引加1的元素进行交换,序列变为[5, 7, 9, 10, 20, 15],这样就完成了一次序列的分割,这样元素9前边的元素都比9小,9后边的元素都比9大;
  3. 一次分割完成了可以得到两个未排序的子序列[5, 7][10, 20, 15],再回到第1、2步递归处理子序列就可以得到一个排序后的数列。

代码实现:

def partition(arr, low, high):
    pivot = arr[high]  # 选取最后一个元素为基准
    i = low  # 最小元素索引

    for j in range(low, high):
        # 当前元素小于或等于 pivot 
        if arr[j] <= pivot:
            arr[i], arr[j] = arr[j], arr[i]
            i += 1
    # 循环完成后,需要把基准值与索引为i的元素进行替换,因为在上边的for循环最后 i+=1
    arr[i], arr[high] = arr[high], arr[i]
    return i  # 返回调整后基准元素所在的索引


# 快速排序函数
def quick_sort(arr, low, high):
    """
    :param arr: 排序数组
    :param low: 起始索引
    :param high: 结束索引
    :return: None
    """
    if low < high:
        pivot_index = partition(arr, low, high)
        # print('----', pivot, arr)
        quick_sort(arr, low, pivot_index - 1)   # 基准值左边排序
        quick_sort(arr, pivot_index + 1, high)  # 基准值右边排序


arr = [10, 9, 15, 5, 7, 9]
quick_sort(arr, 0, len(arr)-1)
print(arr)

此快速排序未新生成新的数据结构,都是在原的list中进行元素的交换,就空间复杂度来说,主要是递归造成的栈空间的使用,平均来说空间复杂度为O(logn),随着序列规模的增大,递归调用的层数增加,进行分割序列的次数及每个子序列的循环次数也会增多,平均来说时间复杂度也为O(nlogn)。关于空间复杂度和时间复杂度请参考:https://blog.csdn.net/weixin_41539756/article/details/97299813

在分割数列时,即partition函数的实现还有其他的方式,还是以arr = [10, 20, 15, 5, 7, 9]序列为例说明:

  1. 选择第一个元素为基准值,从左向右需要比较的起始索引为l=1,从右向左需要比较的索引起始为r=len(arr)-1,如果索引i的值<基准值,再把索引l+1的值与基准值比较,右边也是如此,如果索引r的值>基准值时,再把索引r-1的值再与基准值比较;当有一边不能满足与基准值的比较条件时,那把此时对应的索引的元素进行交换,交换后再与基准值进行比较,如此一个个把元素与基准值进行比较,最后当lr相遇时就没有必要再与基准值进行比较了。

来看下该函数的代码实现:

def partition(arr, low, high):
    pivot = arr[low]
    left = low + 1
    right = high
    while left <= right:
        while left <= right and arr[left] <= pivot:
            left += 1
        while left <= right and arr[right] >= pivot:
            right -= 1
        if left > right:  # 越界了,可以直接退出循环
            break
        else:  # 说明上边的与基准值比较中有不满足条件的,此时就交换
            arr[left], arr[right] = arr[right], arr[left]
    # 最后循环完成后基准值要放在索引为right的位置
    arr[low], arr[right] = arr[right], arr[low]
    return right

选择排序

n个数从左至右,索引从0n-1,两两依次比较,记录大值的索引,此轮所有数比较完成,将大数与索引为0的数交换位置,如果大数为索引0的数则不交换,此时极大值在最左边;第二轮从索引1开始与后边的数进行比较,找出极值,将与索引为1的位置进行交换,如果索引1的值为极值则不交换;依此类推,每次左边都会固定一个极值,最后形成一个降序序列。

test_list = [
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
    [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
    [2, 11, 7, 13, 9, 11, 7, 12, 12, 19]
]

nums = test_list[0]  # 更换测试数据,观察交换数据的次数及迭代的次数
length = len(nums)
count_swap = 0
count = 0

for i in range(length):
    max_value_index = i  # 假设极值索引为i
    for j in range(i + 1, length):
        count += 1
        if nums[j] > nums[max_value_index]: # 右边数大于假定极值时
            max_value_index = j  # 改变极值索引,一轮for循环后就可标记出极大值
    if max_value_index != i:  # 两值不相等说明极值索引已发生改变,需要交换
        nums[max_value_index], nums[i] = nums[i], nums[max_value_index]
        count_swap += 1
        
print(nums)
print(count_swap)
print(count)
        
# 输出
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
5
45

实现的思想是在循环中找出极值的索引,在比较时极值索引被重新指向到新值时,那进行数据交换,与冒泡法相比数据交换次数相对较少,但迭代次数一次都不能少,因为迭代时只是找出极值,并不知道其他值的大小。

直接插入排序

插入排序原理:

  1. 在一个未排序的序列中,构建一个排序序列,直至全部数据排序完成
  2. 将待排序数插入到已排序序列的合适位置
  3. 增加一个哨兵,将待排序数放入到哨兵位,让它和已排序序列进行比较,找到合适的插入点插入

以升序排列为例,如待排序序列为1,9,8,5,6,先增加一个哨兵位,增加后序列为0,1,9,8,5,6;假定1为已排序区,后边的就是未排序区,

第一轮: 9为需要排序的第一个数,先把9放入到哨兵位,序列为9,1,9,8,5,6,将哨兵位的9与排序区的1进行比较,1小于哨兵位的9,不动,有序区变为1,9,本轮结束;

第二轮:把8放入到哨兵位,序列为8,1,9,8,5,6,把哨兵位与已排序区从右向左取值后依次比较,9大于哨兵位8,需要向右移动,1小于哨兵位8,不动,移动后序列为8,1,8,9,5,6,有序区变为1,8,9,本轮结束;

第三轮:把5放入到哨兵位,序列为5,1,8,9,5,6,把哨兵位与已排序区从右向左取值后依次比较,9大于哨兵位5,需要向右移动,8大于哨兵位5,同样需要向右移动,1小于哨兵位5,不动,移动后序列为5,1,5,8,9,6,有序区为为1,5,8,9,本轮结束;

第四轮:把6放入到哨兵位,序列为6,1,5,8,9,6,把哨兵位与已排序区从右向左取值后依次比较,98都大于哨兵位6,向右移,5小于哨兵位5,该位置后处插入哨兵位,移动后序列为6,1,5,6,8,9,本轮结束,排序结束。

m_list = [
    [1, 2, 3, 4, 5, 6],
    [6, 5, 4, 3, 2, 1],
    [23, 10, 21, 45, 3, 10]
]
loop_count = 0
move_count = 0

nums = [0] + m_list[1]  # 加入哨兵位
for i in range(2, len(nums)): # 索引为0是哨兵位,索引为1是初始的比较值,假定的有序区,索引2开始为待比较数
    loop_count += 1
    nums[0] = nums[i]  # 哨兵位赋值
    j = i - 1 # 有序区最右边的索引
    if nums[j] > nums[0]:
        while nums[j] > nums[0]:
            move_count += 1
            nums[j+1] = nums[j]
            j -= 1  # 排序区索引向左走
        nums[j + 1] = nums[0]  # 已排序区的数小于哨兵位时把哨兵位插入到合适的位置,因while中最后多减了1,这里需要再加上1
print(nums)
print(loop_count, move_count)
# 输出
[1, 1, 2, 3, 4, 5, 6]
5 15

最差情况,正好是降序排列,比较迭代n-1次,右移迭代n(n-1)/2

直接插入排序是一种稳定排序算法,列表中值相同的元素不会进行移动,保持原有位置的稳定。

堆排序

堆排序原理

堆排序是建立在完全二叉树的数据结构下。树的每个结点为待排序的数据,堆分为大顶堆小顶堆

大顶堆:完全二叉树的每个非叶子结点都要大于或等于其左右孩子结点的值称为大顶堆;根结点一定是大顶堆中最大值

小顶堆:完全二叉树的每个非叶子结点都要小于或等于其左右孩子结点的值称为小顶堆;根结点一定是小顶堆中最小值

根据构建大顶堆或小顶堆可实现升序或降序排列。

给定一个待排序序列,如:30,20,80,40,50,10,60,70,90,84,依次编号把该序列中的数据放入完全二叉树中,如下图

各个结点中的值与编号正好与[0,30,20,80,40,50,10,60,70,90,84]这个列表对应,索引0处增加一个占位数据。

现在需要让树构建成一个大顶堆或小顶堆,以构建大顶堆为例。

构建大顶堆的核心算法:

1. 度数为2的结点A,如果它的左右孩子结点的最大值比它大,将这个最大值和该结点A交换
2. 度数为1的结点A,如果它的左孩子的值大于它,则进行交换
3. 度数为1或2的结点A的编号为i,其(度数为1时)左孩子编号为2i, (度数为2时)右孩子的编号为2i+1
3. 如果结点A被交换到新的位置,还需要和其孩子结点重复上边的过程

构建大顶堆起始结点的选择:

1. 从完全二叉树的最后编号的结点的双亲结点开始,即最后一层的最右边叶子结点的双亲结点开始
2. 一个有n个结点的完全二叉树,最后一个结点的双亲结点为 n//2,这是完全二叉树的性质,如上图中编号为5结点就是起始结点

构建大顶堆下一结点的选择:

1. 从起始结点开始向左找其同一层的结点,到头后再从上一层的最右边结点开始继续向左逐个查找,直至到根结点。如上图中的查找顺序为编号5,编号4,编号3,编号2,编号1

实现大顶堆的核心算法以及利用完全二叉树的性质已交待清楚,为了更好的观察在实现大顶堆时各个结点上数据的调整,可以先实现一个工具函数,它能将[1, 2, 3, 4, 5, 6, 7, 8, 9]这样一个list打印成一颗树,如下:

               1
       2               3
   4       5       6       7
 8   9

打印一颗树

方案一

元素居中对齐方案

实现思想:

1. 结点数为n(n从1开始)的完全二叉树的的深度depth,那depth=math.ceil(log2(n+1))
2. 深度为depth,那最后一层的宽度为 2**depth - 1
3. 树有几层,就需要打印几行,而层数与每层需要打印结点数有以下规律,如果树的层数也从0开始,那第0层打印一个结点(2**0), 第一层打印两个结点(2**1),第二层打印4个结点(2**2),第三层打印8个结点(2**3)...,即树的深度是几就需要循环几次,即为range(depth)的循环次数,而每层需要打印的结点数又与层数有关,如果层数为i,那 每层需要打印的结点数为"range(2**i)"的取值个数
4. 边界问题,当取到结点编号的最后一个时表示元素已取完
5. 各层每个元素的宽度问题,最后一层的宽度为 2**depth - 1,即是每层打印的总体宽度,第0层结点数为1,元素宽度为(2**depth - 1),第1层结点数为2,元素宽度为(2**depth-1)//2,依此类推,下一层的结点个数是上一层的2倍,而结点的宽度是上一层结点宽度的一半(无法整除时向下取整)

实现代码

import math

def print_tree(array, unit_width=2):
    # array: list
    # unit_width: 基本宽度
    length = len(array)  # 结点个数
    depth = math.ceil(math.log2(length + 1)) # 树的深度
    width = 2**depth - 1 # 最大投影宽度
    
    index = 0
    for i in range(depth): # 打印层数
        for j in range(2 ** i):  # 每层打印元素个数
            print('{0:^{1}}'.format(array[index], width * unit_width), end=' ' * unit_width) # 每个元素按照相应的宽度居中打印,format格式化输出中^表示居中
            index += 1  # 打印一个元素后计数加1
            if index == length:  # 最后一个元素时退出内层循环
                break
        print()
        width = width // 2 # 下一层每个元素的宽度
        
print_tree([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# 输出
              1
      2               3
  4       5       6       7
8   9   10

方案二

投影格栅方案

              1                 
      2               3         
  4       5       6       7     
8   9   10  11  12  13  14  15  

算法实现思想:

1. 结点数为n(n从1开始)的完全二叉树的的深度depth,那depth=math.ceil(log2(n+1))
2. 树中的每个结点看成有一定宽度单位的元素
3. 每一层看成是由“前导空白字符+元素+元素间空白字符”组成的行,
   第一层有1(2**0)个元素,前有7个单位元素宽度的空白,元素间有0个单位元素宽度的空白(只有一个元素,特殊看待)
	 第二层有2(2**1)个元素,前有3个单位元素宽度的空白,元素间有7个单位元素宽度的空白
	 第三层有4(2**2)个元素,前有1个单位元素宽度的空白,元素间有3个单位元素宽度的空白
	 四层有8(2**3)个元素,前有0个单位元素宽度的空白,元素间有1个单位元素宽度的空白
   第一层到第四层的前导空白元素分别为 7, 3, 1, 0,正好是 2**(4-0-1) -1, 2**(4-1-1)-1, 2**(4-2-1)-1, 2**(4-3-1)-1
	 正好在range(4),取值 0,1,2,3  这个循环中所取得值。
	 第一层到第四层各结点元素间的空白间隔分别为 0,7, 3, 1,正好是 0要特殊处理,2**(4-1)-1,2**(4-2)-1, 2**(4-3)-1
   同样是在range(4),取值 0,1,2,3  这个循环中所取得值。
4. 第一次循环取1个元素(2**0),第二次循环取2个元素(2**1),第三次循环取4个元素(2**2),第四次循环取8个元素(2**3)
       lst=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
       循环次数    需要取得元素                       切片操作
           1            1                          lst[1:2] = lst[1:1+2**0]
           2            2,3                        lst[2:4] = lst[2:2+ 2**1]
           3            4,5,6,7                    lst[4:8] = lst[4:4+2**2]
           4            8,9,10,11,12,13,14,15      lst[8:16]= lst[8:8+2**3]
       可以找出一个规律:
In [939]: lst=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]  # 0是占位
In [942]: depth=4  # 为树的深度 
     ...: index = 1  # 索引的起始值 
     ...: for i in range(depth): 
     ...:     line = lst[index:index + 2**i] 
     ...:     print(line) 
     ...:     index += 2**i 
     ...:                                                                 
[1]
[2, 3]
[4, 5, 6, 7]
[8, 9, 10, 11, 12, 13, 14, 15]

代码实现:

import math
def print_tree(array, unit_width=2):
    index = 1
    depth = math.ceil(math.log2(len(array)))  # 不用加1,因为array增加了索引0处的占位
    for i in range(depth):
        # 打印前导空白字符
        print(' '*unit_width * (2**(depth-i-1)-1), end='')
        # 取一行中的元素
        offset = 2**i  # 第一层2**0, 第二层2**1, 第三层2**2,...
        line = array[index:index + offset]
        # 打印元素及元素间的空白
        for j, x in enumerate(line):
            print('{:>{}}'.format(x, unit_width), end='') # 打印元素
            interval = 0 if i == 0 else 2 ** (depth - i) - 1  # 结点元素间的空白宽度
            if j < len(line) - 1: # 一行中最后一个结点打印后,后边的空白宽度不必打印
                print(' '*unit_width * interval, end='') # 元素间的空白
        index += offset
        print()
test_lst = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]  
test_lst.insert(0, 0)
print_tree(test_lst)
# 输出
               1
       2               3
   4       5       6       7
 8   9  10  11  12  13  14  15

有了这个打印树的函数就可以观察大顶堆的构建过程。

堆调整

待排序列表lst = [0,30,20,80,40,50,10,60,70,90,84]中索引0位为占位数据,不管它;那数据长度为n=len(lst)-1,构建大顶堆起始结点索引为n//2,其左孩子索引为2i,右孩子(若有)索引为2i+1

def heap_adjust(n, i, array: list):
    # n: 待比较数的个数
    # i: 起始结点的索引,i=n//2
    # array: 待排序数据,是一个列表,列表在在开始补0
    while 2 * i <= n:  # 2*i与n的关系,如果2*i>n了,那索引i处的结点是叶子结点,不参与调整
        # 索引为i的结点一定有左孩子,不一定有右孩子
        left_child_index = 2 * i
        max_child_index = left_child_index # 假设左孩子值最大
        if left_child_index < n:  # 说明结点i一定有右孩子
            if array[left_child_index + 1] > array[left_child_index]:  # 右孩子大于左孩子
                max_child_index = left_child_index + 1 # 改变最大值的索引
        
        # 左右孩子的大值与该树的根进行比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i] # 交换两个结点的数据
            # 数据交换后,需要判断交换后的数据所在结点是否有左孩子或左右孩子,所以需要把i=max_chile_index,
            # 如果有则再需要调整。即又回到while循环判断 2 * i <= n
            i = max_child_index
            print_tree(array)  # 调整后可打印出来看下树的结构
        else:
            break # 如果条件不满足就退出while循环,不进行数据交换
        
lst = [0, 30, 20, 80, 40, 50, 10, 60, 70, 90, 84]
print_tree(lst)
n = len(lst) - 1
heap_adjust(n, n//2, lst)

# 输出
              30
      20              80
  40      50      10      60
70  90  84
              30
      20              80
  40      84      10      60
70  90  50

# 50和84两个数据进行了交换

构建大顶堆

上边的代码只解决了起始结点(索引为5)的数据调整,还需要依次解决索引分别为4,3,2,1的结点,按照这样的顺序调整完成后就能得到一个大顶堆。

# 构建大顶堆
def max_heap(n, array: list):
    for i in range(n//2, 0, -1):
        heap_adjust(n, i, array)
    return array
  

print_tree(lst)
print(max_heap(n, lst))

# 输出
              30
      20              80
  40      50      10      60
70  90  84
              30
      20              80
  40      84      10      60
70  90  50
              30
      20              80
  90      84      10      60
70  40  50
              30
      90              80
  20      84      10      60
70  40  50
              30
      90              80
  70      84      10      60
20  40  50
              90
      30              80
  70      84      10      60
20  40  50
              90
      84              80
  70      30      10      60
20  40  50
              90
      84              80
  70      50      10      60
20  40  30
[0, 90, 84, 80, 70, 50, 10, 60, 20, 40, 30]

# 最后输出了一个大顶堆

构建大顶堆后有一个规律: 最大的值一定在第一层,第二层一定有一个次大的值

排序

排序思想:

1. 每次都让堆顶的元素和最后一个结点元素进行交换,然后排除最后一个元素,形成一个新的被破坏的堆
2. 被破坏的堆实质上第二层就有一个最大值,直接从索引为1的结点调整,调整后堆顶就是最大值
3. 再次重复1, 2步直至剩下一个元素

代码实现:

# 排序
def sort(n, array: list):
    while n > 1:
        array[1], array[n] = array[n], array[1]   # 第一个位置与最后一个位置交换,极值固定在最后
        n -= 1  # 在最后固定极值后,下一次调整堆时需要排除在外
        heap_adjust(n, 1, array)  # 调整堆,直接调整索引为1的结点,因该结点是被交换后的数据
    return array

print('=' * 20)

ret = sort(n, lst)
print(ret)
print_tree(ret)
# 输出
              84
      30              80
  70      50      10      60
20  40  90
              84
      70              80
  30      50      10      60
20  40  90
              84
      70              80
  40      50      10      60
20  30  90
              80
      70              30
  40      50      10      60
20  84  90
              80
      70              60
  40      50      10      30
20  84  90
              70
      20              60
  40      50      10      30
80  84  90
              70
      50              60
  40      20      10      30
80  84  90
              60
      50              30
  40      20      10      70
80  84  90
              50
      10              30
  40      20      60      70
80  84  90
              50
      40              30
  10      20      60      70
80  84  90
              40
      20              30
  10      50      60      70
80  84  90
              30
      20              10
  40      50      60      70
80  84  90
              20
      10              30
  40      50      60      70
80  84  90
[0, 10, 20, 30, 40, 50, 60, 70, 80, 84, 90]
              10
      20              30
  40      50      60      70
80  84  90
# 最后就是进行升序排序的数据

堆排序总结

  1. 堆排序是利用堆性质的一种选择排序,在堆顶选出最大值或最小值

  2. 时间复杂度,堆排序的时间复杂度为O(NlogN),由于堆排序对原始排序序列的顺序并不敏感,因此它无论是最好的情况(原始序列就是最终的排序序列)、最坏的情况(原始序列与最终序列完全相反),时间复杂度均为O(NlogN)

  3. 空间复杂度,只使用了一个交换用的空间,空间复杂度为O(1)

  4. 稳定性,是一种不稳定的排序算法,不稳定性主要体现在当有相同元素时,可能会被交换