数据结构与算法_part_5_排序:①冒泡排序、②选择排序、③插入排序、④快速排序

0. 排序与搜索

排序算法(Sorting algorithm)是一种能将一串数据依照特定顺序进行排序的一种算法。

0.1 排序算法的稳定性

稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以它们的第一个数字来排序(升序)。

(4, 1) (3, 1) (3, 7) (5, 6)

在这个状况下,有可能产生两种不同的结果,一种是让相等键值的记录维持相对的次序,而另一种则没有:

(3, 1) (3, 7) (4, 1) (5, 6)    (维持次序)   -> 稳定的
(3, 7) (3, 1) (4, 1) (5, 6)    (次序被改变) -> 不稳定的

这就是排序算法的稳定性最通俗易懂的解释😂

1. 冒泡排序 (Bubble Sort)

冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

冒泡排序算法的运作如下:

  1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

1.1 冒泡排序的分析

交换过程图示(第一次):

在这里插入图片描述

1.2 冒泡排序法动画演示

在这里插入图片描述

如果按照这样的思路走下去,我们需要进行 n − 1 n-1 n1次冒泡过程,每次对应的比较计数如下:

第几次遍历比较次数
1 n − 1 n-1 n1
2 n − 2 n-2 n2
3 n − 3 n-3 n3
n − 1 n-1 n11

1.3 冒泡法Python程序实现

def bubble_sort(alist):
    """
        线性表都可以,我们这里用顺序表
        如果作用到链表上去,那么交换的就不是数据区的数据,
        而是交换节点(比顺序表复杂)
    """
    n = len(alist)
    """
        注意索引是从0开始的,所以对于n个元素来说:
            + 第一个元素应该是0;
            + 最后一个元素应该是n-1
        而我们比大小的时候,将指针放到n-2那么数就可以了,
        没必要非得到n-1(可以减少部分计算量)
        
        第1次走:0 ~ n-2 -> range(n-1) j=0
        第2次走:0 ~ n-3 -> range(n-1-1) j=1
        第3次走:0 ~ n-4 -> range(n-1-1-1) j=2
        ...
        第n-1次走:0 ~ 1 -> range(n-1-j) j=n-2
        第n次走:0 ~ 0 -> range(n-1-j) j=n-1
    """
    for j in range(n-1):  # 总共需要n-1个大循环
        for i in range(n-1-j):
            if alist[i] > alist[i+1]:  # 前一个位置的数据比后一个数据大 -> 交换二者
                alist[i], alist[i+1] = alist[i+1], alist[i]
            else:
                pass  # 不需要交换


if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(ls)
    bubble_sort(ls)
    print(ls)

结果:

[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]

Python中的List,如果不用深拷贝则都是浅拷贝,通俗易懂的说法就是,List里面的值一动,那么不管有没有赋值给List,List里面的值也会发生变化(因为List是顺序表,动里面的值,相当于是把数据区的值改变了,而地址不发生变化)

1.4 冒泡排序法的两种代码

1.4.1 正循环

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

for j in range(len(alist) - 1):j从0开始,一直到n-2

[0, 1, 2, 3, ..., n-2]

1.4.2 逆循环

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

for j in range(len(alist)-1, 0, -1):jn-1开始,一直到1

[n-1, n-2, n-3, ..., 1]


这两种方法都可以,哪个好理解就用哪个。

1.5 冒泡法时间复杂度

  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 最优时间复杂度: O ( n 2 ) O(n^2) O(n2)

    因为不会停,所以仍然是 O ( n 2 ) O(n^2) O(n2)

  • 稳定性:稳定

1.6 冒泡排序法的优化版

1.6.1 举例

对于这样一个顺序表[1, 2, 3, 4, 5, 6],即便它本身已经是有序了的,但仍需要重头往后走走一遍。时间复杂度仍是 O ( n 2 ) O(n^2) O(n2)

1.6.2 优化思路

在第一次排序时,如果从头走到尾都没有发生序列的交换 -> Okay,该顺序表是有序的,没必要排序!

1.6.3 代码实现

def bubble_sort(alist):
    for j in range(len(alist)-1):
        count = 0  # 交换计数
        for i in range(len(alist)-1-j):
            if alist[i] > alist[i+1]:
                alist[i], alist[i+1] = alist[i+1], alist[i]
                count += 1
        if count == 0:
            print("数据是有序的, 无须排序!")
            return


if __name__ == "__main__":
    ls = [1, 2, 3, 4, 5, 6]
    print(ls)
    bubble_sort(ls)
    print(ls)

结果:

[1, 2, 3, 4, 5, 6]
数据是有序的,无须排序!
[1, 2, 3, 4, 5, 6]

1.6.4 优化冒泡法后的时间复杂度

  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 最优时间复杂度: O ( n ) O(n) O(n)

    (表示遍历一次发现没有任何可以交换的元素,排序结束)

  • 稳定性:稳定

2. 选择排序(Selection Sort)

2.1 选择排序的概念及定义

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的未尾。
  3. 以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n n n 个元素的表进行排序总共进行至多 n − 1 n-1 n1 次交换。

选择排序最多执行 n − 1 n-1 n1 次元素交换

在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

注意这里的限定条件:完全依靠交换去移动元素的排序方法中 😂

2.1 选择排序分析

在这里插入图片描述

2.2 选择排序动画演示(选择最小的)

在这里插入图片描述

2.3 选择排序代码实现

def selection_sort(alist):
    """选择排序"""
    
    n = len(alist)
    for j in range(0, n-1):  # [0, n-2]
        """
            j = [0, 1, 2, 3, ..., n-2]
                如果从1开始,那么最后一个元素为n -> j = [1, 2, 3, ..., n]
                如果从0开始,那么最后一个元素为n-1 -> j = [0, 1, 2, ..., n-1]
                
            选择排序总共需要排n-1次即可,而j的索引是从0开始的,
            即最后一个元素的索引为n-1,所以这里j的最大索引因该
            为n-1-1 -> n-2,即range(0, n-1)
        """
        min_idx = j  # 最小值的索引
        for i in range(j+1, n):  # [j+1, n-1]
            """
                这里 i = [1, 2, 3, 4, ..., n]
                而我们是前一个数和后一个数作比较,
                所以i只需取到倒数第一个元素即可
                所以 i = [1, 2, 3, 4, ..., n-1],
                即range(j+1, n)
            """
            if alist[min_idx] > alist[i]:
                min_idx = i  # 不停更改min的index
        # 上面循环结束 -> 找到了最小元素的索引 -> 进行元素交换
        alist[j], alist[min_idx] = alist[min_idx], alist[j]
    

if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    selection_sort(ls)
    print(f"after: {ls}")

结果:

before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]

2.4 简洁代码-选择排序

def selection_sort(alist):
    n = len(alist)
    
    for j in range(0, n-1):  # j \in [0, n-2]
        min_idx = j
        for i in range(j+1, n):  # i \in [1, n-1]
            if alist[min_idx] > alist[i]:
                min_idx = i
        alist[j], alist[min_idx] = alist[min_idx], alist[j]

2.5 选择排序的时间复杂度

def selection_sort(alist):
    n = len(alist)
    
    for j in range(0, n-1):  # j \in [0, n-2] -> O(n)
        min_idx = j
        for i in range(j+1, n):  # i \in [1, n-1] -> O(n)
            if alist[min_idx] > alist[i]:
                min_idx = i
        alist[j], alist[min_idx] = alist[min_idx], alist[j]
  • 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)

  • 最优时间复杂度为 O ( n 2 ) O(n^2) O(n2)

    即便是一个有序的列表,选择排序还是会比较每一个元素

  • 稳定性:不稳定

目前选择排序没有更优的方式来实现

2.6 选择排序不稳定证明

e.g.升序每次选择最大的情况

Data: [26, 16, 17, 15, 26, 11, 10, 9]

在这里插入图片描述

如果是一个稳定的算法,那么一开始蓝色26在粉色26的前面,最后排完序后,蓝色的26也还应该在粉色26的前面。但选择排序不行,所以它是不稳定的。

3. 插入算法(Insertion Sort)

3.1 插入算法的概念

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,
找到相应位置井插入。

插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

3.2 插入算法动画演示

在这里插入图片描述

3.3 插入排序分析

data = [54, 26, 93, 17, 77, 31, 44, 55, 20]

"""
	核心思想:依次交换,保证前面数据的有序性
"""
No.1:  [54,          26, 93, 17, 77, 31, 44, 55, 20]
No.1:  [26, 54,     93, 17, 77, 31, 44, 55, 20]
    
No.2:  [26, 54, 93,     17, 77, 31, 44, 55, 20]

No.2:  [26, 54, 17, 93,      77, 31, 44, 55, 20]
No.2:  [26, 17, 54, 93,      77, 31, 44, 55, 20]
No.3:  [17, 26, 54, 93,     77, 31, 44, 55, 20]
    
No.4:  [17, 26, 54, 77, 93,     31, 44, 55, 20]
    
No.5:  [17, 26, 54, 77, 31, 93,     44, 55, 20]
No.5:  [17, 26, 54, 31, 77, 93,     44, 55, 20]
No.5:  [17, 26, 31, 54, 77, 93,     44, 55, 20]    
    
No.6:  [17, 26, 31, 54, 77, 44, 93,     55, 20]
No.6:  [17, 26, 31, 54, 44, 77, 93,     55, 20]
No.6:  [17, 26, 31, 44, 54, 77, 93,     55, 20]
    
No.7:  [17, 26, 31, 44, 54, 77, 55, 93,     20]
No.7:  [17, 26, 31, 44, 54, 55, 77, 93,     20]
    
No.8:  [17, 26, 31, 44, 54, 55, 77, 20, 93]
No.8:  [17, 26, 31, 44, 54, 55, 20, 77, 93]
No.8:  [17, 26, 31, 44, 54, 20, 55, 77, 93]
No.8:  [17, 26, 31, 44, 20, 54, 55, 77, 93]
No.8:  [17, 26, 31, 20, 44, 54, 55, 77, 93]
No.8:  [17, 26, 20, 31, 44, 54, 55, 77, 93]
No.8:  [17, 20, 26, 31, 44, 54, 55, 77, 93] 

3.4 选择排序和插入排序的区别

选择排序和插入排序都将序列看成了两部分,如下所示:

在这里插入图片描述

  • 选择排序是对后面的无序数据选择最小(最大)的,然后按顺序放置 -> 保证前面的是有序
  • 插入排序,直接取后面的一个数,保证前面的是有序

二者有一些类似,但是对后面无序数据的处理方式是不同的。

3.5 插入算法代码实现

3.5.1 第一种方法

def insertion_sort(alist):
    """插入排序"""
    n = len(alist)
    
    for j in range(1, n):  # j \in [1, n-1]
        """
            外层循环:从右边的无序序列中取出多少个元素执行内层循环
            i:内层循环起始值
            内层循环:执行从右边的无序序列中依次取出第一个元素,
            即i位置的元素;然后将其插入到前面的正确位置中
        """
        i = j  # i = [1, 2, 3, ..., n-1]
        while i > 0:
            if alist[i] < alist[i-1]:
                # 交换位置
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1  # 让其继续让前面的数作比较
            else:  # >= 不用移动(循环可以退出)
                break


if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    insertion_sort(ls)
    print(f"after: {ls}")

结果:

before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]

3.5.2 第二种方法

def insertion_sort(alist):
    # 从第二个位置,即下标为1的元素开始向前插入
    for i in range(1, len(alist)):  # i∈[1, n-1] -> 比较n-1次
        # 从第i个元素开始向前比较,如果小于前一个元素,则交换位置
        for j in range(i, 0, -1):  # 逆着比较
            if alist[j] < alist[j-1]:
                alist[j], alist[j-1] = alist[j-1], alist[j]


if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    insertion_sort(ls)
    print(f"after: {ls}")

结果:

before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]

3.6 插入排序时间复杂度

3.6.1 第一种方法的时间复杂度

def insertion_sort(alist):
    n = len(alist)
    
    for j in range(1, n):  # O(n)
        i = j
        while i > 0:  # 最坏的情况就是最后一个是最小的 -> O(n)
            if alist[i] < alist[i-1]:
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1
            else:
                break
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 最优时间复杂度: O ( n ) O(n) O(n)(升序排列,且已有序)

3.6.2 第二种方法的时间复杂度

def insertion_sort(alist):
    for i in range(1, len(alist)):  # O(n)
        for j in range(i, 0, -1):  # O(n)
            if alist[j] < alist[j-1]:
                alist[j], alist[j-1] = alist[j-1], alist[j]
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)

  • 最优时间复杂度: O ( n 2 ) O(n^2) O(n2)

    即便排好了,也要继续

3.7 ⭐️第二种方法的改进

def insertion_sort(alist):
    # 从第二个位置,即下标为1的元素开始向前插入
    for i in range(1, len(alist)):  # i∈[1, n-1] -> 比较n-1次
        # 从第i个元素开始向前比较,如果小于前一个元素,则交换位置
        for j in range(i, 0, -1):  # 逆着比较
            if alist[j] < alist[j-1]:
                alist[j], alist[j-1] = alist[j-1], alist[j]
            else:  # 倘若不成立,则立马break
                break


if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    insertion_sort(ls)
    print(f"after: {ls}")

3.8 小结

  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 最优时间复杂度: O ( n ) O(n) O(n)(升序排列,且已有序)
  • 稳定性:稳定

4. 快速排序(快排,Quick Sort)

4.1 快排的概念和定义

快速排序(Quick sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。


步骤为:

  1. 从数列中挑出一个元素,称为“基准”(pivot)

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基
    准值大的摆在基准的后面(相同的数可以到任一边)。

    在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子
    数列排序。


递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的送代(iteration)中,它至少会把一个元素摆到它最后的位置去。


pivot 英[ˈpɪvət][ˈpɪvət]
n. 支点; 枢轴; 中心点; 最重要的人(或事物); 中心; 核心;
v. (使)在枢轴上旋转(或转动);


partition 英[pɑːˈtɪʃn] 美[pɑːrˈtɪʃn]
n. 隔断; 分割; 隔扇; 隔板墙; 分治; 瓜分;
vt. 分割; 使分裂;


recursive 英[rɪˈkɜːsɪv] 美[rɪˈkɜːrsɪv]
adj. 递归的; 循环的;

4.2 快排的动画演示

在这里插入图片描述

在这里插入图片描述

4.3 快速排序分析

之前我们将的选择排序和插入排序都是将序列划分为两部分来排序,将其中一部分的元素往另外一部分去做,保证一部分是有序的即可。

现在我们需要改变一下思路。

Data: [54, 26, 93, 17, 77, 31, 44, 55, 20]

对于上面的无序序列,我们不把它视为两部分。序列的第一个元素是54,那我们就看一下,54放到有序序列中的位置是哪儿?——我们直接找54的位置。

想要达到这样的效果,我们需要有两个游标(指针)。

在这里插入图片描述

如果我们找到了54的位置:

在这里插入图片描述

如果我们按照升序去排序,那么:

  • 54左边的都比54小
  • 54右边的都比54大

只有满足这两个条件,54的位置才是正确的。


为了达到这样的方式,我们需要使用lowhigh游标:

  • low:所经历过的元素必须全部比54
  • high:所经历过的元素必须全部比54

当二者交汇时,54的位置就确定了!

在这里插入图片描述


那么这个过程应该如何使用代码实现呢?

  1. 首先让low指向剩下元素的第一个,让high指向最后一个元素

  2. low走(high不动)

  3. 26 < 54 26 < 54 26<54 -> 符合条件

  4. 93 > 54 93 > 54 93>54 -> 不符合条件 -> low停!

  5. high走(low停了)

    1. 20 < 54 20 < 54 20<54 -> 不符合条件 -> high

    在这里插入图片描述

  6. 此时我们发现,low指向的元素比54大,high指向的元素比54小 -> 交换这两个元素的位置,两个指针就可以继续走了

    在这里插入图片描述

  7. low继续走

    1. 17 < 54 17 < 54 17<54 -> 符合条件 -> 继续走
    2. 77 > 54 77 > 54 77>54 -> 不符合条件 -> low停!
  8. high继续走

    1. 55 > 54 55 > 54 55>54 -> 符合条件 -> 继续走
    2. 44 < 54 44 < 54 44<54 -> 不符合条件 -> high停!

    在这里插入图片描述

  9. 交换元素

    在这里插入图片描述

  10. low继续走

    1. 31 < 54 31 < 54 31<54 -> 符合条件 -> 继续走 -> lowhigh重合!

    在这里插入图片描述

  11. 54去到自己的位置

    在这里插入图片描述


此时通过54把整个序列分成了两部分:

  • 左边的比54小
  • 右边的比54大

接下来再对序列的两个部分按照相同的方式去排序。

在这里插入图片描述


在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


4.4 更容易理解的快排分析

在这里插入图片描述


4.4.1 第一次

在这里插入图片描述
在这里插入图片描述

4.4.2 第二次

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

4.5 快排代码实现-1

def quick_sort(alist, first: int, last: int):
    """快速排序"""
    # 使用递归嵌套时,一定要终止递归的条件
    if first >= last:  # 当传入的first >= last时 -> 序列中只有一个元素或者first > last时 -> 退出
        return
    
    mid_value = alist[first]  # 存放中间值
    low = first  # low指针(指向的是索引)
    high = last  # high指针(指向的是索引)
    
    while low < high:  # low和high没有相遇
        # 先执行high -> 左移
        # 当low和high不重合时且high一直>mid_value
        # 将mid_value与比较值相等的情况放到一边处理
        while low < high and alist[high] >= mid_value:  
            high -= 1  # high继续走
        alist[low] = alist[high]

        # 再执行low -> 右移
        # 如果alist[low] == mid_value,则移到high那边处理
        while low < high and alist[low] < mid_value:
            low += 1
        alist[high] = alist[low]
    
    # 从循环退出时,low == high
    alist[low] = mid_value  # 使用low还是high都可以(此时二者相等)
    
    """
        此时mid_value就将序列分成了两部分。
        我们对这两部分仍然使用上面的代码(快排)进行排序
        
        自然而然想到了“递归”
        
        相当于是二分法,然后我们对分开的再使用递归
        
        需要注意的是:
            1. 一旦使用递归,我们一定要有一个退出递归的条件,当被二分的列表中元素的个数只有一个时,退出递归!
            2. 我们想要递归的快排也操作原有的序列的话,就需要把原有的序列传进去,而不能使用切片后的序列传进去
    """
#     quick_sort(alist[: low-1])  # 这时在新序列上进行快排,与原序列无关(并不能作用到原有的序列上)
#     quick_sort(alist[low+1:])  # 这时在新序列上进行快排,与原序列无关(并不能作用到原有的序列上)
    """
        所以我们需要传入原有的序列,即:
            quick_sort(alist)
            quick_sort(alist)
        为了让快排知道要排哪些元素,我们还需要引入新的参数:
            1. start:序列中的起始位置
            2. end:序列中结束的位置
        对于左边部分:
            quick_sort(alist, 0, low-1)
            quick_sort(alist, low+1, n-1)
    """
    # 对low左边的序列执行快排
    quick_sort(alist, first, low-1)
    # 对low右边的序列执行快排
    quick_sort(alist, low+1, last)
    
    
if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    quick_sort(ls, first=0, last=len(ls)-1)
    print(f"after: {ls}")

4.6 快排简洁代码

def quick_sort(alist, start, end):
    """快速排序"""
    
    # 递归的退出条件
    if start >= end:
        return
    
    # 设定起始元素为要寻找位置的基准元素
    mid = alist[start]
    
    # low为序列坐标的由左向右移动的游标
    low = start
    
    # high为序列坐标的由右向左移动的游标
    high = end
    
    while low < high:
        # 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动
        while low < high and alist[high] >= mid:
            high -= 1
            
        # 将high指向的元素放到low的位置上
        alist[low] = alist[high]
        
        # 如果low与high未重合且low指向的元素比基准元素小,则low向右移动
        while low < high and alist[low] < mid:
            low += 1
        
        # 将low指向的元素放到high的位置上
        alist[high] = alist[low]
        
    # 退出循环后,low与high重合,此时所指向的位置即为基准元素的正确位置
    # 将基准元素放到该位置上
    alist[low] = mid
    
    # 对基准元素左边的子序列进行快速排序
    quick_sort(alist, start, low-1)
    
    # 对基准元素右边的子序列进行快速排序
    quick_sort(alist, low+1, end)
    
    
if __name__ == "__main__":
    ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
    print(f"before: {ls}")
    quick_sort(ls, start=0, end=len(ls)-1)
    print(f"after: {ls}")

4.7 时间复杂度

  • 最优时间复杂度: O ( n log ⁡ 2 n ) O(n\log_2^n) O(nlog2n)
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 稳定性:不稳定

从一开始快速排序平均需要花费 O ( n log ⁡ n ) O(n\log^n) O(nlogn) 时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用 O ( n ) O(n) O(n) 的时间。在使用结合(concatenation)的版本中,这项运算也是 O ( n ) O(n) O(n)

在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为 1 1 1 的数列前,我们只要作 log ⁡ n \log^n logn 次嵌套的调用。这个意思就是调用树的深度是 O ( log ⁡ n ) O(\log^n) O(logn)

但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要 O ( n ) O(n) O(n) 的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有 O ( n ) O(n) O(n) 个调用,这些被归纳在 O ( n ) O(n) O(n) 系数中)。

结果是这个算法仅需使用 O ( n log ⁡ 2 n ) O(n\log_2^n) O(nlog2n) 时间

所消耗的时间从小到大:
O ( 1 ) < O ( log ⁡ n ) < O ( n ) < O ( n log ⁡ n ) < O ( n 2 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(\log^n) < O(n) < O(n\log^n) < O(n^2) < O(2^n) < O(n!) < O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)<O(n!)<O(nn)

在这里插入图片描述

在这里插入图片描述


4.8 快排时间复杂度图解

在这里插入图片描述

Q:对于上面的序列,到底经过多少个 对半折分 才能拆到子序列中只有一个元素呢?

A 2 × 2 × 2 × . . . = n 2 \times 2 \times 2 \times ... = n 2×2×2×...=n -> n log ⁡ 2 n n\log_2^n nlog2n

每次二分为 log ⁡ 2 n \log_2^n log2n,会经历 n n n 次,所以时间复杂度为 n log ⁡ 2 n n\log_2^n nlog2n
‘red’>不稳定

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值