lucifer

上一篇的地址在这里,没有看过的同学建议先看第一篇 来和大家聊聊我是如何刷题的(第一弹)

这次继续给大家聊聊怎么刷题, 预计分几篇文章来写,今天是第二篇,本系列至少会出三篇。这次分享的内容是代码书写技巧以及调试技巧

本系列旨在分享一些所有题目都适用的技巧以及一些刷题经验,帮助大家高效刷题。如果想重点突破某一类题目,可以关注我的专题系列。

话不多说,直接上干货。

先刷什么?刷多少?

正式介绍技巧之前先回答一个问题,这也是我被问的比较多的两个问题是:我该先刷什么算法?每一种算法我该刷多少?

现在我们就来看下这个问题。

先贴一个 91 天学算法 中某一小节的讲义中的一部分内容:

  • 递归(10)
  • BFS & DFS(20)
  • 双指针(20)
  • 滑动窗口(6)
  • 哈希表(20)
  • 回溯(5)
  • 动态规划(20)
  • 排序(3)
  • 分治(20)
  • 堆(3)
  • 贪心(5)
  • 设计题(5)
  • 图(5)
  • 位运算(5)
  • 并查集(3)

如果你不知道从何刷起,可以参考我的这个刷题顺序,其中括号是我推荐的最小刷题量,就是说再少不能少于这个数字。如果你想多刷,可以按照我的这个比例去刷。等到你自己有个概念,知道自己哪里薄弱了,再去针对加强即可。

具体题目太多了不列举了,给大家几个题目集合做参考:

  1. 🔥 热题 HOT 100
  2. 👨‍💻 精选 TOP 面试题
  3. 企业题库。 比如 🐧 腾讯精选练习 50 题企业题库 - 字节跳动(当你就想去某一家公司的时候可以用)
  4. 剑指 Offer
  5. 网友内幕(主要是面经)
  6. 力扣的探索和标签

知道该刷什么,以及刷多少了,可能你已经迫不及待投入题海了。 不要着急,可以先看下西法有没有写过相关专题,如果写过, 强烈建议你先看下,一定能让你事半功倍。

我已经开始刷专题了,有没有什么通用的技巧呢?答案是有,而且很多。本文只介绍一部分, 后续我们继续这个话题,给大家带来更多干货技巧。

代码书写技巧

代码书写技巧,这次给大家带来三个技巧:

  1. 改参数
  2. zip 函数的妙用
  3. 关于取模

改参数

力扣的参数是可以改名字的,如下图:

你可以将名字改成一个短的或者你熟悉的。比如上面这道题,我写的时候就可以:

1
2
3
class Solution:
def findMedianSortedArrays(self, A: List[int], B: List[int]) -> float:
# can use A and B now

这可以使得代码看起来简洁且具有一致性

经常看我题解的小伙伴应该注意到我的代码比较简洁。一方面是因为我经常用 Python,另一方面就是因为这个技巧。

这里我顺便吐槽一下力扣。力扣的形参命名相当不规范。比如二维数组有时候是 mat,有时候是 nums,有时候是 matrix,有时候又是 grid 。。。 真心不舒服,不过有了这个技巧,大家就不要依赖官方了,自己统一一下就好。

就拿我来说,二维数组我就用 mat 或者 matrix,一维数组用 nums 或者 A 或者 A 和 B(两个一维数组的情况)。比如:

1
2
3
4
5
6
# A 和  B 是两个一维的数组
def test(A, B):
for a in A:
# do something
for b in B:
# do something else

其实不仅仅是形参的命名要统一,我们内部的代码也是一样的。对于我来说:

  • 堆我习惯叫 h
  • 图我习惯叫 graph
  • 队列我习惯叫 q
  • 。。。

大家没有必要和我一样,但是一定要保持一致性,这样可以显著增加代码可读性,可读性高了,调试工作也会变得轻松。

zip 函数的妙用

力扣有一些题目会给你两个或者三个一维数组,这两个一维数组的是有关联的。

比如给你两个一维数组 A 和 B,其中 A[i] 表示第 i 个人的体重,B[i] 表示第 i 个人的身高。也就是说都是表示第 i 个人,但是表示的东西不一样。

其实逻辑上就相当于结构体,而且如下结构体的形式在工作中更常见。

1
2
3
4
interface Person {
weight: number;
height: number;
}

但是力扣以两个数组的形式给你了,其实这样不难啊,不就是用一个索引记录么?

1
2
3
4
for(int i= 0;i<A.length;i++) {
int weight = A[i]
int height = B[i]
}

但是如果我需要对重量排序呢?如果你仅仅对 A 排序了,B 也需要进行相应调整的,否则对应关系就乱了。那遇到这样的情况该怎么办呢?

这里介绍一个我经常使用的技巧 zip

1
2
3
4
5
6
zipped = zip(A, B)
# 下面我对其进行排序也不会改变相对顺序
zipped.sort()
# 比如 A 是 [1,2,3] B 是 [4,5,6]
# 那么 zipped 就是 [[1,4], [2,5], [3,6]]
# 那么 zipped[i][0] 就是第 i 个人的体重,zippd[i][1] 就是第 i 个人的身高

由于 A 和 B “捆绑”到一起了,因此排序也不会改变其相对顺序。

如下是我在力扣 1383 题中使用 zip 技巧的例子:

另外 zip 还有一些其他用处。比如我想要获取当前数组位置的前一项。

不用 zip 可以这么做:

1
2
3
for i in range(1, len(A)):
pre = A[i - 1]
cur = A[i]

如果使用 zip 可以这样:

1
2
for pre, cur in zip(A, A[1:]):
# do something

这里的原理也很简单。我举个例子你就懂了。比如有一个数组 A :[1,2,3,4]。 那么 A[1:] 就是 [2,3,4]

我将如上两个数组 zip 起来就是 [[1,2], [2,3], [3,4]],所以我对 zip 之后的结果进行遍历就可以方便地写代码了。

这个技巧用处不大,可以不必掌握,大家知道有这么回事就行

有的人可能想问,我的语言没有 zip 怎么办? 我的答案是自行实现 zip。 比如 JavaScript 可以这样实现 zip:

1
const zip = (rows) => rows[0].map((_, c) => rows.map((row) => row[c]));

你把它改造成自己的语言版本即可。

关于取模

力扣中有很多题目需要你对返回值取模,而且一般都是对 109 + 7 取模。

题目答案让取模那肯定是答案太大了,为啥太大了呢?有啥想法没?

比如 1680. 连接连续二进制数字

题目描述:

1
给你一个整数 n ,请你将 1 到 n 的二进制表示连接起来,并返回连接结果对应的 十进制 数字对 109 + 7 取余的结果。

如果我忘记取模或者仅在返回的时候取模都可能会报错,正确的姿势是提前取模。

以上面的题目来说,代码这样写是可以过的。

1
2
3
4
5
6
7
class Solution:
def concatenatedBinary(self, n: int) -> int:
ans = 0
mod = 10 **9 + 7
for i in range(1, n + 1):
ans = (ans * pow(2, len(bin(i)[2:])) + i) % mod
return ans % mod

而如果我这么写会超时(没有提前取模,只是在最后返回才取模):

1
2
3
4
5
6
7
class Solution:
def concatenatedBinary(self, n: int) -> int:
ans = 0
mod = 10 **9 + 7
for i in range(1, n + 1):
ans = (ans * pow(2, len(bin(i)[2:])) + i)
return ans % mod

如果不提前 mod, python 可能超时,其他语言可能溢出。

提前取 mod,会把数值限定在 int 能处理的范围,使用机器自身整数运算功能进行快速运算,而如果之后取 mod,由于 python 对大整数支持的特性,会将 ans 转换为大整数再进行运算,计算相对耗时。

说到溢出,我想起来一个小技巧,那就是二分取中间值的时候如果书写不当也可能溢出。

1
2
# 如下代码,如果 l 和 r 比较大,则可能发生大数溢出
mid = (l + r) // 2

这里的 // 是地板除

解决的方案也很简单,这样写就行了:

1
mid = (r - l) // 2 + l

另外一个和取模有关的小技巧是判断奇偶

判断一个数是奇数还是偶数可以通过和 2 取模。 如果返回值是 0 则是偶数,否则是奇数。

需要注意的是和 2 取模为 1 奇数,但是反之不然。即和 2 取模不是 1 也可能是奇数,比如负数,因此还需要多个判断,不如用我上面的方法,即和 2 取模判断是否等于 0

欢迎大家补充其他小技巧~

其他技巧

这里有一个要和大家强调的点,很多刚刷题的人都不知道,那就是尽量不要使用全局变量。

如果使用全局变量且没有及时清除,不仅可能有性能问题,更可能会在多个测试用例之间形成干扰,导致出错。而且在力扣设计题目通常是会多次调用某一个 api 的。这个时候更是如此,所以不要使用全局变量。

有一些朋友向我反馈“为啥本地好好的,放到力扣上提交就不行”,请先检查下有没有使用全局变量。

调试技巧

调试技巧,我们这里先讲两个:

  1. 批量测试
  2. 数据结构可视化(树的可视化)

批量测试

力扣的测试用例其实是可以一次写多个的。

如上图,该题目有两个参数。那两行其实就是一个完整用例。 我这里输入了六行,也就是三个用例。这个时候点击执行,就可以一次执行三个用例。

妈妈再也不用担心我提交太频繁啦~

执行成功后,我们可以一次查看所有的差异。

如果你老是考虑不到各种边界,那这个功能简直是福音。 另外如果你打比赛,你可以把题目给的测试用例批量复制到这里一次执行看结果,非常有用。

为了方便大家复制所有题目内置的测试用例,我的刷题插件 leetcode-cheatsheet增加了一个功能一键复制所有的内置用例

正常情况,下点击之后会提示你复制成功,你只需要 ctrl + v 粘贴到测试用例的输入框即可。

但是力扣网站有很多不是很统一的地方,这就需要我不断进行兼容。比如如下兼容代码:

上面代码指的是力扣测试用例的 html 标签是不定的,并且有时候是”输入:“(注意是中文的:),有时候又是”输入:“(注意是英文的:)。

因此难免有我无法兼容的情况。因此就会发生类似这样的情况

遇到这样的情况,你可以点击弹出信息的反馈文字,给我反馈。不过据我的测试,大部分情况是没问题的。

插件目前已经发布到谷歌商店了,通过谷歌商店安装的朋友审核通过后会自动更新。离线安装的朋友需要手动安装,不过我的更新蛮频繁的,强烈建议在线安装。商店地址:https://chrome.google.com/webstore/detail/leetcode-cheatsheet/fniccleejlofifaakbgppmbbcdfjonle?hl=en-US

上线几天已经有 100 多人安装了, 你确定不试试么?

树的可视化

力扣支持大家对树进行可视化,只要点击这个树结构可视化按钮即可(只有树的题目才有这个按钮)。

如果你写了多个数组,也并不会生成多个树,貌似是以最后一次输入为准。

力扣暂时没有提供其他数据结构的可视化,比如数组,链表等。这可能对大部分人来说没什么,但是对于我这样经常写题解,画图的人就不一样了。如果可以快速画图,那么对我效率肯定有大幅度的提升。

lucifer 建议大家也养成写题解的好习惯。

因此我打算在我的刷题插件里面加其他数据结构的可视化功能, 已经在规划啦~ 现在草稿了一些东西。

比如这样的树:

和这样的树:

现在其实还有些问题,而且我想多加几种数据结构方便写题解,所以就之后再说好了。

本地调试技巧

我们可以通过按照编辑器插件在本地编辑器中写代码,然后通过编辑器插件将其提交到力扣即可。

这样你在本地的调试插件都可以用于算法调试了。 这里推荐两个可视化调试插件:

推荐一个网站

OI Wiki 致力于成为一个免费开放且持续更新的 编程竞赛 (competitive programming) 知识整合站点,大家可以在这里获取与竞赛相关的、有趣又实用的知识。我们为大家准备了竞赛中的基础知识、常见题型、解题思路以及常用工具等内容,帮助大家更快速深入地学习编程竞赛中涉及到的知识。

地址:https://oi-wiki.org/

其他

力扣提交成功之后除了可以看到自己的排名情况(击败百分之多少),还可以查看别人的提交代码。

这里可以看所有分段的分布情况,也可以直接点击对应的柱子,查看别人的代码怎么写的。比如我这里直接点开了击败 100% 的代码,研究下 ta 是怎么写的。

发现确实代码比我的好,于是我就又”学会“了一招(技能++)。

以上就是本文的全部内容了。大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 38K star 啦。大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。

预告

下期给大家讲更加干货的技巧,一定不要错过哦。

  • 一看就会,一写就废, 如何克服?
  • 如何锁定使用哪种算法。比如我看到了这道题,我怎么知道该用什么解法呢?二分?动态规划?

 评论


博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本站使用 Material X 作为主题 。
载入天数...载入时分秒...