第四章 解决面试题的思路
1.画图让抽象问题形象化
剑指 Offer 27. 二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
先序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。
TreeNode* mirrorTree(TreeNode* root) {
if(!root) return nullptr;
root->left = mirrorTree(root->left);
root->right = mirrorTree(root->right);
swap(root->left,root->right);
return root;
}
剑指 Offer 28. 对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
bool isSymmetric(TreeNode* root) {
if(!root) return true;//如果根节点为空,直接返回true
return isMirror(root->left,root->right);
}
bool isMirror(TreeNode * root1,TreeNode *root2){
if(!root1 && !root2) return true;//需要判空
if(!root1 || !root2 || root1->val != root2->val) return false;//如果两个节点只有一个为空,一个不为空 或者值不相等,return false
return isMirror(root1->left,root2->right) && isMirror(root1->right,root2->left);//镜像对称是树A的左儿子 等于B树的右儿子
}
剑指 Offer 29. 顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
简单但是有些麻烦的题,需要把每一行每一列的遍历情况考虑到。
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if(matrix.empty()) return vector<int>{};
int start = 0;
int rows = matrix.size(),cols = matrix[0].size();
vector<int> res;
while(start*2 < rows && start*2 < cols){//循环继续条件,从左上角(0,0)开始,一圈一圈打印
printMatrixInCircle(matrix, res, start);
++start;
}
return res;
}
void printMatrixInCircle(vector<vector<int>>& matrix, vector<int> &res, int start){//打印一圈
int endX = matrix.size() - start -1;//打印这一圈的最大行数
int endY = matrix[0].size() - start -1;//打印这一圈的最大列数
for(int i = start; i <= endY; ++i){//第一步从左往右打印,必有
res.emplace_back(matrix[start][i]);
}
if(endX > start){//第二步从上到下打印一列,存在第二步的前提是至少有两行,终止行号大于起始行号
for(int i = start + 1; i <= endX; ++i){
res.emplace_back(matrix[i][endY]);
}
}
if(endX > start && endY > start){//第三步是从右到左打印一行,存在第三步的前提是终止列数大于起始列数,终止行数大于起始行数
for(int i = endY-1; i >= start; --i){
res.emplace_back(matrix[endX][i]);
}
}
//第四步是从下到上打印一列:存在条件是至少有三行两列,即终止行号比起始行号至少大2
if(endX > start+1 && endY > start){
for(int i = endX-1; i > start; --i){
res.emplace_back(matrix[i][start]);
}
}
}
2.举例让抽象问题具体化
剑指 Offer 30. 包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
我们每次插入原栈时,都向新栈插入一次原栈里所有值的最小值(新栈栈顶和待插入值中小的那一个);每次从原栈里取出数字时,同样取出新栈的栈顶。这样可以保证每次push和pop的时候两个栈内的元素都一样多,而且不管pop几次,辅助栈栈顶都是数据栈已有元素中的最小值。
class MinStack {
public:
stack<int> sk,minSk;
MinStack() {}
void push(int x) {
sk.push(x);
if(minSk.empty() || x <= minSk.top() ){
minSk.push(x);
}else{
minSk.push(minSk.top());
}
}
void pop() {
if(sk.empty()) return;
sk.pop();
minSk.pop();
}
int top() {
return sk.top();
}
int min() {
return minSk.top();
}
};
剑指 Offer 31. 栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
如果下一个弹出的数字刚好是栈顶数字,那么 直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
if(pushed.empty() && popped.empty()) return true;//如果两个都为空,返回true
if(pushed.empty() || popped.empty() || pushed.size() != popped.size()) return false;//如果只有一个为空,或者数组内元素个数不同,则肯定不是弹出序列。
stack<int> sk;
int i = 0;
for(int j = 0;j < popped.size();++j){
while(sk.empty() || sk.top() != popped[j]){//如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止
if(i == pushed.size()) return false;//如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列
sk.push(pushed[i++]);
}
sk.pop();//如果下一个弹出的数字刚好是栈顶数字,那么 直接弹出。
}
return true;
}
剑指 Offer 32 - I. 从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
3
/ \
9 20
/ \
15 7
返回:[3,9,20,15,7]
层次遍历法。
vector<int> levelOrder(TreeNode* root) {
vector<int> res;
if(!root) return res;
queue<TreeNode*> qe;
qe.push(root);
while(!qe.empty()){
TreeNode* node = qe.front();
qe.pop();
res.emplace_back(node->val);
if(node->left) qe.push(node->left);
if(node->right) qe.push(node->right);
}
return res;
}
剑指 Offer 32 - II. 从上到下打印二叉树 II
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(!root) return res;
queue<TreeNode*> qe;
qe.push(root);
while(!qe.empty()){
vector<int> temp;
int n = qe.size();
for(int i = 0;i < n; ++i){
TreeNode * node = qe.front();
qe.pop();
temp.emplace_back(node->val);
if(node->left) qe.push(node->left);
if(node->right) qe.push(node->right);
}
res.emplace_back(temp);
}
return res;
}
剑指 Offer 32 - III. 从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[[3],[20,9],[15,7]]
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(!root) return res;
queue<TreeNode*> qe;
qe.push(root);
int layer = -1;
while(!qe.empty()){
vector<int> temp;
int n = qe.size();
++layer;
for(int i = 0;i < n; ++i){
TreeNode * node = qe.front();
qe.pop();
temp.emplace_back(node->val);
if(node->left) qe.push(node->left);
if(node->right) qe.push(node->right);
}
if(layer&1 == 1) reverse(temp.begin(),temp.end());//如果是奇数行,在层次遍历的基础上reverse一下
res.emplace_back(temp);
}
return res;
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true
,否则返回 false
。假设输入的数组的任意两个数字都互不相同。
5
/ \
2 6
/ \
1 3
输入: [1,3,2,6,5]
输出: true
这道题突破口在利用true和false两个实例模拟判断过程。
bool verifyPostorder(vector<int>& postorder) {
if(postorder.empty() || postorder.size() == 1) return true;
return verifyBST(postorder, 0, postorder.size()-1);
}
bool verifyBST(vector<int>& postorder, int start,int end){
if(start >= end) return true;//如果树只有一个节点,或者无节点,直接返回true
int root = postorder[end];//最后一个元素为根节点
int pos = start;
while(postorder[pos] < root) ++pos;//凡是小于根节点的全部为左子树
int leftEnd = pos-1;//左子树结束位置
while(postorder[pos] > root) ++pos;//右子树的值都应该比根节点大
return pos==end && verifyBST(postorder,start, leftEnd) && verifyBST(postorder, leftEnd+1, end-1);//return 右子树的值都比根节点大 && 左子树为BST && 右子树为BST
}
剑指 Offer 34. 二叉树中和为某一值的路径
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
给定如下二叉树,以及目标和 target = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:[[5,4,11,2],[5,8,4,5]]
当用前序 遍历的方式访问到某一结点时,我们把该结点添加到路径上,并累加 该结点的值。如果该结点为叶结点并且路径中结点值的和刚好等于输 入的整数,则当前的路径符合要求,我们把它打印出来。如果当前结 点不是叶结点,则继续访问它的子结点。当前结点访问结束后,递归 函数将自动回到它的父结点。因此我们在函数退出之前要在路径上删 除当前结点并减去当前结点的值,以确保返回父结点时路径刚好是从 根结点到父结点的路径。
vector<vector<int>> pathSum(TreeNode* root, int target) {
vector<vector<int>> res;
if(!root) return res;
vector<int> path;
findPath(root, target, path, res);
return res;
}
void findPath(TreeNode* root, int target, vector<int> &path, vector<vector<int>> &res){
path.emplace_back(root->val);
if(root->val == target && !root->left && !root->right){//如果是节点值等于路径和,且是叶子节点,则存储这条路径
res.emplace_back(path);
}
//如果当前节点小于目标值,则继续去子树寻找累加和
if(root->left) findPath(root->left, target - root->val, path, res);
if(root->right) findPath(root->right, target - root->val, path, res);
path.pop_back();//在返回父节点之前,在路径上删除当前节点
}
3.分解让复杂问题简单化
剑指 Offer 35. 复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rGA8y5Rl-1622200055146)(images/image-20210526235839362.png)]
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
方法一:第一步仍然是复制原始链表上的每个结点N创建N’,然后把这些创建出来的结点用p->Next链接起来。同时我们把<N,N’>的配对信息放到一个哈希表中。第二步还是设置复制链表上每个结点的random。如果在原始链表中结 点N的random指向结点S,那么在复制链表中,对应的N’应该指向S’。相当于用空间换时间。对于有n个结点的链表我们需要 一个大小为O(n)的哈希表,也就是说我们以O(n)的空间消耗把时间复杂度由O(n2)降低到O(n)。
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
//利用hash表建立原节点和克隆节点的对应关系
Node* copyRandomList(Node* head) {
if(!head) return head;
unordered_map<Node*,Node*> hash;//hash[原节点] = 克隆后链表节点
Node * cur = head;
while(cur){//利用hash表key表示原节点,val对应克隆后的节点,这里建立了与原节点val相同的节点
hash[cur] = new Node(cur->val);
cur = cur->next;
}
cur = head;
while(cur){//建立克隆节点之间的next和random链接关系
hash[cur]->next = hash[cur->next];//cur的克隆节点的next = cur->next的克隆节点
hash[cur]->random = hash[cur->random];//cur的克隆节点的random = cur->random的克隆节点
cur = cur->next;
}
return hash[head];//返回头结点的克隆节点
}
方法二:不用辅助空间的情况下实现O(n) 的时间效率。
第一步仍然是根据原始链表的每个结点N创建对应的N’。这一次,我们把N’链接在N的后面。

第二步设置复制出来的结点的random。假设原始链表上的N的random指向结点S,那么其对应复制出来的N’是N的Next指向的结点,同样S’也是S的Next指向的结点。
第三步把这个长链表拆分成两个链表:把奇数位置的结点用Next链接起来就是原始链表,把偶数位置的结点用Next链接起 来就是复制出来的链表。
//这道题的核心是 克隆节点的next,random也是对应的克隆节点
Node* copyRandomList(Node* head){
cloneNodes(head);//根据原始链表的每个结点N创建对应的N’,把N’链接在N的后面
connectRandom(head);//设置复制出来的结点的random
return reconnect(head);//长链表拆分成两个链表
}
void cloneNodes(Node* &head){
Node* cur = head;
while(cur){
Node* cloned = new Node(cur->val);//把克隆节点连接在原节点后面
cloned->next = cur->next;
cloned->random = nullptr;
cur->next = cloned;
cur = cloned->next;
}
}
void connectRandom(Node* &head){
Node * cur = head;
while(cur){
Node* curCloned = cur->next;//当前节点的克隆节点是 当前节点的下一个
if(cur->random){//如果当前节点的random存在,那么当前节点的克隆节点 的random是当前节点random的下一个克隆节点
curCloned->random = cur->random->next;
}
cur = curCloned->next;
}
}
Node* reconnect(Node* &head){//把奇数位置的结点用Next链接起来就是原始链表,把偶数位置的结点用Next链接起 来就是复制出来的链表。
Node* cur = head;
Node* cloneHead = nullptr;
Node* curCloned = nullptr;
if(cur){//将原始链表指针cur迭代到第二个节点处,将复制链表的指针curCloned迭代到第一个节点的克隆节点处
cloneHead = curCloned = cur->next;
cur->next = curCloned->next;
cur = cur->next;
}
while(cur){
curCloned->next = cur->next;
curCloned = curCloned->next;
cur->next = curCloned->next;
cur = cur->next;
}
return cloneHead;
}
剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

中序遍历算法搜索二叉树后得到是一串递增的序列,所以中序遍历的时候改变其左右指针就能得到递增的双向链表。对于每一层递归来说,根节点的左指针指向其左子树的最大值,即左子树中最右下的结点;右指针指向其右子树的最小值,即右子树中最左下的结点。所以每一层都可以考虑递归。
按照中序遍历的顺序,当我们遍历转换到根结点时,它的左子树已经转换成一个排序的链表了,并且处在链表中的最后一个结点是当前值最大的结点。可以直接把根节点和当前值最大的节点相链接,所以递归函数中传递一个变量来保存当前链表的最大值。
Node* treeToDoublyList(Node* root) {
if(!root) return root;//如果root为空,不用处理
Node* listLast = nullptr;//保存转换后链表的尾结点
convertNode(root,listLast);
Node* listHead = listLast;
while(listHead && listHead->left){//双向链表向左迭代,找到链表的头结点
listHead = listHead->left;
}
listHead->left = listLast;//最后将链表头尾节点相连
listLast->right = listHead;
return listHead;
}
//中序遍历
void convertNode(Node* root, Node * & listLast){
if(!root) return;
if(root->left) convertNode(root->left, listLast);
root->left = listLast;//根节点的左指针指向 左子树的最大值 也就是左子树链表的尾部
if(listLast) listLast->right = root;//左子树链表的尾部的右指针 指向根节点
listLast = root;//将链表最大值迭代为根节点
if(root->right) convertNode(root->right, listLast);//然后递归右子树,也就是在右子树中寻找最小值
}
剑指 Offer 37. 序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树。
你可以将以下二叉树:
1
/ \
2 3
/ \
4 5
序列化为 "[1,2,3,null,null,4,5]"
采用前序遍历,从根节点开始。二叉树的序列化也是从根节点开始,那么相应的反序列化如果读出数值就可以重建;每遍历到一个数就+一个,如果为空,就+“NULL,”;反序列化时利用队列,用相同的顺序重建二叉树。
class Codec {
public:
// Encodes a tree to a single string.
string str = "";
string serialize(TreeNode* root) {
serializeTree(root);
return str;
}
void serializeTree(TreeNode * root){//前序遍历,直接序列化
if(!root) str += "NULL,";
else{
str += to_string(root->val) + ',';
serializeTree(root->left);
serializeTree(root->right);
}
}
// Decodes your encoded data to tree.
queue<string> qe;
TreeNode* deserialize(string data) {
int i = 0, j = 0;//双指针,分别指向逗号分隔字符串的首尾
while(i < data.size()){
while(data[i] != ',' && i < data.size()) ++i;//i指向该字符串后的逗号
string temp = data.substr(j,i-j);//j表示被分隔字符串的首部
qe.push(temp);
++i;//跳过下一个逗号
j = i;//下一个逗号起始的下一个字符串的第一位
}
return deserializeTree();
}
TreeNode* deserializeTree(){
auto t = qe.front();
qe.pop();
if(t == "NULL") return nullptr;//前序遍历第一个入队的是根节点,根节点都为空的话,树不存在
TreeNode* root = new TreeNode(stoi(t));
root->left = deserializeTree();//左儿子是下一次递归取出来的节点
root->right = deserializeTree();
return root;
}
};
剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
与之前排列组合需要push path不同,解法选择将pos的元素和后面的每一个元素进行交换,也能达到全部排列的效果。存在重复元素时则选择用set进行统计,如果出现则跳过当前pos。
vector<string> permutation(string s) {
vector<string> res;
if(s.empty()) return res;
traceBack(s, res, 0);
return res;
}
void traceBack(string s,vector<string> &res, int pos){
if(pos == s.size()-1){
res.emplace_back(s);
return;
}
set<char> st;
for(int i = pos; i < s.size(); ++i){
if(st.find(s[i]) != st.end() ) continue;//c存在重复元素,剪枝
st.insert(s[i]);
swap(s[i],s[pos]);
traceBack(s, res, pos+1);
swap(s[i],s[pos]);
}
}