剑指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 个数字的范围为 0n-1,考虑类似选择排序的思路,通过一次遍历将每个数交换到排序后的位置,如果该位置已经存在相同的数字,那么该数就是重复的

  • 示例

    Code

3.2 不修改数组找出重复的数字

题目描述

  • 要求:时间复杂度O(NlogN),空间复杂度O(1)

思路

  • 二分查找

  • 以长度为 8 的数组 {2, 3, 5, 4, 3, 2, 6, 7} 为例,那么所有数字都在 1~7 的范围内。中间的数字 41~7 分为 1~45~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 跳台阶(递归)

跳台阶 | 变态跳台阶 - NowCoder

题目描述

思路

  • 递归

  • 记跳 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 分成左右两部分:lr

      • |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

最后更新于

这有帮助吗?