从递归算法中领悟:"分而治之"和"快速排序"的巧妙思想
文章目录
本篇文章将使用循序渐进的方式,依次向大家介绍如何实现快速排序,并且如何快速掌握分而治之的思想和递归算法的使用。
一、回顾递归
1、什么是递归?
递归实际上就是一个函数,这个函数自个调用自己,这种函数我们就称之为递归函数,这种算法我们称之为递归算法。使用递归是一个将问题分解小化的过程,也就是说将一个庞大的问题通过递归算法不断的分化,最终解决这个问题。
2、什么是递归深度
通俗来讲,递归实际上就是在一块地上挖坑,你挖坑肯定是有一定的深度,你不可能是一直挖,挖到岩浆把自己烧死吧,所以计算机给你一定的深度或者存储空间让你去递归或者是运算,当你超过计算机所给你的深度时,就给你抛出超出递归深度的异常,那么递归深度到底时多深呢?从理论上来讲递归深度时997次,有的电脑可以到达998次,我的电脑时997次。
# 递归深度异常:
RecursionError: maximum recursion depth exceeded in comparison
# 译为:递归错误:最大递归深度超过比较
3、递归的组成
注:要想实现递归函数,必须有合理的基线条件和递归条件作为支撑,否则将出现 RecursionError 类型的错误。
(1)基线条件
**基线条件是函数不再调用自己,就是停止递归。**在上一篇文章中大家或许都发现了一个问题,每一个递归函数基本上都是由条件判断语句组成,那么 if 判断语句实际上就是基线条件,设置基线条件的目的:1、防止超出递归深度;2、根据需求达到自己的目的
(2)递归条件
**递归条件是指函数不停的调用自己。**相信大家在上一篇文章中也发现 else 语句了,那么这个else语句实际上就是递归条件。
4、递归算法的经典案例
(1)sum( )
**需求:**使用递归算法,实现 sum( ) 函数的功能
分析:
(1)找出基线条件。试想一下,如果一个列表中没有任何元素,那么这计算起来就非常的容易,我们直接返回0就可以了,那么这就是基线条件,基线条件就是要将问题简化到最简单的一步。
(2)基线条件有了,那么如何将一个列表中的元素一次又一次的减少,最后为空呢?试想一下,如果我们每一次都截取一定量的元素,或者是截取的时候元素递减,直到为空为止。那么我们是不是每次可以将第一个元素拿出,剩下的元素再组成一个列表,再进行递归拿去第一个
(3)那么每次拿的第一个元素是什么?其实就是需要累加的元素,我们将元素递归到基线条件后开始返回,从0开始递增,最终是不是就得出了结果。那么第二步和第三步就组成了我们的递归条件
流程图解:
源码:
def sum(list):
# 基线条件
if list == []:
# 当列表为空时,返回0
return 0
# 递归条件
else:
# 当列表不为空时记录每一次列表中的第一个元素 + 再次调用sum函数,参数随着每次的截取而不断减少
return list[0] + sum(list[1:])
if __name__ == '__main__':
mylist = [i if i in range(1,21)]
result = sum(mylist)
print(result)
(2)len ( )
**需求:**实现递归算法实现 len( ) 函数的功能,也就是可以获取列表、元组、字符串的长度
分析:
(1)找出基线条件。首先将这个问题简化到最简短,该容器中没有任何元素,是空的,直接返回 0
(2)找出递归条件。如果这个容器中有元素,那么每拿出一个+1,并截取剩下的元素容器作为参数再次调用该函数,直到容器为空。
流程图解:
源码:
def len(iterable):
# 基线条件
if (iterable == []) or (iterable == ()) or (iterable == ""):
# 当容器为空时,返回0
return 0
# 迭代条件
else:
# 当容器不为空时,先+1,再截取从1开始的元素组成新的列表,将这个新列表作为参数传递给该函数,不断迭代
return 1 + len(iterable[1:])
if __name__ == '__main__':
mylist = [i for i in range(20)]
result = len(mylist)
print(result)
(3)max( )
**需求:**使用递归算法实现 max( ) 函数,并可以计算出列表中的最大值
分析:
(1)找出基线条件。一想到找最大值,我们首先想到的应该时python中的三目运算符。那么将问题简化,如果只有两个元素,那么我们就直接使用三目运算符返回一个最大值即可。
(3)找出迭代条件。首先要想实现基线条件,那么我们就必须想办法递减列表中的值,如何递减?我们可以在每一次迭代之前拿出第一个元素,那么就只有截取从1开始的剩下的元素作为参数调用该函数。这样就保证了每次我们拿到的第一个元素都是不同的元素。等列表中只剩下两个元素的时候执行基线条件,停止递归,拿到末尾两个元素的最大值。然后开始返回,用这个最大值去与每次拿到的第一个元素比较,使用三目运算符最终拿到最大值。
源码:
def max(list):
if len(list) == 2:
return list[0] if list[0] > list[1] else list[1]
else:
reference = max(list[1:])
return list[0] if list[0] > reference else reference
if __name__ == '__main__':
list = [34, 244, 46 ,2 , 6,234, 0]
result = max(list)
print(result)
(4)find( )
**需求:**使用递归算法实现二分查找,并最终实现 find( ) 函数
分析:
(1)不管是哪种算法,进入函数以后必须要拿到折半的索引,并通过索引拿出折半索引对应的折半值
(2)找出基线条件。二分查找的最终目标是查找的的值等于目标值,如果等于那么直接返回该索引
(3)找出递归条件。递归条件有两种,一种是大于,一种是小于,如果查找到的值大于目标值,那么直接将最大索引值-1,如果查找的值小于目标值,那么直接将最小索引+1,然后直接调用并返回该函数即可
(4) 最后在所有代码外判断索引最小值是否小于等于最大值,并且不小于0,否则返回1
源码:
def find(start, end, targer, list):
if start <= end and not start < 0:
index = (start + end) // 2
value = list[index]
if value == target:
return index
else:
if value > target:
return find(start, end-1, target, list)
else:
return find(start+1, end, target, list)
else:
return 1
if __name__ == '__main__':
list = [i for i in range(1,21)]
result = find(0,len(list)-1,5,list)
print(result)
5、小结
相信大家看了这么多经典案例,那么肯定会对递归算法实现的思想有所收获,如果您基本上都能写出来了,那么恭喜你学到了递归的核心思想,那么接下里我将带领大家去学习 第三中排序算法——快速排序。
二、快速排序
1、什么是快速排序?
快速排序是一种常见的排序算法,比我们学习的选择排序快得多。快速排序的实现也使用到了递归算法以及分而治之的思想,那么什么是分而治之的思想呢?刚刚咱们练习递归,先找出基线条件,再找出递归条件,那么这个过程可以看作是分而治之的思想,但是不够典形,在接下来的例子中我将简述分而治之的思想。
2、实现快速排序算法
1、分析:
(1)先找到基线条件。简化这个问题,如果一个列表中只有一个元素,或者是空的元素,我们不需要排序,所以我们可以将列表直接返回
(2)再找到递归条件。试想一下,如何实现基线条件中不能够排序的情况?我们是不是可以将列表分解呢,那么该如何分解?我们可以找一个元素作为基准值,如何列表中有大于基准值的我就将这些大于的元素分到一个列表或者说阵营中,如果这个列表中有小于或者等于该基准值的,那么我就再将这些元素放到另一个列表或者说阵营中。
(3)紧接着,我们将这三个列表拼接,例如:大于基准值的放到前边,基准值放到中间,等于或者小于基准值的放到最后。那么肯定会有人质疑:“你这也没起到排序的作用呀!”。
(4)那么新的问题来了,我们如何再将这两个列表进行分解呢?诶,我这不是递归吗,我可以再使用递归函数,再将前后两个列表分解,找到基准值,再次拼接,等递归到只有一个元素的时候,我在回头拼接所有的元素
(5)那么这样拼接出来的元素,或许就是我们需要的顺序
2、流程图
3、工作原理
(1)如果列表的长度大于1,那么就选择基准值
(2)通过比较,将原始列表分成两个字列表,一个是大于的,一个是等于或者小于的
(3)对这三个列表进行拼接排序
(4)如果两个字列表中还要长度大于1的元素,那么就再找基准值,再分列表,再拼接
(5)直到遇到基线条件,停止递归后,对所有的单个元素进行拼接
(6)最后,返回一个完整的列表
4、源码
def sort(list):
# 基线条件
if len(list) < 2:
# 当列表中的长度小于2,那么就没有必要排序,直接返回
return list
# 递归条件
else:
# 找到基准值
value = list[0]
# 拿到大于基准值的元素列表
max = [i for i in list[1:] if i > value]
# 拿到等于或者小于基准值的元素列表
min = [i for i in list[1:] if i <= value]
# 拼接三个列表,当列表的长度大于1的时候,调用函数自身继续迭代,直到只有一个元素,最后拼接返回新列表
return sort(max) + [value] + sort(min)
if __name__ == '__main__':
list = [32, 12, 5, 7, 1, 5, 767]
newlist = sort(list)
print(newlist)
5、如何选择基准值?
这个选择基准值没有什么特殊的要求,当只有两个元素的时候,可以拿出一个来比较就可以,所以为了保险起见,上述程序拿的都是索引为0的元素。
6、快速排序的特点
(1)从效率上讲是很快的,因为有两个递归函数都在执行递归,所以理论上来讲,在效率上能比冒泡排序、选择排序稍微高一点
(2)但是他不是最完美的,因为有一个缺点,当列表中的数据特点多的时候,就不适合使用快速排序,因为快速排序使用的是递归算法,当超过最大递归深度997就会抛出异常。但是从理论上来讲应该没有那么大的数据量,所以可以放心使用。
较就可以,所以为了保险起见,上述程序拿的都是索引为0的元素。
6、快速排序的特点
(1)从效率上讲是很快的,因为有两个递归函数都在执行递归,所以理论上来讲,在效率上能比冒泡排序、选择排序稍微高一点
(2)但是他不是最完美的,因为有一个缺点,当列表中的数据特点多的时候,就不适合使用快速排序,因为快速排序使用的是递归算法,当超过最大递归深度997就会抛出异常。但是从理论上来讲应该没有那么大的数据量,所以可以放心使用。
以上文章讲述了快速排序算法的实现,以及递归算法的补充,并且还用大量典形的例子讲述了分而治之的思想,如果大家还想来了解其他的排序算法请查看本人其他博客。如果本篇文章引起了歧义,还请各位码友能够指出我的错误。那么今天就讲到这里吧,如有兴趣请观看本人下篇博客,谢谢!