剑指 Offer
参考资料
- 剑指 Offer
面试的流程
略
面试需要的基础知识
面试题 1: 赋值运算符函数
- 返回类型为该类型的引用, 并返回
*this - 传入参数为
const T& - 先判断是否是自赋值, 如果是则直接返回
*this - 否则先释放原有的内存, 再分配新的内存, 最后复制数据
// 这本书的标准答案
class T {
public:
T& operator=(const T& other) {
if(this != &other) {
// 调用拷贝构造函数
T temp(other); // 如果在这里失败, 则不会执行下面的代码
// 保证异常安全
// 交换数据
int* tempData = temp.data;
temp.data = data;
data = tempData;
}
return *this;
// temp 是一个临时对象, 会在函数结束时自动析构
}
private:
int* data;
int size;
};
面试题 2: 实现 Singleton 模式
- 需求: 全局唯一实例, 懒加载, 线程安全, 禁止拷贝
- 错误的实现
- 将构造函数设为私有, 提供一个用于获取实例的静态方法, 一个静态指针指向唯一实例
- 只适用单线程环境, 通过判断指针是否为空来判断是否需要创建实例, 可能会出现线程安全问题
- 加一把同步锁, 可以但是效率低, 因为后续实例化的尝试也需要加锁才能判断是否需要创建实例
- 将构造函数设为私有, 提供一个用于获取实例的静态方法, 一个静态指针指向唯一实例
- 勉强的实现
- 加锁,前后判断指针是否为空
- 最佳实现
- 静态方法的静态局部变量 (进程生命周期)
- C++ 11 保证静态局部变量的初始化是线程安全的, 且只会执行一次
- 会自动调用析构函数, 不会内存泄漏
#include <iostream>
class Singleton {
public:
// 获取单例引用的静态方法
static Singleton& getInstance() {
// C++11 保证: 静态局部变量的初始化是线程安全的
// 且只会执行一次
static Singleton instance;
return instance;
}
// 禁止拷贝构造
Singleton(const Singleton&) = delete;
// 禁止赋值操作
Singleton& operator=(const Singleton&) = delete;
// 示例业务方法
void doSomething() {
std::cout << "Singleton is doing something." << std::endl;
}
private:
// 私有化构造函数
Singleton() {
std::cout << "Singleton Construct" << std::endl;
}
// 私有化析构函数 (可选, 根据需求)
~Singleton() {
std::cout << "Singleton Destruct" << std::endl;
}
};
int main() {
// 调用时才会初始化
Singleton::getInstance().doSomething();
return 0;
}
面试题 3: 二维数组中的查找
在一个二维数组中, 每一行都按照从左到右递增的顺序排序, 一列都按照从上到下递增的顺序排序, 请完成一个函数, 输入这样的一个二维数组和一个整数, 判断数组中是否含有该整数
- 一看都想二分, 一次排除的倒是很多, 状态转移不好做
- 从二维数组的右上角开始查找
- 如果当前元素等于目标值, 则返回
true - 如果当前元素大于目标值, 则说明目标值在当前元素的左边, 列索引减一
- 如果当前元素小于目标值, 则说明目标值在当前元素的下边, 行索引加一
- 如果当前元素等于目标值, 则返回
- 这样稳定将数组分成两部分
面试题 4: 替换空格
将字符串中的空格替换为 %20
- 非常简单, 先保证空间足够, 从后往前替换, 不会覆盖未处理的字符
面试题 5: 从尾到头打印链表
- 非常简单, 递归打印即可
- 也可以用栈来实现, 先入后出
面试题 6: 重建二叉树
根据前序遍历和中序遍历重建二叉树
- 根据前序遍历确定根节点
- 根据中序遍历将数组分成左右两部分
- 递归重建左子树和右子树
面试题 7: 用两个栈实现队列
- 一个栈用于入队, 一个栈用于出队
- 入队时直接入栈
- 出队时, 如果出队栈为空, 则将入队栈中的元素全部弹出并压入出队栈, 然后出队栈弹出栈顶元素
面试题 8: 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾, 我们称之为数组的旋转, 输入一个递增排序的数组的一个旋转, 输出旋转数组的最小元素
- 二分查找
- 如果中间元素大于等于左边界, 则说明中间元素在左半部分, 最小元素在右半部分, 左边界移动到中间元素
- 如果中间元素小于等于右边界, 则说明中间元素在右半部分, 最小元素在左半部分, 右边界移动到中间元素
- 当左边界和右边界相邻时, 右边界就是最小值
- 完备一下
- 考虑数组完全有序的情况, 先比较左边界和右边界, 如果左边界小于右边界, 则数组完全有序, 直接返回左边界
- 考虑左边界和右边界和中间元素都相等的情况, 无法判断, 只能顺序查找 (本质是不能判断旋转左倾还是右倾)
面试题 9: 斐波那契数列
- 非常简单, 递归即可退出面试
- 可以用动态规划优化
- 正解矩阵快速幂
面试题 10: 二进制中 1 的个数
求汉明重量
- 错误的解法
- 循环右移 (移
n) 检验最后一位是否为 \(1\), 负数的右移会补 \(1\), 会导致死循环 - 循环左移 (移
flag) 检验最后一位是否为 \(1\), 可以, 但是效率低
- 循环右移 (移
- 位运算
- 每次将 \(n\) 与 \(n-1\) 做按位与操作, 会将 \(n\) 的二进制表示中的最低位的 \(1\) 变成 \(0\)
- 重复操作直到 \(n\) 变成 \(0\)
- 操作次数就是 \(1\) 的个数
高质量的代码
面试题 11: 数值的整数次方
实现 double pow(x, n) 函数, 计算 \(x\) 的 \(n\) 次方
- 非常简单, 循环即可退出面试
- 要考虑 \(n\) 为负数的情况, 可以先将 \(n\) 取相反数, 最后返回
1 / res(校验res < 0.0000001lf) - 再加一个快速幂的优化
面试题 12: 打印 1 到最大的 n 位数
输入数字 \(n\), 按顺序打印出从 \(1\) 到最大的 \(n\) 位十进制数, 比如输入 \(3\), 则打印出 \(1\), \(2\), \(3\) 一直到最大的 \(3\) 位数 \(999\)
- 非常简单, 开始打印, 退出面试即可
- 用字符串模拟
- 模拟加一, \(n+1\) 位不是 \(0\) 时, 说明已经打印完了 \(n\) 位的所有数
- 打印时忽略前导 \(0\)
- \(n\) 位全排列
面试题 13: 在 \(O(1)\) 时间删除链表节点
- 非常简单, 用下一个节点的值覆盖当前节点, 然后删除下一个节点即可
- 要注意的是, 要删除的节点是否是尾节点或只有一个节点
面试题 14: 调整数组顺序使奇数位于偶数前面
- 非常简单, 用双指针, 一个指向头, 一个指向尾, 头指针遇到偶数, 尾指针遇到奇数, 就交换
- 拓展性
- 可以将判断奇偶的条件抽象出来, 注入可调用对象
面试题 15: 链表中倒数第 k 个节点
- 非常简单, 用两个指针, 一个先走 \(k-1\) 步, 然后两个指针一起走, 当快指针到达尾节点时, 慢指针就是倒数第 \(k\) 个节点
- 要注意的是, 判断 \(k\) 的语义是否正确
- 正数
- 小于等于链表长度
面试题 16: 反转链表
- 非常简单, 用三个指针, 一个指向前驱, 一个指向当前节点, 一个指向后继, 然后依次反转即可
面试题 17: 合并两个排序的链表
- 非常简单, 用两个指针, 一个指向头, 一个指向当前节点, 然后依次比较, 小的就接入当前节点, 然后指针后移
面试题 18: 树的子结构
- 先找相同的根节点, 然后递归判断左右子树是否相同
解决面试题的思路
面试题 19: 二叉树的镜像
- 非常简单, 递归交换左右子树即可
面试题 20: 顺时针打印矩阵
- 非常简单, 模拟
面试题 21: 包含 min 函数的栈
- 最小栈
面试题 22: 栈的压入, 弹出序列
- 模拟, 用一个栈模拟压入, 然后判断弹出序列是否正确
- 弹出序列的每个元素, 都要和栈顶元素比较, 如果相等, 就弹出栈顶元素, 否则, 就继续压入栈
面试题 23: 从上往下打印二叉树
- 层序遍历
面试题 24: 二叉搜索树的后序遍历序列
给你一个数组, 判断是否是二叉搜索树的后序遍历序列
- 递归
- 后序遍历的最后一个元素就是根节点
- 前面的元素可以分为两部分, 一部分是左子树的后序遍历序列, 一部分是右子树的后序遍历序列
- 左子树的后序遍历序列的所有元素都应该小于根节点, 右子树的后序遍历序列的所有元素都应该大于根节点
- 递归判断左子树和右子树是否是后序遍历序列
面试题 25: 二叉树中和为某一值的路径
- 遍历
面试题 26: 复杂链表的复制
- 力扣原题
- 难点在处理随机指针的复制时, 要找到源节点与复制节点的对应关系
- 哈希表 / 复制不拆分
面试题 27: 二叉搜索树与双向链表
将二叉搜索树转换为双向链表 (原地)
- 中序遍历即可
面试题 28: 字符串的排列
给一个字符串, 打印出该字符串中字符的所有排列
- 每次选择一个字符, 放在序列的第一个位置, 然后递归处理剩余的字符
优化时间和空间效率
面试题 29: 数组中出现次数超过一半的数字
- 摩尔投票法, 力扣原题
面试题 30: 最小的 k 个数
- 减治法 / 最大堆
面试题 31: 连续子数组的最大和
- 动态规划 / 贪心, 力扣原题
面试题 32: 从 1 到 n 整数中 1 出现的次数
- 从最高位开始枚举每位, 计算该位上 \(1\) 出现的次数
- 这位若大于 \(1\), 则该位上 \(1\) 出现的次数为 \(10^i\) (i 为当前位数)
- 这位若等于 \(1\), 则该位上 \(1\) 出现的次数为 \(n - 10^i + 1\) (i 为当前位数)
面试题 33: 把数组排成最小的数
数组 [3, 32, 321] 排成的最小的数为 \(321323\)
- 排序, 自定义比较函数 (字典序)
面试题 34: 丑数
只包含因子 \(2\), \(3\), \(5\) 的数称为丑数, 求第 \(n\) 个丑数
- 根据已经算出的丑数组中的 \(T_2\), \(T_3\), \(T_5\), 乘以 \(2\), \(3\), \(5\) 获得最小数 (下一个数)
- \(T_2\), \(T_3\), \(T_5\) 分别指向当前丑数组中, 前面的数都乘以 \(2\), \(3\), \(5\) 小于当前丑数组的最后一个数, 后面的数反之的数
- 考虑缓存
面试题 35: 第一个只出现一次的字符
- 哈希表
面试题 36: 数组中的逆序对
- 归并排序, 合并时左数组小无逆序对
- 右数组小, 则左数组中的每个元素都与其构成逆序对, 数量为 \(mid - i + 1\)
面试题 37: 两个链表的第一个公共节点
- 力扣原题
面试中的各项能力
面试题 38: 数字在排序数组中出现的次数
- 二分查找, 找到第一个小于 \(k\) 的位置, 然后往后找, 直到找到第一个大于 \(k\) 的位置
面试题 39: 二叉树的深度
- 递归, 取左右子树的深度的较大值 + 1
- 后序遍历
面试题 40: 数组中只出现一次的数字
数组中只出现一次的数字有两个, 怎么办
- 还是先异或, 得到的结果就是这两个数的异或结果
- 然后找到异或结果中第一个为 \(1\) 的位, 假设为第 \(i\) 位
- 然后根据第 \(i\) 位是否为 \(1\) 来将数组分为两部分, 分别异或, 就可以得到这两个数
面试题 41: 和为 S 的两个数字 VS 和为 S 的连续正数序列
题目一: 和为 S 的两个数字
- 双指针, 一个指向头, 一个指向尾, 向中间移动
题目二: 和为 S 的连续正数序列
- 滑动窗口, 窗口内的数的和小于 \(S\), 右指针右移, 大于 \(S\), 左指针右移
- 窗口内的数的和等于 \(S\), 记录窗口内的数, 左指针右移
面试题 42: 翻转单词顺序 VS 左旋转字符串
题目一
- 原题
题目二
- 原题
面试题 43: n 个骰子的点数
扔 \(n\) 个骰子, 求点数和的概率分布
- 动态规划
- \(dp[i][j]\) 表示扔 \(i\) 个骰子, 点数为 \(j\) 的次数
- \(dp[i][j] = \sum_{k=1}^6 dp[i-1][j-k]\)
面试题 44: 扑克牌的顺子
- 模拟
面试题 45: 圆圈中最后剩下的数字
- 约瑟夫环问题
- \(f(n, m) = (f(n-1, m) + m) \mod n\)
面试题 46: 求 1 + 2 + ... + n
- 逆天问题
面试题 47: 不用加减乘除做加法
- 模拟一个加法器
面试题 48: 不能继承的类
- 逆天问题
面试题 49: 把字符串转换为整数
- 略
面试题 50: 树中两个节点的最低公共祖先
- 力扣原题
新增面试题
面试题 51: 数组中的重复的数字
在一个长度为 \(n\) 的数组中, 所有的数字都在 \(0\) 到 \(n-1\) 的范围内, 数组中某些数字是重复的, 请找出数组中任意一个重复的数字
- 一开始我想的是位图秒杀
- 正解抽屉原理
- 假设数组中没有重复的数字, 那么数组中每个数字都对应一个下标
- 现在有重复的数字, 那么至少有一个下标对应的数字重复了
面试题 52: 构建乘积数组
给一个数组 \(A[0, 1, \dots, n-1]\), 构建一个数组 \(B[0, 1, \dots, n-1]\), 其中 \(B[i] = A[0] \times A[1] \times \dots \times A[i-1] \times A[i+1] \times \dots \times A[n-1]\)
- 力扣原题
面试题 53: 正则表达式匹配
给一个字符串 \(s\) 和一个正则表达式 \(p\), 实现正则表达式匹配
- 力扣原题
- 模拟, 主要是处理
*, 分叉即可
面试题 54: 表示数值的字符串
- 模拟
面试题 55: 字符流中第一个不重复的字符
- 哈希表
面试题 56: 链表中环的入口节点
- 力扣原题
- 快慢指针
面试题 57: 删除链表中重复的节点
- easy
- 哑节点 + 双指针
面试题 58: 二叉树的下一个节点
- 力扣原题
- 分类讨论即可
面试题 59: 对称的二叉树
- 力扣原题
面试题 60: 把二叉树打印成多行
- 力扣原题
- 层序遍历
面试题 61: 按之字形顺序打印二叉树
- 力扣原题
- 层序遍历, 每一层的节点顺序根据层数的奇偶性来确定
面试题 62: 序列化二叉树
- 力扣原题
- 前序遍历, 用占位符补全为完全二叉树
面试题 63: 二叉搜索树的第 k 个节点
- 力扣原题
- 中序遍历, 第 \(k\) 个节点就是第 \(k\) 小的节点
面试题 64: 数据流中的中位数
- 维护一个大根堆和一个小根堆
- 大根堆存储较小的一半数
- 小根堆存储较大的一半数
- 插入时, 先插入大根堆, 然后将大根堆的堆顶元素弹出, 插入小根堆
- 中位数就是大根堆的堆顶元素, 如果是偶数个元素, 则是大根堆的堆顶元素和小根堆的堆顶元素的平均值
面试题 65: 滑动窗口的最大值
- 力扣原题
- 单调队列
- 队头元素就是当前窗口的最大值
- 新元素入队时, 队尾元素出队, 直到队尾元素大于等于新元素
- 队头元素出队时, 如果队头元素不在当前窗口内, 则出队
面试题 66: 矩阵中的路径
一个由 \(m \times n\) 个格子组成的矩阵, 每个格子中都有一个字符, 问是否有一个路径, 使得路径上的字符按顺序组成字符串 \(s\)
- 回溯
面试题 67: 机器人的运动范围
机器人在一个 \(m \times n\) 的矩阵中, 机器人从 \((0, 0)\) 开始, 只能向右或向下移动, 不能进入行坐标和列坐标的数位之和大于 \(k\) 的格子
- 回溯