前言
由于树的非线性结构特殊,大多数题目均可采用递归的求解方式。但递归带来的问题就是它的递归栈需要额外的空间,并且在某种程度上也会出现重复访问的情况(子问题重叠)。
面试题 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. 二叉树中的最大路径和
使用函数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 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
解题思路
- 情况 1:待删除结点 A 恰好是叶子结点,两后继都为空,可直接删除
- 情况 2:A 只有一个非空子结点,那么它要让的孩子接替自己的位置。
- 情况 3:A 的两个孩子都不为空,为了不破坏 BST 的性质,A 必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。
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. 树的子结构
题目描述
解题思路
分成两步骤:
- 在树
A
中找与B
的根结点值相同的结点R
:
调用isSubStructure
遍历树A
,若发现B的根结点值,则转到第二步判断两个结点子结构; 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大节点
题目描述
解题思路
- RNL逆中序遍历
官方题解
//全局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. 二叉树的中序遍历
-
递归
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; }
-
栈
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
=>key
在root.right
- => 递归进入
root.right
- 将递归(删除后)的结果作为 root 的右子树
- => 递归进入
key
大于root.val
=>key
在root.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节点(相当于交换后删除)
- 由于 root 的左子树的val全部比root小,root右子树的val全部比root大,那么 将右子树中的最小值
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;
}
}