CSDN话题挑战赛第2期
参赛话题: 学习笔记
剑指offer
前言
主要刷题平台为 牛客网,部分题目使用 LeetCode 和 ACwing 作为辅助。每题均包含主要思路、详细注释、时间复杂度和空间复杂度分析,每题均是尽可能最佳的解决办法。
树的结构
题目:
输入两棵二叉树A,B,判断B是不是A的子结构。(我们约定空树不是任意一个树的子结构)
假如给定A为{8,8,7,9,2,#,#,#,#,4,7},B为{8,9,2},2个树的结构如下,可以看出B是A的子结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0sFM6ygT-1665395653652)(https://uploadfiles.nowcoder.com/images/20211027/557336_1635320187489/B1C70B05B2BA3AAA854EE032F2A8D826)]
思路:
类似于字符串匹配的暴力做法
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
if(pRoot2==nullptr||pRoot1==nullptr)
{
return false;
}
if(isPart(pRoot1,pRoot2))
{
return true;
}
//递归判断
return HasSubtree(pRoot1->left, pRoot2)||HasSubtree(pRoot1->right, pRoot2);
}
bool isPart(TreeNode* p1,TreeNode* p2)
{
if(p2==nullptr)
{
//当期分支已经被包含了
return true;
}
if(p1==nullptr||p1->val!=p2->val)
{
//遍历到最后一个了或值不一样
return false;
}
//递归调用
return isPart(p1->left, p2->left)&&isPart(p1->right,p2->right);
}
};
最坏情况下,我们对于树A中的每个节点都要递归判断一遍,每次判断在最坏情况下需要遍历完树B中的所有节点。
所以时间复杂度是
O
(
n
m
)
O(nm)
O(nm),其中
n
n
n 是树A中的节点数,
m
m
m 是树B中的节点数。
二叉树的镜像
题目:
操作给定的二叉树,将其变换为源二叉树的镜像。
数据范围:二叉树的节点数 0 ≤ n ≤ 10000 0 \le n \le 10000 0≤n≤10000 , 二叉树每个节点的值 0 ≤ v a l ≤ 10000 0\le val \le 10000 0≤val≤10000
要求: 空间复杂度 O ( n ) O(n) O(n) 。本题也有原地操作,即空间复杂度 O ( 1 ) O(1) O(1) 的解法,时间复杂度 O ( n ) O(n) O(n)
比如:
源二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EM1FLM71-1665395653653)(https://uploadfiles.nowcoder.com/images/20210922/382300087_1632302001586/420B82546CFC9760B45DD65BA9244888)]
镜像二叉树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EQSDiI1Y-1665395653655)(https://uploadfiles.nowcoder.com/images/20210922/382300087_1632302036250/AD8C4CC119B15070FA1DBAA1EBE8FC2A)]
思路:
对二叉树的每个节点 都进行交换,就达到镜像的目的了
/**
* struct TreeNode {
* int val;
* struct TreeNode *left;
* struct TreeNode *right;
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* };
*/
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pRoot TreeNode类
* @return TreeNode类
*/
TreeNode* Mirror(TreeNode* pRoot) {
// write code here
if(pRoot==nullptr)
{
return nullptr;
}
else
{
//将左右都交换
Mirror(pRoot->left);
Mirror(pRoot->right);
swap(pRoot->left,pRoot->right);
}
//依然返回根节点
return pRoot;
}
};
原树仅被遍历一次,所以时间复杂度是 O ( n ) O(n) O(n)。
对称的二叉树
题目:
给定一棵二叉树,判断其是否是自身的镜像(即:是否对称)
例如: 下面这棵二叉树是对称的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FddLArr7-1665395653655)(https://uploadfiles.nowcoder.com/images/20210926/382300087_1632642756706/A22A794C036C06431E632F9D5E2E298F)]
下面这棵二叉树不对称。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmszfjFD-1665395653656)(https://uploadfiles.nowcoder.com/images/20210926/382300087_1632642770481/3304ABDD147D8E140B2CEF3201BD8372)]数据范围:节点数满足 0 ≤ n ≤ 10000 0 \le n \le 10000 0≤n≤10000,节点上的值满足 ∣ v a l ∣ ≤ 1000 |val| \le 1000 ∣val∣≤1000
要求:空间复杂度 O ( n ) O(n) O(n),时间复杂度 O ( n ) O(n) O(n)
备注:你可以用递归和迭代两种方法解决这个问题
思路:
与上一题类似,还是要找规律判断,对于 树的左右子树,如果是对称的,1️⃣ 两个子树的根节点值相等;
2️⃣ 第一棵子树的左子树和第二棵子树的右子树互为镜像,且第一棵子树的右子树和第二棵子树的左子树互为镜像;
这里使用 递归的深度优先遍历思想实现
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot) {
if(pRoot==nullptr)
{
return true;
}
return dfs(pRoot->left,pRoot->right);
}
bool dfs(TreeNode* left,TreeNode* right)
{
//如果有一个为空
if(left==nullptr||right==nullptr)
{
//如果都为空,就返回true,否则有一方为空,就返回false
return !left&&!right;
}
if(left->val!=right->val)
{
return false;
}
//返回判断是否对称
return dfs(left->left,right->right)&&dfs(left->right,right->left);
}
};
打印矩阵
题目:
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵:
[[1,2,3,4], [5,6,7,8], [9,10,11,12], [13,14,15,16]]
则依次打印出数字
[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]
数据范围:
0 < = m a t r i x . l e n g t h < = 100 0 <= matrix.length <= 100 0<=matrix.length<=100
0 < = m a t r i x [ i ] . l e n g t h < = 100 0 <= matrix[i].length <= 100 0<=matrix[i].length<=100
思路:
这里最好是 使用LeetCode
,将 螺旋矩阵 和 螺旋矩阵II 再回顾回顾,这个是真的面试高频题
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> result;
if(matrix.empty()||matrix[0].empty())
{
return {};
}
int m=matrix.size(); //行数,因为二维数组就相当于 一维数组的每个元素都是一个一维数组
int n=matrix[0].size(); //列数
//确定上下左右四条边的位置 左闭右闭
int up=0,down=m-1,left=0,right=n-1;
int i,j; //遍历的行和列
while(true)
{
//上
for(i=left;i<=right;i++)
{
result.push_back(matrix[up][i]);
}
//第一行完毕,进入第二行,up加一个
//up++;
if(++up>down) //左闭右闭,如果要放到if 判断里,一定要前置运算,先运算 后判断大小
{
//说明最后一行也遍历完毕了,就可以结束了
break;
}
//右
for(j=up;j<=down;j++)
{
result.push_back(matrix[j][right]);
}
//right--;
if(--right<left)
{
break;
}
//下
for(i=right;i>=left;i--)
{
result.push_back(matrix[down][i]);
}
//down--;
if(--down<up)
{
break;
}
//左
for(j=down;j>=up;j--)
{
result.push_back(matrix[j][left]);
}
//left++;
if(++left>right)
{
break;
}
}
return result;
}
};
矩阵中每个格子遍历一次,所以总时间复杂度是 O ( n 2 ) O(n^2) O(n2)
返回min 的栈
题目:
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。
思路:
这里用到了单调栈的思想,最好再练习一下单调栈相关的内容,比如:接雨水 这题也是 面试的高频题,经久不衰!!
class Solution {
public:
//正常栈normal 和 自栈顶向栈底 递减的栈
stack<int> normal,minval;
void push(int value) {
normal.push(value);
//单调栈, 如果新元素小,就将其放入 单调栈中
if(minval.empty()||minval.top()>=value)
{
minval.push(value);
}
}
void pop() {
//如果相等,则两个栈 都需要弹出元素
if(normal.top()==minval.top())
{
minval.pop();
}
normal.pop();
}
int top() {
return normal.top();
}
int min() {
//返回单调栈
return minval.top();
}
};
四种操作都只有常数次入栈出栈操作,所以时间复杂度都是 O ( 1 ) O(1) O(1)
栈的弹出顺序
题目:
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
0 < = p u s h V . l e n g t h = = p o p V . l e n g t h < = 1000 0<=pushV.length == popV.length <=1000 0<=pushV.length==popV.length<=1000
− 1000 < = p u s h V [ i ] < = 1000 -1000<=pushV[i]<=1000 −1000<=pushV[i]<=1000
p u s h V pushV pushV 的所有数字均不相同
思路:
借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。
class Solution {
public:
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
//如果该弹出顺序合法,则按照该弹出顺序,则一定将栈变为空
stack<int> st;
int i=0,j=0;
while(i<pushV.size())
{
//
if(pushV[i]!=popV[j])
{
//先将 pushV 的值放入栈中
st.push(pushV[i]);
i++;
}
else
{
//按照出栈顺序就是放入栈中后,立马弹出的元素
++i;
++j;
//按出栈顺序 出栈
while(!st.empty()&&st.top()==popV[j])
{
st.pop();
++j;
}
}
}
return st.empty();
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n), 用了一个辅助栈,最坏情况下会全部入栈
层序遍历
题目:
不分行从上往下打印出二叉树的每个节点,同层节点从左至右打印。例如输入{8,6,10,#,#,2,1},如以下图中的示例二叉树,则依次打印8,6,10,2,1(空节点不打印,跳过),请你将打印的结果存放到一个数组里面,返回
思路:
层序遍历,在二叉树中,测序遍历还是很有用的,可以解决很多二叉树的问题,也是广度优先搜索的入门款,一定要掌握!!,最好再练一下 LeetCod
e的:这10题都是关于层序遍历的
上面 LeetCode
的就是属于分行打印的,可以在 acwing
上做:分行层序遍历
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
vector<int> PrintFromTopToBottom(TreeNode* root) {
//层序遍历
vector<int> result;
if(root==nullptr)
{
return {};
}
//队列先进先出
queue<TreeNode* > qu;
qu.push(root);
while(!qu.empty())
{
int size=qu.size();
for(int i=0;i<size;i++)
{
TreeNode* cur=qu.front();
qu.pop();
result.push_back(cur->val);
if(cur->left) qu.push(cur->left);
if(cur->right) qu.push(cur->right);
}
}
return result;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n),二叉树的每个节点遍历一次
空间复杂度:
O
(
n
)
O(n)
O(n),二叉树的每个节点入队列一次
二叉搜索树的后序遍历
题目:
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回 true ,否则返回 false 。假设输入的数组的任意两个数字都互不相同。
数据范围: 节点数量 0 ≤ n ≤ 10000 0 \le n \le 10000 0≤n≤10000 ,节点上的值满足 1 ≤ v a l ≤ 1 0 5 1 \le val \le 10^{5} 1≤val≤105,保证节点上的值各不相同
要求:空间复杂度 O ( n ) O(n) O(n) ,时间复杂度 O ( n 2 ) O(n^2) O(n2)
思路:
先展示一种错误的做法:由于是二叉搜索树,所以将后续遍历的序列排序就可以得到该二叉树中序遍历的序列,我们就可以根据中序遍历和后续遍历的序列得到这个二叉树了,然后由于 二叉搜索树的 中序遍历是唯一的,所以我们再遍历这棵二叉树得到中序遍历序列,通过比较,就可以判断是否是后续遍历结果了,但是这样的做法时间复杂度和空间复杂度都太高了!!!
class Solution {
public:
TreeNode* getTree(vector<int>& inorder,int inBegin,int inEnd,vector<int>& postorder,int postBegin,int postEnd)
{
if(postEnd==postBegin)
{
return nullptr;
}
int rootValue=postorder[postEnd-1];
TreeNode* root=new TreeNode(rootValue);
if(postEnd-postBegin==1)
{
return root;
}
int delIndex;
for(delIndex=inBegin;delIndex<inEnd;delIndex++)
{
if(inorder[delIndex]==rootValue)
{
break;
}
}
int inLeftBegin=inBegin;
int inLeftEnd=delIndex;
int inRightBegin=delIndex+1;
int inRightEnd=inEnd;
int postLeftBegin=postBegin;
int postLeftEnd=postBegin+delIndex-inBegin;
int postRightBegin=postBegin+delIndex-inBegin;
int postRightEnd=postEnd-1;
root->left=getTree(inorder, inLeftBegin, inLeftEnd, postorder, postLeftBegin, postLeftEnd);
root->right=getTree(inorder, inRightBegin, inRightEnd, postorder, postRightBegin, postRightEnd);
return root;
}
vector<int> getInorder(TreeNode* root)
{
vector<int> result;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
result.push_back(cur->val); // 中
cur = cur->right; // 右
}
}
return result;
}
bool VerifySquenceOfBST(vector<int> sequence) {
if(sequence.empty())
{
return false;
}
//中序遍历序列
vector<int> inorder(sequence.begin(),sequence.end());
sort(inorder.begin(), inorder.end());
//根据 中序和 后续遍历构建二叉树,如果无法构建出就说明不是二叉树
// for(int i=0;i<inorder.size();i++)
// {
// cout<<inorder[i]<<"\t";
// }
//构建二叉树
TreeNode* root=getTree(inorder,0,inorder.size(),sequence,0,sequence.size());
//得到构建的二叉树的中序序列
vector<int> getOrder=getInorder(root);
// for(int i=0;i<getOrder.size();i++)
// {
// cout<<getOrder[i]<<"\t";
// }
for(int i=0;i<sequence.size();i++)
{
if(inorder[i]!=getOrder[i])
{
return false;
}
}
return true;
}
};
思路:
我们可以重复利用 二叉搜索树 的性质,得到结果,因为 二叉搜索树 的中序遍历结果是唯一的,而且是递增的,我们知道 该二叉搜索树的 后续遍历序列,就可以快速定位到 根节点——后续遍历的最后一个节点,而且在 二叉搜索树中,所有的左节点都小于根节点(可以理解为,在去掉根节点后,该后续遍历序列一定可以直接分为两部分,前半部分的所有值均小于根节点,后半部分的值均大于根节点),那么我们遍历这个 后续序列,就可以 得到 左子树 和 右子树,再重复这个过程,就可以得到一棵树了
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
if(sequence.empty())
{
return false;
}
return dfs(sequence,0,sequence.size()-1);
}
bool dfs(vector<int>& sequence,int l,int r)
{
//遍历完所有元素就为 true
if(l>=r)
{
return true;
}
int root=sequence[r];
int k=l;
//寻找左子树, 进行分割
while(k<r&&sequence[k]<root)
{
k++;
}
//判断 分割出来的右子树 是否都大于根节点
for(int i=k;i<r;i++)
{
if(sequence[i]<root)
{
return false;
}
}
//只有左右子树都满足条件才可以 要记得去掉根元素
return dfs(sequence, l, k-1)&&dfs(sequence, k, r-1);
}
};
非递归的写法,基本思路是一致的:
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
//非递归写法
if(sequence.empty())
{
return false;
}
int start=0;
int end=sequence.size()-1; //根节点
while(end>=0)
{
//左子树
while(sequence[start]<sequence[end])
{
start++;
}
//右子树
while(sequence[start]>sequence[end])
{
start++;
}
if(start<end)
{
return false;
}
//取到根节点
end--;
start=0;
}
return true;
}
};
之字形遍历二叉树
题目:
给你二叉树的根节点
root
,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)
思路:
在 层序遍历的基础上,控制层序遍历,通过另外设置一个变量,每次通过这个变量来决定是否需要 反转当前的结果,就可以实现 层序遍历的要求了。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
if(root==nullptr)
{
return {};
}
queue<TreeNode*> qu;
qu.push(root);
vector<vector<int>> result;
//控制打印顺序
bool zigzag=false;
while(!qu.empty())
{
vector<int> res;
int size=qu.size();
for(int i=0;i<size;i++)
{
TreeNode* cur=qu.front();
qu.pop();
//放入本层
res.push_back(cur->val);
if(cur->left)
{
qu.push(cur->left);
}
if(cur->right)
{
qu.push(cur->right);
}
}
if(zigzag)
{
reverse(res.begin(),res.end());
}
//取反,确保相邻两次打印顺序不一致
zigzag=!zigzag;
result.push_back(res);
}
return result;
}
};
树中每个节点仅会进队出队一次,所以时间复杂度是 O ( n ) O(n) O(n)。
二叉树中和为某一值II
题目:
输入一颗二叉树的根节点root和一个整数
expectNumber
,找出二叉树中结点值的和为expectNumber
的所有路径。1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
如二叉树root为{10,5,12,4,7},
expectNumber
为22
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yzYGWLQ3-1665395653657)(https://uploadfiles.nowcoder.com/images/20210929/557336_1632915294911/0A4B8F161306A7054899D42C0C6937FD)]则合法路径有[[10,5,7],[10,12]]
数据范围:
树中节点总数在范围 [0, 5000] 内
− 1000 < = 节点值 < = 1000 -1000 <= 节点值 <= 1000 −1000<=节点值<=1000
− 1000 < = e x p e c t N u m b e r < = 1000 -1000 <= expectNumber <= 1000 −1000<=expectNumber<=1000
思路:
主要就是遍历 二叉树的过程,使用 回溯法,或者说是 深度优先遍历。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
//保存最后结果
vector<vector<int>> ans;
//保存路径
vector<int> path;
vector<vector<int>> FindPath(TreeNode* root,int expectNumber) {
dfs(root,expectNumber);
return ans;
}
void dfs(TreeNode* root, int expectNumber)
{
//叶节点
if(root==nullptr)
{
return ;
}
//放入当前值
path.push_back(root->val);
expectNumber-=root->val;
//非叶节点
if(!root->left&&!root->right&&!expectNumber)
{
ans.push_back(path);
}
dfs(root->left,expectNumber);
dfs(root->right,expectNumber);
//恢复现场
path.pop_back();
}
};
时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n)
最后
一起提高,慢慢变强。