基于力扣700题的高效刷题方法

329 阅读16分钟

前言

0e80fa94ae53844f3ddaee9e6d52b1f1.png 好,直接说,我们怎么做题?

看题,做题就完事了,哪有这么多婆婆妈妈的事情。

02134e6cdb4c78c8c948fb360153b6e.png

但是如果你有以下问题,可以参考我的刷题方法

思路模糊,不知道用哪个知识点
思维清晰,但写代码的时候忘了许多细节
代码逻辑迷糊,不知道先后顺序

经过700多道题的训练,我形成一个行之有效的体系结构来刷题,能够去帮助大家尽可能发挥自己的实力。本人能力有限,这亦是个人根据自己的情况所制定的,更希望大家在评论区共同讨论刷题方法,共同成长,共同进步。

预期目标

能快速回顾自己已掌握的知识点,将自己熟悉的题型尽可能快速写出来,同时通过知识数据库作为验收机制,高效吸收自己的刷过的题

做题的整体流程

这是流程化做题以及优化方向

  • 识别是什么 要处理什么 -- 这个只能提前在代码前面写点注释,提醒自己
  • 检测知识机制 -- 知识性问题提醒机制
  • 具体处理机制 -- 具体处理的问题提醒机制
  • 整理做题总结复盘 -- 搭建可用而且高效的知识库
65599fe49e93187c3757463b058441c.png

问题提醒机制

产生的原因

1cfa3cd4860d44004302be293a1aa06.png

面对一个题目,当我们不能直接的想出知识点,运用的时候,那我们可以像计算机一样,把我们学过的知识一一过一遍,是否可以用到我们所学的知识点。因此我们应该有一个知识检查机制,来帮助我们回顾知识点,起码总比毫无头绪好一点。

但与此同时,人又不能像计算机一样机械,能够有耐心去看完我们所学知识的机制,或者看到这个知识名称不能够很好地激发我们学过的具体知识点。因此,结合心理学一些知识,我们可以将我们的这个检测知识点改成为反问自己的问题,去激活大脑的使用,回顾自己学过的知识点,我称之为问题提醒机制。

问题提醒机制,它既可以用作宏观知识点回顾,也可以详细帮助我们代码的撰写

  • 问题提醒机制要做到有自己逻辑在里面
  • 问题是留给自己回顾知识点所提出的问题,来刺激自己的大脑进行回顾
  • 机制更多的是根据自己所掌握的算法进度而制定的,不能盲目照搬他人的

以下抛砖引玉

知识性问题提醒机制

66310280d884506bd1ecbad6e53a047.png

大家可以看看我目前的制作机制

个人的知识性问题提醒机制

1.识别问题阶段
- 这个问题可以化成其他以前你处理过的问题吗?
- 这个问题可以用很简单的方法去处理吗?
- 能不能用图形去描述这个处理逻辑过程?

2.站在答案的角度上看
- 可以排除一些特殊用例吗?
- 能不能从答案去反推代码的一些逻辑?
- 真的是从起点才能去解决问题吗?
- 能不能是从反向前开始的吗?
- 它是处理数据过程中得出的答案,还是在处理完数据中得出答案?数据要不要预处理?

4.他应该使用什么容器(集合)来处理数据?
- 自然容器: 数组 + 链表(插入方向是什么?) + 队列(或者两个以上) + 栈(或者两个以上) +
  固定哈希(数组进行表示) + 
- 有序/无序哈希表(或者两个以上) + 有序/无序集合(或者两个以上) +
  有序可以自动排序(可以优化复杂度)+二维动态数组(图一般存储) + 优先队列
- 数据处理中的容器:线段树(尤其涉及区间修改的地方)
- 根据处理完数据的容器: 并查集 + 单调栈/队列(从大到小?还是小到大?)

5.它是应该怎样进行遍历的呢?
- 是从起点出发?还是终点出发?还是左右(或者中间)两端进行遍历的呢?

6.考虑数据的数值特性没?
- 可以使用区间思想吗? 
- 数值的增减性? 数据和数据长度的奇偶性? 因数?余数?素数?当前最值?
- 是否考虑到数值的二进制? 运算符号想来没?& ^ | 

7.是否能批量数据?
- 是否可以排序? 可以用几个指针去处理问题? 字母数值化处理可以不? 

8.是否要记录过程?
- 记录的方向是什么?
- 用数组记录初始或最后的位置? 记录出现的次数? 累加累减的前后缀数组?
- 可以使用滑动窗口吗? 过程中的最值(单调栈)? kmp思想?
- 可以使用差分数组吗?  可以使用BFS吗? 是否能使用DP思想?
- DP想不出的前提下,是否可以使用记忆化搜索?

9.向下选择过程 -- 即递归?
- 是否不存在固定方式找到最优解,只能枚举所有情况来判断?
- 是线性结构递归?是树递归?还是图递归?


10.有无思考数学处理的观点?
- 能不能推出相关公式? 集合论观点去看待问题(常配合哈希表来处理)? 组合数学去看待问题?

2cdb7a4da1a3dbb6ff97a80ca999fae.png

具体处理问题提醒机制

跟上一个同理,但有点建议的是,这个机制是形成于反复遗忘反复记忆的具体知识点,就是你把反复遗忘的点做成对应的问题。再加上对这个知识点的反复思考。

同样拿出我对写递归的问题处理逻辑

个人对递归具体的逻辑思考

递归常用的结构:
- 数组 + 链表 + 二叉树 + 以动态数组存储的图结构
- 线性递归 + 树递归 + 图递归

#梳理逻辑方法:
画图 + 模拟队列 + 

#常见处理技巧与问题提醒:
- 是否要进行记录,记录要不要用全局变量来处理?
  这个全局变量是可变化长度的数组吗?
  还是一开始就要固定长度的,若是固定长度,它应该是几维度数组的?
  他的初始化是什么值?意义有什么?
- 递归的返回类型是什么?
- 他的参数列表要有几个参数?
- 怎么返回答案?返回是真正的答案吗?还是递归过程中答案通过全局变量获得?
- 是否需要辅助函数来进行判断?
- 是否要进行回溯?
- 递归的起点是什么?它是怎么样来进行水平方向进行移动的?左还是右?
  是否存在条件,才能使其进行递归?它是怎么样往下递归的呢?
- 递归是怎么让它进行划分区间的?

6ea598d25667784a212a362d6556299.png

知识数据库的搭建

产生的原因

计算机可以帮助我们存储我们当下所思考的,所学的知识(当然前提是你写文档写注释哈)。不像人的大脑,过一段时间就可以把你的一些知识点给忘记掉。而且我们通过前面的问题提醒机制的话,那我们回顾到某个知识点有用的话,但恰好又忘记了它的那个具体细节,那么我们可以搜索我们曾经编写过的知识文档进行查看,去回顾。

落到实践的话,做完题之后进行复盘迭代,将你做题过程中的思考知识点逻辑顺序,写代码的时候个人认为要注意的点,以及处理对应知识点的感悟和可复用的代码进行记录。

然后将知识点进行模块化,像平时写工程代码一样,最后形成自己写代码时候的工具

b5f6e0f4d51bb8339be3e2726a8f340.png

结构划分

前提说明,每个人的层次水平和知识层次水平都不一样,每个人还是要根据自己的水平去制定自己的划分逻辑。

  • 用面向对象的方法来说,将其分为结构和算法。

  • 用面向过程的方法来说,在内部,个人思考(如何逻辑疏通等之类) + 问题提醒机制 + 模板函数 +复用代码

基于hot100制作的个人知识库为例,如图:

266fcc7074938edf7a4b32477253481.png

实践做题

以四道题为例子,来进行方法论的实践,可以提高自己的刷题效率

分割回文串1

131. 分割回文串 - 力扣(LeetCode)

2bd56e5295f0de0d0c3202d1a7bfc73.png

逻辑分析

首先没思路的时候,结合知识性问题提醒机制,发现这是一个不能固定方式来获取的答案,只能全部枚举,那就是关于递归的知识点。

其次,拿出对于递归问题提醒机制,那么进行比对。

image.png

那就是全局变量的动态二维数组 + 以字符串长度为终止条件 + 回文串检查函数 + 回溯字符串

参照灵茶山艾府的代码

灵神的代码很精美,我的代码太史了,所以这四道题全部选择灵神的代码,在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

132. 分割回文串 II - 力扣(LeetCode)

image.png

逻辑分析

结合知识型问题提醒机制和递归

image.png

将其拆分为判断两个节点之间是否组成回文串 + 记忆化搜索

  • 但考虑到每次判断回文串需要消耗大量的时间,那就进行提前处理完,用二维数组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)

image.png

逻辑分析

判断两个区间成为回文串的花费 + 分割成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

1745. 分割回文串 IV - 力扣(LeetCode)

image.png

可以根据题三的题意转换,转换成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;
    }
};

后话以及还可以细化部分

精进一步的方向

还可以提供对应的方向,希望大家去在实践之中去形成自己的问题提醒机制

运行错误修改问题机制 -- 面对代码出错的处理和进入思维误区的处理

搭建知识数据库的验收机制 -- 高效吸收题目精髓

d36f63721af5392b92c7b253bfbe4ae.png

心里的话

这些方法皆是作者本人的做题熟练度不够高,为提高自己的效率,想出来的一些解决方案。原因如下:

  • 一从正态分布来看,我承认我是一个普通人,我并不是在算法题上面有天赋的人,只能选择一个擅长我思维方法来提高效率。
  • 二是从功利的角度来说,对于普通人来说,练习算法其实更多就是锻炼一些思维能力,在实际工作之中用的不多,而且进入工作之中,能留给练习算法的时间其实并不多,能高效点尽量高效点。

但本质上,最后当我们不再用所谓的问题提醒机制和知识数据库的时候,那已经是一个卖油翁的无他,惟手熟尔状态

道歉

分割字符串这系列题的正解更多在于动态规划这一部分,但为了验证我的问题提醒机制和知识库搭建,多少带点手里有铁锤,看什么都是钉子的感观。大家理解相对高效的方法就行,具体详细的解答请移步到力扣平台,那里的解答更加丰富更加完美。

还是向大家说声,对不起。这也是我第一次写文档方面的东西,熟练度不够高,希望大家谅解。也希望大家在评论区提供相应的指导建议,大家一起进步。

image.png