二叉树
在leetcode上,树的默认数据结构是
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
和链表相比也就是多一个指针
1.求二叉树的最大深度
思路:使用深度优先搜索,二叉树最大的深度有公式表达也就是max(l,r)+1
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0; //递归终止条件
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
不过写两行太复杂了,该用户很懒,只写了一行代码
int maxDepth(TreeNode* root) {
return root?1 + max(maxDepth(root->left),maxDepth(root->right)):0;
}
时间复杂度O(n),空间复杂度O(h) 其中 h 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(n)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(log n)。
2.求二叉树最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量,叶子节点是没有左子树和右子树的节点
思路:自顶向下递归如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。
反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。
最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。
int minDepth(TreeNode* root) {
if (root == nullptr) {
return 0;
}
if (root->left == nullptr && root->right == nullptr) {
return 1;
}
if(root->left == nullptr){
return minDepth(root->right)+1;
}
if(root->right == nullptr){
return minDepth(root->left)+1;
}
return min(minDepth(root->left), minDepth(root->right)) + 1;
时间复杂度O(n),空间复杂度O(h)
3.判断二叉树是否对称
思路:
如何判断一棵树是否对称?答案:如果节点为空那么对称,如果节点不为空,那么他的左子树和右子树对称的话,他对称。
那么他的左子树和右子树对不对称要怎么判断?答案:如果左树的左孩子节点和右子树的右孩子结点一样并且左子树的右孩子结点和右子树的左孩子节点一样的话,那么左右子树对称
bool isSymmetric(TreeNode* root) {
return root ? check(root->left, root->right) : true;
}
bool check(TreeNode* left, TreeNode* right) {
if (left == nullptr && right == nullptr) {//左和右都为空
return true;
}
else if (left == nullptr || right == nullptr) {//左和右只有一个为空
return false;
}
else if (left->val != right->val) {//左和右的值不一样
return false;
}
return check(left->left, right->right) && check(left->right, right->left);//继续往下递归判断当前左子树的左孩子节点和右子树和右孩子节点,左子树的右孩子节点和右子树的左孩子节点
}
时间复杂度O(n),空间复杂度O(h)
4.判断二叉树是否相同
沿用上一题模板解题即可
bool isSameTree(TreeNode* p, TreeNode* q) {
if(p == nullptr && q == nullptr){
return true;
}
else if(p == nullptr || q == nullptr){
return false;
}
else if(p->val != q->val){
return false;
}
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
时间复杂度O(n),空间复杂度O(h)
5.层序遍历
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if (root == nullptr) return result;
queue<TreeNode*> q;
q.push(root);//先把根节点加入队列
while (!q.empty()) {
int size = q.size();//因为队列的大小会动态改变,所以要先求取大小
vector<int> res;
for (int i = 0; i < size; ++i) {
TreeNode* node = q.front();//根节点出队
q.pop();
res.push_back(node->val);
if (node->left) q.push(node->left);//左右节点分别入队
if (node->right) q.push(node->right);
}
result.push_back(res);
}
return result;
}
时间复杂度O(n),空间复杂度O(h)
6.二叉树的遍历
二叉树的先序遍历-根左右,中序遍历-左根右,后序遍历-左右根
(1)递归法
前序遍历:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
if(root == NULL)
return ans;
ans.push_back(root->val);//根
preorderTraversal(root->left);/左
preorderTraversal(root->right);//右
return ans;
}
中序遍历:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
if(root == NULL)
return ans;
preorderTraversal(root->left);/左
ans.push_back(root->val);//根
preorderTraversal(root->right);//右
return ans;
}
后序遍历:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
if(root == NULL)
return ans;
preorderTraversal(root->left);/左
preorderTraversal(root->right);//右
ans.push_back(root->val);//根
return ans;
}
所以递归法实现树的前序中序后序遍历的时候只要根据情况调整根,左,右操作的代码位置即可
时间复杂度O(n),空间复杂度O(h)
(2)迭代法:
迭代法的实现就是模拟递归的过程,因为递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因
前序遍历是根左右,每次先处理的是根节点,那么先将跟节点放入栈中,保存根节点,根节点出栈,把然后将右孩子加入栈,再加入左孩子。
前序遍历:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (!root) return res;
stack<TreeNode*> s;
s.push(root);
while (!s.empty()) {
int size = s.size();
for (int i = 0; i < size; ++i) {
TreeNode* node = s.top();
s.pop();
res.push_back(node->val);
if (node->right) s.push(node->right);
if (node->left) s.push(node->left);
}
}
return res;
}
这里的代码和层序遍历基本上差不多,只不过层序遍历使用队列,前序遍历使用栈。层序遍历按照左右的顺序存储节点,前序遍历按照右左的顺序保存节点
中序遍历:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
while (root != nullptr || !s.empty()) {
while (root != nullptr) { //用指针来访问节点,到最底层
s.push(root); //将访问的节点入栈
root = root->left; //左
}
root = s.top(); //从栈顶弹出的数据就是要处理的数据
s.pop();
res.push_back(root->val); //中
root = root->right; //右
}
return res;
}
后序遍历:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
if (!root) return res;
stack<TreeNode*> s;
s.push(root);
while (!s.empty()) {
int size = s.size();
for (int i = 0; i < size; ++i) {
TreeNode* node = s.top();
s.pop();
res.push_back(node->val);
if (node->left) s.push(node->left); //跟前序遍历相反的入栈顺序
if (node->right) s.push(node->right);
}
}
reverse(res.begin(), res.end());
return res;
}
后序遍历的代码对比先序遍历其实只改变入栈顺序,得到的结果是根右左,然后把结果集反转以下就是左右根了
在递归解法中,可以调整代码的顺序,但是在迭代算法中中序算法并不能通过改变代码顺序来实现的原因是在先序和后序的过程中都是先访问的元素是中间节点,要处理的元素也是中间节点,也就是访问和处理的顺序是一样的,但在中序遍历时按照左根右的顺序,导致了访问和处理的顺序不一致,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
如果想用迭代法像递归一样有个模板的话方便记忆的话也不是不可以,只是不太好理解
//前序遍历:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
}
else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
//中序遍历:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
}
else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
//后序遍历:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
}
else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
时间复杂度O(n),空间复杂度O(h)
7.求取二叉树每层节点的平均数
也就是层序遍历,做一个求和和除法运算即可
vector<double> averageOfLevels(TreeNode* root) {
vector<double> result;
if (!root) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int size = q.size();
double sum = 0;
for (int i = 0; i < size; ++i) {
TreeNode* node = q.front();
q.pop();
sum += node->val;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
double res = sum / size;
result.push_back(res);
}
return result;
}
时间复杂度O(n),空间复杂度O(h)
8.判断二叉树是否平衡
一棵高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1
思路:和求取二叉树深度差不多,只不过在每层多加一个高度绝对值的判断即可
bool isBalanced(TreeNode* root) {
return calDepth(root) != -1;
}
int calDepth(TreeNode* root) {
if (!root) return 0;
int left = calDepth(root->left);
int right = calDepth(root->right);
if (left == -1 || right == -1 || abs(left - right) > 1) {
return -1;
}
return 1 + max(left, right);
}
时间复杂度O(n),空间复杂度O(h)
9.求二叉树最长的直径
求一个二叉树的最长直径。直径的定义是二叉树上任意两节点之间的无向距离
思路:和递归求最大深度一样,对于本题需要求出每个节点的最大的左+最大的右,然后和存储的最大的值作比较在遍历完一遍二叉树后就可以得到结果,在递归到上一层时返回当前节点左子树和右子树的最大值即可
int diameterOfBinaryTree(TreeNode* root) {
int res = 0;
calMaxDfs(root, res);
return res;
}
int calMaxDfs(TreeNode* root, int& res) {
if (!root) return 0;
int left = calMaxDfs(root->left, res);
int right = calMaxDfs(root->right, res);
res = max(left + right, res); //更新全局变量
return max(left, right) + 1;//返回路径中的较大者
}
时间复杂度O(n),空间复杂度O(h)