65.9K
CodeProject 正在变化。 阅读更多。
Home

C 语言中的 Treaps

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (11投票s)

2015年5月4日

CPOL

4分钟阅读

viewsIcon

20281

downloadIcon

429

C 语言中的 Treap 实现

引言

Treap 是一种数据结构,它结合了堆(binary heap)和二叉搜索树(BST)。它是一种自组织、自平衡的数据结构。与 AVL 树或红黑树等其他自平衡树相比,它的主要优势在于实现简单且性能相当不错。在实现其他数据结构(如优先级队列)时,它可以作为一种高效的后端。Treap 上所有操作的时间复杂度都是 O(log n),这使其成为一种非常有吸引力的数据结构。

有关这种数据结构的更多详细信息,我鼓励读者参考 Wikipedia 上简洁而精彩的描述。

在本文的其余部分,我将假设读者对数据结构构建和维护的原理已有基本了解。

 

背景

最近我需要用 C 实现一个优先级队列。我花了一段时间思考该使用哪种数据结构?我的第一个想法当然是 。访问具有最低优先级的元素是 O(1),这正是我所需要的。但我的另一个要求是能够快速找到任何元素,而不仅仅是具有最低优先级的那个。这就有问题了。考虑数组堆的实现,查找具有最低优先级的元素将是 O(1),但查找任何其他(随机访问)元素将是 O(n)。如果你的堆很小,这可以接受,但随着大小的增加,性能会迅速下降。

经过一些研究,我最终决定使用 Treap。使用 Treap 会使查找具有最低优先级的元素变慢。它是 O(log n),但随机查找元素也同样是 O(log n),这似乎是一个公平的权衡。

当然,如果你的随机数生成器非常差,Treap 中的查找时间可能会恶化,任何操作甚至可能达到 O(n)。但在大多数情况下,使用标准的 POSIX rand(),我得到了相当令人满意的结果。

 

实现细节

我引入了两种数据类型来实现 Treap。必不可少的一种代表 Treap 的节点。它包含堆的优先级、实际数据以及指向子节点的 BST 指针。这是最基本的需求,但大多数时候我喜欢有一个包装器,将数据结构本身作为一个单独的实体来表示。这样我可以存储一些额外的信息。

/**
 * @brief leaf enumeration
 */
typedef enum _treap_leaf {
    L_LEFT = 0,
    L_RIGHT,
    L_MAX,
} treap_leaf;


/**
 * @brief treap node declaration
 */
struct treap_node {
    struct treap_node *l[L_MAX];

    /* random priority */
    int p;

    /* the data */
    treap_data_t d;
};


/**
 * @brief treap data type declaration
 */
struct treap {
    struct treap_node *root;
    treap_cmp_t cmp;
    size_t n;
};

 

使用代码

有一些先决条件。该实现提供了朴素的类型无关性。正如 `struct treap_node` 声明所示,需要 `treap_data_t` 类型定义。另一件事是 `struct treap` 定义中可见的 `treap_cmp_t`。这是一个对封装的数据类型进行操作的比较函数。`treap.h` 头文件要求在包含路径中存在 `treap_data_type.h` 头文件。该头文件应定义 `treap_data_t` 并声明上述比较函数。这里有一个 `int` 的示例

typedef int treap_data_t;
int treap_int_cmp(treap_data_t *a, treap_data_t *b);

 

比较函数在格式上与标准库的 qsort 使用的几乎相同。它的作用也应该相同。示例文本可能看起来像这样

int treap_int_cmp(treap_data_t *a, treap_data_t *b) {
    return (*a > *b ? 1 : (*a == *b ? 0 : -1));
}

 

这些都是先决条件。为了使用 Treap,需要对其进行分配。该实现提供了一组统一的 API 来操作它。示例如下

void treap_usage() {
    struct treap *t = treap_alloc();
    struct treap_node *n = NULL;
    int min, max;

    t->cmp = treap_int_cmp;

    treap_insert(t, 123);
    treap_insert(t, 456);
    treap_insert(t, 789);

    n = treap_find(t, 456);
    if (n) treap_delete(t, n);

    min = treap_min(t)->d;
    max = treap_max(t)->d;

    treap_dealloc(t);
}

 

提供了以下一组操作(大多数函数名是不言自明的,Doxygen 文档包含了其余必要的详细信息)

  • treap_alloc

  • treap_dealloc

  • treap_insert

  • treap_delete

  • treap_modify

  • treap_traverse_in

  • treap_traverse_level

  • treap_max_height_get

  • treap_balance_factor_get

  • treap_parent_find

  • treap_find

  • treap_min

  • treap_max

关注点

基于该数据结构实现的优先级队列表现相当不错。在我的用例中,需要同时具有随机访问和最低优先级元素访问,性能非常令人满意。它比数组堆实现和线性搜索要好得多。

关于该数据结构的自平衡能力,我也为其实现编写了一套简单的测试。一个测试用例(它实际上不做任何代码验证)将 10000 个元素按排序顺序插入 Treap,然后删除其中三分之一,再插入另外 10000 个。目的是只测量树的平衡因子。提醒一下,平衡因子是根节点的左子树和右子树之间的差值。它越接近零,平衡性越好。我做了一些测试。确实存在 rand() 没有提供均匀分布的随机数范围的情况,这显然会对平衡产生影响。然而,大多数时候平衡性相当好,尤其考虑到传入的数据总是以排序顺序插入。为了有一些参考,我运行了几次测试,这里是一些示例数字

32 次运行后。

平均平衡因子

#6667 个元素: -1.44

#10000 个元素: 0.59

#16667 个元素: -0.13

 

最佳观测平衡因子

#6667 个元素: 0

#10000 个元素: 1

#16667 个元素: 0

 

最差观测平衡因子

#6667 个元素: 13

#10000 个元素: 17

#16667 个元素: 14

© . All rights reserved.