刷题技巧.md 14.4 KB
Newer Older
L
labuladong 已提交
1
# 算法笔试骗分套路
L
labuladong 已提交
2 3 4

<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
5
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
L
labuladong 已提交
6 7 8 9
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
10
![](https://labuladong.github.io/algo/images/souyisou1.png)
L
labuladong 已提交
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.0;[第 13 期刷题打卡](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名!另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
L
labuladong 已提交
13 14 15



L
labuladong 已提交
16 17 18
**-----------**

首先回答一个问题,刷力扣题是直接在网页上刷比较好还是在本地 IDE 上刷比较好?
L
labuladong 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

如果是牛客网笔试那种自己处理输入输出的判题形式,一定要在 IDE 上写,这个没啥说的,但**像力扣这种判题形式,我个人偏好直接在网页上刷**,原因有二:

**1、方便**

因为力扣有的数据结构是自定的,比如说 `TreeNode``ListNode` 这种,在本地你还得把这个类 copy 过去。

而且在 IDE 上没办法测试,写完代码之后还得粘贴到网页上跑测试数据,那还不如直接网页上写呢。

算法又不是工程代码,量都比较小,IDE 的自动补全带来的收益基本可以忽略不计。

**2、实用**

到时候面试的时候,面试官给你出的算法题大都是希望你直接在网页上完成的,最好是边写边讲你的思路。

如果平时练习的时候就习惯没有 IDE 的自动补全,习惯手写代码大脑编译,到时候面试的时候写代码就能更快更从容。

L
labuladong 已提交
36
之前我面快手的时候,有个面试官让我 [实现 LRU 算法](https://labuladong.github.io/article/fname.html?fname=LRU算法),我直接把双链表的实现、哈希链表的实现,在网页上全写出来了,而且一次无 bug 跑通,可以看到面试官惊讶的表情😂
L
labuladong 已提交
37 38 39

我秋招能当 offer 收割机,很大程度上就是因为手写算法这一关超出面试官的预期,其实都是因为之前在网页上刷题练出来的。

L
labuladong 已提交
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
当然,实在不想在网页上刷,也可以用我的 vscode 刷题插件或者 JetBrains 刷题插件,插件和我的网站内容都有完美的融合:

![](https://labuladong.github.io/algo/images/others/全家桶.jpg)

### 避实就虚

大家也知道,大部分笔试题目都需要你自己来处理输入数据,然后让程序打印输出。判题的底层原理是,把你程序的输出用 Linux 重定向符 `>` 写到文件里面,然后比较你的输出和正确答案是否相同。

那么有的问题难点就变得形同虚设,我们可以偷工减料,举个简化的例子,假设题目说给你输入一串用空格分隔的字符,告诉你这代表一个单链表,请你把这个单链表翻转,并且强调,一定要把输入的数字转化成单链表之后再翻转哦!

那你怎么做?真就自己定义一个 `ListNode` 单链表节点类,然后再写代码把输入转化成一个单链表,然后再用让人头晕的指针操作去老老实实翻转单链表?

搞清楚我们是来 AC 题目的,不是来学习算法思维的。正确的做法是直接把输入存到数组里,然后用 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=双指针技巧) 几行代码给它翻转了,然后打印出来完事儿。

我就见过不少这种题目,比如题目说输入的是一个单链表,让我分组翻转链表,而且还特别强调要用递归实现,就是我们前文 [K 个一组翻转链表](https://labuladong.github.io/article/fname.html?fname=k个一组反转链表) 的算法。嗯,如果用数组进行翻转,两分钟就写出来了,嘿嘿。
 
还有我们前文 [扁平化嵌套列表](https://labuladong.github.io/article/fname.html?fname=nestInteger) 讲到的题目,思路很巧妙,但是在笔试中遇到时,输入是一个形如 `[1,[4,[6]]]` 的字符串,那直接用正则表达式把数字抽出来,就是一个扁平化的列表了……

### 巧用随机数

再说一个鸡贼的技巧,注意那些输出为「二值」的题目,二值就是类似布尔值,或者 0 和 1 这种组合有限的。

比如说很多题目是这样,巴拉巴拉给你说一堆条件,然后问你输入的数据能不能达成这些条件,如果能的话请输出 `YES`,不能的话输出 `NO`

如果你会做当然好,如果不会做怎么办?

首先这样提交一下:

```java
public class Main {
    public static void main(String[] args) {
        System.out.println("YES");
    }
}
```

看下 case 通过率,假设是 60%,那么说明结果为 `YES` 有 60% 的概率,所以可以这样写代码:

```java
public class Main {
    public static void main(String[] args) {
        // 60% 的概率输出 YES,40% 的概率输出 NO
        System.out.println((new Random().nextInt() % 100) < 60 ? "YES" : "NO");
    }
}
```

嘿嘿,这题你可以不会,但是一定要在力所能及的范围内做到极致!

### 编程语言的选择

仅从做算法题的角度来说,我个人比较建议使用 Java 作为笔试的编程语言。因为 JetBrain 家的 IntelliJ 实在是太香了,相比其他语言的编辑器,不仅有 `psvm``sout` 这样的快捷命令(你要是连这都不知道,赶紧面壁去),而且可以帮你检查出很多笔误,比如说 `while` 循环里面忘记递增变量,或者 `return` 语句错写到循环里这种由于疏忽所导致的问题。

C++ 也还行,但是我觉得没有 Java 好用。我印象中 C++ 连个分割字符串的 `split` 函数都没有,光这点我就不想用 C++ 了……

还有一点,C++ 代码对时间的限制高,别的语言时间限制 4000ms,C++ 限制 2000ms,我觉得挺吃亏的。怪不得看别人用 C++ 写算法,为了提高速度,都不用标准库的 `vector` 容器,非要用原始的 `int[]` 数组,我看着都头疼。

Python 的话我刷题用的比较少,因为我不太喜欢用动态语言,不好调试。不过这个语言的奇技淫巧太多,如果你深谙 Python 的套路,可以在某些时候投机取巧。比如说我们前文写到的 [表达式求值算法](https://labuladong.github.io/article/fname.html?fname=实现计算器) 是一个困难级别的算法,但如果用 Python 内置的 `exec` 函数,直接就能算出答案。

这个在笔试里肯定是很占便宜的,因为之前说了,我们要的是结果,没人在乎你是怎么得到结果的。

### 解法代码分层

代码分层应该算是一种比较好的习惯,可以增加写代码的速度和降低调试的难度。

简单说就是,不要把所有代码都写在 `main` 函数里面,我一直使用的套路是,`main` 函数负责接收数据,加一个 `solution` 函数负责统一处理数据和输出答案,然后再用诸如 `backtrack` 这样一个函数处理具体的算法逻辑。

举个例子,比如说一道题,我决定用带备忘录的动态规划求解,代码的大致结构是这样:

```java
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 主要负责接收数据
        int N = scanner.nextInt();
        int[][] orders = new int[N][2];
        for (int i = 0; i < N; i++) {
            orders[i][0] = scanner.nextInt();
            orders[i][1] = scanner.nextInt();
        }
        // 委托 solution 进行求解
        solution(orders);
    }

    static void solution(int[][] orders) {
        // 排除一些基本的边界情况
        if (orders.length == 0) {
            System.out.println("None");
            return;
        }
        // 委托 dp 函数执行具体的算法逻辑
        int res = dp(orders, 0);
        // 负责输出结果
        System.out.println(res);
    }

    // 备忘录
    static HashMap<String, Integer> memo = new HashMap<>();
    static int dp(int[][] orders, int start) {
        // 具体的算法逻辑
    }
}
```

你看这样分层是不是很清楚,每个函数都有自己主要负责的任务,如果哪里出了问题,你也容易 debug。

倒不是说要把代码写得多规范,至于 `private` 这种约束免了也无妨,变量用拼音命名也 OK,关键是别把代码直接全写到 `main` 函数里面,真的乱,不出错也罢,一旦出错,估计要花一番功夫调试了,找不到问题乱了阵脚,那是要尽量避免的。
L
labuladong 已提交
147 148 149

### 如何给算法 debug

L
labuladong 已提交
150
代码的错误是无法避免的,有时候可能整个思路都错了,有时候可能是某些细节问题,比如 `i``j` 写反了,这种问题怎么排查?
L
labuladong 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183

我想一般的算法问题肯定不难排查,肉眼检查应该都没啥问题,再不济 `print` 打印一些关键变量的值,总能发现问题。

**比较让人头疼的的应该是递归算法的问题排查**

如果没有一定的经验,函数递归的过程很难被正确理解,所以这里就重点讲讲如何高效 debug 递归算法。

有的读者可能会说,把算法 copy 到 IDE 里面,然后打断点一步步跟着走不就行了吗?

这个方法肯定是可以的,但是之前的文章多次说过,递归函数最好从一个全局的角度理解,而不要跳进具体的细节。

如果你对递归还不够熟悉,没有一个全局的视角,这种一步步打断点的方式也容易把人绕进去。

**我的建议是直接在递归函数内部打印关键值,配合缩进,直观地观察递归函数执行情况**

最能提升我们 debug 效率的是缩进,除了解法函数,我们新定义一个函数 `printIndent` 和一个全局变量 `count`

```cpp
// 全局变量,记录递归函数的递归层数
int count = 0;

// 输入 n,打印 n 个 tab 缩进
void printIndent(int n) {
    for (int i = 0; i < n; i++) {
        printf("   ");
    }
}
```

接下来,套路来了:

**在递归函数的开头,调用 `printIndent(count++)` 并打印关键变量;然后在所有 `return` 语句之前调用 `printIndent(--count)` 并打印返回值**

L
labuladong 已提交
184
举个具体的例子,比如说上篇文章 [练琴时悟出的一个动态规划算法](https://labuladong.github.io/article/fname.html?fname=转盘) 中实现了一个递归的 `dp` 函数,大致的结构如下:
L
labuladong 已提交
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237

```cpp
int dp(string& ring, int i, string& key, int j) {
    /* base case */
    if (j == key.size()) {
        return 0;
    }

    /* 状态转移 */
    int res = INT_MAX;
    for (int k : charToIndex[key[j]]) {
        res = min(res, dp(ring, j, key, i + 1));
    }

    return res;
}
```

这个递归的 `dp` 函数在我进行了 debug 之后,变成了这样:

```cpp
int count = 0;
void printIndent(int n) {
    for (int i = 0; i < n; i++) {
        printf("   ");
    }
}

int dp(string& ring, int i, string& key, int j) {
    // printIndent(count++);
    // printf("i = %d, j = %d\n", i, j);
    
    if (j == key.size()) {
        // printIndent(--count);
        // printf("return 0\n");
        return 0;
    }
    
    int res = INT_MAX;
    for (int k : charToIndex[key[j]]) {
        res = min(res, dp(ring, j, key, i + 1));
    }
    
    // printIndent(--count);
    // printf("return %d\n", res);
    return res;
}
```

**就是在函数开头和所有 `return` 语句对应的地方加上一些打印代码**

如果去掉注释,执行一个测试用例,输出如下:

L
labuladong 已提交
238
![](https://labuladong.github.io/algo/images/刷题技巧/1.jpg)
L
labuladong 已提交
239 240 241 242 243

这样,我们通过对比对应的缩进就能知道每次递归时输入的关键参数 `i, j` 的值,以及每次递归调用返回的结果是多少。

**最重要的是,这样可以比较直观地看出递归过程,你有没有发现这就是一棵递归树**

L
labuladong 已提交
244
![](https://labuladong.github.io/algo/images/刷题技巧/2.jpg)
L
labuladong 已提交
245

L
labuladong 已提交
246
前文 [动态规划套路详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 说过,理解递归函数最重要的就是画出递归树,这样打印一下,连递归树都不用自己画了,而且还能清晰地看出每次递归的返回值。
L
labuladong 已提交
247 248 249

**可以说,这是对刷题「幸福感」提升最大的一个小技巧,比 IDE 打断点要高效**

L
labuladong 已提交
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
### 考前复习策略

考前就别和某一道算法题死磕了,不划算。

应该尽可能多的看各种各样的题目,思考五分钟,想不出来解法的话直接看别人的答案。看懂思路就行了,甚至自己写一遍都没必要,因为比较浪费时间。

笔试的时候最怕的是没思路,所以把各种题型都过目一下,起码心里不会慌,只要有思路,平均一道题二三十分钟搞定还是不难的。

前面不是说了么,没有什么问题是暴力穷举解决不了的,直接用 [回溯算法套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 硬上,大不了加个备忘录,不就成 [动态规划套路框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 了么,再大不了这题我不做了么,暴力过上 60% 的 case 也挺 OK 的。

别的不多说了,套路这个东西,说来简单,一点就透,但问题是不点就不透。本文我简单介绍了几个笔试算法的技巧,各位好好品味~

最后,请秋招的同学多向身边的朋友推荐 labuladong 公众号。算法真的没那么难,这一切只是手段而已,过算法笔试拿 offer 才是目的。为了达到目的,套路是必须的,可以少走很多弯路,你的朋友会感谢你的,我也会感谢你的😏



<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [带权重的随机选择算法](https://labuladong.github.io/article/fname.html?fname=随机权重)

</details><hr>



L
labuladong 已提交
276 277 278 279


**_____________**

L
labuladong 已提交
280
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
281

L
labuladong 已提交
282
![](https://labuladong.github.io/algo/images/souyisou2.png)