【LeetCode 笔记】树

前言

由于树的非线性结构特殊,大多数题目均可采用递归的求解方式。但递归带来的问题就是它的递归栈需要额外的空间,并且在某种程度上也会出现重复访问的情况(子问题重叠)。

参考 写树算法的套路框架 - labuladong


面试题 04.04. 检查平衡性

题目描述

题目描述

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。

解题思路

采用递归的思想:

  • 二叉树平衡,则其每个结点都平衡
  • 递归地检查左子树、右子树,分别计算高度差是否不超过1
#define MAX(a, b) (a > b ? a : b)
#define ABS(a, b) (a > b ? (a - b) : (b - a))

int Height(struct TreeNode* root){
    if(root == NULL)
        return 0;
    return MAX(Height(root->left), Height(root->right)) + 1;
}

bool isBalanced(struct TreeNode* root){
    if(root == NULL)
        return true;	//空树必平衡
    return isBalanced(root->left) 
    	&& isBalanced(root->right) 
    	&& (ABS(Height(root->left), Height(root->right)) <= 1);
}

101. 对称二叉树

题目描述

题目描述

解题思路

算法思路

递归:如果一个树的左、右子树镜像对称,那么这棵树是对称的。则:

  • 它们两个的根节点具有相同的值
  • 每棵树的右子树与另一棵树的左子树镜像对称
bool isMirror(struct TreeNode* p, struct TreeNode* q){
    if(!p && !q){			//都为 NULL
        return true;
    } else if(!p || !q){	//有一个为 NULL
        return false;
    }

    return (p->val == q->val) 
    		&& isMirror(p->left, q->right) 
    		&& isMirror(p->right, q->left);
}

bool isSymmetric(struct TreeNode* root){
	if(!root)
		return true;
    return isMirror(root->left, root->right);
}
  • 时空复杂度 : O ( n ) O(n) O(n)

104. 二叉树的最大深度

题目描述

题目描述

解题思路

递归:

  • 分别查找左、右子树中最大高度
  • 将两者进行比较,较大者再加上根节点的高度即为所答
int maxDepth(struct TreeNode* root){
    if(root == NULL)
        return 0;
        
    int left = maxDepth(root->left);
    int right = maxDepth(root->right);
    
    return (left > right ? left : right) + 1;
}
  • 时空复杂度 : O ( n ) O(n) O(n)

111. 二叉树的最小深度

题目描述

题目描述

解题思路

int minDepth(struct TreeNode* root){
    if(root == NULL) return 0;

    //1.左孩子和右孩子都为空的情况,说明到达了叶子节点,直接返回1即可
    if(root->left == NULL && root->right == NULL)
    	return 1;
    
    //2.如果左孩子和由孩子其中一个为空,那么需要返回比较大的那个孩子的深度        
    int min1 = minDepth(root->left);
    int min2 = minDepth(root->right);
    //这里其中一个节点为空,说明min1和min2有一个必然为0,所以可以返回min1 + min2 + 1;
    if(root->left == NULL || root->right == NULL)
    	return min1 + min2 + 1;
    
    //3. 左右孩子都不为空,返回最小深度+1即可
    return (min1 < min2 ? min1 : min2) + 1; 
}

112. 路径总和

题目描述

路径总和

解题思路

  • 递归

观察要求我们完成的函数,我们可以归纳出它的功能:询问是否存在从当前节点 root 到叶子节点的路径,满足其路径和为 sum。

假定从根节点到当前节点的值之和为 val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val。

不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val 即可(因为路径和已经确定,就是当前节点的值,我们只需要判断该路径和是否满足条件)。若当前节点不是叶子节点,我们只需要递归地询问它的子节点是否能满足条件即可。

bool hasPathSum(struct TreeNode* root, int sum){
    if(root == NULL)
        return false;
    if(root->left == NULL && root->right == NULL)
        return sum == root->val;
    return hasPathSum(root->left, sum - root->val) 
        || hasPathSum(root->right, sum - root->val);
}

时间复杂度: O ( N ) O(N) O(N),其中 N N N 是树的节点数。对每个节点访问一次。
空间复杂度: O ( H ) O(H) O(H),其中 H H H 是树的高度。空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O ( N ) O(N) O(N)。平均情况下树的高度与节点数的对数正相关,空间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)


124. 二叉树中的最大路径和

题目描述1
题目描述2
使用函数maxGain计算每个节点及其后继的贡献值,当且仅当该节点的值为正时,才将其计入最大路径和

  • 空节点的最大贡献值等于 0
  • 非空节点的最大贡献值等于节点值与其子节点中的最大贡献值之和(对于叶节点而言,最大贡献值等于节点值)
int max_sum;

int maxGain(struct TreeNode* root){
    if(root == NULL)
        return 0;
    // 只有在最大贡献值大于 0 时,才会选取对应子节点
    int leftGain = fmax(maxGain(root->left), 0);
    int rightGain = fmax(maxGain(root->right), 0);
    
    int current = leftGain + rightGain + root->val;
    max_sum = fmax(current, max_sum);

    return root->val + fmax(leftGain, rightGain);
}

int maxPathSum(struct TreeNode* root){
    max_sum = INT_MIN;
    maxGain(root);
    return max_sum;
}

226. 翻转二叉树

题目描述

题目描述

解题思路

  • 直接采用递归
  • 分别将每个子树中的左和右孩子的值进行交换
struct TreeNode* invertTree(struct TreeNode* root){
    if(root == NULL)
        return root;

    struct TreeNode* temp = root->left;
    root->left = root->right;
    root->right = temp;

    invertTree(root->left);
    invertTree(root->right);

    return root;
}

235. 二叉搜索树的最近公共祖先

题目描述

题目描述

解题思路

  • 搜索左、右子树 —— 空,则没找到;非空,则找到
struct TreeNode* lowestCommonAncestor(struct TreeNode* root, struct TreeNode* p, struct TreeNode* q) {
    if(root == NULL)
        return root;
    
    // 找到了p或q,将其返回
    if(root == p || root == q)
        return root;

    // 在左子树中找
    struct TreeNode* left = lowestCommonAncestor(root->left, p, q);
    // 在右子树中找
    struct TreeNode* right = lowestCommonAncestor(root->right, p, q);

    if(left != NULL && right !=NULL)    // 左右都不空,则p、q存在当前根结点的子树中,此时返回root
        return root;
    else if(left != NULL)   // 左不空,则在当前根节点的左子树中找到了p或q结点
        return left;
    else if(right != NULL)  // 右不空,则在当前根节点的右子树中找到了p或q结点
        return right;
    
    return NULL;            // 左右均空,则p或q不在当前根节点的子树中
}

450. 删除二叉搜索树中的节点

题目描述

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。


一般来说,删除节点可分为两个步骤:

  • 首先找到需要删除的节点;
  • 如果找到了,删除它。

在这里插入图片描述

解题思路

写树算法的套路 - labuladong

  • 情况 1:待删除结点 A 恰好是叶子结点,两后继都为空,可直接删除
  • 情况 2:A 只有一个非空子结点,那么它要让的孩子接替自己的位置。图片来自LeetCode
  • 情况 3:A 的两个孩子都不为空,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。图片来自LeetCode
if (root->left && root->right) {
    // 找到右子树的最小节点
    struct TreeNode* minNode = getMin(root->right);
    // 把 root 改成 minNode
    root->val = minNode->val;
    // 转而去删除 minNode
    root->right = deleteNode(root->right, minNode->val);
}

总代码:

struct TreeNode* getMin(struct TreeNode* node) {
    // BST 最左边的就是最小的
    while (node->left != NULL) node = node->left;
    return node;
} 

struct TreeNode* deleteNode(struct TreeNode* root, int key){
    if(root == NULL)
        return root;
    if(root->val == key){
        if (root->left == NULL) return root->right;
        if (root->right == NULL) return root->left;
        struct TreeNode* minNode = getMin(root->right);
        root->val = minNode->val;
        root->right = deleteNode(root->right, minNode->val);
    }
    else if(root->val < key)
        root->right = deleteNode(root->right, key);
    else
        root->left = deleteNode(root->left, key);
    return root;
}

897. 递增顺序查找树

题目描述

给你一个树,请你 按中序遍历 重新排列树,使树中最左边的结点现在是树的根,并且每个结点没有左子结点,只有一个右子结点。


提示:
给定树中的结点数介于 1 和 100 之间。
每个结点都有一个从 0 到 1000 范围内的唯一整数值。
示例

解题思路

struct TreeNode* dfs(struct TreeNode* root, struct TreeNode* pre){
	// 摘自 @rebellious_robot
    // 第二个参数是父节点,相当于中序遍历,
    
    if(!root)
        return pre;
        
    struct TreeNode* head = dfs(root->left, root);
    // 递归到最左节点,如果没有右子树,最左节点指向它的父节点,
    root->left = NULL;
    
	// 然后回溯重复这个过程,如果存在右子树,把父节点传进去,由右子树去生成。
    if(root->right)
        root->right = dfs(root->right, pre);
    else
        root->right = pre;
        
	// head相当于子树的最小节点,也就是链表头。
    return head;
}

struct TreeNode* increasingBST(struct TreeNode* root){
    return dfs(root, NULL);
}

剑指 Offer 26. 树的子结构

题目描述

在这里插入图片描述

解题思路

分成两步骤:

  1. 在树A中找与B的根结点值相同的结点R
    调用isSubStructure遍历树A,若发现B的根结点值,则转到第二步判断两个结点子结构;
  2. R中是否包含与B根结点相同的子结构(递归):
    终止条件: 当遍历到之前的结点值均相同,且最后子树为空时,
    · 父树若也是空,则说明两棵树完全一样;
    · 若父结点非空,则子树是父树的一部分。
bool doesTree1HaveTree2(struct TreeNode* t1, struct TreeNode* t2){
    if(t2 == NULL)		//t2遍历全部遍历完成都能对应上,则返回true
        return true;
    if(t1 == NULL)		//t2遍历完了,而t1还剩余,则说明B不是A的子结构
        return false;
    if(t1->val != t2->val)	//如果其中有一个点对应不上,则B不是A的子结构
        return false;
       
    //如果根结点对应得上,那么分别去左、右子结点里面匹配
    return doesTree1HasTree2(t1->left, t2->left) && doesTree1HasTree2(t1->right, t2->right);
}

bool isSubStructure(struct TreeNode* A, struct TreeNode* B){
    bool result = false;
	
	//当 A 和 B 都不空时,才进行比较;否则直接返回 false
    if(A != NULL && B != NULL){
        if(A->val == B->val)	//找到A中对应B的根结点 R
            result = doesTree1HasTree2(A, B);	//以该结点R作为起点,判断子树中是否包含B的子树
        if(!result)				//找不到,则以A->left作为起点
            result = isSubStructure(A->left, B);
        if(!result)				//找不到,则以A->right作为起点
            result = isSubStructure(A->right, B);
    }

    return result;
}

剑指 Offer 54. 二叉搜索树的第k大节点

题目描述

在这里插入图片描述

解题思路

//全局res记录第k个结点的值,count用于遍历的倒计数
int res, count;

void InOrder(struct TreeNode* root){
    if(root == NULL)
        return;
    InOrder(root->right);   //中序遍历右子树
    if(count == 0)
        return;             //第k个直接返回
    if(--count == 0)
        res = root->val;    //count减0时,记录返回值
    InOrder(root->left);    //中序遍历左子树
}

int kthLargest(struct TreeNode* root, int k){
    count = k;
    InOrder(root);
    return res;
}
  • 时间复杂度: O ( n ) O(n) O(n)

94. 二叉树的中序遍历

  1. 递归

    void inorder(struct TreeNode* root, int* res, int* resSize) {
        if (!root) 
            return;
            
        inorder(root->left, res, resSize);
        res[(*resSize)++] = root->val;
        inorder(root->right, res, resSize);
    }
    
    int* inorderTraversal(struct TreeNode* root, int* returnSize) {
        int* res = malloc(sizeof(int) * 501);
        *returnSize = 0;
        inorder(root, res, returnSize);
        return res;
    }
    
  2. int* inorderTraversal(struct TreeNode* root, int* returnSize) {
        *returnSize = 0;
        int* res = malloc(sizeof(int) * 501);
        struct TreeNode** stk = malloc(sizeof(struct TreeNode*) * 501);
        int top = 0;
        while (root != NULL || top > 0) {
            while (root != NULL) {
                stk[top++] = root;
                root = root->left;
            }
            root = stk[--top];
            res[(*returnSize)++] = root->val;
            root = root->right;
        }
        return res;
    }
    

以上两种方法时空复杂度均为 O ( n ) O(n) O(n)


530. 二叉搜索树的最小绝对差

题目描述

题目描述

解题思路

  • 中序遍历二叉搜索树得到一个递增序列
  • 对升序数组 a 求任意两个元素之差的绝对值的最小值,答案一定为相邻两个元素之差的最小值
void dfs(struct TreeNode* root, int* pre, int* ans){
    if(root == NULL)
        return;

    //中序遍历,得到递增序列,找到相邻两项差值最小的即为答案
    dfs(root->left, pre, ans);

    if(*pre == -1)  //初始遍历,pre指向根节点
        *pre = root->val;
    else {
        *ans = fmin(*ans, root->val - (*pre));
        *pre = root->val;
    }

    dfs(root->right, pre, ans);
}

int getMinimumDifference(struct TreeNode* root){
    int ans = INT_MAX, pre = -1;
    dfs(root, &pre, &ans);
    return ans;
}

注:此题也可以将中序遍历后的递增序列存储到数组中,再遍历数组求最小差

时空复杂度: O ( n ) O(n) O(n)


450. 删除二叉搜索树中的节点

题目描述

在这里插入图片描述在这里插入图片描述

解题思路

【递归】根据二叉搜索树的性质来分情况讨论:

  • (极端情况)空树:return null
  • key 小于 root.val =>keyroot.right
    • => 递归进入 root.right
    • 将递归(删除后)的结果作为 root 的右子树
  • key 大于 root.val => keyroot.left
    • => 递归进入 root.left
    • 将递归(删除后)的结果作为 root 的左子树
  • key 等于 root.val => 调整删除 root 后的子树
    • 若root就是叶子 => 直接返回null
    • root 只有right => 返回root.right
    • root 只有left => 返回root.left
    • root 有左右子树:
      • 由于 root 的左子树的val全部比root小,root右子树的val全部比root大,那么 将右子树中的最小值rightMin替换root (rightMin大于root.left,且小于root.right 自身除外)
      • 首先找到 rightMin,将 root 的值替换,然后再删除rightMin节点(相当于交换后删除)
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return null;
        if (root.val == key) {
            // 这两个 if 把情况 1 和 2 都正确处理了
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            // 处理情况 3
            TreeNode minNode = getMin(root.right);
            root.val = minNode.val;
            root.right = deleteNode(root.right, minNode.val);
        } else if (root.val > key) {
            root.left = deleteNode(root.left, key);
        } else if (root.val < key) {
            root.right = deleteNode(root.right, key);
        }
        return root;
    }

    public TreeNode getMin(TreeNode node) {
        // BST 最左边的就是最小的
        while (node.left != null) node = node.left;
        return node;
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Beta Lemon

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值