说在前面:本文是通过阅读作者魏梦舒的《漫画算法》所总结的笔记,仅供学习,如侵即删
1.什么是算法(alorithm)
数学领域里:算法是用于解决某一类问题的公式和思想
计算机领域里:它的本质是一系列程序指令,用于解决特定的运算和逻辑问题
2.算法有高效的,也有拙劣的
衡量算法好坏的重要标准有两个:时间复杂度和空间复杂度
算法的应用领域:(1)数学运算还不简单?,其实不然比如求两个数的最大公约数,要求做到效率的极致;求两个超大整数的和,不导致变量溢出,也要进行思考
(2)查找(3)排序(4)最优决策:求迷宫最佳路线等(5)面试(如果这条也算的话):考察你对计算机底层知识的了解or你的逻辑思维能力
3.什么是数据结构
data structure ,是算法的基石,是数据的组织,管理和存储格式,使用目的是为了高效地访问和修改数据
组成方式有哪些?
(1)线性结构
线性结构是最简单的结构,包括数组,链表,以及衍生出来的栈,队列,哈希表
(2)树
稍微有些复杂,有代表性的是二叉树
(3)图
更复杂的数据结构,呈现多对多的关联关系
(4)其他
各种由基本数据结构变形而来,用于解决某些特定问题
一,时间复杂度
1.算法的好与坏:
举个例:一个代码运行一次花100ms,占用内存5MB
一个代码运行一次花100s,占用内存500MB
后者可以收拾东西走人了
衡量程序的好坏的重要因素:运行时间的长短和占用内存空间的大小
2.基本操作执行次数
(1)一个面包10cm,2分钟吃掉1cm,吃掉整个面包要多久?2*10即20分钟
如果面包长度为ncm,所以为2*n
用一个函数表达所需时间,可以记作T(n)=2n
(2)一个16cm的面包,5分钟吃掉面包剩余长度的一半,即第5分钟吃掉8cm,第10分钟吃掉4cm,第15分钟吃掉2cm,那么吃到只剩1cm,需要多久?
这个问题化成数学问题即,16一直除以2,那么除几次结果等于1?涉及对数,即以2为底16的对数
所以到1cm即5*log16即20分钟
用一个函数表达所需时间,可以记作T(n)=5logn
(3)一个长度为10cm的面包和一个鸡腿,每两分钟吃掉一个鸡腿,那么吃掉整个鸡腿需要多久?
当然是2分钟,这里只要求吃鸡腿,和面包没有关系
用一个函数表达所需时间,可以记作T(n)=2
(4)一个10cm的面包,吃第一个1cm花1分钟,吃第二个1cm花2分钟,每吃1cm所花时间就比上次多1分钟,即1累加到10的综合,也就是55分钟。
如果面包长度为n呢?1+2+3+...+(n-1)+n
用一个函数表达所需时间,可以记作T(n)=0.5n^2+0.5n
上面讲的是吃东西花费的时间,这个思想同样适用于对程序基本操作执行次数的统计
设T(n)为基本操作执行次数的函数(也可以认为是程序的相对执行时间函数),n为输入规模,
3.渐进时间复杂度:
若存在函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)时T(n)的同量级函数,记作T(n)=O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度
直白的说,时间复杂度就是把程序的想对执行时间函数T(n)简化为一个数量级,这个数量级可以时n,n^2,n^3等
如何推导出时间复杂度呢?遵循一下几个原则:
(1)如果运行时间是常数量级,则用常数1表示
(2)只保留时间函数中的最高阶项
(3)如果最高阶项存在,则省去最高阶项前面的系数
例如:T(n)=3n,最高阶项为3n,省去系数3,则转化成T(n)=O(n)
T(n)=5logn,最高阶项为5logn,省去系数5,则转化成T(n)=O(logn)
T(n)=2,只有常数量级,则转化成T(n)=O(1)
T(n)=0.5n^2+0.5n,最高阶项为0.5n^2,省去系数0.5,则转化成T(n)=O(n^2)
所以当n足够大时谁用时更长,谁更节省时间呢?
O(1)<O(logn)<O(n)<O(n^2)
O(1)就是最低的时空复杂度了,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间都不变。 哈希算法就是典型的O(1)时间复杂度,无论数据规模多大,都可以在一次计算后找到目标(不考虑冲突的话)冲突的话很麻烦的,指向的value会做二次hash到另外一快存储区域
二,空间复杂度
1.什么是空间复杂度:简单来说,空间复杂度就是执行算法的空间成本,是对一个算法在运行过程中临时占用存储空间大小的量度
举个例子:给n个整数,其中有两个是重复的,要求找出这两个重复的整数
3 | 1 | 2 | 5 | 4 | 8 | 7 | 2 |
一般最朴素的就是一个双重循环遍历,每遍历一个新的整数就回顾之前遍历过的所有整数,但是这个算法的时间复杂度太高O(n^2)
为了提高算法的效率,我们可以采用一个中间数据
比如每遍历一个整数,就把这个数存起来,放到字典里,不用再和之前的比对,只需要直接查找,看看是否有对应的整数即可
key | value |
3 | 1 |
1 | 1 |
2 | 1 |
5 | 1 |
4 | 1 |
9 | 1 |
7 | 1 |
key代表整数的值,value代表出现的次数
当遍历到最后一个整数的时候很轻松就能找到2曾经出现过
key | value |
3 | 1 |
1 | 1 |
2 | 2 |
5 | 1 |
4 | 1 |
9 | 1 |
7 | 1 |
因为读写字典的时间复杂度是O(1),所以整个算法的时间复杂度是O(n),比双重循环相比,运行效率增大了很多
但是内存空间是有限的,在时间复杂度相同的情况下,算法占用的内存空间当然是越小越好
所以程序占用空间大小的计算公式记作S(n)=O(f(n)),n为问题规模,f(n)为算法所占空间的函数
2.空间复杂度的计算
(1)常量空间:空间大小固定,和输入规模没有直接关系的时候,空间复杂度记为O(1)
(2)线性空间:当算法分配的空间是一个线性的集合(如数组),并且集合大小和输入规模n成正比,时间复杂度为O(n)
(3)二维空间:当算法分配的空间是一个二维数组的集合,并且集合长度和宽度都与输入规模n成正比,时间复杂度为O(n^2)
(4)递归空间:是一个比较特殊的场景,虽然没有明显的声明变量或集合,但是计算机执行的时候,也会专门分出一块内存,用来存储“方法调用栈”,执行递归操作所需要的内存和空间和递归的深度是成正比的。纯粹的递归操作的空间复杂度也是线性的,如果递归的深度是n,那么空间复杂度就是O(n)
3.时间与空间的取舍
在绝大多数时候,时间复杂度更为重要一些,我们宁可多分配一些内存空间也要提高程序的执行速度