目 录CONTENT

文章目录

数据结构:树

TalentQ
2025-09-10 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

1 树的基本概念

  • 节点 (Node): 树的基本组成部分,每个节点存储数据和指向其他节点的链接。图中的每个方框都是一个节点。

  • 根节点 (Root): 树的顶端节点,没有父节点。一棵树只有一个根

  • 父节点 (Parent) & 子节点 (Child): 一个节点指向的节点是它的子节点,指向它的节点是它的父节点

  • 边 (Edge): 连接两个节点的线。

  • 叶子节点 (Leaf): 没有子节点的节点,也叫终端节点。

  • 子树 (Subtree): 任何一个节点及其所有后代节点组成的结构,本身也是一棵树。

  • 深度 (Depth): 从根节点到该节点所经过的边的数量。根的深度为 0。

  • 高度 (Height): 从该节点到最远的叶子节点所经过的边的数量。所有叶子节点的高度为 0。树的高度等于根节点的高度。

2 二叉树 & 二叉搜索树 (BST)

2.1 二叉树 (Binary Tree)

这是一种特殊的树,其中每个节点最多有两个子节点,通常称为左子节点右子节点

二叉树的遍历(Traversal):

1 深度优先遍历 (DFS - Depth-First Search)

中序遍历 (In-order)

  • 顺序:左子树 -> 根节点 -> 右子树

  • 结果:对 BST 进行中序遍历,会得到一个升序排列的序列。极其有用!

先序遍历 (Pre-order)

  • 顺序:根节点 -> 左子树 -> 右子树

  • 应用:可用于复制一棵树的结构。

后序遍历 (Post-order)

  • 顺序:左子树 -> 右子树 -> 根节点

  • 应用:常用于删除树节点(先删除子节点再删父节点)或计算目录大小。

2 广度优先遍历 (BFS - Breadth-First Search) (层序遍历

层序遍历(Level-order)

  • 实现:通常使用队列 (Queue) 辅助实现。

2.2 二叉搜索树 (Binary Search Tree, BST)

这是一种更特殊的二叉树,它增加了一个关键规则,使得搜索效率极高:

规则: 对于树中的任意一个节点,其左子树所有节点的值都小于该节点的值,其右子树所有节点的值都大于该节点的值。这个规则使得查找一个值的过程就像二分查找一样。

BST 的操作:

  • 查找:从根开始,目标值比当前节点小就往左走,大就往右走,相等就找到。

  • 插入:类似查找,找到应该放置新节点的位置(一个空子节点位置)然后插入。

  • 删除:稍复杂,需要考虑被删除节点是否有 0、1 或 2 个子节点。

BST 的缺陷:如果插入的数据是有序的(如 1, 2, 3, 4, 5),BST 会退化成一条链表!这时,查找的时间复杂度就从理想的 O(log n) 恶化到了最坏的 O(n)

接下来用 C++ 代码实现二叉搜索树。

首先来看一下头文件 binary_search_tree.h,包含了 BinarySearchTree 类的声明,类中在 private 部分声明了节点的结构体 Node、根节点 root_、插入、搜索、删除、中序、前序、后序、层序,在 public 部分对外提供了 插入、搜索、删除、中序、前序、后序、层序的接口。

#ifndef BINARY_SEARCH_TREE_H_
#define BINARY_SEARCH_TREE_H_

#include <memory>

// A binary search tree implementation using smart pointers.
//
// This class provides basic operations for a binary search tree (BST),
// including insertion, search, deletion and traversal methods. It uses
// std::unique_ptr for automatic memory management, ensuring no memory leaks.
class BinarySearchTree {
 public:
  // Constructs an empty binary search tree.
  BinarySearchTree();

  // Destructor is not needed as unique_ptr automatically handles memory cleanup.

  // Inserts a value into the BST.
  //
  // Args:
  //   value: The integer value to insert.
  void Insert(int value);

  // Searches for a value in the BST.
  //
  // Args:
  //   value: The integer value to search for.
  //
  // Returns:
  //   True if the value is found, false otherwise.
  bool Search(int value) const;

  // Removes a value from the BST.
  //
  // Args:
  //   value: The integer value to remove.
  //
  // Returns:
  //   True if the value was found and removed, false otherwise.
  bool Remove(int value);

  // Performs in-order traversal of the BST.
  //
  // In-order traversal visits nodes in ascending order.
  void InOrder() const;

  // Performs pre-order traversal of the BST.
  //
  // Pre-order traversal visits the root first, then left subtree, then right subtree.
  void PreOrder() const;

  // Performs post-order traversal of the BST.
  //
  // Post-order traversal visits left subtree first, then right subtree, then root.
  void PostOrder() const;

  // Performs level-order traversal of the BST.
  //
  // Level-order traversal visits nodes level by level from top to bottom and left to right.
  void LevelOrder() const;

 private:
  // Internal node structure for the BST.
  struct Node {
    int data;                   // The value stored in the node
    std::unique_ptr<Node> left;  // Pointer to the left child
    std::unique_ptr<Node> right; // Pointer to the right child

    // Constructor for creating a new node with the given value.
    //
    // Args:
    //   value: The integer value to store in the node.
    explicit Node(int value);
  };

  std::unique_ptr<Node> root_;  // Root node of the BST

  // Recursively inserts a value into the BST.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  //   value: The integer value to insert.
  void Insert(std::unique_ptr<Node>& node, int value);

  // Recursively searches for a value in the BST.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  //   value: The integer value to search for.
  //
  // Returns:
  //   True if the value is found, false otherwise.
  bool Search(const std::unique_ptr<Node>& node, int value) const;

  // Finds the minimum value in a subtree.
  //
  // Args:
  //   node: Pointer to the root of the subtree.
  //
  // Returns:
  //   The node containing the minimum value in the subtree.
  Node* FindMin(Node* node) const;

  // Recursively removes a value from the BST.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  //   value: The integer value to remove.
  //
  // Returns:
  //   True if the value was found and removed, false otherwise.
  bool Remove(std::unique_ptr<Node>& node, int value);

  // Recursively performs in-order traversal.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  void InOrder(const std::unique_ptr<Node>& node) const;

  // Recursively performs pre-order traversal.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  void PreOrder(const std::unique_ptr<Node>& node) const;

  // Recursively performs post-order traversal.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the current node.
  void PostOrder(const std::unique_ptr<Node>& node) const;

  // Performs level-order traversal.
  //
  // Args:
  //   node: Reference to a unique_ptr pointing to the root node.
  void LevelOrder(const std::unique_ptr<Node>& root) const;
};

#endif  // BINARY_SEARCH_TREE_H_

接下来看源文件 binary_search_tree.cc,包含了上述类和方法的具体实现。

比较复杂的是节点的删除操作,需要考虑待删除节点如果有 0、1、2 个子节点的情况。

#include "binary_search_tree.h"
#include <iostream>
#include <queue>

// Node constructor implementation
BinarySearchTree::Node::Node(int value) 
    : data(value), left(nullptr), right(nullptr) {}

// BinarySearchTree constructor implementation
BinarySearchTree::BinarySearchTree() : root_(nullptr) {}

// Public Insert method
void BinarySearchTree::Insert(int value) {
  Insert(root_, value);
}

// Private recursive Insert method
void BinarySearchTree::Insert(std::unique_ptr<Node>& node, int value) {
  if (node == nullptr) {
    node = std::make_unique<Node>(value);
  } else if (value < node->data) {
    Insert(node->left, value);
  } else if (value > node->data) {
    Insert(node->right, value);
  }
  // If value == node->data, do nothing (no duplicates allowed)
}

// Public Search method
bool BinarySearchTree::Search(int value) const {
  return Search(root_, value);
}

// Private recursive Search method
bool BinarySearchTree::Search(const std::unique_ptr<Node>& node, int value) const {
  if (node == nullptr) return false;
  if (value == node->data) return true;
  if (value < node->data) return Search(node->left, value);
  return Search(node->right, value);
}

// Public Remove method
bool BinarySearchTree::Remove(int value) {
  return Remove(root_, value);
}

// Private recursive Remove method
bool BinarySearchTree::Remove(std::unique_ptr<Node>& node, int value) {
  if (node == nullptr) return false;  // Value not found

  if (value < node->data) {
    return Remove(node->left, value);
  } else if (value > node->data) {
    return Remove(node->right, value);
  } else {
    // Node to delete found
    if (node->left == nullptr && node->right == nullptr) {
      // Case 1: No children
      node.reset();
    } else if (node->left == nullptr) {
      // Case 2: Only right child
      node = std::move(node->right);
    } else if (node->right == nullptr) {
      // Case 3: Only left child
      node = std::move(node->left);
    } else {
      // Case 4: Two children
      Node* min_node = FindMin(node->right.get());
      node->data = min_node->data;
      Remove(node->right, min_node->data);
    }
    return true;
  }
}

// FindMin method implementation
BinarySearchTree::Node* BinarySearchTree::FindMin(Node* node) const {
  while (node->left != nullptr) {
    node = node->left.get();
  }
  return node;
}

// Public InOrder method
void BinarySearchTree::InOrder() const {
  InOrder(root_);
  std::cout << std::endl;
}

// Private recursive InOrder method
void BinarySearchTree::InOrder(const std::unique_ptr<Node>& node) const {
  if (node == nullptr) return;
  InOrder(node->left);
  std::cout << node->data << " ";
  InOrder(node->right);
}

// Public PreOrder method
void BinarySearchTree::PreOrder() const {
  PreOrder(root_);
  std::cout << std::endl;
}

// Private recursive PreOrder method
void BinarySearchTree::PreOrder(const std::unique_ptr<Node>& node) const {
  if (node == nullptr) return;
  std::cout << node->data << " ";
  PreOrder(node->left);
  PreOrder(node->right);
}

// Public PostOrder method
void BinarySearchTree::PostOrder() const {
  PostOrder(root_);
  std::cout << std::endl;
}

// Private recursive PostOrder method
void BinarySearchTree::PostOrder(const std::unique_ptr<Node>& node) const {
  if (node == nullptr) return;
  PostOrder(node->left);
  PostOrder(node->right);
  std::cout << node->data << " ";
}

// Public LevelOrder method
void BinarySearchTree::LevelOrder() const {
  LevelOrder(root_);
  std::cout << std::endl;
}

// Private LevelOrder method
void BinarySearchTree::LevelOrder(const std::unique_ptr<Node>& root) const {
  if (root == nullptr) {
    std::cout << "Tree is empty" << std::endl;
    return;
  }

  // 使用裸指针队列进行层序遍历是安全的,因为:
  // 1. 遍历期间树的结构不会改变(此函数是const的)
  // 2. 裸指针只在函数局部使用,不影响所有权
  // 3. std::unique_ptr 仍然拥有节点的完整所有权
  std::queue<Node*> node_queue;
  node_queue.push(root.get());
  
  while (!node_queue.empty()) {
    Node* current = node_queue.front();
    node_queue.pop();
    
    std::cout << current->data << " ";
    
    if (current->left != nullptr) {
      node_queue.push(current->left.get());
    }
    if (current->right != nullptr) {
      node_queue.push(current->right.get());
    }
  }
}

最后看 main.cc,编译运行查看测试效果。

#include "binary_search_tree.h"
#include <iostream>


int main() {
  BinarySearchTree bst;
  
  // Insert some values
  bst.Insert(50);
  bst.Insert(30);
  bst.Insert(70);
  bst.Insert(20);
  bst.Insert(40);
  bst.Insert(60);
  bst.Insert(80);
  
  // Perform traversals
  std::cout << "In-order traversal: ";
  bst.InOrder();
  
  std::cout << "Pre-order traversal: ";
  bst.PreOrder();
  
  std::cout << "Post-order traversal: ";
  bst.PostOrder();
  
  std::cout << "Level-order traversal: ";
  bst.LevelOrder();
  
  // Search for values
  std::cout << "Search for 40: " << (bst.Search(40) ? "Found" : "Not found") << std::endl;
  std::cout << "Search for 90: " << (bst.Search(90) ? "Found" : "Not found") << std::endl;
  
  // Remove a value
  std::cout << "Removing 20: " << (bst.Remove(20) ? "Success" : "Not found") << std::endl;
  std::cout << "In-order traversal after removal: ";
  bst.InOrder();
  
  return 0;
}
g++ binary_search_tree.cc main.cc -o main
./main

3 平衡二叉树:AVL树和红黑树

为了解决 BST 可能退化的问题,引入了自平衡二叉搜索树。它们通过在插入和删除时进行自动调整(旋转),来保持树的高度大致为 O(log n)。两种最重要的平衡树:AVL树、红黑树

3.1 AVL 树

AVL树是一种高度平衡的二叉搜索树,它通过维护一个平衡因子来确保树的平衡。平衡因子 = 左子树高度 - 右子树高度。对于任何节点,其左子树和右子树的高度差(平衡因子)绝对值不超过 1

  • 查找性能极高:由于严格平衡,查找时间复杂度稳定在O(log n)

  • 插入/删除代价高:可能需要多次旋转来维持平衡

  • 适合读多写少的场景:如数据库索引、字典等

当插入或删除操作导致平衡因子超出范围时,需要通过旋转来恢复平衡:

  1. 左旋 (Left Rotation):用于处理右子树过高的情况

  2. 右旋 (Right Rotation):用于处理左子树过高的情况

  3. 左右旋 (Left-Right Rotation):先左旋后右旋

  4. 右左旋 (Right-Left Rotation):先右旋后左旋

3.2 红黑树

红黑树是一种大致平衡的二叉搜索树。通过五个规则来确保没有路径会比其他路径长出两倍。它是 C++ STL 中 std::map, std::set, std::multimap, std::multiset 的底层实现容器。

特点: 它不是高度平衡的,但可以保证从根到叶子的最长路径不会超过最短路径的两倍。这是一种折中,虽然查找比 AVL 稍慢,但插入和删除的效率更高,需要的旋转操作更少。

特点

  • 插入/删除性能好:相比AVL树,需要的旋转操作更少

  • 查找性能稍差:没有AVL树那么平衡,但仍是O(log n)

  • 综合性能均衡:适合频繁插入删除的场景

五条规则

  1. 每个节点是红色或黑色

  2. 根节点是黑色

  3. 所有叶子节点(NIL)是黑色

  4. 红色节点的子节点必须是黑色(不能有连续红色节点)

  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

当插入或删除违反规则时,通过以下操作恢复平衡:

  1. 变色 (Recoloring):改变节点颜色

  2. 旋转 (Rotation):与 AVL 树类似的旋转操作

3.3 为什么 C++ STL 选择红黑树

主要是因为STL中的map和set等关联容器需要频繁执行插入和删除操作,而红黑树在整体性能上提供了更好的平衡。它牺牲了少量严格的平衡性来换取更高效的插入和删除效率,这对于STL的通用设计哲学更为重要。

这是一个典型的工程上的权衡(Trade-off)。虽然AVL树在查找上有着理论上的优势,但C++ STL的设计者经过权衡后认为,对于其目标应用场景(需要大量增删改查的通用编程),红黑树在整体性能和实现复杂度上提供了更好的折衷。

在实际硬件上,由于缓存等原因,红黑树较少的旋转操作也带来了更好的性能表现。

4 扩展

如何计算一棵树的高度/最大深度?

int MaxDepth(std::unique_ptr<Node>& node) {
  if (node == nullptr) return 0;
  return std::max(MaxDepth(node->left), MaxDepth(node->right)) + 1;
}

如何判断两颗二叉树是否相同?

bool IsSameTree(std::unique_ptr<Node>& p, std::unique_ptr<Node>& q) {
  if (p == nullptr && q == nullptr) return true;
  if (p == nullptr || q == nullptr) return false;
  if (p->data != q->data) return false;
  return IsSameTree(p->left, q->left) && IsSameTree(p->right, q->right);
}

如何判断一棵二叉树是否是对称二叉树/镜像对称的?

bool IsISymmetrical(std::unique_ptr<Node>& node) {
  if (node == nullptr) return true;
  std::unique_ptr<Node>& p = node->left;
  std::unique_ptr<Node>& q = node->right;
  return IsISymmetricalPQ(p, q);
}

bool IsISymmetricalPQ(std::unique_ptr<Node>& p, std::unique_ptr<Node>& q) {
  if (p == nullptr && q == nullptr) return true;
  if (p == nullptr || q == nullptr) return false;
  if (p->data != q->data) return false;
  return IsISymmetricalPQ(p->left, q->right) && IsISymmetricalPQ(p->right, q->left);
}

如何使用迭代的方式实现二叉树的前序、中序、后序遍历?

void PreOrderByStack(std::unique_ptr<Node>& root) {
  if (root == nullptr) return;
  std::stack<Node*> s;
  s.push(root.get());
  while (!s.empty()) {
    Node* current = s.top();
    s.pop();
    std::cout << current->data << " ";
    if (current->right != nullptr) s.push(current->right.get());
    if (current->left != nullptr) s.push(current->left.get());
  }
}

void InOrderByStack(std::unique_ptr<Node>& root) {
  if (root == nullptr) return;
  std::stack<Node*> s;
  Node* current = root.get();
  while (current != nullptr || !s.empty())
  {
    while (current != nullptr) {
      s.push(current);
      current = current->left.get();
    }
    current = s.top();
    s.pop();
    std::cout << current->data << " ";
    current = current->right.get();
  }
}

void PostOrderByStack(std::unique_ptr<Node>& root) {

}

C++ STL 中的 map(或 set)底层是如何实现的?它有什么特点?

底层通常是红黑树(一种自平衡二叉搜索树)。

  • 元素是自动排序的(按 key 排序)。

  • 查找、插入、删除操作的时间复杂度都是 O(log n)

  • 支持高效的区间遍历和范围查询。

  • 对比 unordered_mapunordered_map 底层是哈希表,元素无序,但平均查找时间复杂度是 O(1)。

为什么 std::map 选择红黑树而不是 AVL 树作为底层结构?

虽然 AVL 树更平衡,查找更快(O(log n)),但红黑树在插入和删除操作时需要更少的旋转来维持平衡,整体性能更均衡。对于需要频繁插入删除的通用关联容器(如 std::map),红黑树是更好的选择。

什么是线索二叉树?它有什么用处?

  • :在线索二叉树中,所有为 nullptr 的左、右指针被重新利用:如果一个节点的左指针为空,则让它指向其前驱节点;如果右指针为空,则指向其后继节点(这里的“前驱”和“后继”指的是中序遍历序列中的前后关系)。

  • 用处:它使得在不使用栈或递归的情况下进行中序遍历成为可能,并且可以更快地找到前驱和后继节点。它节省了内存空间(不需要为每个节点维护指向父节点的指针),但增加了复杂性。

0

评论区