二叉搜索树(Binary Search Tree, BST),也称为二叉排序树,是一种重要的数据结构。它将树形结构的灵活性与有序性结合起来,使得查找、插入和删除等操作的平均时间复杂度都能达到 O(logN)。
二分搜索算法,其底层逻辑恰好对应在一棵隐形的二叉搜索树上的查找过程。例如,对有序数组 [0, 5, 24, 34, 41, 58, 62, 64, 67, 69, 78]
进行二分搜索,其过程完全可以可视化为在一棵以 58
(中间值)为根的BST上进行查找。
基本概念:
一棵二叉树要成为BST,必须满足以下条件:
对于树中的任意一个节点,其左子树中所有节点的值都小于该节点的值,其右子树中所有节点的值都大于该节点的值。
这个性质保证了树的有序性。对一棵BST进行中序遍历,可以得到一个严格递增的有序序列。
对于一棵理想的平衡二叉树,其层数(高度)L
和节点总数 N
之间存在如下关系:
N=20+21+22+⋯+2(L−1)=N
2^L - 1 = N
2^L = N+1
由此可得:
L≈log(N+1)
这就是为什么BST相关操作的时间复杂度是 O(logN) 的原因——操作的路径长度基本不超过树的高度。
节点结构:
#include
using namespace std;
// 定义树的节点
struct Node {
int data;
Node* left;
Node* right;
// 构造函数
Node(int val) : data(val), left(nullptr), right(nullptr) {}
};
n_insert
逻辑:
root
是nullptr
),新节点就是根节点。parent
指针记录当前节点的父节点。nullptr
位置时,该位置就是新节点的插入点。根据新值与parent
值的大小关系,将其链接到parent
的left
或right
。// 非递归插入操作
void n_insert(Node*& root, int val) {
if (root == nullptr) {
root = new Node(val);
return;
}
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr) {
parent = cur;
if (val < cur->data) {
cur = cur->left;
} else if (val > cur->data) {
cur = cur->right;
} else {
return; // 树中已存在相同的值,不插入
}
}
// 循环结束后,parent指向要插入位置的父节点
if (val < parent->data) {
parent->left = new Node(val);
} else {
parent->right = new Node(val);
}
}
r_insert
逻辑:
nullptr
,说明找到了插入位置,创建新节点并返回。node->left
。node->right
。// 递归插入操作
Node* r_insert(Node* node, int val) {
if (node == nullptr) {
return new Node(val); // 找到插入位置,返回新节点
}
if (val < node->data) {
node->left = r_insert(node->left, val);
} else if (val > node->data) {
node->right = r_insert(node->right, val);
}
// 如果 val == node->data,什么都不做,直接返回原节点
return node;
}
n_query
逻辑:从根节点开始,根据值的大小关系,向左或向右移动,直到找到匹配节点或遇到nullptr
。
// 非递归查询操作
bool n_query(Node* root, int val) {
Node* cur = root;
while (cur != nullptr) {
if (val == cur->data) {
return true; // 找到了
} else if (val < cur->data) {
cur = cur->left;
} else {
cur = cur->right;
}
}
return false; // 遍历结束没找到
}
r_query
逻辑:
nullptr
,说明没找到;如果当前节点值匹配,说明找到了。// 递归查询操作
bool r_query(Node* node, int val) {
if (node == nullptr) {
return false;
}
if (val == node->data) {
return true;
} else if (val < node->data) {
return r_query(node->left, val);
} else {
return r_query(node->right, val);
}
}
删除是BST中最复杂的操作。
逻辑:
首先找到要删除的节点cur
及其父节点parent
。
nullptr
。n_remove
// 非递归删除操作
void n_remove(Node*& root, int val) {
if (root == nullptr) return;
Node* cur = root;
Node* parent = nullptr;
// 1. 找到要删除的节点(cur)及其父节点(parent)
while (cur != nullptr && cur->data != val) {
parent = cur;
if (val < cur->data) {
cur = cur->left;
} else {
cur = cur->right;
}
}
if (cur == nullptr) return; // 没找到要删除的节点
// 2. 如果要删除的节点有两个孩子 (情况三)
if (cur->left != nullptr && cur->right != nullptr) {
// 找前驱节点(左子树的最大节点)
Node* pre = cur->left;
Node* pre_parent = cur;
while (pre->right != nullptr) {
pre_parent = pre;
pre = pre->right;
}
// 用前驱节点的值覆盖当前节点
cur->data = pre->data;
// 现在问题转化为删除前驱节点pre
cur = pre;
parent = pre_parent;
}
// 3. 处理情况一和情况二 (cur最多只有一个孩子)
Node* child = nullptr;
if (cur->left != nullptr) {
child = cur->left;
} else {
child = cur->right;
}
if (parent == nullptr) { // 删除的是根节点
root = child;
} else if (parent->left == cur) {
parent->left = child;
} else {
parent->right = child;
}
delete cur;
}
r_remove
逻辑:函数的返回值是删除操作后子树的新根。
node
为nullptr
,直接返回nullptr
。val
不等于当前节点值,则在左子树或右子树递归删除,并更新node->left
或node->right
。val == node->data
):
// 递归删除操作
Node* r_remove(Node* node, int val) {
if (node == nullptr) {
return nullptr;
}
if (val < node->data) {
node->left = r_remove(node->left, val);
} else if (val > node->data) {
node->right = r_remove(node->right, val);
} else { // 找到了要删除的节点
// 情况3: 有两个孩子
if (node->left != nullptr && node->right != nullptr) {
Node* pre = node->left;
while (pre->right != nullptr) {
pre = pre->right; // 找到前驱节点
}
node->data = pre->data; // 用前驱的值覆盖
node->left = r_remove(node->left, pre->data); // 在左子树中删除前驱
} else { // 情况1或2: 零个或一个孩子
Node* temp = node;
if (node->left != nullptr) {
node = node->left;
} else {
node = node->right;
}
delete temp;
}
}
return node;
}
遍历是访问树中每一个节点一次的过程。L、V、R分别代表遍历左子树、访问当前节点、遍历右子树。
preOrder
// 递归前序遍历
void preOrder(Node* node) {
if (node != nullptr) {
std::cout << node->data << " "; // 访问根
preOrder(node->left); // 遍历左
preOrder(node->right); // 遍历右
}
}
preOrder
思路:
非递归遍历通常需要借助栈(Stack)来实现深度优先的逻辑。
将根节点root
入栈。
当栈不为空时,循环执行:
a. 弹出栈顶节点cur
并访问它(打印值)。
b. 关键:先将cur
的右孩子入栈(如果存在)。
c. 再将cur
的左孩子入栈(如果存在)。
之所以先压入右孩子,是因为栈是“后进先出”的,这样就能保证左孩子比右孩子先被弹出和访问,从而实现 V -> L -> R 的顺序。
#include // 需要包含头文件
void n_preOrder(Node* root) {
if (root == nullptr) {
return;
}
std::stack<Node*> s;
s.push(root);
while (!s.empty()) {
Node* cur = s.top();
s.pop();
std::cout << cur->data << " "; // V: 访问节点
// 先将右孩子入栈
if (cur->right != nullptr) {
s.push(cur->right);
}
// 再将左孩子入栈,保证左孩子先被访问
if (cur->left != nullptr) {
s.push(cur->left);
}
}
}
inOrder
// 递归中序遍历
void inOrder(Node* node) {
if (node != nullptr) {
inOrder(node->left); // 遍历左
std::cout << node->data << " "; // 访问根
inOrder(node->right); // 遍历右
}
}
n_inOrder
思路:
中序遍历的非递归实现稍微复杂,因为它需要先处理完整个左子树才能访问根节点。
cur
,初始时cur
指向根节点。cur
不为空或栈不为空时,循环执行:cur
不为空,就将cur
入栈,然后更新cur = cur->left
。cur
为空时,说明左边走到底了。此时从栈中弹出一个节点,访问它(打印值)。cur
指向该节点的右孩子 (cur = popped_node->right
),然后重复步骤a。#include // 需要包含头文件
void n_inOrder(Node* root) {
std::stack<Node*> s;
Node* cur = root;
while (cur != nullptr || !s.empty()) {
// 1. 将当前节点及其所有左孩子一路入栈
while (cur != nullptr) {
s.push(cur);
cur = cur->left; // L
}
// 2. 左边到头了,从栈中取出一个节点并访问
cur = s.top();
s.pop();
std::cout << cur->data << " "; // V
// 3. 转向该节点的右子树,准备处理右子树
cur = cur->right; // R
}
}
postOrder
// 递归后序遍历
void postOrder(Node* node) {
if (node != nullptr) {
postOrder(node->left); // 遍历左
postOrder(node->right); // 遍历右
std::cout << node->data << " "; // 访问根
}
}
n_postOrder
思路:
直接实现L -> R -> V
比较困难。可以转变思路:
L -> R -> V
。V -> R -> L
。V -> R -> L
这个顺序和前序遍历 V -> L -> R
非常相似!我们只需要在遍历孩子时,交换左右顺序即可。V -> R -> L
的序列,将其存入另一个结果栈或vector
中,最后再统一逆序输出。#include
#include
#include // for std::reverse
void n_postOrder(Node* root) {
if (root == nullptr) {
return;
}
std::stack<Node*> s;
std::vector<int> result; // 用 vector 存储 V -> R -> L 的结果
s.push(root);
// 1. 实现 V -> R -> L 的遍历
while (!s.empty()) {
Node* cur = s.top();
s.pop();
result.push_back(cur->data);
// 先压入左孩子
if (cur->left != nullptr) {
s.push(cur->left);
}
// 再压入右孩子,这样右孩子会先被弹出和访问
if (cur->right != nullptr) {
s.push(cur->right);
}
}
// 2. 将 V -> R -> L 的结果逆序,得到 L -> R -> V
std::reverse(result.begin(), result.end());
// 3. 打印最终结果
for (int val : result) {
std::cout << val << " ";
}
}
层序遍历是一种广度优先搜索(BFS),它从上到下、从左到右逐层访问节点。递归实现稍显取巧,需要先计算树的高度,然后对每一层进行一次遍历。
// 递归求二叉树高度
int get_height(Node* node) {
if (node == nullptr) {
return 0;
}
int left_height = get_height(node->left);
int right_height = get_height(node->right);
return left_height > right_height ? left_height + 1 : right_height + 1;
}
// 递归求节点总数
int count_nodes(Node* node) {
if (node == nullptr) {
return 0;
}
int left = count_nodes(node->left);
int right = count_nodes(node->right);
return 1 + left + right;
}
// 打印指定层(level)的所有节点
void levelOrder_level(Node* node, int level) {
if (node == nullptr) {
return;
}
if (level == 0) {
cout << node->data << " ";
} else if (level > 0) {
levelOrder_level(node->left, level - 1);
levelOrder_level(node->right, level - 1);
}
}
// 递归层序遍历
void levelOrder(Node* root) {
int h = get_height(root);
for (int i = 0; i < h; i++) {
levelOrder_level(root, i);
}
}
思路:
层序遍历是典型的广度优先搜索(BFS),其最佳实现是使用队列(Queue)。
root
入队。cur
并出队。cur
(打印值)。cur
的左孩子入队(如果存在)。cur
的右孩子入队(如果存在)。#include // 需要包含头文件
void n_levelOrder(Node* root) {
if (root == nullptr) {
return;
}
std::queue<Node*> q;
q.push(root);
while (!q.empty()) {
Node* cur = q.front();
q.pop();
std::cout << cur->data << " ";
if (cur->left != nullptr) {
q.push(cur->left);
}
if (cur->right != nullptr) {
q.push(cur->right);
}
}
}
int main(){
int arr[] = {58, 24, 67, 0, 34, 62, 69, 5, 41, 64, 78};
Node* root = nullptr;
// 使用递归插入构建BST
for (int val : arr) {
root = n_insert(root, val);
}
levelOrder(root);
cout << endl;
cout << r_query(root, 24) << endl;
root = r_remove(root, 24);
cout << r_query(root, 24) << endl;
return 0;
}
问题描述:
给定一棵二叉搜索树(BST)和一个闭区间 [k1, k2]
,找出并打印树中所有值在此区间内的节点。
解题思路:
最直观的方法是遍历整棵树,然后判断每个节点的值是否在区间内。但这样做完全没有利用BST的有序性,效率不高。
更优的思路是进行一次剪枝的中序遍历。我们知道中序遍历(L->V->R)能按升序访问节点。在遍历过程中,可以根据当前节点值与区间的关系,决定是否需要继续搜索子树:
cur->data
小于 k1
,那么它的整个左子树的值都将小于k1
,因此我们无需遍历左子树,只需继续在右子树中查找。cur->data
大于 k2
,那么它的整个右子树的值都将大于k2
,因此我们无需遍历右子树,只需继续在左子树中查找。cur->data
在区间内 (k1 <= cur->data <= k2
),我们就访问(打印)这个节点,然后需要继续在左、右两个子树中查找其他可能符合条件的节点。代码实现:
#include
// 递归查找在[k1, k2]区间内的所有节点值
void findInRange(Node* node, int k1, int k2, std::vector<int>& result) {
if (node == nullptr) {
return;
}
// 1. 如果当前节点值 > k1,说明左子树中可能有符合条件的节点
if (node->data > k1) {
findInRange(node->left, k1, k2, result);
}
// 2. 如果当前节点值在区间内,则记录下来
if (node->data >= k1 && node->data <= k2) {
result.push_back(node->data);
}
// 3. 如果当前节点值 < k2,说明右子树中可能有符合条件的节点
if (node->data < k2) {
findInRange(node->right, k1, k2, result);
}
}
复杂度分析:
H
是树的高度,K
是位于区间内的节点数量。这远比遍历所有 N
个节点的 O(N) 方案要高效。问题描述:
给定一棵二叉树,判断它是否是一棵有效的二叉搜索树。
解题思路:
要严格满足BST的定义:
一个常见的错误思路是:仅判断 node->left->data < node->data
和 node->right->data > node->data
。这忽略了整个子树的约束(例如,一个节点的右孩子可能比它的祖先节点还要小)。
正确的解法有两种:
方法一:利用中序遍历的性质 (推荐)
一棵有效的BST,其中序遍历的结果必然是一个严格递增的序列。我们可以利用这个特性。
在进行中序遍历时,我们用一个变量 prev
记录前一个被访问的节点的值。每当访问一个新节点时,就将其值与 prev
比较。如果当前节点值小于或等于 prev
,则说明它不是一棵BST。
代码实现 (中序遍历法):
// 辅助函数,prev 必须是引用,以便在递归中共享状态
bool isBST_inorder_util(Node* node, Node*& prev) {
if (node == nullptr) {
return true;
}
// 1. 检查左子树
if (!isBST_inorder_util(node->left, prev)) {
return false;
}
// 2. 检查当前节点与前一个节点的关系
if (prev != nullptr && node->data <= prev->data) {
return false;
}
prev = node; // 更新 prev 为当前节点
// 3. 检查右子树
return isBST_inorder_util(node->right, prev);
}
// 主调用函数
bool isBST(Node* root) {
Node* prev = nullptr;
return isBST_inorder_util(root, prev);
}
方法二:递归并传递有效范围
我们也可以通过递归来判断。对于每个节点,我们检查它的值是否在一个允许的 (min, max)
范围内。
(-∞, +∞)
。(min, parent->data)
,即左孩子的值必须小于其父节点。(parent->data, max)
,即右孩子的值必须大于其父节点。代码实现 (范围判断法):
#include // For LONG_MIN, LONG_MAX
bool isBST_range_util(Node* node, long min_val, long max_val) {
if (node == nullptr) {
return true;
}
// 检查当前节点值是否在有效范围内
if (node->data <= min_val || node->data >= max_val) {
return false;
}
// 递归检查左右子树,并更新范围
// 左子树的上限是当前节点的值
// 右子树的下限是当前节点的值
return isBST_range_util(node->left, min_val, node->data) &&
isBST_range_util(node->right, node->data, max_val);
}
// 主调用函数
bool isBST_range(Node* root) {
// 使用 long 类型避免节点值恰好是 INT_MIN 或 INT_MAX 的边界问题
return isBST_range_util(root, LONG_MIN, LONG_MAX);
}
复杂度分析:
问题描述:
给定两棵二叉树 T1
和 T2
,判断 T2
是否是 T1
的子结构。所谓子结构,是指在 T1
中是否存在一个节点,其下的子树与 T2
有着完全相同的结构和节点值。
解题思路:
这个问题可以分解为两个步骤:
isIdentical(node1, node2)
,用来判断以 node1
和 node2
为根的两棵树是否完全一样。T1
查找匹配点:我们需要遍历 T1
的每一个节点。对于 T1
中的每一个节点,我们都将其作为根,调用 isIdentical
函数,与 T2
进行比较。如果任何一次比较返回 true
,那么 T2
就是 T1
的子结构。代码实现:
// 辅助函数:判断两棵树是否完全相同
bool isIdentical(Node* r1, Node* r2) {
// 如果两棵树都为空,则它们相同
if (r1 == nullptr && r2 == nullptr) {
return true;
}
// 如果只有一棵树为空,则它们不同
if (r1 == nullptr || r2 == nullptr) {
return false;
}
// 如果节点值不同,则它们不同
if (r1->data != r2->data) {
return false;
}
// 递归地比较左右子树
return isIdentical(r1->left, r2->left) && isIdentical(r1->right, r2->right);
}
// 主函数:判断 T2 是否是 T1 的子结构
bool isSubstructure(Node* T1, Node* T2) {
// 空树是任何树的子结构
if (T2 == nullptr) {
return true;
}
if (T1 == nullptr) {
return false;
}
// 检查以 T1 为根的树是否与 T2 相同
if (isIdentical(T1, T2)) {
return true;
}
// 如果不相同,则递归地在 T1 的左子树和右子树中寻找 T2
return isSubstructure(T1->left, T2) || isSubstructure(T1->right, T2);
}
复杂度分析:
M
是 T1
的节点数,N
是 T2
的节点数。在最坏情况下,我们需要对 T1
中的每个节点都完整地比较一次 T2
。求两个节点的最近公共祖先节点
问题描述:
给定一棵二叉树和树中的两个节点 p
和 q
,找到这两个节点的最近公共祖先(Lowest Common Ancestor, LCA)。
LCA的定义:在一个树 T
中,节点 p
和 q
的最近公共祖先是树 T
中同时拥有 p
和 q
作为后代的最深节点(这里我们允许一个节点是它自己的后代)。
在上图中,节点 0 和 41 的LCA是 24。节点 64 和 78 的LCA是 67。
对于一棵普通的二叉树(不一定是BST),可以使用一种非常优雅的后序遍历递归方法。
解题思路:
我们从根节点开始递归,对于任意一个节点 cur
:
cur
为空,或者 cur
就是 p
或 q
中的一个,那么 cur
就是一个潜在的LCA,我们返回 cur
。cur
的左、右子树中递归查找 p
和 q
。
left_lca = lca(cur->left, p, q)
right_lca = lca(cur->right, p, q)
left_lca
和 right_lca
都非空,这意味着 p
和 q
分别位于 cur
的左右两侧,那么 cur
本身就是LCA。left_lca
非空,这意味着 p
和 q
都在左子树中,那么LCA也必然在左子树中,返回 left_lca
。right_lca
非空,同理,返回 right_lca
。p
和 q
都不在以 cur
为根的子树中。代码实现:
// 针对普通二叉树的LCA查找
Node* findLCA_for_BinaryTree(Node* root, Node* p, Node* q) {
// 基准情况:如果树为空,或者找到了p或q,就返回当前节点
if (root == nullptr || root == p || root == q) {
return root;
}
// 在左右子树中递归查找
Node* left_lca = findLCA_for_BinaryTree(root->left, p, q);
Node* right_lca = findLCA_for_BinaryTree(root->right, p, q);
// 如果p和q分别在左右子树,那么当前root就是LCA
if (left_lca != nullptr && right_lca != nullptr) {
return root;
}
// 如果p和q都在左子树,则LCA在左子树,返回left_lca
// 如果都在右子树,则LCA在右子树,返回right_lca
return (left_lca != nullptr) ? left_lca : right_lca;
}
复杂度分析:
N
是树的节点总数,因为最坏情况下我们需要访问所有节点。H
是树的高度,主要消耗于递归调用栈。如果给定的树是一棵BST,可以利用其节点值的有序性来大大优化查找过程,将时间复杂度降低到对数级别。
解题思路:
从根节点开始遍历,对于当前节点 cur
:
p
和 q
的值都小于 cur
的值,说明它们的LCA必然在 cur
的左子树中。p
和 q
的值都大于 cur
的值,说明它们的LCA必然在 cur
的右子树中。cur
,另一个大于 cur
,或者其中一个节点就是 cur
,那么当前节点 cur
就是 p
和 q
分叉的地方,即它们的最近公共祖先。这个过程就像是在树中寻找一个“分叉点”,这个分叉点就是LCA。这个算法可以用迭代实现,非常高效。
代码实现:
// 针对BST的LCA查找 (迭代法)
Node* findLCA_for_BST(Node* root, Node* p, Node* q) {
if (root == nullptr || p == nullptr || q == nullptr) {
return nullptr;
}
Node* cur = root;
while (cur != nullptr) {
// 如果p和q都比当前节点小,去左子树找
if (p->data < cur->data && q->data < cur->data) {
cur = cur->left;
}
// 如果p和q都比当前节点大,去右子树找
else if (p->data > cur->data && q->data > cur->data) {
cur = cur->right;
}
// 否则,当前节点就是分叉点,即LCA
else {
return cur;
}
}
return nullptr; // 理论上对于有效的p和q,不会执行到这里
}
复杂度分析:
H
是树的高度。对于一棵平衡的BST,复杂度为 O(logN)。我们只需要从根节点向下走一条路径即可。问题描述:
给定一棵二叉树,将其变换为原树的镜像。即,对于树中的任意一个节点,它的左、右子节点进行互换。
解题思路:
这是一个非常适合用递归解决的问题。我们可以采用后序遍历(左右根)的思路,自底向上地进行翻转。
left
和 right
指针,就完成了当前层的翻转。代码实现:
该函数会直接修改原树,并返回翻转后的树的根节点。
C++
// 翻转二叉树,使其成为自身的镜像
Node* mirrorTree(Node* root) {
// 基准情况:如果节点为空,无需操作
if (root == nullptr) {
return nullptr;
}
// 1. 递归翻转左子树
mirrorTree(root->left);
// 2. 递归翻转右子树
mirrorTree(root->right);
// 3. 交换当前节点的左右孩子
Node* temp = root->left;
root->left = root->right;
root->right = temp;
return root;
}
复杂度分析:
H
是树的高度。问题描述:
给定一棵二叉树,判断它是否是轴对称的(即,它本身是否是自身的镜像)。
解题思路:
这个问题不能简单地通过遍历来解决。我们需要判断根节点的左子树是否与根节点的右子树“互为镜像”。
这需要一个辅助函数 isMirror(t1, t2)
,用来判断 t1
和 t2
这两棵树是否互为镜像。
isMirror
的判断逻辑如下:
t1
和 t2
都为空,它们是镜像的。t1
的左子树 必须和 t2
的右子树 互为镜像,并且 t1
的右子树 必须和 t2
的左子树 互为镜像。代码实现:
C++
// 辅助函数:判断 t1 和 t2 是否互为镜像
bool isMirror(Node* t1, Node* t2) {
// 基准情况:两棵树都为空,是镜像
if (t1 == nullptr && t2 == nullptr) {
return true;
}
// 如果一个为空一个不为空,或者节点值不同,则不是镜像
if (t1 == nullptr || t2 == nullptr || t1->data != t2->data) {
return false;
}
// 递归地判断:t1的左子树是否是t2右子树的镜像
// 并且t1的右子树是否是t2左子树的镜像
return isMirror(t1->left, t2->right) && isMirror(t1->right, t2->left);
}
// 主函数:判断一棵树是否轴对称
bool isSymmetric(Node* root) {
if (root == nullptr) {
return true;
}
return isMirror(root->left, root->right);
}
复杂度分析:
问题描述:
假设输入一棵二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
核心思路:
这是一个非常经典的题目,其解法完全依赖于两种遍历方式的特性:
重建步骤(递归):
优化:每次都在中序序列中线性查找根节点的位置效率较低。我们可以预先用一个哈希表(unordered_map
)存储中序序列中每个值对应的索引,将查找时间从 O(N) 降为 O(1)。
代码实现:
#include
#include
// 辅助函数,执行实际的递归构建
// preorder/inorder: 遍历序列
// pre_start, pre_end: 当前处理的前序序列的起止索引
// in_start, in_end: 当前处理的中序序列的起止索引
// map: 中序遍历的值->索引的映射,用于快速查找
Node* buildTreeHelper(const std::vector<int>& preorder, const std::vector<int>& inorder,
int pre_start, int pre_end, int in_start, int in_end,
std::unordered_map<int, int>& map) {
// 基准情况:序列为空
if (pre_start > pre_end || in_start > in_end) {
return nullptr;
}
// 1. 创建根节点 (前序序列的第一个元素)
int root_val = preorder[pre_start];
Node* root = new Node(root_val);
// 2. 在中序序列中找到根的位置
int in_root_idx = map[root_val];
int left_subtree_size = in_root_idx - in_start;
// 3. 递归构建左子树
root->left = buildTreeHelper(preorder, inorder,
pre_start + 1, pre_start + left_subtree_size,
in_start, in_root_idx - 1,
map);
// 4. 递归构建右子树
root->right = buildTreeHelper(preorder, inorder,
pre_start + left_subtree_size + 1, pre_end,
in_root_idx + 1, in_end,
map);
return root;
}
// 主函数
Node* buildTree(const std::vector<int>& preorder, const std::vector<int>& inorder) {
if (preorder.empty() || inorder.empty()) {
return nullptr;
}
// 预处理,构建中序遍历的哈希映射
std::unordered_map<int, int> map;
for (int i = 0; i < inorder.size(); ++i) {
map[inorder[i]] = i;
}
return buildTreeHelper(preorder, inorder, 0, preorder.size() - 1, 0, inorder.size() - 1, map);
}
关于BST的特别说明:
该 buildTree
函数对于任何二叉树都适用。如果明确知道要重建的是一棵 BST,其实还有一个更简单的方法:因为BST的中序遍历就是其所有节点排序后的结果,所以我们只需要前序遍历序列就足够了。我们可以遍历前序序列,将每个元素依次插入到一棵新的、初始为空的BST中,最终得到的树就是重建后的BST。但“从两种遍历重建树”是更具普适性的经典算法。
复杂度分析 (使用哈希表优化后):
完全二叉树的定义
一棵深度为 k 的二叉树,如果它的第 1 层到第 k−1 层的节点都排满了,并且第 k 层的节点都从左到右连续地排列,那么这棵树就是完全二叉树。满二叉树是一种特殊的完全二叉树。
算法思路:
这个算法的关键在于,每次递归调用都至少有一边的子树可以直接通过高度计算出节点数,而不需要完全遍历,从而大大减少了计算量。
#include
#include // For pow()
// 定义二叉树节点 (同上)
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
/**
* @brief 计算以 node 为根的树的高度 (沿着左子节点)
* * @param node 根节点
* @return int 树的高度
*/
int getHeight(TreeNode* node) {
int height = 0;
while (node) {
height++;
node = node->left;
}
return height;
}
/**
* @brief 利用完全二叉树性质高效计算节点个数
* * @param root 树的根节点
* @return int 节点的总数
*/
int countNodes_Optimized(TreeNode* root) {
if (root == nullptr) {
return 0;
}
int leftHeight = getHeight(root->left);
int rightHeight = getHeight(root->right);
if (leftHeight == rightHeight) {
// 左子树是满二叉树
// 节点总数 = 根节点(1) + 左子树节点数 + 右子树节点数
// 左子树节点数 = 2^leftHeight - 1
// 所以总数 = 1 + (pow(2, leftHeight) - 1) + countNodes_Optimized(root->right)
// = pow(2, leftHeight) + countNodes_Optimized(root->right)
return (1 << leftHeight) + countNodes_Optimized(root->right); // (1 << leftHeight) 相当于 2^leftHeight
} else {
// 右子树是满二叉树
// 节点总数 = 根节点(1) + 左子树节点数 + 右子树节点数
// 右子树节点数 = 2^rightHeight - 1
// 所以总数 = 1 + countNodes_Optimized(root->left) + (pow(2, rightHeight) - 1)
// = pow(2, rightHeight) + countNodes_Optimized(root->left)
return (1 << rightHeight) + countNodes_Optimized(root->left); // (1 << rightHeight) 相当于 2^rightHeight
}
}
时间复杂度分析:
在countNodes_Optimized函数中,首先计算左右子树的高度,这需要 O(logN) 的时间。然后,递归地对一个子树进行处理。每次递归,问题规模都会减半。因此,总的时间复杂度为 O((logN)*(logN))。这比 O(N) 的遍历法要高效得多。
1
),表示“不平衡”。算法流程如下:
nullptr
),说明它是一棵空树,是平衡的,高度为 0。返回 0
。checkHeight
,得到左子树的高度 leftHeight
。leftHeight
为 1
,说明左子树已经不平衡了,整棵树必定不平衡,无需继续判断,直接返回 1
。checkHeight
,得到右子树的高度 rightHeight
。rightHeight
为 1
,说明右子树不平衡,同样直接返回 1
。abs(leftHeight - rightHeight)
。1
。1 + max(leftHeight, rightHeight)
。返回这个高度值。时间复杂度为 O(N)。空间复杂度为 O(H),其中 H 是树的高度,用于递归栈空间(最坏情况下为 O(N))。
int checkHeight(TreeNode* node) {
// 基本情况:空树是平衡的,高度为 0。
if (node == nullptr) {
return 0;
}
// 递归计算左子树的高度
int leftHeight = checkHeight(node->left);
// 剪枝:如果左子树已经不平衡,直接返回 -1
if (leftHeight == -1) {
return -1;
}
// 递归计算右子树的高度
int rightHeight = checkHeight(node->right);
// 剪枝:如果右子树已经不平衡,直接返回 -1
if (rightHeight == -1) {
return -1;
}
// 检查当前节点的平衡性
if (std::abs(leftHeight - rightHeight) > 1) {
return -1; // 不平衡
} else {
// 平衡,返回当前子树的高度
return 1 + std::max(leftHeight, rightHeight);
}
}
**bool isBalanced(TreeNode* root) {
return checkHeight(root) != -1;
}**
问题描述:
给定一棵二叉树和一个整数 K
,找出在中序遍历(LVR)序列中,排在倒数第 K
位的那个节点。
解题思路:
方法的核心在于逆向思维。
K
个,就是从后往前数的第 K
个。这样,只需要对树进行一次 RVL 遍历,并在过程中计数,数到第 K 个时,就是我们的答案。
void findKthToLastHelper(TreeNode* node, int& k, TreeNode*& result) {
// 基准情况:节点为空,或者已经找到了结果,就没必要继续了
if (node == nullptr || result != nullptr) {
return;
}
// 1. (R) 优先遍历右子树
findKthToLastHelper(node->right, k, result);
// 2. (V) 访问当前节点
// 只有在结果还未找到时才进行处理
if (result == nullptr) {
// 每访问一个节点,k就减1
k--;
// 如果 k 减到 0,说明当前节点就是我们要找的第 k 个
if (k == 0) {
result = node; // 记录结果
return; // 提前返回,剪枝
}
}
// 3. (L) 最后遍历左子树
findKthToLastHelper(node->left, k, result);
}
TreeNode* findKthToLast(TreeNode* root, int k) {
// 处理无效输入
if (root == nullptr || k <= 0) {
return nullptr;
}
TreeNode* result = nullptr;
findKthToLastHelper(root, k, result);
return result;
}