算法就是为了解决某一个问题而采取的具体有效的操作步骤
算法的复杂度,表示代码的运行效率,用一个大写的O加括号来表示,比如O(1),O(n)
认为算法的复杂度是渐进的,即对于一个大小为n的输入,如果他的运算时间为n3+5n+9,那么他的渐进时间复杂度是n3
递归
递归就是在函数中调用本身,大多数情况下,这会给计算机增加压力,但是有时又很有用,比如下面的例子:
汉诺塔游戏
把A柱的盘子,移动到C柱上,最少需要移动几次,大盘子只能在小盘子下面
递归实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
用递归打印斐波那契数列
1 2 3 4 5 6 7 |
|
你会发现,即使n只有几十的时候,你的计算机内存使用量已经飙升了
其实,如果结合生成器,你会发现不管n有多大,都不会出现卡顿,但这是生成器的特性,本篇博客不重点介绍
1 2 3 4 5 6 7 8 9 10 |
|
关于递归次数,Python中有个限制,可以通过sys模块来修改
查找
1.顺序查找
这个没的说,就是for循环呗,时间复杂度O(n)
2.二分查找
时间复杂度O(logn)
就是一半一半的查找,看目标值在左边一半还是右边一半,然后替换左端点或者右端点,继续判断
非递归版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
递归版本的二分查找
1 2 3 4 5 6 7 8 9 10 11 |
|
排序
速度慢的三个:
1.冒泡排序
原理就是,列表相邻的两个数,如果前边的比后边的小,那么交换顺序,经过一次排序后,最大的数就到了列表最前面
代码:
1 2 3 4 5 6 7 8 |
|
冒泡排序的最差情况,即每次都交互顺序的情况,时间复杂度是O(n2)
存在一个最好情况就是列表本来就是排好序的,所以可以加一个优化,加一个标志位,如果没有出现交换顺序的情况,那就直接return
1 2 3 4 5 6 7 8 9 10 11 |
|
2.插入排序
原理:把列表分为有序区和无序区两个部分。最初有序区只有一个元素。然后每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。
1 2 3 4 5 6 7 8 9 |
|
时间复杂度是O(n2)
3.选择排序
原理:遍历列表一遍,拿到最小的值放到列表第一个位置,再找到剩余列表中最小的值,放到第二个位置。。。。
1 2 3 4 5 6 7 8 9 10 |
|
时间复杂度是O(n2)
速度快的几种排序:
4.快速排序(快排)
原理:让指定的元素归位,所谓归位,就是放到他应该放的位置(左变的元素比他小,右边的元素比他大),然后对每个元素归位,就完成了排序
可以参考这个动图来理解下面的代码
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
正常的情况,快排的复杂度是O(nlogn)
快排存在一个最坏情况,就是每次归位,都不能把列表分成两部分,此时复杂度就是O(n2)了,如果要避免设计成这种最坏情况,可以在取第一个数的时候不要取第一个了,而是取一个列表中的随机数
5.归并排序
原理:列表分成两段有序,然后分解成每个元素后,再合并成一个有序列表,这种操作就叫做一次归并
应用到排序就是,把列表分成一个元素一个元素的,一个元素当然是有序的,将有序列表一个一个合并,最终合并成一个有序的列表
图示:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
照例,时间复杂度是O(nlogn)
特殊的,归并排序还有一个O(n)的空间复杂度
6.堆排序
把这个放到最后,是因为这个是最麻烦的,把最麻烦的放到最后,是一种对工作负责的表现
如果要说堆排序,首先得先把‘树’搞明白
树
树是一种数据结构;
树是由n个节点组成的集合; -->如果n为0,那这是一颗空树,如果n>0,那么那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
一些可能会用到的概念:
根节点:树的第一个节点,没有父节点的节点
叶子节点:不带分叉的节点
树的深度(高度):就是分了多少层
孩子节点、父节点:节点与节点之间的关系
图示:
二叉树
然后在树的基础上,有一个二叉树,二叉树就是每个节点最多有两个子节点的树结构,比如这个:
满二叉树:除了叶子节点,所有节点都有两个孩子,并且所有叶子节点深度都一样
完全二叉树:是有满二叉树引申而来,假设二叉树深度为k,那么除了第k层,之前的每一层的节点数都达到最大,即没有空的位置,而且第k层的子节点也都集中在左子树上(顺序)
二叉树的存储方式
有链式存储和顺序存储的方式(列表),本篇只讨论顺序存储的方式
思考:
父节点和左孩子节点的编号下标有什么关系? 0-1 1-3 2-5 3-7 4-9 i ----> 2i+1
父节点和右孩子节点的编号下标有什么关系? 0-2 1-4 2-6 3-8 4-10 i -----> 2i+2
再来了解下堆,堆说起来又麻烦了,我将在另一篇博客中单独写堆,栈等这些数据结构,本篇先讨论与排序有关的东西
堆
堆是一类特殊的树,要求父节点大于或小于所有的子节点
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大 ,升序用大根堆
小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
堆的调整:当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变换成一个堆。
所谓一次向下调整,就是说把堆顶的值,向下找一个合适的位置,是一次一次的找,跟他交换位置的值,也要找到一个合适的位置
“浏览器写的没保存,丢失了,所以这块不想再写了。。。”
堆排序的过程
1.构造堆
2.得到堆顶元素,就是最大的元素
3.去掉堆顶,将堆的最后一个元素放到堆顶,此时可以通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤3,直到堆为空
其中构造堆的过程:
挨个出数的过程:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
时间复杂度也是O(nlogn)
来扩展一下,如果要取一个列表的top10,就是取列表的前十大的数,怎么做?
可以用堆来实现,取堆的前十个数,构造成一个小根堆,然后依次遍历列表后面的数,如果比堆顶小,则忽略,如果比堆顶大,则将堆顶替换成改元素,然后进行一次向下调整,最终这个小根堆就是top10
其实Python自带一个heapq模块,就是帮我们对堆进行操作的
heapq模块
队列中的每个元素都有优先级,优先级最高的元素优先得到服务(操作),这就是优先队列,而优先队列通常用堆来实现
如果用heapq模块来实现堆排序,就简单多了:
import heapq
def heapq_sort(li):
h = []
for value in li:
heapq.heappush(h,value)
return [heapq.heappop(h) for i in range(len(h))]
而想取top10 ,直接一个方法就行了
heapq.nlargest(10,li)
这三种速度快的排序方式就说完了,其中,快排是速度最快的,即使这样,也不如Python自带的sort快
再来介绍两种排序,希尔排序和计数排序
7.希尔排序
希尔排序是一种分组插入排序的算法
思路:
首先取一个整数d1=n/2,将元素分为d1个组,每组相邻量元素之间距离为d1,在各组内进行直接插入排序;
取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序。
希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
图示:
代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
通过diamante也能看出来,其实希尔排序和插入排序是非常相像的,插入排序就可以看做是固定间隔为1的希尔排序,希尔排序就是把插入排序分了个组,同一个组内,相邻两个数之间不是相差1,而是相差gap
时间复杂度:O((1+t)n),其中t是个大于0小于1的数,取决于gap的取法,当gap=len(li)//2的时候,t大约等于0.3
8.计数排序
需求:有一个列表,列表中的数都在0到100之间(整数),列表长度大约是100万,设计算法在O(n)时间复杂度内将列表进行排序
分析:列表长度很大,但是数据量很少,会有大量的重复数据。可以考虑对这100个数进行排序
代码:
1 2 3 4 5 6 7 8 9 10 |
|
1 2 |
|