03 | 复杂度分析(上)
1. 复杂度O(n)表示的是单位代码执行时间T(n) 随数据规模n增长的变化趋势
2. 分时间复杂度和空间复杂度(表示存储空间大小)
3. 常见的复杂度并不多,从低阶到高阶有:O(1)、O(n)、O(nlogn)、O(n2 )
4. 有两个增长数据时,如果是嵌套关系,就是O(m*n),是并列关系时,就是O(m+n)
04 | 复杂度分析(下)
1. 时间复杂度分为: 最好情况时间复杂度, 最坏情况时间复杂度, 平均情况时间复杂度, 均摊时间复杂度。
同一段代码,在不同输入的情况下,复杂度量级有可能是不一样的.
2. 均摊时间复杂度就是一种特殊的平均时间复杂度。定义:
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系。
05 | 数组
1. 线性表: 数据排成像一条线一样的前后关系的结构。 包括:数组,链表、队列、栈
非线性表: 数据之间并不是简单的前后关系。包括:二叉树、堆、图。
2. 链表适合插入、删除,时间复杂度 O(1)
数组适合查找,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)
3. 数组用连续的内存空间, 来存储相同类型的一组数据。
最大的特点就是支持随机访问,但插入、删除操作也因此变得比较低效,平均情况时间复杂度为O(n)。
4. 在业务开发中,可以使用编程语言提供的容器类;在底层的开发, 使用数组效率更高
06 | 链表(上)
1. LRU 缓存淘汰策略, 就是把最近使用过的数据,放在最前面。
2. 为实现LRU 缓存淘汰策略,涉及数据的插入和删除。所以使用链表的数据结构,比数组更合适。
3. 链表分: 单项链表,双向链表、循环链表、双向循环链表。 复杂链表的思路是空间换时间。
4. 数组可以有效利用CPU缓存,但链表不可以
07 | 链表(下)
1. 要写出无BUG的链表的代码,要注意以下几种情况下,能不能正常工作:
(1) 链表为空时
(2) 链表只有一个结点时
(3) 链表只有两个结点时
(4) 代码逻辑在处理头结点和尾结点的时候,是否能正常工作
2. 可以使用哨兵的方法来规避复杂情况,具体流程是:
//(1) 处理好最后一个元素的逻辑
//(2) 用哨兵替换掉最后一个元素,哨兵是一个必定能触发跳出循环的元素。
//(3) 只考虑哨兵跳出,不考虑边界跳出,直接执行逻辑。
在队头添加哨兵元素(不存储数据),当逻辑遇到哨兵时,什么都不用判断,直接退出。
这样就无论插入还是删除结点,逻辑都一样。因为首结点是哨兵,首结点以外的逻辑都一样。
3. 5个练习链表代码能力的例子:
(1) 单链表反转
(2) 链表中环的检测
(3) 两个有序的链表合并
(4) 删除链表倒数第 n 个结点
(5) 求链表的中间结点
4. LRU缓存淘汰法要点
(1) 如果数据已经在链表中,就把原来位置的结点删除掉,然后插入到链表头部。
(2) 如果数据不在链表中,并且链表未满,就插入到链表头部。
(3) 如果数据不在链表中,并且链表已满,就删除尾部结点,然后插入到链表头部。
08 | 栈
1. 如何实现浏览器的前进和后退功能
(1) 使用两个栈X和Y,访问的页面依次入栈X
(2) 页面后退时,X出栈、Y入栈;页面前进时,X入栈,Y出栈
(3) 访问新页面时,X入栈,Y栈清空
09 | 队列
1. 线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处处理?
(1) 非阻塞的处理方式,直接拒绝任务请求
(2) 阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。即,使用队列。
2. 为解决队列数据搬移的问题,可以使用循环队列。当队满时,(tail+1)%n=head
3. 阻塞队列就是入队、出队操作可以阻塞
4. 并发队列就是队列的操作多线程安全
10 | 递归
1. 满足“三个条件”的问题就可以通过递归代码来解决
(1) 一个问题的解可以分解为几个子问题的解
(2) 子问题,除了数据规模不同,求解思路完全一样
(3) 存在递归终止条件
2. 递归的好处:简洁高效
3. 递归的坏处:
(1) 堆栈溢出,不容易计算堆栈深度,确保安全。
(2) 重复计算,可以通过一个数据结构(比如散列表)来保存已经求解过的 f(k)来避免问题。
(3) 函数调用耗时多
(4) 空间复杂度高