树结构模块:二叉树、二叉搜索树、平衡树的基本接口

树结构,说白了就是嵌入式世界里最优雅的数据组织方式之一。我做了十几年嵌入式,从最早的AVR到现在的ARM Cortex-M系列,树结构几乎贯穿了我所有的项目。你想想看,链表虽然简单,但查找起来太慢;数组虽然快,但插入删除又麻烦。树结构正好在两者之间找到了平衡点。

今天咱们聊聊二叉树、二叉搜索树和平衡树。嗯,别被这些名字吓到,其实核心思想就一个:怎么让数据组织得更聪明

二叉树:最基础的树结构

二叉树,每个节点最多有两个孩子。左孩子、右孩子,就这么简单。我在项目中遇到过最典型的场景——用二叉树来组织菜单结构。每个菜单项要么有子菜单(左子树),要么有同级菜单(右子树),遍历起来特别直观。

// 二叉树节点结构
typedef struct TreeNode {
    void *data;                 // 数据指针
    struct TreeNode *left;      // 左孩子
    struct TreeNode *right;     // 右孩子
} TreeNode;

// 创建节点
TreeNode* tree_create_node(void *data) {
    TreeNode *node = (TreeNode*)malloc(sizeof(TreeNode));
    if (node) {
        node->data = data;
        node->left = NULL;
        node->right = NULL;
    }
    return node;
}

// 前序遍历
void tree_preorder(TreeNode *root, void (*visit)(void*)) {
    if (!root) return;
    visit(root->data);
    tree_preorder(root->left, visit);
    tree_preorder(root->right, visit);
}

小提示:二叉树遍历有三种方式——前序、中序、后序。我个人习惯用中序遍历二叉搜索树,因为出来的结果就是有序的。这个技巧在调试时特别有用。

二叉搜索树:让查找更快

二叉搜索树(BST)有个核心规则:左子树所有节点都小于根节点,右子树所有节点都大于根节点。这个规则一加上,查找效率就从O(n)降到了O(log n)。

我曾经在一个传感器数据采集项目中用过BST。传感器ID是整数,需要快速查找对应的校准参数。用BST做索引,几千个传感器查一遍也就十几次比较,比遍历链表快了一个数量级。

// 二叉搜索树插入
BSTNode* bst_insert(BSTNode *root, int key, void *data) {
    if (!root) {
        return bst_create_node(key, data);
    }
    
    if (key < root->key) {
        root->left = bst_insert(root->left, key, data);
    } else if (key > root->key) {
        root->right = bst_insert(root->right, key, data);
    } else {
        // 键值已存在,更新数据
        root->data = data;
    }
    return root;
}

// 二叉搜索树查找
void* bst_search(BSTNode *root, int key) {
    while (root) {
        if (key == root->key) return root->data;
        if (key < root->key) root = root->left;
        else root = root->right;
    }
    return NULL;  // 没找到
}

注意:二叉搜索树有个致命问题——如果插入的数据是有序的,树会退化成链表。我踩过这个坑,当时用递增的ID建树,结果查找效率直接掉到O(n)。后来才意识到需要平衡树。

平衡树:解决退化问题

平衡树的核心思想很简单:在插入或删除后,通过旋转操作让树保持平衡。常见的平衡树有AVL树和红黑树。AVL要求更严格(左右子树高度差不超过1),红黑树则宽松一些。

我个人更推荐红黑树。为什么?因为AVL的旋转太频繁了,在嵌入式系统里,每次旋转都要做内存操作,性能开销不小。红黑树虽然平衡性稍差,但旋转次数少,整体性能反而更好。

核心要点:平衡树不是完美的平衡,而是「足够好」的平衡。红黑树保证最长路径不超过最短路径的2倍,这个约束在实际应用中完全够用。

实战:实现一个简单的红黑树模块

好了,理论说完了,咱们动手写一个红黑树模块。红黑树有5条规则,我记了十年都没忘:

  1. 每个节点要么红要么黑
  2. 根节点是黑色
  3. 叶子节点(NULL)是黑色
  4. 红色节点的子节点必须是黑色
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

嗯,第4条和第5条是最关键的。第4条保证了不会出现连续的红节点,第5条保证了树的平衡性。

// 红黑树节点颜色
typedef enum { RED, BLACK } Color;

// 红黑树节点
typedef struct RBNode {
    int key;
    void *data;
    Color color;
    struct RBNode *left, *right, *parent;
} RBNode;

// 左旋操作
void rb_left_rotate(RBTree *tree, RBNode *x) {
    RBNode *y = x->right;
    x->right = y->left;
    
    if (y->left != NULL) {
        y->left->parent = x;
    }
    y->parent = x->parent;
    
    if (x->parent == NULL) {
        tree->root = y;
    } else if (x == x->parent->left) {
        x->parent->left = y;
    } else {
        x->parent->right = y;
    }
    
    y->left = x;
    x->parent = y;
}

// 插入修复(核心逻辑)
void rb_insert_fixup(RBTree *tree, RBNode *z) {
    while (z->parent && z->parent->color == RED) {
        if (z->parent == z->parent->parent->left) {
            RBNode *y = z->parent->parent->right;  // 叔叔节点
            if (y && y->color == RED) {
                // 情况1:叔叔是红色,变色即可
                z->parent->color = BLACK;
                y->color = BLACK;
                z->parent->parent->color = RED;
                z = z->parent->parent;
            } else {
                // 情况2:叔叔是黑色,需要旋转
                if (z == z->parent->right) {
                    z = z->parent;
                    rb_left_rotate(tree, z);
                }
                z->parent->color = BLACK;
                z->parent->parent->color = RED;
                rb_right_rotate(tree, z->parent->parent);
            }
        } else {
            // 对称情况,左右互换
            // ... 代码类似,left/right互换
        }
    }
    tree->root->color = BLACK;
}

调试技巧:红黑树最容易出错的地方是插入修复时的指针操作。我曾经花了一整天调试一个红黑树bug,最后发现是parent指针没更新。建议你在每次旋转后,用断言检查所有节点的parent指针是否正确。

红黑树模块的接口设计

一个好的模块,接口要清晰。我习惯把红黑树封装成这样的接口:

// 红黑树模块接口
typedef struct {
    RBNode *root;
    int (*compare)(const void *a, const void *b);
    size_t size;
} RBTree;

// 创建红黑树
RBTree* rb_create(int (*compare)(const void*, const void*));

// 插入节点
int rb_insert(RBTree *tree, void *key, void *data);

// 删除节点
int rb_delete(RBTree *tree, void *key);

// 查找节点
void* rb_search(RBTree *tree, void *key);

// 遍历(中序)
void rb_inorder(RBTree *tree, void (*visit)(void*, void*));

// 销毁树
void rb_destroy(RBTree *tree);

你看,接口只有6个函数,但覆盖了增删查改所有操作。这就是模块化设计的好处——使用者不需要关心内部的红黑树旋转逻辑,只需要调用接口就行。

知识体系总览

下面这张图展示了树结构模块的核心知识体系,从基础到实战,一目了然:

树结构模块知识体系 树结构模块 二叉树 二叉搜索树 平衡树 核心特性 每个节点最多两个孩子 核心特性 左小右大,查找O(log n) 核心特性 旋转保持平衡 实战:红黑树模块 创建/销毁 插入/删除 查找/遍历 旋转/修复

从这张图可以看出,树结构的学习路径很清晰:从最基础的二叉树开始,加上排序规则变成二叉搜索树,再加上平衡机制变成平衡树,最后落地到红黑树实战。每一步都是在前一步的基础上增加复杂度。

总结一下:树结构模块的核心就三件事——节点怎么组织、怎么插入删除、怎么保持平衡。红黑树虽然代码看起来复杂,但核心逻辑就是插入修复那几行代码。你把这个搞懂了,其他平衡树都是类似的思路。

好了,红黑树模块就聊到这儿。代码我已经给了核心部分,剩下的删除操作和销毁操作,建议你自己动手实现一遍。相信我,自己写一遍比看十遍都管用。

公众号:蓝海资料掘金营,微信deep3321