通过本次为期两个月的算法训练使自己对于算法中一些名词有了更加深刻的理解。接下来我将按照这段时间的算法题目做一个总结。
在进入算法训练之前首先应对时间复杂度和空间复杂度有一个认识,我们在完成一道代码题后,对其进行优化的前提是可以从目前已完成的代码分析,做出优化。而这其中递归算法的分析是重要一环
递归算法时间复杂度 = 递归的次数 * 每次递归中的操作次数
递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度
接下来进入到了算法最开始的题目,在数组题目中的螺旋矩阵是最令我印象深刻的,这道题让我对于循环不变量有了直观的理解,在数字填充过程中一定要始终保持一种原则进行填充,否则会更加混乱。
在链表这一章节中,虚拟头节点给解题带来了极大的帮助,借助虚拟头节点,可以不必再对真正的头结点进行另行讨论,简化了代码。环形链表的出现是借助数学理论进行结果的判断,如果使用数学推理对于解题有很大阻力。
在哈希表中常见的三种哈希结构(数组,set(集合),map(映射))的定义及应用有了详细的理解,一般来说哈希表都是用来快速判断一个元素是否出现集合里。对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用,哈希函数是把传入的key映射到符号表的索引上。哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
在字符串章节中KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
那么使用KMP可以解决两类经典问题:匹配问题;重复子串问题
接下来是双指针法,双指针法是应用比较广的一个算法,从数组移除重复元素,到链表中的翻转链表,到哈希表中的三数之和(四数之和)问题,到字符串替换单词,均有涉及,双指针法的出现可以极大帮助我们解题,提高时间复杂度。
在栈和队列章节中,我们首先要对其定义有了解,从一开始的用栈实现队列,用队列实现栈来掌握的栈与队列的基本操作。接着,通过括号匹配问题、字符串去重问题、逆波兰表达式问题来系统讲解了栈在系统中的应用,以及使用技巧。通过求滑动窗口最大值,以及前K个高频元素介绍了两种队列:单调队列和优先级队列,这是特殊场景解决问题的利器,是一定要掌握的。
在二叉树章节,从遍历方式到二叉树的属性,从二叉树的修改与构造到二叉搜索树的属性,再到二叉公共祖先问题,二叉搜索树的修改与构造。二叉树我们要掌握二叉树的种类,遍历方式,存储方式,定义,在掌握这些的基础上,有助于解题的规范化。
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
求二叉搜索树的属性,一定是中序了,不然就浪费了有序性了。
在回溯法章节我们了解到,其实回溯法并不是高效的算法,回溯法的本质其实是穷举。
回溯法,一般可以解决如下几种问题:
解回溯算法的题遵照回溯三部曲
1、回溯函数模板返回值以及参数
2、回溯函数终止条件
3、回溯搜索的遍历过程
接下来是贪心算法章节,贪心的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法本身并没有什么清晰的定义,对于什么时候用贪心,取决于我们是否可以从局部最优达到全局最优。
贪心算法一般分为如下四步:
接下来是动态规划章节,动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
对于动态规划问题,需要掌握动规五部曲
动态规划题目最重要的一步便是举例推导dp数组,通过举例推导可以证实我们的思路是否正确严谨。
在单调栈章节中,单调栈的使用场景通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时就要想到可以用单调栈了。时间复杂度为O(n)。单调栈的本质是空间换时间,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。
在使用单调栈的时候首先要明确如下几点:
1、单调栈里存放的元素是什么?
单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。
2、单调栈里元素是递增呢? 还是递减呢?
如果求一个元素右边第一个更大元素,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。
同时注意使用单调栈主要有三个判断条件。
通过这两个月的学习,我最大的感受是每天解代码题是一件十分有成就感的事,本人目前大四,每天做的事情就是搞毕设,写论文,找工作,十分枯燥,本来之前考研以后,想靠刷题来提升自己进复试的,奈何成绩不足以进复试,只能找工作了,但是代码随想录是我每天开始新的一天的第一件事,通过代码随想录使我对于算法的了解更深入了,最起码以后当我碰到类似问题时,我不会说“我之前了解过,我去网上再深入了解一下”等无能回答。
最后感谢一下卡哥及卡哥助手_围城,他们每天在群里耐心解答各种问题,很负责,虽然我个人提问较少,但是每天看到群里面的各种疑惑解答,自己对于该问题的答案也就更明了了。