LeetCode 二叉树问题
@
目录
构建二叉查找树
对于每个父节点,其左子树中所有节点的值小于等于父结点的值,其右子树中所有节点的值大于等于父结点的值,因此对于一个二叉查找树,我们可以在$O(nlogn)$ 的时间内查找一个值是否存在:从根节点开始,若查找值小于当前节点的值则向左下走,若查找值大于当前节点的值则向右下走。
二叉查找树的中序遍历结果即为排序好的数组
- 二叉查找树的实现:
- 清空操作:
- 当前节点为空:返回空指针
- 当前节点不为空:递归清空左、右子树,删除当前节点,返回空指针
- 插入操作:
- 当前节点为空:直接插入元素,并令其左、右子节点为空
- 当前节点不为空:
- 插入值小于当前节点的值:递归插入左子树
- 插入值大于当前节点的值:递归插入右子树
- 返回当前节点
- 查找操作:
- 当前节点为空: 返回空指针
- 当前节点不为空:
- 查找值小于当前节点的值:返回递归查找左子树的结果
- 查找值大于当前节点的值:返回递归查找右子树的结果
- 查找值等于当前节点的值:返回当前节点
- 删除操作:
- 当前节点为空:返回空指针
- 当前节点不为空:
- 删除值小于当前节点的值:令左子树等于递归删除左子树的结果
- 删除值大于当前节点的值:令右子树等于递归删除右子树的结果
- 删除值等于当前节点的值:
- 左、右子节点不为空:找到右子树的最小值,令当前节点的值等于该最小值,令右子树等于递归删除右子树最小值节点的结果
- 左、右子节点至少一个为空:左子树为空,令当前节点等于右子树;右子树为空,令当前节点等于左子树
- 清空操作:
template <class T>
class BST {
struct Node {
T data;
Node* left;
Node* right;
};
Node* root;
Node* makeEmpty(Node* t) { // 清空操作
if (t == NULL)
return NULL;
makeEmpty(t->left);
makeEmpty(t->right);
delete t;
return NULL;
}
Node* insert(Node* t, T x) { // 插入操作
if (t == NULL) {
t = new Node;
t->data = x;
t->left = t->right = NULL;
} else if (x < t->data) {
t->left = insert(t->left, x);
} else if (x > t->data) {
t->right = insert(t->right, x);
}
return t;
}
Node* find(Node* t, T x) { // 查找操作
if (t == NULL)
return NULL;
if (x < t->data)
return find(t->left, x);
if (x > t->data)
return find(t->right, x);
return t;
}
Node* findMin(Node* t) {
if (t == NULL || t->left == NULL)
return t;
return findMin(t->left);
}
Node* findMax(Node* t) {
if (t == NULL || t->right == NULL)
return t;
return findMax(t->right);
}
Node* remove(Node* t, T x) { // 删除操作
Node* temp;
if (t == NULL)
return NULL;
else if (x < t->data)
t->left = remove(t->left, x);
else if (x > t->data)
t->right = remove(t->right, x);
else if (t->left && t->right) {
temp = findMin(t->right);
t->data = temp->data;
t->right = remove(t->right, t->data);
} else {
temp = t;
if (t->left == NULL)
t = t->right;
else if (t->right == NULL)
t = t->left;
delete temp;
}
return t;
}
public:
BST()
: root(NULL) {}
~BST() {
root = makeEmpty(root);
}
void insert(T x) {
insert(root, x);
}
void remove(T x) {
remove(root, x);
}
};
1. 遍历问题
105. 从前序与中序遍历序列构造二叉树
给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
- 思路分析:
- preorder 的第一个数一定是整个二叉树的根节点
- preorder 的第一个数在 inorder 中的索引可以将 inorder 分为左右两部分,分别为左右子树
- 左右子树继续根据性质递归划分
- 具体实现:
- 建立哈希表存储 inorder 中节点值与其索引的映射
- 假设根节点的在 inorder 的索引为 index
- 左子树的节点数为 leftLen = (index - 1) - s0 + 1
- 左子树的根节点在前序遍历数组的索引为 s1 + 1
- 右子树的根节点在前序遍历数组的索引为 s1 + leftLen + 1
- 参数说明:
- s0 树在中序遍历的起始索引
- e0 树在中序遍历的终止索引
- s1 树的根节点在前序遍历数组的索引
- s0、e0为子树在中序遍历结果中的起止索引,s1为子树根节点在前序遍历中的索引,前序遍历只起到提供子树根节点值的作用
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.empty()) {
return nullptr;
}
unordered_map<int, int> hash;
// 建立中序遍历结果中节点值与索引的哈希映射
for (int i = 0; i < preorder.size(); i++) {
hash[inorder[i]] = i;
}
return helper(hash, preorder, 0, preorder.size() - 1, 0);
}
TreeNode* helper(unordered_map<int, int>& hash, vector<int>& preorder, int s0, int e0, int s1) {
if (s0 > e0) {
return nullptr;
}
int mid = preorder[s1], index = hash[mid], leftLen = index - 1 - s0 + 1;
TreeNode *node = new TreeNode(mid);
node->left = helper(hash, preorder, s0, index - 1, s1 + 1); // 构建左子树
node->right = helper(hash, preorder, index + 1, e0, s1 + leftLen + 1); // 构建右子树
return node;
}
};
- 简洁实现法
- 通过构建类的变量存储哈希映射
- 根据前序遍历结果确定根节点的索引 root
- 根据根节点索引 root 以及中序遍历的哈希映射,确定树的左、右边界 left right
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
this->preorder = preorder;
for (int i = 0; i < inorder.size(); i++) {
hash[inorder[i]] = i;
}
return helper(0, 0, inorder.size() - 1);
}
private:
vector<int> preorder;
unordered_map<int, int> hash;
TreeNode* helper(int root, int left, int right) {
if (left > right) {
return nullptr;
}
int rootval = preorder[root];
int m = hash[rootval]; // 根节点在中序遍历的索引
TreeNode* node = new TreeNode(rootval);
node->left = helper(root + 1, left, m - 1); // 左子树的左边界一直为 left = 0
node->right = helper(root + m - left + 1, m + 1, right); // 右子树的右边界一直味 right = size() - 1
return node;
}
};
124. 二叉树中的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个节点,且不一定经过根节点。路径和 是路径中各节点值的总和。Link
- 递归求解
- 当前节点的最大值为 val + leftGain + rightGain
- 当前节点向上一节点的返回值为 val + max(leftGain,rightGain)
- 因为同一节点不能经过两次,因此必须选择当前节点左、右子树中较大的一个进行返回
- 例如,节点 11 的最大值为 20,但其向上返回的最大值为 7 + 11 = 18,因此一个节点只能出现一次,而要同时访问 7 2 必须经过 11 两次
class Solution {
private:
int maxSum = INT_MIN;
public:
int maxPathSum(TreeNode* root) {
maxGain(root);
return maxSum;
}
int maxGain(TreeNode* root) {
if (!root) return 0;
int leftGain = max(maxGain(root->left), 0);
int rightGain = max(maxGain(root->right), 0);
int innerMax = root->val + leftGain + rightGain; // 以当前节点为根节点的最大值
maxSum = max(maxSum, innerMax);
return root->val + max(leftGain, rightGain); // 经过当前节点的最大增益
}
};
2. 二叉查找树
99. 恢复二叉搜索树
给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。
- 递归思想:
- 终止条件:节点为空
- 递归操作:
- 中序遍历:设一个指针指向中序遍历的前一个节点,与当前节点的值做对比,如果小于前一个节点,说明排序错误
- 技巧: 整个遍历过程只出现一次排序错误,交换两个相邻节点;出现两次排序错误,交换这两个节点
- 节点为空时不进行操作直接返回上一级递归
class Solution {
public:
void recoverTree(TreeNode* root) {
TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;
inorder(root, mistake1, mistake2, prev);
if (mistake1 && mistake2) {
// 将错误节点的值暂存
int temp = mistake1->val;
mistake1->val = mistake2->val;
mistake2->val = temp;
}
}
void inorder(TreeNode* root, TreeNode* &mistake1, TreeNode* &mistake2, TreeNode* &prev) {
if (!root) {
return;
}
if (root->left) {
inorder(root->left, mistake1, mistake2, prev); // 中序遍历左子节点
}
if (prev && root->val < prev->val) {
if (!mistake1) {
mistake1 = prev; // 一次排序错误
mistake2 = root;
}else {
mistake2 = root; // 两次排序错误
}
}
prev = root; // 访问根节点
if (root->right) {
inorder(root->right, mistake1, mistake2, prev); // 中序遍历右子节点
}
}
};
669. 修剪二叉搜索树
给你二叉搜索树的根节点 root ,同时给定最小边界 low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。
- 递归思想:
- 终止条件:当前节点为空
- 递归操作:
- 当前节点小于 low,返回其右子树的修剪结果
- 当前节点大于 high,返回其左子树的修剪结果
- 当前节点值在[low, high]中,左、右子节点等于其对应的修剪结果
- 返回值:根据当前节点与边界值的大小返回对应的节点指针
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
// 终止条件
if (!root) {
return root;
}
// 当前节点值大于 high,则右子树均不满足条件
if (root->val > high) {
return trimBST(root->left, low, high);
}
if (root->val < low) {
return trimBST(root->right, low, high);
}
// 当前节点值在 [low, high]中
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
3. 字典树
208. 实现 Trie (前缀树)
尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。
- 一个字符相当于一个字典树的节点,其含有 26 个子节点指针指向下一个字符
- 单词最后一个字符的节点,设置一个 true 标志着单词的结束
- 不同单词前 N 个相同的字符可以共用该字符节点
- 直接利用类建立前缀树
- 1、建立一个指针指向当前节点
- 2、根据指针查找单词是否存在
- 3、根据查找情况建立新的节点
- 4、将指针指向下一个字符节点
class Trie {
public:
bool isEnd;
vector<Trie *> next;
Trie() {
isEnd = false;
for(int i = 0; i < 26; i++) {
next.push_back(nullptr);
}
}
void insert(string word) {
Trie *cur = this; // 重要位置
for (char &c : word) {
if (cur->next[c - 'a'] == nullptr) {
cur->next[c - 'a'] = new Trie();
}
cur = cur->next[c - 'a'];
}
cur->isEnd = true;
}
bool search(string word) {
Trie *cur = this;
for (char &c : word) {
if (cur->next[c - 'a'] == nullptr) {
return false;
}
cur = cur->next[c - 'a'];
}
return cur->isEnd;
}
bool startsWith(string prefix) {
Trie *cur = this;
for (char &c : prefix) {
if (cur->next[c - 'a'] == nullptr) {
return false;
}
cur = cur->next[c - 'a'];
}
return true;
}
};