前言
好,直接说,我们怎么做题?
看题,做题就完事了,哪有这么多婆婆妈妈的事情。
但是如果你有以下问题,可以参考我的刷题方法
思路模糊,不知道用哪个知识点
思维清晰,但写代码的时候忘了许多细节
代码逻辑迷糊,不知道先后顺序
经过700多道题的训练,我形成一个行之有效的体系结构来刷题,能够去帮助大家尽可能发挥自己的实力。本人能力有限,这亦是个人根据自己的情况所制定的,更希望大家在评论区共同讨论刷题方法,共同成长,共同进步。
预期目标
能快速回顾自己已掌握的知识点,将自己熟悉的题型尽可能快速写出来,同时通过知识数据库作为验收机制,高效吸收自己的刷过的题
做题的整体流程
这是流程化做题以及优化方向
- 识别是什么 要处理什么 -- 这个只能提前在代码前面写点注释,提醒自己
- 检测知识机制 -- 知识性问题提醒机制
- 具体处理机制 -- 具体处理的问题提醒机制
- 整理做题总结复盘 -- 搭建可用而且高效的知识库
问题提醒机制
产生的原因
面对一个题目,当我们不能直接的想出知识点,运用的时候,那我们可以像计算机一样,把我们学过的知识一一过一遍,是否可以用到我们所学的知识点。因此我们应该有一个知识检查机制,来帮助我们回顾知识点,起码总比毫无头绪好一点。
但与此同时,人又不能像计算机一样机械,能够有耐心去看完我们所学知识的机制,或者看到这个知识名称不能够很好地激发我们学过的具体知识点。因此,结合心理学一些知识,我们可以将我们的这个检测知识点改成为反问自己的问题,去激活大脑的使用,回顾自己学过的知识点,我称之为问题提醒机制。
问题提醒机制,它既可以用作宏观知识点回顾,也可以详细帮助我们代码的撰写。
- 问题提醒机制要做到有自己逻辑在里面
- 问题是留给自己回顾知识点所提出的问题,来刺激自己的大脑进行回顾
- 机制更多的是根据自己所掌握的算法进度而制定的,不能盲目照搬他人的
以下抛砖引玉:
知识性问题提醒机制
大家可以看看我目前的制作机制
个人的知识性问题提醒机制
1.识别问题阶段
- 这个问题可以化成其他以前你处理过的问题吗?
- 这个问题可以用很简单的方法去处理吗?
- 能不能用图形去描述这个处理逻辑过程?
2.站在答案的角度上看
- 可以排除一些特殊用例吗?
- 能不能从答案去反推代码的一些逻辑?
- 真的是从起点才能去解决问题吗?
- 能不能是从反向前开始的吗?
- 它是处理数据过程中得出的答案,还是在处理完数据中得出答案?数据要不要预处理?
4.他应该使用什么容器(集合)来处理数据?
- 自然容器: 数组 + 链表(插入方向是什么?) + 队列(或者两个以上) + 栈(或者两个以上) +
固定哈希(数组进行表示) +
- 有序/无序哈希表(或者两个以上) + 有序/无序集合(或者两个以上) +
有序可以自动排序(可以优化复杂度)+二维动态数组(图一般存储) + 优先队列
- 数据处理中的容器:线段树(尤其涉及区间修改的地方)
- 根据处理完数据的容器: 并查集 + 单调栈/队列(从大到小?还是小到大?)
5.它是应该怎样进行遍历的呢?
- 是从起点出发?还是终点出发?还是左右(或者中间)两端进行遍历的呢?
6.考虑数据的数值特性没?
- 可以使用区间思想吗?
- 数值的增减性? 数据和数据长度的奇偶性? 因数?余数?素数?当前最值?
- 是否考虑到数值的二进制? 运算符号想来没?& ^ |
7.是否能批量数据?
- 是否可以排序? 可以用几个指针去处理问题? 字母数值化处理可以不?
8.是否要记录过程?
- 记录的方向是什么?
- 用数组记录初始或最后的位置? 记录出现的次数? 累加累减的前后缀数组?
- 可以使用滑动窗口吗? 过程中的最值(单调栈)? kmp思想?
- 可以使用差分数组吗? 可以使用BFS吗? 是否能使用DP思想?
- DP想不出的前提下,是否可以使用记忆化搜索?
9.向下选择过程 -- 即递归?
- 是否不存在固定方式找到最优解,只能枚举所有情况来判断?
- 是线性结构递归?是树递归?还是图递归?
10.有无思考数学处理的观点?
- 能不能推出相关公式? 集合论观点去看待问题(常配合哈希表来处理)? 组合数学去看待问题?
具体处理问题提醒机制
跟上一个同理,但有点建议的是,这个机制是形成于反复遗忘反复记忆的具体知识点,就是你把反复遗忘的点做成对应的问题。再加上对这个知识点的反复思考。
同样拿出我对写递归的问题处理逻辑
个人对递归具体的逻辑思考
递归常用的结构:
- 数组 + 链表 + 二叉树 + 以动态数组存储的图结构
- 线性递归 + 树递归 + 图递归
#梳理逻辑方法:
画图 + 模拟队列 +
#常见处理技巧与问题提醒:
- 是否要进行记录,记录要不要用全局变量来处理?
这个全局变量是可变化长度的数组吗?
还是一开始就要固定长度的,若是固定长度,它应该是几维度数组的?
他的初始化是什么值?意义有什么?
- 递归的返回类型是什么?
- 他的参数列表要有几个参数?
- 怎么返回答案?返回是真正的答案吗?还是递归过程中答案通过全局变量获得?
- 是否需要辅助函数来进行判断?
- 是否要进行回溯?
- 递归的起点是什么?它是怎么样来进行水平方向进行移动的?左还是右?
是否存在条件,才能使其进行递归?它是怎么样往下递归的呢?
- 递归是怎么让它进行划分区间的?
知识数据库的搭建
产生的原因
计算机可以帮助我们存储我们当下所思考的,所学的知识(当然前提是你写文档写注释哈)。不像人的大脑,过一段时间就可以把你的一些知识点给忘记掉。而且我们通过前面的问题提醒机制的话,那我们回顾到某个知识点有用的话,但恰好又忘记了它的那个具体细节,那么我们可以搜索我们曾经编写过的知识文档进行查看,去回顾。
落到实践的话,做完题之后进行复盘迭代,将你做题过程中的思考知识点逻辑顺序,写代码的时候个人认为要注意的点,以及处理对应知识点的感悟和可复用的代码进行记录。
然后将知识点进行模块化,像平时写工程代码一样,最后形成自己写代码时候的工具
结构划分
前提说明,每个人的层次水平和知识层次水平都不一样,每个人还是要根据自己的水平去制定自己的划分逻辑。
-
用面向对象的方法来说,将其分为结构和算法。
-
用面向过程的方法来说,在内部,个人思考(如何逻辑疏通等之类) + 问题提醒机制 + 模板函数 +复用代码
基于hot100制作的个人知识库为例,如图:
实践做题
以四道题为例子,来进行方法论的实践,可以提高自己的刷题效率
分割回文串1
逻辑分析
首先没思路的时候,结合知识性问题提醒机制,发现这是一个不能固定方式来获取的答案,只能全部枚举,那就是关于递归的知识点。
其次,拿出对于递归问题提醒机制,那么进行比对。
那就是全局变量的动态二维数组 + 以字符串长度为终止条件 + 回文串检查函数 + 回溯字符串
参照灵茶山艾府的代码
灵神的代码很精美,我的代码太史了,所以这四道题全部选择灵神的代码,在C++代码进行一些修改,不用灵神的lambda表达式。
java版本
class Solution {
private final List<List<String>> ans = new ArrayList<>();
private final List<String> path = new ArrayList<>();
private String s;
public List<List<String>> partition(String s) {
this.s = s;
dfs(0);
return ans;
}
private void dfs(int i) {
if (i == s.length()) {
ans.add(new ArrayList<>(path)); // 复制 path
return;
}
for (int j = i; j < s.length(); j++) { // 枚举子串的结束位置
if (isPalindrome(i, j)) {
path.add(s.substring(i, j + 1));
dfs(j + 1);
path.remove(path.size() - 1); // 恢复现场
}
}
}
private boolean isPalindrome(int left, int right) {
while (left < right) {
if (s.charAt(left++) != s.charAt(right--)) {
return false;
}
}
return true;
}
}
C++版本
class Solution {
public:
string s;
vector<vector<string>> ans;
vector<string> path;
vector<vector<string>> partition(string s) {
this->s = s;
dfs(0);
return ans;
}
bool check(int i, int j){
while(i < j){
if(s[i++] != s[j--]) return false;
}
return true;
}
void dfs(int i){
if(i == s.size()){
ans.push_back(path);
return;
}
for(int j = i; j < s.size(); ++j){//枚举子串的结束位置
if(check(i, j)){
path.push_back(s.substr(i, j - i + 1));
dfs(j + 1);
path.pop_back();//恢复现场
}
}
}
};
做完题目,并不等于完全吸收这个题目,我们还要进行复盘与总结。不如从以下几个问题去问一问自己,当然还是每个人的层次不一样进度不一样,最好还是因地制宜去反问自己问题。
- 是否存在优化的地方?比如有什么感悟?思维上的进一步精简,代码复杂度是否可以减小等等,优化或补充问题提醒机制?
- 是否存在可复用的代码?
- 是否存在还有新的解法?
分割回文串2
逻辑分析
结合知识型问题提醒机制和递归
将其拆分为判断两个节点之间是否组成回文串 + 记忆化搜索
-
但考虑到每次判断回文串需要消耗大量的时间,那就进行提前处理完,用二维数组palMemo[i][j]来表示i到j的区间是否为一个回文串,进行记忆化搜索
-
从最右端节点开始递归,返回值为int,参数为右端点,结束条件为[0, r]是否为一个回文串,返回为0.
-
同时,从第1个元素枚举到第r个元素(进行遍历的元素为left),如果[l, r]成立回文串,则去递归判断[0, left - 1]去判断是否为回文串,进行记忆化搜索,降低时间复杂度
java版本
class Solution {
public int minCut(String S) {
char[] s = S.toCharArray();
int n = s.length;
int[][] palMemo = new int[n][n];
for (int[] row : palMemo) {
Arrays.fill(row, -1); // -1 表示没有计算过
}
int[] dfsMemo = new int[n];
Arrays.fill(dfsMemo, -1); // -1 表示没有计算过
return dfs(n - 1, s, palMemo, dfsMemo);
}
private int dfs(int r, char[] s, int[][] palMemo, int[] dfsMemo) {
if (isPalindrome(0, r, s, palMemo)) { // 已是回文串,无需分割
return 0;
}
if (dfsMemo[r] != -1) { // 之前计算过
return dfsMemo[r];
}
int res = Integer.MAX_VALUE;
for (int l = 1; l <= r; l++) { // 枚举分割位置
if (isPalindrome(l, r, s, palMemo)) {
res = Math.min(res, dfs(l - 1, s, palMemo, dfsMemo) + 1); // 在 l-1 和 l 之间切一刀
}
}
return dfsMemo[r] = res; // 记忆化
}
private boolean isPalindrome(int l, int r, char[] s, int[][] palMemo) {
if (l >= r) {
return true;
}
if (palMemo[l][r] != -1) { // 之前计算过
return palMemo[l][r] == 1;
}
boolean res = s[l] == s[r] && isPalindrome(l + 1, r - 1, s, palMemo);
palMemo[l][r] = res ? 1 : 0; // 记忆化
return res;
}
}
C++版本
class Solution {
public:
string s;
vector<vector<int>> pal;
vector<int> dfs_memo;
int minCut(string s) {
int n = s.size();
this->s = s;
pal.resize(n, vector<int>(n, -1));
dfs_memo.resize(n, INT_MAX);
return dfs(n - 1);
}
bool is_palindrome(int left, int right){//验证是否回文
if(left >= right) return true;
int& res = pal[left][right];
if(res != -1) return res;
return res = s[left] == s[right] && is_palindrome(left + 1, right - 1);
}
int dfs(int right){//从右端向左端枚举
if(is_palindrome(0, right)) return 0;
int& res = dfs_memo[right];
if(res != INT_MAX) return res;
for(int left = 1; left <= right; ++left){
if(is_palindrome(left, right))
res = min(res, dfs(left - 1) + 1);
}
return res;
}
};
可以将判断回文串预处理的代码内化成自己的复用代码
分割回文串3
1278. 分割回文串 III - 力扣(LeetCode)
逻辑分析
判断两个区间成为回文串的花费 + 分割成k个字符串(二者都记忆化搜索)
递归函数的参数为当前第几个区间,当前右端点位置,结束条件在于到了第0个区间,遍历的方式是当前第几个区间到当前右端点位置(遍历元素为left),递归为(当前第几个区间 - 1, left)
在上一题,我们将回文串预处理代码内化为复用代码,修改一下便是判断两个区间成为回文串的花费
java版本
class Solution {
public int palindromePartition(String s, int k) {
int n = s.length();
int[][] memoChange = new int[n][n];
for (int[] row : memoChange) {
Arrays.fill(row, -1); // -1 表示没有计算过
}
int[][] memoDfs = new int[k][n];
for (int[] row : memoDfs) {
Arrays.fill(row, -1); // -1 表示没有计算过
}
return dfs(k - 1, n - 1, s.toCharArray(), memoDfs, memoChange);
}
// 把 s[:r+1] 切 i 刀,分成 i+1 个子串,每个子串改成回文串的最小总修改次数
private int dfs(int i, int r, char[] s, int[][] memoDfs, int[][] memoChange) {
if (i == 0) { // 只有一个子串
return minChange(0, r, s, memoChange);
}
if (memoDfs[i][r] != -1) { // 之前计算过
return memoDfs[i][r];
}
int res = Integer.MAX_VALUE;
// 枚举子串左端点 l
for (int l = i; l <= r; l++) {
res = Math.min(res, dfs(i - 1, l - 1, s, memoDfs, memoChange) + minChange(l, r, s, memoChange));
}
return memoDfs[i][r] = res; // 记忆化
}
// 把 s[i:j+1] 改成回文串的最小修改次数
private int minChange(int i, int j, char[] s, int[][] memoChange) {
if (i >= j) { // 子串只有一个字母,或者子串是空串
return 0; // 无需修改
}
if (memoChange[i][j] != -1) { // 之前计算过
return memoChange[i][j];
}
int res = minChange(i + 1, j - 1, s, memoChange);
if (s[i] != s[j]) {
res++;
}
return memoChange[i][j] = res; // 记忆化
}
}
C++版本
class Solution {
public:
vector<vector<int>> memo_change;
vector<vector<int>> memo_dfs;
string s;
int palindromePartition(string s, int k) {
int n = s.size();
this->s = s;
memo_change.resize(n, vector<int>(n, -1));
memo_dfs.resize(k, vector<int>(n, -1));
return dfs(k - 1, n - 1);
}
int minChange(int i, int j){
if(i >= j) return 0;
int& res = memo_change[i][j];
if(res != -1) return res;
return res = minChange(i + 1, j - 1) + (s[i] != s[j]);
}
int dfs(int i, int r){
if(i == 0) return minChange(0, r);
int& res = memo_dfs[i][r];
if(res != -1) return res;
res = INT_MAX;
for(int l = i; l <= r; l++){
res = min(res, dfs(i - 1, l - 1) + minChange(l, r));
}
return res;
}
};
我们可以将区间划分的代码进行变成复用代码
分割回文串4
可以根据题三的题意转换,转换成K(即3)个区间的回文串花费为0
Java
class Solution {
public boolean checkPartitioning(String s) {
int n = s.length();
int k = 3;
int[][] memoChange = new int[n][n];
for (int[] row : memoChange) {
Arrays.fill(row, -1); // -1 表示没有计算过
}
int[][] memoDfs = new int[k][n];
for (int[] row : memoDfs) {
Arrays.fill(row, -1); // -1 表示没有计算过
}
return dfs(k - 1, n - 1, s.toCharArray(), memoDfs, memoChange) == 0;
}
private int dfs(int i, int r, char[] s, int[][] memoDfs, int[][] memoChange) {
if (i == 0) {
return minChange(0, r, s, memoChange);
}
if (memoDfs[i][r] != -1) {
return memoDfs[i][r];
}
int res = Integer.MAX_VALUE;
for (int l = i; l <= r; l++) {
res = Math.min(res, dfs(i - 1, l - 1, s, memoDfs, memoChange) + minChange(l, r, s, memoChange));
}
return memoDfs[i][r] = res;
}
private int minChange(int i, int j, char[] s, int[][] memoChange) {
if (i >= j) {
return 0;
}
if (memoChange[i][j] != -1) {
return memoChange[i][j];
}
int res = minChange(i + 1, j - 1, s, memoChange);
if (s[i] != s[j]) {
res++;
}
return memoChange[i][j] = res;
}
}
C++
class Solution {
public:
vector<vector<int>> memo_change;
vector<vector<int>> memo_dfs;
string s;
int minChange(int i, int j){
if(i >= j) return 0;
int& res = memo_change[i][j];
if(res != -1) return res;
return res = minChange(i + 1, j - 1) + (s[i] != s[j]);
}
int dfs(int i, int r){
if(i == 0) return minChange(0, r);
int& res = memo_dfs[i][r];
if(res != -1) return res;
res = INT_MAX;
for(int l = i; l <= r; l++){
res = min(res, dfs(i - 1, l - 1) + minChange(l, r));
}
return res;
}
bool checkPartitioning(string s) {
int n = s.size();
this->s = s;
memo_change.resize(n, vector<int>(n, -1));
memo_dfs.resize(3, vector<int>(n, -1));
return dfs(2, n - 1) == 0;
}
};
后话以及还可以细化部分
精进一步的方向
还可以提供对应的方向,希望大家去在实践之中去形成自己的问题提醒机制
运行错误修改问题机制 -- 面对代码出错的处理和进入思维误区的处理
搭建知识数据库的验收机制 -- 高效吸收题目精髓
心里的话
这些方法皆是作者本人的做题熟练度不够高,为提高自己的效率,想出来的一些解决方案。原因如下:
- 一从正态分布来看,我承认我是一个普通人,我并不是在算法题上面有天赋的人,只能选择一个擅长我思维方法来提高效率。
- 二是从功利的角度来说,对于普通人来说,练习算法其实更多就是锻炼一些思维能力,在实际工作之中用的不多,而且进入工作之中,能留给练习算法的时间其实并不多,能高效点尽量高效点。
但本质上,最后当我们不再用所谓的问题提醒机制和知识数据库的时候,那已经是一个卖油翁的无他,惟手熟尔状态
道歉
分割字符串这系列题的正解更多在于动态规划这一部分,但为了验证我的问题提醒机制和知识库搭建,多少带点手里有铁锤,看什么都是钉子的感观。大家理解相对高效的方法就行,具体详细的解答请移步到力扣平台,那里的解答更加丰富更加完美。
还是向大家说声,对不起。这也是我第一次写文档方面的东西,熟练度不够高,希望大家谅解。也希望大家在评论区提供相应的指导建议,大家一起进步。