1、复杂度分析(1)
- 复杂度分析相比于过后统计法,可以不依赖于运行环境和数据规模的大小,而计算程序的运行效率。
- 时间复杂度大O表示法 T(n) = O(f(n)) n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。O表示代码的执行时间 T(n) 与 f(n) 表达式成正比,因为f(n)能够忽略低阶和常数项,因此计算的技巧:只计算循环最多、量级最大的那段代码;嵌套代码的复杂度等于内外复杂度的乘积
- 经常使用时间复杂度 O(1) 、 O(logn)、 O(nlogn)、O(m+n)、O(m*n)
- 空间复杂度分析: 常见的空间复杂度就是 O(1)、O(n)、O(n2)。咱们说空间复杂度的时候,是指除了本来的数据存储空间外,算法运行还须要额外的存储空间。
2、复杂度分析(2)
几种时间复杂度分析算法
- 最好和最坏状况时间复杂度,看两种极端状况的复杂度
- 平均时间复杂度, 用几率论,求平均状况的复杂度。即各类状况下出现的几率乘以各类状况下的复杂度,再加起来。获得 加权平均值。
- 使用场景: 多时候,咱们使用一个复杂度就能够知足需求了。只有同一块代码在不一样的状况下,时间复杂度有量级的差距,咱们才会使用这三种复杂度表示法来区分。
- 均摊时间复杂度:不一样时间复杂度状况的出现有必定的规律,看是否能将较高时间复杂度那次操做的耗时,平摊到其余那些时间复杂度比较低的操做上。并且,在可以应用均摊时间复杂度分析的场合,通常均摊时间复杂度就等于最好状况时间复杂度。
3、数组
- 定义: 线性表结构(逻辑上) + 连续的存储空间(存储方式上) + 存储相同的数据类型。
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。由查找第i个数据的公式为:
a[i]_address = base_address + i * data_type_size
二位数组的寻址公式为,对于数组 m*n
a[i]_address = base_address + (i n + j) data_type_size
- 缺点: 插入、删除须要移动数据,复杂度O(n)。
- 高级语言中的‘数组’每每是对数组进行封装,或者使用非数组的方式去实现(JavaScript的数组实际上是对象),例如封装了删除、插入等操做的细节、支持动态扩容。
4、链表(上)
- 定义: 线性表结构(逻辑上) + 非连续的存储空间(存储方式上)
- 分类: 单链表、双向链表、循环链表
- 循环链表是一种特殊的单向或双向链表。
- 链表相比数组,随机访问更麻烦。数组的劣势在于须要连续的存储空间,扩容须要作数据迁移。链表更耗空间。
- 双向链表对于单向链表的优点:在删除特定指向的节点或者在特定指向的节点钱插入时,不用遍历查找其前驱节点。
5、链表(下)
- 理解指针或引用的含义: 指针是变量(链表上的结点)的地址,它能够访问结点。而结点上又存储着一个指针地址。因此指针地址既是结点上的一个数据,又能够经过它访问另外一个结点。
- 警戒指针丢失和内存泄漏:这一点要注意的是指针指向语句的顺序
- 利用哨兵简化实现难度: 增长无用的哨兵结点,使得操做更统一,不须要对链表为空(对于插入)或只有一个 元素(对于删除)的状况做额外判断。
- 重点留意边界条件处理,4个边界条件:
若是链表为空时,代码是否能正常工做?
若是链表只包含一个节点时,代码是否能正常工做?
若是链表只包含两个节点时,代码是否能正常工做?
代码逻辑在处理头尾节点时是否能正常工做?
- 举例画图, 释放一些脑容量,留更多的给逻辑思考
6、栈
1、栈是一种“操做受限”的线性表,只容许在一端插入和删除数据。可使用数组或链表来实现。理论上数组或链表能够替代栈的功能,而栈是对特定场景的抽象,暴露的可操做接口少,比较可控。
2、复杂度分析数组
- 普通栈: 时间空间复杂度O(1)
- 可动态扩容的数组实现的栈: 入栈均摊时间复杂度就为 O(1)
3、应用:函数调用栈、编译器求和、检查括号匹配数据结构
7、队列
1、队列和栈相似,是一种操做受限的线性表数据结构
2、队列使用数组实现,随着入队和出队操做整个队列会后移,这是须要在入队发现队列满了时作数据迁移。假设head 指针和 tail 指针指向第一个结点和最后一个结点,n表示队列大小。 tail==n 时,会有数据搬移操做。
3、循环队列: 队空的判断条件是 head == tail,当队满时,(tail+1)%n=head
4、应用:使用阻塞队列,就是在队列为空的时候,从队头取数据会被阻塞。由于此时尚未数据可取,直到队列中有了数据才能返回;若是队列已经满了,那么插入数据的操做就会被阻塞,直到队列中有空闲位置后再插入数据,而后再返回。能够实现一个“生产者 - 消费者模型”。函数
8、递归
1、用递归来解决的问题要知足三个条件:优化
- 一个问题的解能够分解为几个子问题的解
- 这个问题与分解以后的子问题,除了数据规模不一样,求解思路彻底同样
- 存在递归终止条件
2、编写递归函数的思路:分析问题与子问题的关系,求出递归公式,找到递归的终止条件。编写递归代码的关键是,只要遇到递归,咱们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每一个步骤。
3、递归的例子:斐波那契数列、 阶乘
4、递归的优化指针
- 预防堆栈溢出,能够计算递归的深度,限制递归层数。或者使用尾递归。
- 减小重复计算:因为递归过程当中存在函数值重复计算的问题,咱们能够将计算过的值存起来,当调用函数的参数相同时,就没必要重复计算了
- 使用循环代替递归,不过复杂度高,不够简洁直观