二叉树是每个节点最多有两个子节点的树形数据结构,这种结构在计算机科学中有着广泛的应用场景。从文件系统的目录结构到数据库索引的B树变种,再到机器学习中的决策树算法,二叉树的身影无处不在。
二叉树的每个节点包含三个基本部分:存储的数据、指向左子节点的指针和指向右子节点的指针。这种简洁的结构却能够表达复杂的层次关系,这正是它的魅力所在。根据节点排列方式的不同,二叉树可以分为几种典型类型:
在实际应用中,我们经常需要遍历二叉树来访问所有节点。常见的遍历方式有四种:
提示:二叉搜索树的中序遍历结果是一个有序序列,这个特性常被用于实现有序集合。
在C++中实现二叉树,首先需要设计节点结构。现代C++提供了多种实现方式,各有优缺点。下面是一个典型的模板化节点实现:
cpp复制template <typename T>
struct TreeNode {
T data;
std::unique_ptr<TreeNode> left;
std::unique_ptr<TreeNode> right;
explicit TreeNode(const T& value)
: data(value), left(nullptr), right(nullptr) {}
// 禁用拷贝构造和赋值
TreeNode(const TreeNode&) = delete;
TreeNode& operator=(const TreeNode&) = delete;
// 允许移动语义
TreeNode(TreeNode&&) = default;
TreeNode& operator=(TreeNode&&) = default;
};
这个设计有几个值得注意的特点:
std::unique_ptr管理子节点,自动处理内存释放对于需要频繁修改树结构的场景,原始指针可能比智能指针更合适:
cpp复制template <typename T>
struct RawTreeNode {
T data;
RawTreeNode* left;
RawTreeNode* right;
explicit RawTreeNode(const T& value)
: data(value), left(nullptr), right(nullptr) {}
~RawTreeNode() {
delete left;
delete right;
}
};
注意:使用原始指针时需要手动管理内存,在析构函数中递归删除子节点,避免内存泄漏。
递归是处理树形结构的自然方式,代码简洁直观。以下是四种基本遍历的递归实现:
cpp复制template <typename T, typename Visitor>
void preorderTraversal(const std::unique_ptr<TreeNode<T>>& root, Visitor visit) {
if (!root) return;
visit(root->data); // 访问当前节点
preorderTraversal(root->left, visit); // 递归左子树
preorderTraversal(root->right, visit); // 递归右子树
}
cpp复制template <typename T, typename Visitor>
void inorderTraversal(const std::unique_ptr<TreeNode<T>>& root, Visitor visit) {
if (!root) return;
inorderTraversal(root->left, visit); // 递归左子树
visit(root->data); // 访问当前节点
inorderTraversal(root->right, visit); // 递归右子树
}
cpp复制template <typename T, typename Visitor>
void postorderTraversal(const std::unique_ptr<TreeNode<T>>& root, Visitor visit) {
if (!root) return;
postorderTraversal(root->left, visit); // 递归左子树
postorderTraversal(root->right, visit); // 递归右子树
visit(root->data); // 访问当前节点
}
递归实现的优势在于代码简洁,直接反映算法定义。但存在两个主要问题:
在实际工程中,对于已知深度有限的树(如平衡二叉树),递归是很好的选择。对于可能很深的树,应该考虑迭代实现。
迭代实现使用显式栈来模拟递归调用过程,避免了递归的缺点。以下是各种遍历的迭代实现:
cpp复制template <typename T, typename Visitor>
void iterativePreorder(TreeNode<T>* root, Visitor visit) {
if (!root) return;
std::stack<TreeNode<T>*> stack;
stack.push(root);
while (!stack.empty()) {
auto node = stack.top();
stack.pop();
visit(node->data);
// 右子节点先入栈,保证左子节点先处理
if (node->right) stack.push(node->right);
if (node->left) stack.push(node->left);
}
}
cpp复制template <typename T, typename Visitor>
void iterativeInorder(TreeNode<T>* root, Visitor visit) {
std::stack<TreeNode<T>*> stack;
auto current = root;
while (current || !stack.empty()) {
// 深入左子树
while (current) {
stack.push(current);
current = current->left;
}
// 回溯并访问节点
current = stack.top();
stack.pop();
visit(current->data);
// 转向右子树
current = current->right;
}
}
cpp复制template <typename T, typename Visitor>
void iterativePostorder(TreeNode<T>* root, Visitor visit) {
if (!root) return;
std::stack<TreeNode<T>*> stack, output;
stack.push(root);
while (!stack.empty()) {
auto node = stack.top();
stack.pop();
output.push(node);
if (node->left) stack.push(node->left);
if (node->right) stack.push(node->right);
}
while (!output.empty()) {
visit(output.top()->data);
output.pop();
}
}
层序遍历使用队列而非栈,按层次处理节点:
cpp复制template <typename T, typename Visitor>
void levelOrderTraversal(TreeNode<T>* root, Visitor visit) {
if (!root) return;
std::queue<TreeNode<T>*> q;
q.push(root);
while (!q.empty()) {
auto node = q.front();
q.pop();
visit(node->data);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
二叉搜索树(BST)是一种特殊的二叉树,它保持以下性质:对于每个节点,左子树所有节点的值都小于它,右子树所有节点的值都大于它。
cpp复制template <typename T>
void BSTInsert(std::unique_ptr<TreeNode<T>>& root, const T& value) {
if (!root) {
root = std::make_unique<TreeNode<T>>(value);
return;
}
if (value < root->data) {
BSTInsert(root->left, value);
} else if (value > root->data) {
BSTInsert(root->right, value);
}
// 如果值已存在,可以选择忽略或更新
}
cpp复制template <typename T>
bool BSTSearch(const std::unique_ptr<TreeNode<T>>& root, const T& value) {
if (!root) return false;
if (value == root->data) {
return true;
} else if (value < root->data) {
return BSTSearch(root->left, value);
} else {
return BSTSearch(root->right, value);
}
}
删除操作较为复杂,需要考虑三种情况:
cpp复制template <typename T>
void BSTDelete(std::unique_ptr<TreeNode<T>>& root, const T& value) {
if (!root) return;
if (value < root->data) {
BSTDelete(root->left, value);
} else if (value > root->data) {
BSTDelete(root->right, value);
} else {
// 情况1:叶子节点
if (!root->left && !root->right) {
root.reset();
}
// 情况2:只有一个子节点
else if (!root->left || !root->right) {
root = std::move(root->left ? root->left : root->right);
}
// 情况3:有两个子节点
else {
// 找到右子树的最小节点
auto minNode = root->right.get();
while (minNode->left) {
minNode = minNode->left.get();
}
// 复制最小值到当前节点
root->data = minNode->data;
// 删除右子树中的最小节点
BSTDelete(root->right, minNode->data);
}
}
}
普通二叉搜索树在极端情况下可能退化为链表,导致操作时间复杂度从O(log n)变为O(n)。平衡二叉树通过旋转操作保持树的平衡,确保操作效率。
AVL树通过四种旋转操作保持平衡:
以下是右旋的实现示例:
cpp复制template <typename T>
void rotateRight(std::unique_ptr<TreeNode<T>>& node) {
auto newRoot = std::move(node->left);
node->left = std::move(newRoot->right);
newRoot->right = std::move(node);
node = std::move(newRoot);
}
红黑树是另一种常见的平衡二叉搜索树,它通过以下性质保持平衡:
红黑树的实现较为复杂,通常使用标准库中的std::map和std::set,它们底层就是红黑树实现。
二叉树可以用来表示数学表达式,其中:
cpp复制// 表达式:(3 + 4) * 5
auto exprTree = std::make_unique<TreeNode<std::string>>("*");
exprTree->left = std::make_unique<TreeNode<std::string>>("+");
exprTree->left->left = std::make_unique<TreeNode<std::string>>("3");
exprTree->left->right = std::make_unique<TreeNode<std::string>>("4");
exprTree->right = std::make_unique<TreeNode<std::string>>("5");
哈夫曼树用于数据压缩,频率高的字符使用短编码,频率低的字符使用长编码。
构建步骤:
在机器学习中,决策树用于分类和回归:
cpp复制struct DecisionNode {
std::string feature; // 用于分割的特征
double threshold; // 分割阈值
std::unique_ptr<DecisionNode> left; // 小于阈值
std::unique_ptr<DecisionNode> right; // 大于等于阈值
int result; // 叶子节点的分类结果
};
频繁的节点分配释放可能导致内存碎片,使用内存池可以提高性能:
cpp复制template <typename T>
class TreeNodePool {
std::vector<std::unique_ptr<TreeNode<T>>> pool;
size_t index = 0;
public:
TreeNode<T>* createNode(const T& value) {
if (index >= pool.size()) {
pool.push_back(std::make_unique<TreeNode<T>>(value));
} else {
pool[index]->data = value;
pool[index]->left.reset();
pool[index]->right.reset();
}
return pool[index++].get();
}
void clear() { index = 0; }
};
在多线程环境下使用二叉树需要注意:
将二叉树保存到文件或网络传输需要序列化:
cpp复制template <typename T>
void serialize(const std::unique_ptr<TreeNode<T>>& root, std::ostream& out) {
if (!root) {
out << "# "; // 使用#表示空节点
return;
}
out << root->data << " ";
serialize(root->left, out);
serialize(root->right, out);
}
template <typename T>
void deserialize(std::unique_ptr<TreeNode<T>>& root, std::istream& in) {
std::string token;
if (!(in >> token) || token == "#") return;
std::istringstream iss(token);
T value;
iss >> value;
root = std::make_unique<TreeNode<T>>(value);
deserialize(root->left, in);
deserialize(root->right, in);
}
使用智能指针可以避免大部分内存泄漏问题。对于原始指针,可以使用以下方法检测:
递归算法可能导致栈溢出,预防措施:
验证二叉树是否平衡的算法:
cpp复制template <typename T>
bool isBalanced(const std::unique_ptr<TreeNode<T>>& root) {
return checkHeight(root) != -1;
}
template <typename T>
int checkHeight(const std::unique_ptr<TreeNode<T>>& node) {
if (!node) return 0;
int leftHeight = checkHeight(node->left);
if (leftHeight == -1) return -1;
int rightHeight = checkHeight(node->right);
if (rightHeight == -1) return -1;
if (abs(leftHeight - rightHeight) > 1) return -1;
return std::max(leftHeight, rightHeight) + 1;
}
打印二叉树结构有助于调试:
cpp复制template <typename T>
void printTree(const std::unique_ptr<TreeNode<T>>& root, int space = 0, int gap = 4) {
if (!root) return;
space += gap;
printTree(root->right, space);
std::cout << std::endl;
for (int i = gap; i < space; ++i) std::cout << " ";
std::cout << root->data << "\n";
printTree(root->left, space);
}