最重要的复杂度问题:
时间复杂度: 并不表示代码真正的执行时间 只表示代码执行时间随数据规模增长的变化趋势(所以即使某段代码常量1000000 虽然对这段代码执行时间来说是有影响 但是只要不涉及n 我们就忽略)
所有代码的执行时间 T(n) 与每行代码的执行次数成正比 T(n) = O(f(n)) O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。
低阶、常量、系数 都就可以忽略 只需要记录一个最大量级
分析方法:(1)只关注循环执行次数最多的那一段代码就可以了
(2)总复杂度等于量级最大的那段代码的复杂度
(3)嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
多项式时间: O(1) 只要代码的执行时间不随 n 的增大而增长 算法中不存在循环语句、递归语句
O(logn)(循环不断乘以一个数以至于达到n) O(nlogn)(其实就是O(logn) 循环n次) 底数可以忽略 直接写成O(logn)
O(n) :如果两个变量m n 不知道哪个大 那就是O(m+n)
O(n平方) O(n立方) O(nk次方)
非多项式时间:O(2的n次方) O(n!) np问题 当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长
---为了表示代码在不同情况下 n的不同时间复杂度(例如有时候n循环提前结束):
(1)最好情况时间复杂度:
理想情况下执行这段代码的时间复杂度
(2)最坏情况时间复杂度
(3)平均情况时间复杂度(加权平均时间复杂度)
(4)均摊时间复杂度:分析方法:平摊分析
对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,一般等于最好情况时间复杂度
空间复杂度:比较简单 就是判断占用空间多少(也是以n计算) 一般是O(1)、O(n)、O(n2)
数据结构
(1)数组:一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据
提一点 在数组中插入如果不理会顺序 可以直接在该位置插入 把这个位置的数字放到末尾 ---快排
在数组中删除也可以在该位置删除然后把后末尾的移动过去 或者先标记好删除的元素 在最后没空间的时候一次性来一次删除操作----jvm的清理机制
arraylist 和 数组的选择:arraylist只能用包装类 拆包装包是有性能消耗的 --开发底层代码例如框架的时候可以选择数组
数据大小已知 并且不需要那么多封装好的api 可以直接用数组
在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高
(2)链表:通过指针将一组零散的内存块串联在一起
链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
对链表进行频繁的插入、删除操作会导致频繁的内存申请释放===》内存碎片====》频繁的GC
单链表
双向链表 (例如LinkedHashMap):每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点 ------比单链表优点: 如果需要在某个节点前插入一个节点或者删除所在的节点 可以直接索引到他的前驱节点而不用重新遍历
循环链表:循环链表的尾结点指针是指向链表的头结点
小应用:可以用 散列表+单链表 实现LRU缓存
----------------怎么写好链表代码:e.g 反转 有序链表合并等--------------------------
(1)理解好指针
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量
(2)警惕指针丢失(自己指向自己)和内存泄漏
(3)利用哨兵(没有值)简化实现难度: 哨兵解决边界问题
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理
引入哨兵节点后 head指针会一直指向这个哨兵节点 此时进行插入 删除就不用特殊处理第一个和最后一个节点
(4)注意监测边界条件:
如果链表为空时,代码是否能正常工作?
如果链表只包含一个结点时,代码是否能正常工作?
如果链表只包含两个结点时,代码是否能正常工作?
代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
链表的几个问题:
单链表反转: 递归法 遍历法
链表中环的检测 :快慢指针法 足迹法
两个有序的链表合并 :while循环法(跟合并两个有序数组是一样的) 递归法(专门针对链表的)
删除链表倒数第 n 个结点: 递归计数法 还有一个牛逼的:双指针法!
求链表的中间结点: 先遍历一次记录个数n 再遍历n/2拿中间节点 仍然可以用双指针法(两倍速度的遍历到末尾时 一倍速度的就是中间)!
(3)栈
顺序栈:用数组实现的栈
链式栈:用链表实现的栈
(4)队列:可以应用在任何有限资源池中,用于排队请求
顺序队列:用数组实现的队列
链式队列:用链表实现的队列
循环队列:解决顺序队列入栈数据搬移导致时间复杂度o(n)的问题
循环队列满:规定为尾指针的下一个指针到头指针时 (也可以设置一个标识位来标志是满还是空 因为两个状态都是head = tail) : (tail+1)%n=head 会浪费一个空间
阻塞队列:
并发队列:线程安全的阻塞队列
(5)二叉树
每个节点最多有两个子节点(包括满二叉树和完全二叉树)
1链式存储法 : 三个字段 数据 指向左右子节点的指针
2数组存储法:从1开始存储 节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点 --------非完全二叉树会浪费很多空间(所以完全二叉树的规定是最后两层 且都靠左 并且完全二叉树用数组存储最省空间)
二叉树的前、中、后序遍历就是一个递归的过程
二叉树遍历:每个节点最多访问两次,时间复杂度o(n)
(6)二叉查找树(二叉搜索树,二叉排序树:因为可以中序遍历o(n)输出一个有序的数据序列):支持快速查找 插入 删除一个数据 类似o(1)的散列表
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值
时间复杂度:三个都是跟高度成正比
平衡二叉查找树的高度接近 logn :O(logn) 但是有的不平衡的二叉查找树如果像链表一样就退化成o(n)了 所以平衡很重要
散列表插入删除查找都是o(1),那即使是平衡二叉查找树的优点在哪里?
1散列表数据是无序的, 要输出有序的数据需要另外排序,而二叉查找树只需要中序遍历既可以o(n)输出
2散列表扩容耗时很多 而且容易因为散列冲突而导致时间复杂度不稳定 但是平衡二叉树使用起来稳定趋近于o(logn)
3哈希函数耗时 且 o(1)的常量查找时间并不一定比 o(logn)小
4散列表需要考虑 散列因子 扩容 缩容 哈希冲突 哈希函数性能等 平衡二叉树只需要考虑平衡(且这点的解决方法已很成熟)
(7)平衡二叉查找树avl:目的只是为了尽量保证左右子树高度低一点 为了解决二叉查找树因为动态更新导致的性能退化问题 时间复杂度就更稳定于o(logn)(红黑树不是完全定义上的平衡二叉查找树 但是只要在logn量级附近即可 ) 且为了维持高度的平衡 插入删除都要做调整 所以比较复杂耗时 对于插入删除操作多的不适合使用avl
二叉查找树 + 二叉树中任意一个节点的左右子树的高度相差不能大于 1
红黑树: 高度只比avl树最多大了一倍(不准确 实际上红黑树性能更好)
根节点是黑色
红黑树中的节点,一类被标记为黑色,一类被标记为红色
每个叶子节点不存储数据 都是黑色的空节点(NIL)
父子之间不会出现相连的红色节点,被黑色节点分开
每个节点到达他可以到达的叶子节点中经过的路径 每个路径上的黑色节点数量相同
红黑树只是近似平衡 以保证插入 删除 查找各项性能都比较稳定 都是o(logn)
比较一下对于插入删除查找常用的算法:
散列表:插入删除查找都是O(1), 是最常用的,但其缺点是不能顺序遍历以及扩容缩容的性能损耗。适用于那些不需要顺序遍历,数据更新不那么频繁的。
跳表:插入删除查找都是O(logn), 并且能顺序遍历。缺点是空间复杂度O(n)。适用于不那么在意内存空间的,其顺序遍历和区间查找非常方便。
红黑树:插入删除查找都是O(logn), 中序遍历即是顺序遍历,稳定。缺点是难以实现,去查找不方便。其实跳表更佳,但红黑树已经用于很多地方了。
--红黑树调整: 遇到哪种排布进行哪种调整 (红黑树规定最后叶子节点一定是黑色空节点是为了让插入删除时候平衡操作更有规律 简洁 实际中使用公用的一个黑色节点即可)
插入操作:插入的节点必须是红色的 + 新插入的节点都是放在叶子节点上 ,插入后破坏了红黑树 就需要 左旋转或者右旋转来重平衡
删除操作:两次调整 分别解决 红黑树定义的 第四个 和 第三个问题
(8)递归树: 借用递归树可以分析递归算法的时间复杂度
(9)堆: 堆排序 空间复杂度o(1) 时间复杂度o(nlogn)
堆 是一种特殊的树