1. 怎么学习算法?
1)复杂度分析方法
2)要学习它的“来历”“自身的特点”“适合解决的问题”以及“实际的应用场景"
2. 做算法题注意事项:
1) 临界条件
2)时间复杂度,空间分复杂度
3)终止条件
算法学习:
1. 边学边练,适度刷题
2. 多问,多思考,多互动
3. 打怪升级学习方法
4. 知识需要沉淀,不要试图一下子学会所有
时间复杂度分析:
1. 只关注循环执行最多的一段代码
2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
3. 乘法原则:嵌套代码的复杂度等于嵌套内外代码的复杂度乘积
4. 多项式量级,非多项式量级(O(2n次方) 和 O(n!))
空间复杂度分析
浅析最好、最坏、平均、均摊时间复杂度
数组:如何实现随机访问?
1)线性表
2)第二个是连续的内存空间和相同类型的数据
容器能否完全替代数组?
不能,例如:动态扩容会浪费空间。
低效的插入和删除操作
链表和数组对比:
链表的空间不一定连续,而数组空间必须连续。
数组和链表的对比,并不能局限于时间复杂度。而且,在实际的软件,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。
这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。
双向链表和单向链表比较:
除了插入、删除操作有优势之外,对于一个有序链表,双向链表的按值查询的效率也要比单链表高一些。因为,我们可以记录上次查找的位置 p,每次查询时,根据要查找的值与 p 的大小关系,决定是往前还是往后查找,所以平均只需要查找一半的数据。
现在,你有没有觉得双向链表要比单链表更加高效呢?这就是为什么在实际的软件开发中,双向链表尽管比较费内存,但还是比单链表的应用更加广泛的原因。如果你熟悉 Java 语言,你肯定用过 LinkedHashMap 这个容器。如果你深入研究 LinkedHashMap 的实现原理,就会发现其中就用到了双向链表这种数据结构。
用空间换时间:当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。
如何正确写出正确的链表代码?
1. 理解指针或引用的含义。
2. 警惕指针丢失和内存泄露
3. 利用哨兵和简化实现难度
4. 重点留意边界条件处理
5. 举例画图,辅助思考
6. 多写多练没有捷径
栈:
1. 用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。
队列:
顺序队列和链式队列
循环队列(队列满的条件:(tail+1)%n=head)、并发队列、阻塞队列(锁、CAS)
递归的三个条件:
1. 一个问题可以分解成几个子问题
2. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一致。
3. 存在递归终止条件
注意:
1. 递归代码要警惕堆栈溢出
2. 递归代码要警惕重复计算
3. 将递归代码改成非递归代码
二分法:
1. 二分查找,尽量用while循环来做,避免栈溢出。考虑使用 low<=high,而不是 low<high。
2. mid=(low+high)/2 这种写法是有问题的。因为如果 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2。
更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。因为相比除法运算来说,计算机处理位运算要快得多。
3. 注意边界条件,比最小值还小,比最大值更大的特殊情况。
4. mid值判断不符合条件,赋值的时候,可以使用 low = mid + 1 或者 high = mid - 1 。这个需要注意具体使用场景是否可用。
5. low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。比如,当 high=3,low=3 时,如果 a[3] 不等于 value,就会导致一直循环不退出。
小的知识点:
1. 整数相乘或者相加,如果超过最大整数值,会变成负数