Python排序算法总结

来源:互联网 发布:程序员考试考什么 编辑:程序博客网 时间:2024/06/11 05:42

Python排序算法总结

递归

在正式讲算法之前,先介绍一下递归。递归是一种解决问题的思路。

特点

  1. 调用自身
  2. 必须有一个明确的结束条件,比如if...
  3. 递归的两个阶段:
    1. 递推(压栈):到某个阶段,该阶段返回一个值(没有返回值,默认返回None)
    2. 回溯(出栈):从那个阶段回溯
  4. 每进入更深一次递归时,问题规模减少
  5. 递归效率不高(保存系统堆栈,跳进去,还要再跳出来)

应用场景

知道结束的条件,但不确定循环次数。

示例1

观察以下函数,如果x=3,哪些是递归,输出结果是什么

def func1(x):    print(x)    func1(x-1)# func1 没有结束条件,passdef func2(x):    if x>0:        print(x)        func2(x+1)# func2 结束条件永远不成立,passdef foo(x):    if x>0:        print(x)        foo(x-1)# foo 结果:3 2 1def bar(x):    if x>0:        bar(x-1)        print(x)# bar 结果:1 2 3

上面的几个函数,foo 和 bar 的输出结果是相反的,我们来分析一下它们的执行流程:

foo函数
  • 调用foo(3),入栈,stack=[foo(3)]
    • if 3>0 成立
    • 打印当前x值为3
    • 调用 foo(2),入栈,stack=[foo(3), foo(2)]
      • if 2>0 成立
      • 打印当前x值为2
      • 调用 foo(1),入栈,stack=[foo(3), foo(2), foo(1)]
        • if 1>0 成立
        • 打印当前x值为1
        • 调用 foo(0),入栈,stack=[foo(3), foo[2], foo(1), foo(0)]
          • if 0>0,条件不成立,下面代码不再执行。
          • foo(0)执行完毕,出栈,stack=[foo(3), foo[2], foo(1)]
        • foo(1) 执行完毕,出栈,stack=[foo(3), foo[2]]
      • foo(2) 执行完毕,出栈,stack=[foo(3)]
    • foo(3)执行完毕,出栈,栈空,stack=[ ]

图示:

1.png

bar函数
  • 调用bar(3),入栈,stack=[bar(3)]
    • if 3>0 成立
    • 调用 bar(2),入栈,stack=[bar(3), bar(2)]
      • if 2>0 成立
      • 调用 bar(1),入栈,stack=[bar(3), bar(2), bar(1)]
        • if 1>0 成立
        • 调用 bar(0),入栈,stack=[foo(3), foo[2], foo(1), foo(0)]
          • if 0>0,条件不成立,下面代码不再执行。
          • foo(0)执行完毕,出栈,stack=[bar(3), bar[2], bar(1)]
        • 打印当前x值为1
        • foo(1) 执行完毕,出栈,stack=[foo(3), foo[2]]
      • 打印当前x值为2
      • foo(2) 执行完毕,出栈,stack=[foo(3)]
    • 打印当前x值为3
    • foo(3)执行完毕,出栈,栈空,stack=[ ]

图示:

2.png

小结

观察bar和foo的执行,都是要”跳进去“,然后”跳出来“,bar进去的时候打印,foo是出来的时候打印,因此它们的输出相反。正式因为还要跳出来,导出递归效率不高,尽管如此,有些问题必须用递归思想思想才能解决。

其实如果不跳出来,递归的速度也不慢。这个涉及尾递归,在此不讨论。

示例2

用递归打印下面这句话:

3.png

观察跳进去的时候打印,跳出来的时候也打印,实现如下

def little_fish(x):    print('抱着',end='')    if x == 0:        print('我的小鲤鱼',end='')    else:        little_fish(x-1)    print('的我',end='')print('吓得我抱起了')little_fish(2)"""吓得我抱起了抱着抱着抱着我的小鲤鱼的我的我的我"""

示例3

汉诺塔问题,将所有的盘子,从a柱移到c柱,保持小的在上面,大的在下面,问怎么移?

5.png

def hanoi(x, a, b, c): # 所有的盘子从 a 移到 c    if x>0:        hanoi(x-1, a, c, b)  # step1:除了下面最大的,剩余的盘子 从 a 移到 b        print('%s->%s'%(a, c))  # step2:最大的盘子从 a 移到 c        hanoi(x-1, b, a, c) # step3: 把剩余的盘子 从 b 移到 chanoi(2, 'a', 'b', 'c')""" 2个的情况,不论有多少个,最终都是这个模式a->ba->cb->c"""hanoi(3, 'a', 'b', 'c')""" 3个的情况a->ca->bc->ba->cb->ab->ca->c"""

时间复杂度

看代码,猜快慢

下面四组代码,哪组运行时间最短?

print('Hello World')
for i in range(n):    print('Hello World')
for i in range(n):    for j in range(n):        print('Hello World')
for i in range(n):    for j in range(n):        for k in range(n):            print('Hello World')

直觉告诉我们,肯定是第一组。那么用什么方式来体现代码(算法)运行的快慢呢?时间复杂度

我们来类比一下生活中的场景:

  1. 眨一下眼:一瞬间/几毫秒
  2. 口算“29+68”:几秒
  3. 烧一壶水:几分钟
  4. 睡一觉:几小时
  5. 完成一个项目:几天/几星期/几个月
  6. 飞船从地球飞出太阳系:几年

也就是说,时间复杂度是一个估算的结果,用它来描述算法的快慢。用描述上限的数学符号O()来表示算法在最坏情况下的运行时间。

渐进分析

1)对于一些输入,第一个算法可能比第二个快,对于另外一些输入呢,第二个又比第一个好。

2)也有可能对于一些输入,第一个算法在一个机器上比第二个算法好,但是在另一台机器上第二个又比第一个好。

渐近分析是一个大问题,它就是在算法分析中处理上面的问题的。在渐近分析中,我们用输入的大小来评估算法的性能(我们不测量具体的运行时间)。我们计算的是随着输入大小的增加,算法所需要的时间(或者空间)。例如,我们考虑一个有序数组的搜索问题(搜索一个指定项)。
一个方法就是线性查询(递增顺序是线性的),另一个方法就是二分查询(递增顺序是对数级的)。为了能够很好滴理解渐近分析是怎样在算法分析中解决上面提到的问题,我们假设让线性查找在一个快的机器上跑,而让二分查询在一个慢的机器上跑。对于输入数组的大小比较小的时候,那么快的计算机花费的时间可能较少。但是,当输入的数组大小增长到一定程度的时候,二分查询的花费时间毫无疑问要比线性查询花费的时间要少,尽管二分查询是在比较挫的机器上跑的。原因是对递增数组进行二分查询对于输入的大小是对数级的,而线性查询则是线性级的。所以在特定的输入大小之后,机器的本身是可以忽略的。

在确定时间复杂度度时,使用渐近分析的方式:我们不关注常数因子和低阶项,比如有如下表达式:

T(n)=168n3+65n2+n+10000

根据数学原理,当一个函数(如这里的T(n))的n变得非常大以至于趋于无穷时,函数值的大小主要是由函数的最高阶项来决定的。T(n)的最高阶项是n3,去掉低阶项和常数因子后,T(n)的时间复杂度可以用O(n3)来表示。

根据渐进分析的思想,现在我们可以大致估计,之前四组代码的时间复杂度分别是O(1),O(n),O(n2),O(n3)

继续猜

print('Hello World')print('Hello Python')print('Hello Algorithm')

答案:O(1)

for i in range(n):    print('Hello World’)    for j in range(n):        print('Hello World')

答案:O(n2)

while n > 1:    print(n)    n = n // 2# n=64输出:64 32 16 8 4 2

先科普一下对数,忘了的自己补中学数学知识

26=64

log264=6

答案:O(log2n)或者O(logn) 计算机中都是以2为底,2常常省去

一眼判断算法的时间复杂度

  1. 循环减半的过程是O(logn)
  2. 几次循环就是n的几次方的复杂度

常见的时间复杂度

O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n2logn)<O(n3)

tips: O(logn)O(n)比较,可以这样类比,切一根1米长棍子,前者每次切剩余的一半,后者每次切1公分,自然是前者快。剩余的比较直接作除法约分。

空间复杂度

用来评估算法内存占用大小的,以目前硬件性能我们较少关注它。另外,很多算法都是以“空间换时间的”。

列表查找

列表查找:从列表中查找指定元素

  1. 输入:列表、待查找元素
  2. 输出:元素下标或未查找到元素

对于一段有序列表[1,2,3,4,5,6,7,8,9],我们可以通过以下两种方式来进行查找

顺序查找

也称为线性查找,从列表第一个元素开始,顺序进行搜索,直到找到为止

def linear_search(data_set, value):    for i in data_set:        if data_set[i] == value:            return iprint(linear_search([1,2,3,4,5,6,7,8,9],4))

线性查找,查最小的快,查最大的慢。在最坏的情况下,它的时间复杂度是O(n)

二分查找

搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

二分查找每一次比较都使搜索范围缩小一半,因此它的时间复杂度是O(logn)

下面用代码实现一下,这里选择移动下标而不是切片的方式(每次切片都会将列表切片部分复制一份,空间开销大):

方法一:循环
def bin_search(data, val):    low = 0    high = len(data)-1    while low <= high: # 当low和high重合后,还没有找到,那么循环结束,默认返回None        mid = (low + high) // 2        if data[mid] == val:            return mid        elif data[mid] > val:            high = mid - 1        else:            low = mid + 1print(bin_search(data, 4)) # 3
方法二:递归
def bin_search_rec(data, val, low, high):    if low <= high:        mid = (low + high) // 2        if data[mid] == val:            return mid        elif data[mid] > val:            return bin_search_rec(data, val, low, mid-1)         else:            return bin_search_rec(data, val, mid+1, high)print(bin_search_rec(data, 11, 0, len(data)-1)) # 结果是:3# 注意,需要return递归的值,否则结果为None

二分查找虽然快,但是要求列表是有序列表,这就要求对列表进行排序

列表排序

定义

将无序列表变为有序列表

输入:无序列表;输出:有序列表

应用场景

  1. 各种榜单
  2. 各种表格
  3. 给二分查找用
  4. 给其他算法用

LOW B 三人组

冒泡排序

思路

列表中每两个相邻的数,如果前边的比后边的大,那么交换这两个数……

关键词

  • loop(趟),loop等于列表长度-1 (最后一趟只剩最小的数在下面,不用比较,整个列表已经有序)
  • 有序区,初始时,有序区长度为0
  • 无序区,初始时,无序区长度为列表长度

过程
这里写图片描述

  • loop1
    • 指针在0,指针所指元素和下一个元素比较,大数上浮
    • 指针在1,指针所指元素和下一个元素比较,大数上浮
    • ……
    • loop1 结束,无序区中最大数上浮至无序区顶部;有序区长度 + 1,无序区长度 - 1
  • loop2
    • 指针在0,指针所指元素和下一个元素比较,大数上浮
    • 指针在1,指针所指元素和下一个元素比较,大数上浮
    • ……
    • loop2 结束,无序区中最大数上浮至无序区顶部;有序区长度 + 1,无序区长度 - 1
  • ……
def bubble_sort(data):    for i in range(len(data)-1): # 趟数        exchange = False        # 遍历无序区:(-i)每一趟,无序区长度减1        for p in range(len(data)-i-1):             if data[p]>data[p+1]: # 最后一个元素时,(p+1) 超出索引,所以上面范围-1                data[p], data[p+1] = data[p+1], data[p]                exchange = True        if not exchange: # 如果冒泡排序中某一趟没有发生交换,则列表已经有序,提前结束            return

data传入算法后,不需要返回,因为Python中列表是可变数据类型。

选择排序

思路

一趟趟遍历找出最小的数往前放

关键词

  • loop(趟),loop等于列表长度-1 (最后一趟只剩最大的数在后边,不用比较,整个列表已经有序)
  • 有序区,初始时,有序区长度为0
  • 无序区,初始时,无序区长度为列表长度

过程

  • loop1
    • min_index指向第1个元素
    • 循环剩余元素,只要有元素的值小于min_index所指元素,就令min_index指向该元素;循环结束后,min_index将指向列表中的最小元素
    • ……
    • loop1 结束,如果min_index不指向第1个元素,第1个元素与min_index所指元素交换值;有序区长度 + 1,无序区长度 - 1
  • loop2
    • min_index指向第2个元素
    • 循环无序区,只要有元素的值小于min_index所指元素,就令min_index指向该元素,循环结束后,min_index将指向无序区的最小元素
    • ……
    • loop2 结束,如果min_index不指向第2个元素,第2个元素与min_index所指元素交换值;有序区长度 + 1,无序区长度 - 1
  • ……
def select_sort(data):    for i in range(len(data)-1):  # n-1个最小数排好后,列表就已经有序了        min_index = i        for p in range(i+1, len(data)):            if data[p] < data[min_index]:                min_index = p        if min_index != i:            data[i], data[min_index] = data[min_index], data[i]

插入排序

思路

列表被分为有序区和无序区两个部分:最初有序区只有一个元素,每次从无序区取出一个元素,插入到有序区的位置,直到无序区变空。

10.png

关键词

  • 有序区,初始时,有序区长度为1
  • 无序区,初始时,无序区长度为列表长度-1

过程
这里写图片描述

  • loop1
    • 取出无序区的第1个元素,赋值为tmp
    • 指针p指向有序区最后一个元素,如果tmp大于等于p所指的元素,直接放在p的下一个位置。如果tmp小于p所指的元素,交换二者的值
    • 指针p往前移,继续判断tmp和p所指元素的大小
    • ……
    • loop1 结束,tmp被放置在有序区合适的位置;有序区长度 + 1,无序区长度 - 1
  • loop2
    • ……
  • ……
def insert_sort(data):    for i in range(1, len(data)):        tmp = data[i]        p = i - 1  # 指向有序区最后一个元素的指针        while p >=0 and tmp < data[p]: # 插牌            # 如果小于,那么交换值,并且令p往前移,循环判断tmp在有序区的大小            data[p + 1] = data[p]             p = p - 1        data[p + 1] = tmp

data[p + 1] 就是tmp的值,不直接写tmp,是为了保证随着指针p的移动,tmp值不变

小结

冒泡排序 插入排序 选择排序:

时间复杂度都是:O(n2)

空间复杂度都是:O(1) ,因为都是基于移动下标(指针)的方式

NB 三人组

快速排序

好写的排序算法里最快的,快的排序算法里最好写的。

思路

  • 取一个元素p(第一个元素),使元素p归位;
  • 列表被p分成两部分,左边都比p小,右边都比p大;写一个partition 归位函数
  • 递归完成排序。

11.png

关键词

  • 整理(partition函数)
  • 递归

过程

f1.png f2.png f3.png

  • partition 归位函数

    • 指针left指向列表第一个元素,right指向最后一个元素
    • left指向的元素 赋值给临时变量tmp,left 指向的位置为空
    • 将所有的元素和 tmp 比较,直到比它小的在左边,比它大的在右边
      • left 指向的位置为空时,right 指向的值和 tmp 比较:
        • 如果 right 指向的元素 >= tmp,说明该元素应该在tmp的右边,位置不变
          • right 指针向左移动1位,准备将下一个元素和 tmp 进行比较
        • 否则该元素在tmp的左边
          • right 指向的元素移动至left所指的空位,right 指向的位置为空
      • 一旦right 所指的位置为空,left 向右移动,left 指向的元素和 tmp 比较:
        • 如果left 指向的元素 <= tmp,说明该元素应该在tmp的左边,位置不变
          • left 指针向右移动1位,准备将下一个元素和 tmp 进行比较
        • 否则该元素在 tmp 右边
          • left 指向的元素 移动至 right 所指的空位,left 所在位置为空
      • 当 left 和 right 指向同一位置时,tmp 归位,这时左边的元素都比它小,右边的元素都比它大
      • 这时的 left (或者right)所在位置,就是列表中元素大小的分界线,函数返回left。
  • quick_sort 快排:不断地调用 partition 归位函数,直到整个列表有序。

    • 指针 left 指向列表的左边,right 指向列表的右边
    • 只要 left < right :
      • 调用 partition 归位函数,找出列表的分界线,将列表分为左右两部分,分别调用quick_sort函数
def partition(data, left, right):    tmp = data[left]    while left < right:        # right 所指元素和 tmp 比较        while left < right and data[right] >= tmp:            right -= 1  # right        data[left] = data[right]  # 交换位置        # left 所指元素和 tmp 比较        while left < right and data[left] <= tmp:            left += 1        data[right] = data[left]  # 交换位置    data[left] = tmp  # tmp 归位    return left"""data = [5,7,4]partition(data, 0, len(data)-1)print(data) # [4, 5, 7]"""def quick_sort(data, left, right):    if left < right:  # 说明至少有两个元素        mid = partition(data, left, right)        quick_sort(data, left, mid-1)        quick_sort(data, mid+1, right)data = [5,7,4,6,3,1,2,9,8]quick_sort(data, 0, len(data)-1)print(data)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

快排每次排序问题规模缩小一半, 在大多数场景下都是最快的,但是也有最坏的情况,那就数据是逆序的情况,比如9,8,7,…2,1 这时的时间复杂度变为O(n2)。为了避免这种情况,可以选择不取第一个元素,而是随机取。

堆排序

比较复杂,在NB三人组中是最慢的,先占个坑。

归并排序

假设现在的列表分两段有序,如何将其合成为一个有序列表

这里写图片描述
这种操作被称为一次归并

一次归并过程

def merge(data, low, mid, high):    i = low    j = mid + 1    ltmp = []    # 依次将两段有序部分的元素作比较,将小的加入临时列表中    while i <= mid and j<= high:        if data[i] <= data[j]:            ltmp.append(data[i])            i += 1        else:            ltmp.append(data[j])            j += 1    # 两段有序部分的长度可能不一致,比较完后,其中一段可能有剩余元素未加入临时列表中    while i <= mid:        ltmp.append(data[i])        i += 1    while j <= high:        ltmp.append(data[j])        j += 1    data[low: high+1] = ltmpdata = [2,5,7,8,9,1,3,4,6]  merge(data, 0, len(data)//2, len(data)-1)print(data)

思路

26.png

  • 分解:将列表越分越小,直至分成一个元素。
  • 一个元素是有序的。
  • 合并:将两个有序列表归并,列表越来越大。
def mergesort(data, low, high):    if low < high:        mid = (low + high) // 2        mergesort(data, low, mid)        mergesort(data, mid+1, high)        merge(data, low, mid, high)data = [7,255,7,56,9,1,3,99,6]mergesort(data, 0, len(data)-1)print(data)  # [1, 3, 6, 7, 7, 9, 56, 99, 255]

小结

三种排序算法的时间复杂度都是O(nlogn)

一般情况下,就运行时间而言:

  • 快速排序 < 归并排序 < 堆排序

三种排序算法的缺点:

  • 快速排序:极端情况下排序效率低
  • 归并排序:需要额外的内存开销
  • 堆排序:在快的排序算法中相对较慢
原创粉丝点击