文章目录
一、前言
最近发现了一个网站,叫 牛客网(www.nowcoder.com),看了看上面的题目,普遍偏简单,于是找回了大学时候刷题的快感,赶紧来刷一刷。选了一个企业,把它的面试题刷完了。

大致做一波总结,如果有小伙伴有兴趣的,可以照着我的目录来刷,所有题目基本都属于没有很强算法基础就能做的,分成两大块内容:数据结构 和 算法。数据结构包含:链表、队列、栈、二叉树。算法包含:排序、二分枚举、搜索、动态规划、贪心、位运算应用、模拟等等。

二、数据结构
1、单向链表
1)链表的删除
- 单向链表的删除,需要先找到这个结点,并且记录它的前驱结点,让前驱结点指向它的后继结点,整个过程最坏时间复杂度为 O ( n ) O(n) O(n),主要是遍历的时间复杂度,删除结点这个操作本身是 O ( 1 ) O(1) O(1) 的。

2)链表的翻转
- 对于翻转链表的问题,比较好的做法就是遍历链表,删除遍历到的结点,插到头部,即所谓头插法,只需要实现
delNode和insertNode两个接口即可。时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)。
- 如图所示,先把需要翻转的结点删除,然后插到头部,然后执行下一个结点,直到整个链表遍历完毕,就完成了链表的翻转。

- 当然,有些问题可能是选择某一段区间的链表结点进行翻转,方法一样,为了处理头结点的特殊情况,可以在链表头增加一个 “伪头结点”,这样就可以防止链表头随时变化的情况,返回链表头只需要返回"伪头结点" 的
next即可。
3)链表的快慢指针
- 判断一个链表里面有没有环,采用的是快慢指针。总体思路是一个指针走1步,一个指针走2步,由于 1 和 2 互素,如果有环,他们一定会相遇。

- 假设没有环的部分,长度为 x x x,环的长度为 y y y,环入口点到相遇点的距离为 z z z,那么慢指针走过的距离为 x + z x+z x+z,快指针走过的距离为 n y + x + z ny + x + z ny+x+z,又知道慢指针速度为1,快指针速度为2,如果它们相遇,则相遇的时间相等,有: ( x + z ) 1 = ( n y + x + z ) 2 \frac {(x+z)} 1 = \frac {(ny+x+z)} 2 1(x+z)=2(ny+x+z)
- n n n 为任意整数,所以这个方程 z = n y − x z = ny - x z=ny−x,这个公式的含义是:慢指针只要再走 x x x 步 就能到达环的入口了。
2、双向链表
- 双向链表比链表本身多了一个指针,指向它的前驱。一般对一些时间复杂度要求较高的问题采用双向链表。
- 以下两题双向链表的题目,涉及到其它数据结构,较为复杂,建议最后做。
3、栈
4、二叉树
- 二叉树的遍历有先序、中序、后序、层序;
- 先序、中序、后序遍历,考察的点主要是递归,一般做题思路是:根据左右子树的遍历结果,来决定当前树的值。子树的结果可以通过返回值返回,也可以通过传参引用返回。
- 层序遍历就是广度优先搜索了。对于广搜,主要应用是图的最短路问题,如果需要详细学习的话,可以参见以下这篇文章:夜深人静写算法(十)- 单向广搜。
三、算法
1、排序
- 排序的话,一般面试最喜欢问的是快排,基本大部分技术博客都讲过了,我就不讲了。但是,实际应用中一般用 STL 的
sort配合类重写仿函数就能解决大部分多关键字的排序问题了。
- 静态的第 k k k 大的数,如果时间复杂度没有太苛刻的要求,基本也都是排个序就解决的事情。
- 倒是可以用下面的题来练习写一下快排。
2、二分枚举
- 二分查找作为最经典的 O ( l o g 2 n ) O(log_2n) O(log2n) 的算法,也是面试考察的重点,所以手写一个二分查找也是必须的,实际应用中一般用 STL 的
lower_bound就能实现数组的二分查找了。
- 二分查找不光可以在数组中找数据,还可以对单调函数,二分枚举答案求可行解,例如:求一个数的平方根。
- 实际上有序数组也是特殊的的单调函数,只不过它的值是离散的。
3、广度优先搜索
4、深度优先搜索
- 深度优先搜索的最大的用处是穷举问题的所有情况,并且进行适当的合法剪枝,所以没有思路的时候可以试着用深搜来求解问题。深搜的实现是递归。
- 一般枚举全排列、全组合的时候也要用到深搜。
5、动态规划
- 这里的动态规划都是最简单的,基本把所有 “简单动态规划” 的内容都涵盖了,这里说一下每一类动态规划的思路。
1)递推
【NC68 跳台阶】一只青蛙一次可以跳上 1 1 1 级台阶,也可以跳上 2 2 2 级。求该青蛙跳上一个 n n n 级的台阶总共有多少种跳法。
- 递推的问题一般依赖前一项,或者前两项,或者前 c c c 项,且是方案数相加的形式;
- 对于 n n n 个台阶,我跳 1 1 1 步以后,剩下就是跳 n − 1 n-1 n−1 步的情况;跳 2 2 2 步以后,剩下就是跳 n − 2 n-2 n−2 步的情况;不能一下子跳 3 3 3 步,所以这个问题的状态转移方程就是: f [ n ] = f [ n − 1 ] + f [ n − 2 ] f[n] = f[n-1] + f[n-2] f[n]=f[n−1]+f[n−2]
2)子段最优值
【NC19 子数组的最大累加和问题】给定一个数组 a a a,返回子数组的最大累加和,例如, a [ ] = [ 1 , − 2 , 3 , 5 , − 2 , 6 , − 1 ] a[] = [1, -2, 3, 5, -2, 6, -1] a[]=[1,−2,3,5,−2,6,−1],所有子数组中, [ 3 , 5 , − 2 , 6 ] [3, 5, -2, 6] [3,5,−2,6] 可以累加出最大的和 12 12 12,所以返回 12 12 12 。
-
子段最优值问题,一般是有一个数组序列,要求求一个连续(或非连续)的子序列,运用某种运算达到最优值。
-
我们一般可以这么设计状态:令 f [ n ] f[n] f[n] 表示以第 n n n 个数结尾能够达到的最优解。
-
如果要求序列连续,那么 f [ n ] f[n] f[n] 的最优解取决于 f [ n − 1 ] f[n-1] f[n−1] 选 和 不选两种情况的最优值。
-
如果不要求序列连续,那么 f [ n ] f[n] f[n] 的最优解取决于 选 f [ i ] ( i < n ) f[i] (i < n) f[i](i<n) 中的其中一个所达成的最优值。
-
上面这个问题的状态转移方程(属于连续子段最优值问题)就是: f [ i ] = a [ i ] + { 0 f [ i − 1 ] ≤ 0 f [ i − 1 ] f [ i − 1 ] > 0 f[i] = a[i] + \begin{cases} 0 & f[i-1] \le 0 \\ f[i-1] & f[i-1] > 0\end{cases} f[i]=a[i]+{
0f[i−1]f[i−1]≤0f[i−1]>0
3)区间 DP
- 区间 DP 的状态转移方程如下: d p [ i ] [ j ] = o p t ( d p [ i + 1 ] [ j − 1 ] + c o s t ( i , j ) , d p [ i ] [ k ] + d p [ k + 1 ] [ j ] ) dp[i][j] = opt(dp[i+1][j-1] + cost(i,j), dp[i][k]+dp[k+1][j]) dp[i][j]=opt(dp[i+1][j−1]+cost(i,j),dp[i][k]+dp[k+1][j])
4)二维DP
- 最长公共子序列、最长公共子串、最小编辑距离 都属于二维DP,一般用于两个字符串的匹配问题。
- 一般的状态转移方程为: d p [ i ] [ j ] = o p t ( d p [ i − 1 ] [ j ] + x , d p [ i ] [ j − 1 ] + y , d p [ i − 1 ] [ j − 1 ] + z ) dp[i][j] = opt(dp[i-1][j]+x, dp[i][j-1]+y, dp[i-1][j-1]+z) dp[i][j]=opt(dp[i−1][j]+x,dp[i][j−1]+y,dp[i−1][j−1]+z)
- 如图所示,比较直观的想法就是,红色位置的状态取决于三个蓝色位置的最优值:

6、贪心
- 贪心说起来比较微妙,就是穷尽你平生所学来想这样做对不对!就这么简单(难)。
- 贪心一般配合排序来做。
- 遇到一些没什么想法的题,可以先排个序,然后看看有什么规律,再来决定用贪心还是动态规划。
7、尺取法
- 尺取法算 ACM 里比较经典的一种解题技巧了,其实就是双指针,左右两个指针,不断推进右指针,然后根据限制条件来推进左指针,每个指针只会往前不会后退,所以时间复杂度是 O ( n ) O(n) O(n) 的。
8、字符串模拟
9、树状数组
- 放入面试范畴属于较难的数据结构,因为平时也不会去关注这种数据结构,但是如果想学,其实是很简单的,它的特点是:单点更新,成段求和,并且都能在 O ( l o g 2 n ) O(log_2n) O(log2n) 内完成,常数小于线段树,奉上链接:夜深人静写算法(十三)- 树状数组
10、模拟
- 所谓模拟,就是最直白的跟着题目模拟,题目让你怎么做你就怎么做。
11、位运算