剑指Offer
说明
主要编程语言为 C/C++
涉及字符串的问题可能会使用 Python
题目编号以原书为准,如“面试题 3:数组中重复的数字”
因为题目不多,所以就不做分类了
所有代码均通过 OJ 测试
在线 OJ 地址:剑指Offer_编程题 - 牛客网
Reference
《剑指 Offer(第二版)》 - 何海涛
Interview-Notebook/剑指 offer 题解.md · CyC2018/Interview-Notebook
牛客网相关问题讨论区
Index
3.1 数组中重复的数字
数组中重复的数字 - NowCoder
题目描述
要求:时间复杂度
O(N),空间复杂度O(1)示例
思路
复杂度要求表明不能使用排序,也不能使用 map/set
注意到 n 个数字的范围为
0到n-1,考虑类似选择排序的思路,通过一次遍历将每个数交换到排序后的位置,如果该位置已经存在相同的数字,那么该数就是重复的示例
Code
3.2 不修改数组找出重复的数字
题目描述
要求:时间复杂度
O(NlogN),空间复杂度O(1)
思路
二分查找
以长度为 8 的数组
{2, 3, 5, 4, 3, 2, 6, 7}为例,那么所有数字都在1~7的范围内。中间的数字4将1~7分为1~4和5~7。统计1~4内数字的出现次数,它们一共出现了 5 次,说明1~4内必要重复的数字;反之,若小于等于 4 次,则说明5~7内必有重复的数字。因为不能使用额外的空间,所以每次统计次数都要重新遍历整个数组一次
Code
4. 二维数组中的查找
二维数组中的查找 - NowCoder
题目描述
示例
思路
从左下角开始查找,它左边的数都比它小,下边的数都比它大;因此可以根据 target 和当前元素的大小关系来缩小查找区间
同理,也可以从右上角开始查找
时间复杂度:
O(M + N)
Code
5. 替换空格
替换空格 - NowCoder
题目描述
思路
先遍历一次,找出空格的数量,得到替换后的长度;然后从后往前替换
Code
6. 从尾到头打印链表
从尾到头打印链表 - NowCoder
题目描述
思路
栈
头插法(双端队列或数组)
Code
7. 重建二叉树
重建二叉树 - NowCoder
题目描述
思路
涉及二叉树的问题,应该条件反射般的使用递归(无优化要求时)
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为左子树的中序遍历结果,右部分为右子树的中序遍历的结果。
示例
Code - 无优化
Code - 优化
8. 二叉树的下一个结点
二叉树的下一个结点 - NowCoder
题目描述
思路
回顾中序遍历的顺序
如果一个节点的右子树不为空,那么下一个节点是该节点右子树的最左叶子;
否则(右子树为空),沿父节点向上直到找到某个节点是其父节点的左孩子,那么该父节点就是下一个节点
Code
9. 用两个栈实现队列
用两个栈实现队列 - NowCoder
题目描述
思路
假设
stack_in用于处理入栈操作,stack_out用于处理出栈操作stack_in按栈的方式正常处理入栈数据;关键在于出栈操作
当
stack_out为空时,需要先将每个stack_in中的数据出栈后压入stack_out反之,每次弹出
stack_out栈顶元素即可
Code
10.1 斐波那契数列
斐波那契数列 - NowCoder
题目描述
思路
递归
递归可能会重复计算子问题——例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了
可以利用额外空间将计算过的子问题存起来
查表
因为只需要前 40 项,所以可以先将值都求出来
Code - 递归
Code - 循环
Code - 查表
10.2 跳台阶(递归)
题目描述
思路
递归
记跳 n 级台阶有
f(n)种方法如果第一次跳 1 级,那么之后的 n-1 级有
f(n-1)种跳法如果第一次跳 2 级,那么之后的 n-2 级有
f(n-2)种跳法
实际上就是首两项为 1 和 2 的斐波那契数列
Code
10.3 变态跳台阶(动态规划)
变态跳台阶 - NowCoder
题目描述
思路
动态规划
递推公式
Code - DP
Code - 空间优化
10.4 矩形覆盖(动态规划)
矩形覆盖 - NowCoder
题目描述
思路
动态规划
递推公式
即前两项为 1 和 2 的斐波那契数列
Code
11. 旋转数组的最小数字(二分查找)
旋转数组的最小数字 - NowCoder
题目描述
思路
二分查找
二分查找需要有一个目标值 target,这里的 target 可以选
nums[hi]或nums[lo],这里使用过的是nums[hi]注意有重复的情况,特别是
{3, 4, 5, 1, 2, 3},这里有一个简单的处理方法
Code
Code(改进)
12. 矩阵中的路径(DFS)
矩阵中的路径 - NowCoder
题目描述
例如下面的矩阵包含了一条 bfce 路径。
思路
深度优先搜索(DFS)
注意边界判断
Code
13. 机器人的运动范围(DFS) TODO
机器人的运动范围 - NowCoder
题目描述
思路
深度优先搜索(DFS)
注意边界条件判断
Code
14. 剪绳子(动态规划 | 贪心)
整数拆分 - LeetCode
题目描述
思路
动态规划
递推公式
注意:当
n <= 3时因为必须剪至少一次的缘故,导致f(1)=0, f(2)=1*1=1, f(3)=1*2=2;但是当n>=4时,将n<=3的部分单独作为一段能提供更大的乘积因此,初始化时应该
dp[1]=1≠f(1), dp[2]=2≠f(2), dp[3]=3≠f(3),同时将f(1), f(2), f(3)单独返回时间复杂度:
O(N^2),空间复杂度:O(N)
贪心
当
n>=5时,尽可能多剪长度为 3 的绳子;当n=4时,剪成两段长度为 2 的绳子证明
时间复杂度:
O(1),空间复杂度:O(1)
Code - 动态规划
Code - 贪心
15. 二进制中 1 的个数(位运算)
二进制中1的个数 - NowCoder
题目描述
思路
位运算 - 移位计数
时间复杂度:
O(N),N 为整型的二进制长度注意移位判断有两种方式:一是移动 n,一是移动"1",后者更好
当 n 为 负数时,移动 n 可能导致死循环
位运算 - 利用
n&(n-1)该运算的效果是每次除去 n 的二进制表示中最后一个 1
时间复杂度:
O(M),M 为二进制中 1 的个数
Code - 移位计数
Code - 移位计数(改进)
Code - n&(n-1)
16. 数值的整数次方(位运算)
数值的整数次方 - NowCoder
题目描述
思路
位运算 - 快速幂
示例
时间复杂度
O(logN)
Code
17. 打印从 1 到最大的 n 位数(字符串 + DFS)
题目描述
思路
由于 n 可能会非常大,因此不能直接用
int表示数字,包括long,long long正确的做法是用
char数组进行存储。由于使用
char存储数字,那么就不适合使用普通的运算操作了,此时可以使用 DFS 来获取所有的数字
Code
18.1 在 O(1) 时间内删除链表节点(链表)
题目描述
思路
因为不能遍历,所以只能通过修改节点的值来实现这个操作
简单来说,就是将该节点的值修改为其下一个节点的值,实际上删除的是该节点的下一个节点(题目的描述可能会带来误导)
如果该节点不是尾节点,那么按上述操作即可——时间的复杂度为
O(1)如果该节点是尾节点,此时必须通过遍历来找到该节点的前一个节点,才能完成删除——时间复杂度为
O(N)如果是 C++,一定要注意 delete 指针指向的内存后,必须将指针重新指向 nullptr
总的时间复杂度:
[(n-1)O(1) + O(n)] / n = O(1)
Code
18.2 删除链表中重复的结点(链表)
删除链表中重复的结点 - NowCoder
题目描述
思路
注意重复的节点不保留,所以要特别注意头结点也重复的情况——最好的做法是新设一个头结点
delete 指针指向的内存后,必须将指针重新指向 nullptr
Code
19. 正则表达式匹配(自动机:动态规划 | DFS)
正则表达式匹配 - NowCoder
题目描述
思路
'.' 用于当做一个任意字符,'*' 用于重复前面的字符,注意两者区别
下面提供
dfs(C++)和dp(Java)两种做法
Code - dfs
Code - dp
20. 表示数值的字符串(自动机 | 正则)
表示数值的字符串 - NowCoder
题目描述
思路
if 判断 - 自动机
正则表达式
Code - 自动机
Code - 正则(Python)
Code - 正则(C++)
Code - 正则(Java)
21. 调整数组顺序使奇数位于偶数前面(数组)
调整数组顺序使奇数位于偶数前面 - NowCoder
题目描述
要求:空间复杂度
O(1)本题与原书不同,这里要求相对顺序不变,原书的侧重点在于函数指针
思路
如果可以使用额外空间,那么问题就很简单
如果不想使用额外空间,那么
只能通过循环移位来达到避免覆盖的目的,时间复杂度O(N^2)可以利用“冒泡排序”的思想避免循环位移
Code - 使用额外空间
Code - 不使用额外空间
讨论区第二个回答
```C++ class Solution { public: void reOrderArray(vector &array) {
};
输入一个链表,输出该链表中倒数第k个结点。
23. 链表中环的入口结点(链表 + 双指针)
链表中环的入口结点 - NowCoder
题目描述
要求:不使用额外空间
思路
快慢双指针
快指针 fast 每次移动 2 步,慢指针 slow 每次 1 步;因为存在环,fast 和 slow 总会相遇,此时 fast 刚好比 slow 多走一圈(?)
如图,假设他们相遇在 z1 点,此时将 fast/slow 之一重新指向头结点,继续每次一步移动,它们再次相遇的点就是入口
Code
24. 反转链表(链表)
反转链表 - NowCoder
题目描述
要求:不使用额外空间
思路
辅助图示思考
Code - 迭代
Code - 递归
25. 合并两个排序的链表(链表)
合并两个排序的链表 - NowCoder
题目描述
思路
迭代
递归
Code(迭代)
Code(递归)
26. 树的子结构(二叉树)
树的子结构 -NowCoder
题目描述
图示
思路
递归
有两个递归的点:一、递归寻找与子树根节点相同的点;二、递归判断子结构是否相同
Code
27. 二叉树的镜像(二叉树)
二叉树的镜像 - NowCoder
题目描述
图示
思路
前序遍历,每次交换节点的左右子树;即必须先交换节点的左右子树后,才能继续遍历
Code
28 对称的二叉树(二叉树)
对称的二叉树 NowCoder
题目描述
思路
递归
同时遍历左子树和右子树,然后是“左子树的左子树和右子树的右子树”,及左子树的右子树和右子树的左子树,递归以上步骤
Code
29. 顺时针打印矩阵(二维数组)
顺时针打印矩阵 - NowCoder
题目描述
图示
注意,不是蛇形打印,而是一层一层顺时针打印
思路
二维数组遍历
Code
30. 包含 min 函数的栈(数据结构:栈)
包含min函数的栈 - NowCoder
题目描述
要求:时间复杂度
O(1)
思路
因为要求在常数时间内完成所有操作,所以不能有排序操作
使用一个辅助栈保存最小、次小、...
Code
31. 栈的压入、弹出序列(数据结构:栈)
栈的压入、弹出序列 -NowCoder
题目描述
思路
使用一个辅助栈
依次将入栈序列入栈,如果栈顶元素等于出栈序列的栈顶元素,则弹出
当流程无法继续时,如果辅助栈是空的,则出栈序列是符合的
Code
32.1 从上往下打印二叉树(BFS)
从上往下打印二叉树 - NowCoder
题目描述
图示
思路
广度优先搜索 + 队列
注意入队时先左子节点,后右节点
注意不需要修改原二叉树
Code
32.2 分行从上到下打印二叉树(BFS)
把二叉树打印成多行 - NowCoder
题目描述
思路
除了利用队列和 BFS
为了分行输出,还需要两个变量:一个表示在当前层中还没有打印的节点数、一个表示下一层的节点数
注意根节点为空的情况
Code
32.3 按之字形顺序打印二叉树(BFS)
按之字形顺序打印二叉树 - NowCoder
题目描述
思路 1. 利用一个队列+一个栈,分奇偶讨论; 1. 使用两个栈,根据奇偶,改变左右子树的入栈/入队顺序 2. 利用双端队列,分奇偶改变入队/出队方向(C++ 不推荐,编码量大)(有坑,不好写) 3. 反转层结果:根据奇偶,判断是否反转中间结果(最直观的方法)
Code - 两个栈
Code - 层反转
33. 二叉搜索树的后序遍历序列(二叉树:递归)
二叉搜索树的后序遍历序列 - NowCoder
题目描述
思路
二叉搜索树:左子树都小于根节点,右子树都大于根节点,递归定义
后序遍历:会先输出整个左子树,再输出右子树,最后根节点;也就是说,数组可以被划分为三个部分
示例:
1,2,3 | 5,6,7 | 4第一部分都小于最后的元素,第二部分都大于最后的元素——虽然这不是一颗二叉搜索树,但是它满足第一次判断的结果,后序再递归判断左右子树
Code
34. 二叉树中和为某一值的路径(DFS)
二叉树中和为某一值的路径 - NowCoder
题目描述
思路
注意:必须要从根节点到叶子节点,才叫一条路径,中间结果都不算路径,这样的话问题的难度一下子降低了很多
Code
35. 复杂链表的复制(链表)
复杂链表的复制 - NowCoder
题目描述
要求:时间复杂度
O(N)
思路
基本思路
O(N^2)第一步,依次复制每个节点
第二步,对每个节点,寻找特殊指针指向的节点;因为特殊指针的位置不定,必须从头开始找
假设经过 m 步找到了某个节点的特殊节点,那么在新链表中也走 m 步
问题的难点在于不知道特殊指针所指的节点在新链表中位置
一个经典的方法:
第一步,复制每个节点,如:原来是
A->B->C变成A->A'->B->B'->C->C';第二步,遍历链表,使:
A'->random = A->random->next;第三步,拆分链表
Code
36. 二叉搜索树与双向链表(DFS)
二叉搜索树与双向链表 - NowCoder
题目描述
思路
因为要求是有序链表,因此考虑中序遍历
利用两个额外的指针保存前一个节点和头结点,具体见代码中注释
Code
37. 序列化二叉树(DFS)*
序列化二叉树 - NowCoder
题目描述
比如中序遍历就是一个二叉树序列化
反序列化要求能够通过序列化的结果还原二叉树
空节点用 '#' 表示,节点之间用空格分开
思路
一般在做树的遍历时,会以非空叶子节点作为最底层,此时还原二叉树必须要前序遍历+中序遍历或后序遍历
如果以空节点作为树的最底层,那么只需要前序遍历就能还原二叉树,而且能与反序列化同步进行(这是最关键的一点)
Code
38. 字符串的排列(DFS)
字符串的排列 - NowCoder
排列组合专题 TODO
题目描述
思路
深度优先搜索
Code
39.1 数组中出现次数超过一半的数字(多数投票问题)
数组中出现次数超过一半的数字 - NowCoder
题目描述
要求:时间复杂度
O(N),空间复杂度O(1)
思路 1. 多数投票问题(Majority Vote Algorithm)
设置一个计数器 cnt 和保存最多元素的变量 majority
如果
cnt==0,则将 majority 设为当前元素如果 majority 和当前元素值相同,则
cnt++,反之cnt--重复以上两步,直到扫描完数组
cnt 赋值为 0,再次扫描数组,如果数组元素与 majority 相同,
cnt++如果扫描结束后,
cnt > nums.size() / 2,则返回 majority,否则返回 0。
Code
40. 找出数组中第 k 大的数字(数据结构:堆)*
数组中的第K个最大元素 - LeetCode
最小的K个数 - NowCoder
海量数据 Top K 专题 TODO
题目描述
正序找第 k 大的元素和逆序找第 k 大的元素方法是一致的;牛客是前者,LeetCode 是后者
可以改变原数组
要求:时间复杂度
O(N),空间复杂度O(1)
不可以改变原数组
要求:时间复杂度
O(NlogK),空间复杂度O(K)
实际上,找出数组中出现次数超过一半的数字可以看做是找出数组中第 n/2 大的数字
思路
可以改变原数组时:
参考快速排序中的
partition([pɑ:ˈtɪʃn]) 过程经过一次 partition 后,数组被 pivot 分成左右两部分:
l和r。当
|l| = k-1时,pivot 即是所找的第 k 大的数;当
|l| < k-1,所找的数位于r中;当
|l| > k-1,所找的数位于l中.
不可以改变原数组
使用额外空间 - 优先队列(堆)或 multiset
Code - 优先队列(无优化 O(NlogN))(牛客)
Code - 优先队列(优化 O(NlogK))(牛客)
Code - 可以改变数组(牛客)
Code - 第 k 个最大的数(LeetCode)
41. 数据流中的中位数(数据结构:堆)
数据流中的中位数 - NowCoder
题目描述
思路
使用平衡二叉树 AVL
不推荐,因为一般没有哪个语言会实现这个结构,它的综合性能不如红黑树
使用两个堆:一个最大堆,一个最小堆
保持两个堆的大小平衡
Code - 使用*_heap系列函数
Code - 使用优先队列
42. 连续子数组的最大和
连续子数组的最大和 - NowCoder
题目描述
思路
因为不需要输出路径,所以不需要 DP
法1)暴力枚举-两层循环
法2)实际上,只要遍历一次即可,首先用一个变量保存当前的最大值
如果当前和 sum 为负数,那么直接舍弃,用下一个值作为当前和
否则,加上下一个值(无论正负)
与当前最大值比较,保留最大的
Code - 法1
Code - 法2
43. 从 1 到 n 整数中 1 出现的次数(Trick)
整数中1出现的次数(从1到n整数中1出现的次数) - NowCoder
数字 1 的个数 - LeetCode
题目描述
思路
暴力枚举,时间复杂度
O(NlogN)找规律
O(logN)
Code - 暴力枚举
LeetCode 会超时
Code - 找规律
[LeetCode 讨论区](https://leetcode.com/problems/number-of-digit-one/discuss/64381/4+-lines-O(log-n)-C++JavaPython)
44. 数字序列中的某一位数字(Trick)
题目描述
思路
暴力求解
累加每个数的长度
Trick-类似二分的思想
比如第 1001 位,
0~9 的长度为 10——10 < 1001
10~99 的长度为 180——10+180 < 1001
100~999 的长度 为 2700——10+180+2700 > 1001
(1001 - 10 - 180) = 811 = 270*3 + 1
100 + 270 = 370 的第 1 位(从 0 计数),即 7
Code
45. 把数组排成最小的数(排序)
把数组排成最小的数 - NowCoder
题目描述
思路
自定义排序
在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,
如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。
利用 stringstream 拼接数字
C++ 中 int 转 string 的函数
to_string()
Code - 使用比较函数
Code - 使用Lambda表达式
46. 把数字翻译成字符串(解码方法)(动态规划)
解码方法 - LeetCode
题目描述
剑指Offer 上是数字范围为 0~25,其他一致
思路
动态规划
递推公式
Code(Python)
47. 礼物的最大价值(年终奖)(动态规划)
年终奖_牛客网
题目描述
思路
深度优先搜索-复杂度大
动态规划
二维递推公式
注意边界条件
一维递推公式(优化版)
Code - 二维DP
Code - 一维DP
48. 最长不含重复字符的子字符串(动态规划)
题目描述
思路
暴力枚举,时间复杂度
O(N^3),如果使用 set 的结构,可以降到O(N^2)动态规划
递推思路
Code - DP
Code - 优化
49. 丑数(动态规划)
丑数 - NowCoder
题目描述
思路
动态规划
Code
50.1 第一个只出现一次的字符位置(Hash)
第一个只出现一次的字符 - NowCoder
题目描述
思路
Hash 表
因为是字符,可以使用数组模拟哈希表
Code - 数组
Code - Hash(C++ map)
Code - Hash(Python dict)
50.2 字符流中第一个只出现一次的字符(数据结构:队列)
字符流中第一个不重复的字符 - NowCoder
题目描述
思路
计数排序——使用一个数组数组保存出现元素的次数
使用队列保存出现的元素
Code - 队列
51. 数组中的逆序对
数组中的逆序对 - NowCoder
题目描述
思路
Code
题目描述
思路
Code
题目描述
思路
Code
题目描述
思路
Code
题目描述
思路
Code
题目描述
思路
Code
题目描述
思路
Code
最后更新于
这有帮助吗?