查看原文
其他

算法小白如何高效刷LeetCode?

果冻虾仁 编程往事 2022-08-22

心理建设篇

在讲述具体的方法之前,先要明白一件事。凡事都分『道』与『术』。本问题下面大部分回答介绍的都是『术』。而对于刷LeetCode这件事,无论你看到多少高明的的方法,如果你不能持之以恒,都没有用。所有在刷LeetCode这件事上,他的『道』就是:如何能持之以恒的保持刷题热情。我认为『道』更重要。我先谈一下这部分。

寻找标尺,让进步可衡量

为什么王者荣耀有段位,有青铜、白银、黄金。以我个人的经验来看,这些都是刺激你持续玩游戏的游戏机制。通过段位,彰显个人的实力,没错,装逼是人类进步的一大动力。同时也让进步可以衡量。刷LeetCode也是同样,一般刚开始我们会关注解决了多少道题。更进一步,我们需要关心自己的排名。LeetCode也有排名机制。每刷一道题,就前进一点点。可以每周给自己做一个记录,记一下自己的排名。让自己重视排名。逐渐的自己就会把积极性调动起来,想让自己的排名越来越高,就是打排位一样。

除了排名,你还可以每周记录一下通过率。

寻找小惊喜,给自己正反馈

坚持做每日一题,LeetCode每天都有一道题,完成后,你的打卡日历就会勾上一个圈。

如果打满一个月就会获得一个月度徽章。另外LeetCode有很多“学习计划”,每个学习计划完成后也会有一个徽章。

我有段时间就喜欢收集徽章,为了收集这些小玩意,会刺激自己持续解题。其实和游戏里面也差不多。学会游戏化学习。

不要死磕,学会放弃

开始刷LeetCode感到吃力是正常的。我这里说的学会放弃,不是说放弃刷力扣,而是说碰到自己无法解决的题目,不要死磕。赶紧看一下题解去学习,这没有什么丢人的。每个人都是从这个阶段过来的。就比如小学生无法做出高中生的题目,并不是因为小学生笨,而是因为中间有很多知识,小学生没学过。对于算法而言,自然也是如此。靠自己死磕,是很难想到很多前辈科学家们总结出来的算法。这时候去看题解学习也不为过。

如果遇到特别难的题,看题解也不理解。也不用纠结。就隔着它。可以把题号记下来。过段时间再回顾。

好记性不如烂笔头

我是笔记强迫症患者……我现在刷LeetCode,每道做过的题,我都会记到Notion里。打上几个标签,方便回顾。

记忆是有遗忘曲线的,不能因为你现在能做出一道题,就认为自己能永远做出来。小学时,老师就教导我们:

好记性不如烂笔头

讲完『道』,我也谈一下我的『术』。我这里不对具体的算法和数据结构题型做解析了,我主要给你指路。

我伸手给你指路,希望你看见的是路,而不是我的手

刷题顺序篇

当然我不建议从头按顺序刷,我分为“正排”刷题和“倒排”刷题,两种策略。

所谓“正排刷题”可以刷LeetCode上面的题单。

我个人建议从《剑指Offer》开始刷。《剑指Offer》现有出了两册,在LeetCode上都有题单。建议从《剑指Offer》刷起,碰到不会的,可以看书中的讲解。

根据书来刷题,当你刷完一本书后,常见的题目类型几乎都覆盖到了。当然同一种类型的题目数不胜数,重要是培养一下各种题型大概的思路。这属于“正排”刷题。

“倒排”刷题,是打破常规,不根据题目类型或按照某种顺序来做题。玩的就是措手不及。比如坚持做每日一题。坚持一个月你就会见到各种各样的题型,有的甚至很刁钻。方便查漏补缺。另外如果有信心的话,可以参加一下周赛,进一步给自己惊喜。

网站/APP使用篇

自定义测试用例

不要着急提交代码。多用测试用例自测。如果是提交之后发现某个case不过,调试的时候一定要在测试用例这里调试。

不要用重复提交代码的方式调试。每次提交代码都会跑N多个case,时间慢。另外就是会降低你在LeetCode上的“通过率”

高效安排刷题时间

如果你是学生,那么恭喜你有一大把的时间用来刷题。但是如果你是已工作人士,则需要高效利用时间了。由于我经常做每日一题,而每日一题,是零点更新。有时候躺在床上没睡,就用力扣APP打开看一眼题目,如果题目不难,就直接用APP刷了。另外你也可以利用上下班通勤的时间、出去玩乘坐地铁、公交的时间来用手机看看题解,或者直接用手机刷。比如我坐长途车从家里回北京的路上就用手机刷过题。

手机刷题,其实官方APP,有很多编程语言中的特殊符号的候选功能,可以加快不少手机输入的速度。

另外你可以给你的输入法用自定义,加入一些常用的代码,进一步提高手机输入的效率。

Trick篇

修改函数参数

leetcode的参数名,有时候很长,你可以改名字,减少输入的字符数。当然正常在网站上刷题有补全功能。但是改一下名字,还是简化不少。另外就是有两个例外情况是没有补全的:

  1. 手机刷题(力扣手机APP可以刷题,但是没有补全功能)
  2. 参加周赛(周赛IDE,无自动补全功能)

调用待补全函数自身

如果函数的参数是等价的,但是你根据某种策略,把他们分出来差别。但是你不确定哪个参数是满足的。比如这道:

415. 字符串相加

https://leetcode-cn.com/problems/add-strings

官方给出的代码是这样:

class Solution {
public:
    string addStrings(string num1, string num2) {

    }
};

num1和num2在题目中含义是等价的。但是我们希望以长度长的那个num作为base,来叠加长度短的那个num。这样可以方便处理。你可以直接去判断长度,然后再搞两个变量出来。比如:

auto& base = num1.size() > num2.size() ? num1: num2;
auto& delta = &base == &num1 ? num2 : num1;

然后让base再去加delta。。但其实没必要这么麻烦,直接这样:

class Solution {
public:
    string addStrings(string num1, string num2) {
        if (num1.size() < num2.size()) {
            return addStrings(num2, num1); // 调用自身
        }
        reverse(num1.begin(), num1.end());
        reverse(num2.begin(), num2.end());
        // num1长度 肯定大于等于num2
        stringstream ss;
        int c = 0;
        for (int i = 0; i < num1.size(); ++i) {
            int x = num1[i] - '0';
            int y = i < num2.size() ? num2[i] - '0' : 0;
            int n = x + y + c;
            int d = n%10;
            ss<<d;
            c = n/10
        }
        if (c) {
            ss << c;
        }
        string s = ss.str();
        reverse(s.begin(), s.end());
        return s;
    }
};

给解题函数增加默认值

每道题,LeetCode都会给出一个待补全的函数,这个函数,你是可以增加默认值的。

对于某些递归的解法,可以少定义一个函数.

官方库提升刷题效率(C++)

因为我是C++程序员所以下面就只介绍C++了。

记住一定要学好STL!

swap替代手写交换

不要在自己写三行式去写交换了。直接用swap。

针对链表的Node也使用哦。比如:

226. 翻转二叉树:

https://leetcode-cn.com/problems/invert-binary-tree

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

class Solution {
public:
    TreeNode* mirrorTree(TreeNode* root) {
        if (!root) {
            return root;
        }

        swap(root->left, root->right);

        mirrorTree(root->left);
        mirrorTree(root->right);

        return root;
    }
};

pair替代二元数据结构

有时候我们可能需要一些临时的数据结构。如果这个数据结构只有两个成员变量,那么你大可不必定义出来一个struct或class。直接上pair!比如这道:

38. 外观数列

https://leetcode-cn.com/problems/count-and-say

class Solution {
public:
    string countAndSay(int n) {
        string num = "1";
        for (int i = 1; i < n; ++i) {
            auto s = say(num);
            num.swap(s);
        }
        return num;
    }
    string say(const string& num) {
        vector<pair<charint>> v;  // 直接用pair
        v.emplace_back(num[0], 1);
        for (int i = 1; i < num.size(); ++i) {
            if (v.back().first == num[i]) {
                v.back().second++;
            } else {
                v.emplace_back(num[i], 1); // pair对emplace友好
            }
        }

        stringstream ss;
        for (auto& pa: v) {
            ss<<pa.second<<pa.first-'0';
        }
        return ss.str();
    }
};

pair还有其他好处,那就是它自带 operator< 比较运算符。如果你的自定义类型需要排序的话,那么pair就不需要你自己写这个比较函数了。而自定义类型则需要。

而且pair的构造函数可以直接接受first和second的值做参数。这对于emplace或者emplace_back都很友好!

accumulate()求数组的和

int sum = accumulate(nums.begin(), nums.end(), 0);

partial_sum()部分求和

对于某些前缀和相关的题目,可以简化代码

    vector<int> v = {1,2,3,5,6,7};
    vector<int> pre;
    partial_sum(v.begin(), v.end(), back_inserter(pre));
    for (int i: pre) {
        cout<< i<<endl;
    }

输出:

1
3
6
11
17
24

min()和max()

比如我们一般怎么求最小值呢?

if (v[i][t] < v[f][t-1] + price) {
    v[i][t] = v[f][t-1] + price;
}

可以直接用std::min()来简化if else:

v[i][t] = min(v[i][t], v[f][t-1] + price)

工作中不太需要,刷题适合。如果你需要从三个数字中求最小值呢?加一层{}

val = min({a, b, c});

如果求最大值就用std::max()

max_element()和min_element()

如果要从一个数组中找出最大值呢?

一般我们可能这么写:

int val = -1// 假设-1是肯定小于dp[i]的
for (int& v: dp) { // dp是vector<int> 类型
    if (v > val) {
        v = val;
    }
}
return val;

太长了,可以直接:

return *max_element(dp.begin(), dp.end()); // 这样写的前提是能保证dp不是空的,否则段错误!

二分查找相关

upper_bound()lower_bound()有时可以直接拿来解二分法相关的题目,避免自己手写出错。

往期推荐

从旁观者到committer,我与brpc的故事
大四那一年
C/C++:堆栈面面观
过往工作中的一些趣事


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存