在讲述具体的方法之前,先要明白一件事。凡事都分『道』与『术』。本问题下面大部分回答介绍的都是『术』。而对于刷LeetCode这件事,无论你看到多少高明的的方法,如果你不能持之以恒,都没有用。所有在刷LeetCode这件事上,他的『道』就是:如何能持之以恒的保持刷题热情。我认为『道』更重要。我先谈一下这部分。
为什么王者荣耀有段位,有青铜、白银、黄金。以我个人的经验来看,这些都是刺激你持续玩游戏的游戏机制。通过段位,彰显个人的实力,没错,装逼是人类进步的一大动力。同时也让进步可以衡量。刷LeetCode也是同样,一般刚开始我们会关注解决了多少道题。更进一步,我们需要关心自己的排名。LeetCode也有排名机制。每刷一道题,就前进一点点。可以每周给自己做一个记录,记一下自己的排名。让自己重视排名。逐渐的自己就会把积极性调动起来,想让自己的排名越来越高,就是打排位一样。
除了排名,你还可以每周记录一下通过率。
坚持做每日一题,LeetCode每天都有一道题,完成后,你的打卡日历就会勾上一个圈。
如果打满一个月就会获得一个月度徽章。另外LeetCode有很多“学习计划”,每个学习计划完成后也会有一个徽章。
我有段时间就喜欢收集徽章,为了收集这些小玩意,会刺激自己持续解题。其实和游戏里面也差不多。学会游戏化学习。
开始刷LeetCode感到吃力是正常的。我这里说的学会放弃,不是说放弃刷力扣,而是说碰到自己无法解决的题目,不要死磕。赶紧看一下题解去学习,这没有什么丢人的。每个人都是从这个阶段过来的。就比如小学生无法做出高中生的题目,并不是因为小学生笨,而是因为中间有很多知识,小学生没学过。对于算法而言,自然也是如此。靠自己死磕,是很难想到很多前辈科学家们总结出来的算法。这时候去看题解学习也不为过。
如果遇到特别难的题,看题解也不理解。也不用纠结。就隔着它。可以把题号记下来。过段时间再回顾。
我是笔记强迫症患者……我现在刷LeetCode,每道做过的题,我都会记到Notion里。打上几个标签,方便回顾。
记忆是有遗忘曲线的,不能因为你现在能做出一道题,就认为自己能永远做出来。小学时,老师就教导我们:
好记性不如烂笔头
讲完『道』,我也谈一下我的『术』。我这里不对具体的算法和数据结构题型做解析了,我主要给你指路。
我伸手给你指路,希望你看见的是路,而不是我的手
当然我不建议从头按顺序刷,我分为“正排”刷题和“倒排”刷题,两种策略。
所谓“正排刷题”可以刷LeetCode上面的题单。
我个人建议从《剑指Offer》开始刷。《剑指Offer》现有出了两册,在LeetCode上都有题单。建议从《剑指Offer》刷起,碰到不会的,可以看书中的讲解。
根据书来刷题,当你刷完一本书后,常见的题目类型几乎都覆盖到了。当然同一种类型的题目数不胜数,重要是培养一下各种题型大概的思路。这属于“正排”刷题。
“倒排”刷题,是打破常规,不根据题目类型或按照某种顺序来做题。玩的就是措手不及。比如坚持做每日一题。坚持一个月你就会见到各种各样的题型,有的甚至很刁钻。方便查漏补缺。另外如果有信心的话,可以参加一下周赛,进一步给自己惊喜。
不要着急提交代码。多用测试用例自测。如果是提交之后发现某个case不过,调试的时候一定要在测试用例这里调试。
不要用重复提交代码的方式调试。每次提交代码都会跑N多个case,时间慢。另外就是会降低你在LeetCode上的“通过率”
如果你是学生,那么恭喜你有一大把的时间用来刷题。但是如果你是已工作人士,则需要高效利用时间了。由于我经常做每日一题,而每日一题,是零点更新。有时候躺在床上没睡,就用力扣APP打开看一眼题目,如果题目不难,就直接用APP刷了。另外你也可以利用上下班通勤的时间、出去玩乘坐地铁、公交的时间来用手机看看题解,或者直接用手机刷。比如我坐长途车从家里回北京的路上就用手机刷过题。
手机刷题,其实官方APP,有很多编程语言中的特殊符号的候选功能,可以加快不少手机输入的速度。
另外你可以给你的输入法用自定义,加入一些常用的代码,进一步提高手机输入的效率。
leetcode的参数名,有时候很长,你可以改名字,减少输入的字符数。当然正常在网站上刷题有补全功能。但是改一下名字,还是简化不少。另外就是有两个例外情况是没有补全的:
如果函数的参数是等价的,但是你根据某种策略,把他们分出来差别。但是你不确定哪个参数是满足的。比如这道:
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++了。
记住一定要学好STL!
不要在自己写三行式去写交换了。直接用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;
}
};
有时候我们可能需要一些临时的数据结构。如果这个数据结构只有两个成员变量,那么你大可不必定义出来一个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<char, int>> 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
都很友好!
int sum = accumulate(nums.begin(), nums.end(), 0);
对于某些前缀和
相关的题目,可以简化代码
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
比如我们一般怎么求最小值呢?
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()
如果要从一个数组中找出最大值呢?
一般我们可能这么写:
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()
有时可以直接拿来解二分法相关的题目,避免自己手写出错。