写这篇文章是为了总结记录《数据结构与算法》课程中的技术要点。
一、数组
- 在数组中插入的时候,如果要插入指定的下标,会使数组的其他元素后移,此时插入到指定下标的时间复杂度是O(n),如果不是非必要不一定要插入到指定下标,可以插入末尾,或者直接替换下标数据。
- 删除数组中元素的时候,会使数组产生复制操作,此时时间复杂度是O(n),为了节省时间,可以批量删除,降低时间复杂度。
- ArrayList:内部是数组,默认数组容量是10,如果达到数据达到数组容量,扩容为元素组3倍容量,扩容会产生数组复制,是比较耗时的操作。
二、链表
- 链表做插入操作的时候有可能元素为空,如果每次插入都要判断首节点是否为null,需要多一次逻辑判断,此时可以用哨兵模式,head节点就设置为空,这样每次插入的时候,就插到尾部下一个即可。
三、递归
- 递归的核心是“写出递推公式,找到终止条件”,引用一段《数据结构与算法之美》的例子说明:
假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?如果有 7 个台阶,你可以 2,2,2,1 这样子上去,也可以 1,2,1,1,2 这样子上去,总之走法有很多,那如何用编程求得总共有多少种走法呢?
我们仔细想下,实际上,可以根据第一步的走法把所有走法分为两类,第一步走一个台阶,第二类是第一步走了两个台阶,所以n个台阶的走法就相当于走了一个台阶后n-1个台阶的走法,加上走了两个台阶后,n-2个台阶的走法,用公式表示就是:f(n) = f(n-1)+f(n-2)
有了递推公司,递归代码基本就完成了一半。我们再看下终止条件。当有一个台阶时,我们不需要再继续递归,就只有一种走法,所以f(1)=1。这个递归终止条件足够么,我们用比较小的n=2,n=3来试验一下。
n=2时,f(2)=f(1)+f(0),如果递归终止条件只有一个n=1,f(2)就无法求解了,因为没有0个台阶这种情况,所以我们把f(2)=2作为终止条件,表示走两个台阶有两种走法,一步走完或者分两步走。所以递归的终止条件有两个:f(1)=1,f(2)=2。这个时候你可以拿n=3,n=4来验证一下,看是否符合条件。我们把递归终止条件和刚刚得到的递推公式放到一起就是这样的:
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2)
有了这个公式,我们转化成递归代码就简单多了。最终的递归代码是这样的:
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
- 总结:计算机擅长做重复的事情,人脑喜欢做平铺直叙的运算,我们在做递归算法的时候,喜欢脑子中跟着一层层往下调用,其实这是进入了思维误区,人脑其实根本没有办法把“递”和“归”的过程一步步想清楚。
- 很多时候我们理解起来困难,是因为自己给自己制造了这种思维障碍,正确的思维方式应该是怎样的呢:
如果问题A可以被分解为问题B、C、D,你可以假设B、C、D问题已经解决,在此基础上思考问题A与B、C、D问题的联系,思考怎么解决,不需要一层层往下思考子问题与子子问题之间的关系,屏蔽掉递归细节,这样理解起来就简单多了。
因此,编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
警惕递归代码重复计算
上面的递归例子,我们用图解分解一下,如下:
图中可以看到f(3)被计算了3次,f(4)被计算了两次。
为了避免重复计算,我们可以把计算过的值存到hashMap中,当遇到之前计算过的值,直接取出即可,改造之前代码如下:
public int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// hasSolvedList 可以理解成一个 Map,key 是 n,value 是 f(n)
if (hasSolvedList.containsKey(n)) {
return hasSovledList.get(n);
}
int ret = f(n-1) + f(n-2);
hasSovledList.put(n, ret);
return ret;
}
递归调试方式
- 打印日志发现,递归值。
- 结合条件断点进行调试。
递归调用细节问题解决
- 避免递归太深:可以设置一个阈值。
参考文献
专栏:数据结构与算法之美:递归
四、排序
排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|
冒泡、插入、选择 | O(n2) | 是 |
快排、归并 | O(nlogn) | 是 |
桶、计数、基数 | O(n) | 否 |
- 在java的List和Array的排序算法中,默认使用的是在插入排序上优化过的Tim排序,在32个元素内进行插入排序,32个元素以上进行归并排序,时间复杂度O(n log n),最坏情况下的时间复杂度是O(n²)。
五、HashMap
- 初始大小是16,扩容为原容量的二倍大小,当元素个数>负载因子*容量,进行扩容,扩容要进行数据移动,也是比较耗时的操作。
六、有序集合
七、二叉树
八、堆排序的应用:
- 堆的排序应用有:优先级队列、利用堆求topK、利用堆求中位数。
九、深度和广度优先搜索
- 搜索的最基础就是广度与深度优先搜索算法,此处有一篇文章,请收好:深度和广度优先搜索
十、字符串匹配
- 在java中,字符串匹配是用的最简单的方式,一个个比较。
- 关于搜索匹配,最高效的算法是BM算法:在linux中的grep命令就是用BM算法实现
十一、AC自动机
当你有上千万的敏感词,需要过滤,你输入的一句话,你能一个个地把敏感词与你输入的语句进行匹配么,此时就需要用到AC自动机。
其实后来楼主想了下,如果不懂AC自动机,用散列其实效率也不会下降非常厉害。
十二、高端的算法思想
- 分治算法、动态规划
- 最短路径:地图软件是如何计算出行的最短路径的
- 位图:实现网页爬虫中的url去重功能
- 布隆过滤器:基于BitMap,可以在非常大批量的数据中统计哪些被使用过,性能很高