算法
…
动态规划
动态规划问题的一般形式就是求最值,例如:最长递增子序列呀,最小编辑距离等。求解动态规划的核心问题是穷举。
动态规划三要素:
- 重叠子问题:需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
- 最优子结构:子问题间必须互相独立。
- 状态转移方程:最困难部分,思路:明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。
备忘录:一般使用数组,也可以使用哈希表。
DP table:采用自底向上的方法,去除递归,改为循环。
进一步优化:采用自底向上方法,还可以节省空间,去除不再需要的历史状态。
时间复杂度计算:子问题总数 * 每个子问题的时间
解题思路
- 首先确定子问题以及子问题是否相互独立。
- 列出状态转义方程。
- 确定状态:原问题和子问题中变换的变量。
- 确定dp函数:被递归调用的函数(自变量,因变量)。
- 确定选择状态的条件
- 明确最小子问题base case
- 解除重复子问题
- 使用备忘录
- 使用dp table,定义dp数组(自变量,因变量)
最优子结构:并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题。
如果问题中没有最优子结构,那么久改造它,让它有。找最优子结构的过程,其实就是证明状态转移方程正确性的过程,方程符合最优子结构就可以写暴力解了,写出暴力解就可以看出有没有重叠子问题了,有则优化,无则 OK。
回溯
解决一个回溯问题,实际上就是一个决策树的遍历过程。不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
- 路径:也就是已经做出的选择。
- 选择列表:也就是你当前可以做的选择。
- 结束条件:也就是到达决策树底层,无法再做选择的条件。
1 | result = [] |
二分查找
二分查找:思路很简单,细节是魔鬼。
为了能够清晰展示所有细节,尽量用else if
而不是else
1 | int binarySearch(int[] nums, int target) { |
一般的查找一个数(没有重复元素)
1 | int binarySearch(int[] nums, int target) { |
A: while(left < right)
的终止条件是 left == right
,这时候区间[x, x]
被漏掉,要在最后单独判断该区间。否则就用while(left <= right)
作为终止条件。
B: left = mid + 1
,right = mid - 1
,由于mid
已经被搜索过了,因此在新的区间中去掉mid
。
寻找左侧边界的二分搜索
1 | int left_bound(int[] nums, int target) { |
双指针
unordered_map
就是哈希表(字典),它的一个方法 count(key)
相当于 containsKey(key)
可以判断键 key 是否存在。可以使用方括号访问键对应的值 map[key]
。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key]
赋值为 0
。
滑动窗口:
- 初始化 left = right = 0,把索引闭区间 [left, right] 称为一个「窗口」。
- 不断地增加 right 指针扩大窗口 [left, right],直到窗口中的字符串符合要求。
- 我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right],直到窗口中的字符串不再符合要求。
- 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
总结:先移动右指针找到可行解,再移动左指针优化可行解。最后找到所有优化的可行解。
1 | int left = 0, right = 0; |
快慢指针:
判定链表中是否含有环:如果有环,那么快指针总会碰到慢指针。
1 | boolean hasCycle(ListNode head) { |
找到环:当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
1 | ListNode detectCycle(ListNode head) { |
找链表中点;
找链表倒数第K个元素。
左右指针:一般初始化为 left = 0, right = nums.length - 1 。
二分查找;
两数之和;
反转数组;
滑动窗口。
排序
排序应该考虑数据的特点:
- 一般情况下:快排
- 包含大量重复元素:三路快排
- 近乎有序:插入排序
- 有限范围:计数排序
- 稳定排序:归并排序
- 内存不足:外排序
- 无法随机存取:归并排序
面试准备
准备的主要内容:
- 梳理项目
- 项目经历
- 遇到的实际问题
- 遇到过的难以解决的BUG
- 面向对象与设计模式
- 网络、安全、内存、并发等问题
- 系统规模,系统设计,承载量
个人经历,结合项目:
- 遇到的最大的挑战
- 犯过的错误
- 经历的失败
- 最享受的工作内容
- 遇到冲突的解决方法
- 做过的与众不同的事
做好问面试官的问题:
- 小组的运行模式
- 项目的后续规划
- 产品中的某个问题如何解决的
- 为什么选择这个技术和标准
- 我在这个小组中有机会深入学习某个技术吗
低概率算法面试题:
- 红黑树
- B-Tree
- 斐波那契堆
- 计算几何
- 数论
- FFT
常见算法面试题:
- 排序算法
- 数据结构的实现:堆,二叉树,图
- 数据结构的使用:链表,栈,队列,哈希表,图,Trie,并查集
- 算法:深度优先,广度优先,二分查找,递归
- 基本思想:递归,分治,回溯,贪心,动态规划
推荐
- LeetCode
- HackerRank
关键词:
- 是否有序
- 时间复杂度
- 空间复杂度
- 数据规模
找思路和优化:
- 设计几个简单的测试用例
- 考虑暴力解法,再优化
- 使用常见的算法和数据结构
- 空间换时间
- 排序
一定要会白板编程:
- 堆
- 快排
- …
算法复杂度
一秒内各个复杂度能力:
- n^2 - 10^4
- n - 10^8
- nlogn - 10^7
对数器
使用最不容易出错的方法实现一个程序,再用这个程序测试自己的算法。
数组
BFPRT
KMP
位运算
异或:满足交换律。
找到一个数组中一个出现奇数次的数:偶数个数异或为零。
取int最右侧的1:N & ((~N) + 1),
有两个数(a,b)出现奇数次,其他都是偶数次:所有数异或,相对于 a 异或 b 且结果不为 0,取结果int最右侧的 1。例如是第x位,那么a和b的第x位一定不同。此时可以按照第八位不同来将数组划分成两半。各自找出组内唯一的奇数次数。
大小写转化
- 转小写
('a' | ' ') = 'a', ('A' | ' ') = 'a'
- 转大写
('a' & '_') = 'A', ('A' & '_') = 'A'
- 大小写呼唤
('a' ^ '_') = 'A', ('A' ^ '_') = 'a'
并查集
链表
快慢指针:
- 找中点(上中点,下中点),中点上一个点:先处理头节点,不足3个节点的情况。
判断链表回文:
- 栈:所有元素入栈,然后取出一个元素,与链表元素比对,直到完成所有比对。
- 修改链表后半个方向
划分为元素左边小,中间等,右边大:考虑换头。
- 利用数组
- 划分为3个区,每个区存储2个边界,再把链表放入3个区中,最后串联所有区。
复制链表:链表上每个节点还有额外指针指向其他元素
- 使用哈希表,旧节点与新节点一一映射。
判断链表是否相交,可能存在环
- 判断环,得到环的第一个节点。
- 使用 Set,存储内存地址。
- 快慢指针,相遇后,快指针从头开始,每次一步,直到再相遇。
- 如果都无环,一定是 Y 结构
- 使用 Set
- 记录两个链表长度,先判断他们最后节点是同一个节点,如果是,则一定相交。长链表先走,走到长度一致的位置开始,然后两链表一起走,一定会同时走到相交节点。
- 如果有环,一个无环,则一定不相交
- 两个都有环:不相交,相交且有相同的入环节点,相交且入环节点不同。
- 相交且有相同的入环节点:与 Y 结构解法类似
- 相交且入环节点不同、不相交:判断一个环上是否同时存在两个入环节点。
约瑟夫环问题
- 循环链表
二叉树
非递归遍历:
- 先序遍历:弹出就打印,先压入右,再压入左。
- 中序遍历:先压缩左边界所有节点,之后弹出一个节点打印,来到它的右树,再压入其所有左边界。
- 后序遍历:不严格方式,先序遍历的逆序,用栈收集元素。严格方式,要借用辅助指针。h指针表示上一个处理的节点,c指针表示当前处理的节点,然后判断c的左右两个还是是否右h,根据情况判断处理子树还是处理栈中元素。
- 按层遍历:使用队列。查看一行是否遍历结束,使用标志变量。
序列化、反序列化:遇到空节点也要输出。
- 先序遍历
- 层序遍历
打印一颗二叉树:
- 补成满二叉树打印
- 设计树的输出顺序:右-头-左,然后按行输出
中序找后继节点
- 右树上最左结点,或祖先是左子树的结点
判断平衡树
- 使用二叉树:考虑左右子树需要为自己提供的信息
返回整棵树的最大距离
- 左树的最大距离 + 右树的最大距离,或左(右)树自己的最大距离
找出二叉树中的最大搜索二叉树
- 左(右)树自己的最大搜索二叉树,或包含自己的最大搜索二叉树
- 获取子树的:最大值,最小值,是否是搜索二叉树,包含的搜索二叉树大小
判断满二叉树:
- 2^L - 1 <= H
判断完全二叉树:
- 宽度优先遍历,有右子树但无左子树则不是,都有则继续,只有左或没有子树,则后续都必须是叶子结点。
找两个结点的公共祖先
- 利用 Set ,并用 Map 记录子父关系
- 自己是目标结点,目标结点在左子树,或右子树,或不存在。
贪心算法:证明有效的方法是举反例
- 排序:不一定有传递性,贪心算法的排序需要传递性
线段树:区间中增删改查,算法复杂度是 log N
- 数组先补成 2^N 长度,再以此构建一个满二叉树
- 每个结点设计标记:lazy,change,update 标记
图算法
遍历
- 宽度优先遍历
- 考虑环的问题:标记法,数组 或 Set
- 深度优先遍历
拓扑排序
- 队列:每次入度为零的点加入
- 数组:排序结果
最小生成树
- 堆:存放边
最短路径
- Dijkstra
动态规划
一共4
种模型。
打印字符串的子序列
- 暴力递归,参数包括:字符串,当前的字符序号,之前所形成的答案(叶节点用),当前使用的路径。叶结点处理路径结果。
- 去重后只求个数,动态规划
打印字符串的全排列
- 暴力递归:
str[i]
之后的所有字符都有机会到str[i]
位置,之前的都是定好的。
从左往右尝试模型:
111
可以转化为AAA
或KA
- 0-1背包:不变量,货物的重量,价值,包裹的大小;变量,选择了哪些货物,已经存放了多少重量(或剩余的空间);因变量,总价值
范围上尝试模型
- 数值不同的纸牌排成一条线,玩家A和玩家B依次拿走纸牌,且每次只能拿最左或最右的纸牌。如果A先拿,则获胜者的分数是多少。需要参数:数组,范围的左右边界。
- base case - 如果范围内只有一个纸牌,则直接返回这个纸牌
- 要么选左边的牌,要么选右面的牌,选完后变后手。
- 站在先手的角度考虑,求先手的函数要max,求后手的函数要min
多样本位置全对应的尝试模型
寻找业务限制的尝试模型
经典问题
- N 皇后问题
- 优化:位运算,斜线限制使用位移
动态规划的解法:先暴力写法,再改记忆化搜索算法,再改经典动态规划(可不改)。如果状态出现枚举行为,则改经典动态规划后会继续优化(利用状态转移做优化)。看一个格子依赖谁,每次依赖多少,就可以压缩到多少。
- 记忆化搜索:键要包括变量,值要设定初始值。
特殊问题:
- 取石子问题,合并石子问题,四边形不等式。
- 摆放广告牌问题。
- 斜率优化
- 初始值设置
预处理
- 前缀和
- 前缀树
- 排序
其他问题
海盗分金币问题:分析设定,从小依次递推
欧拉信封问题:找递推公式 F(n) = (n-1) * (f(n-2) + f(n-1)), 初值 0, 1, 2