C语言经典算法之败者树

本文介绍了败者树的数据结构,包括其代码实现、空间和时间复杂度分析,讨论了其优点如高效查找、动态更新和空间效率,同时也指出了初始化成本高、数据特性依赖和实现复杂性等缺点。文章列举了败者树在外部排序、在线比赛系统、实时流处理和分布式系统等领域的实际应用。
摘要由CSDN通过智能技术生成

目录

前言

A.建议

B.简介

一 代码实现

二 时空复杂度

A.空间复杂度:

B.时间复杂度:

C.总结

三 优缺点

A.优点:

B.缺点:

四 现实中的应用


前言

A.建议

1.学习算法最重要的是理解算法的每一步,而不是记住算法。

2.建议读者学习算法的时候,自己手动一步一步地运行算法。

B.简介

败者树(Loser Tree)是一种用于快速找出一组数据中当前最小或最大元素的数据结构,主要用于外部排序等场合。在败者树中,每个叶节点代表一个输入数据项,而内部节点记录的是其子树中的“败者”,即相对较大的值(如果是求最小元素时)或较小的值(如果是求最大元素时)。败者树通常实现为完全二叉树,并且可以通过数组来紧凑地表示。

一 代码实现

// 假设 `data` 是一个包含 n 个元素的数组,`loser_tree` 是一个大小为 2*n - 1 的数组用于存储败者树
#define MAX_TREE_SIZE (2*n - 1)

typedef struct {
    int value; // 节点对应的数值
    int index; // 数值在原始数组中的索引
} LoserTreeNode;

void initialize_loser_tree(LoserTreeNode loser_tree[MAX_TREE_SIZE], int data[], int n) {
    for (int i = 0; i < n; ++i) { // 初始化叶子节点
        loser_tree[n + i].value = data[i];
        loser_tree[n + i].index = i;
    }
    
    // 从下往上构建败者树
    for (int i = n - 1; i > 0; --i) {
        update_loser_tree(loser_tree, i);
    }
}

void update_loser_tree(LoserTreeNode loser_tree[MAX_TREE_SIZE], int level) {
    int left = (level << 1) + 1;
    int right = left + 1;

    if (left >= MAX_TREE_SIZE) return; // 如果已经是叶子节点则无需比较

    // 找出左右孩子中的败者
    if (loser_tree[left].value <= loser_tree[right].value) {
        loser_tree[level].value = loser_tree[left].value;
        loser_tree[level].index = loser_tree[left].index;
    } else {
        loser_tree[level].value = loser_tree[right].value;
        loser_tree[level].index = loser_tree[right].index;
    }

    // 递归向上更新直到根节点
    update_loser_tree(loser_tree, (level - 1) / 2);
}

// 获取当前最小/最大值及其索引
int get_min_max_index(LoserTreeNode *root) {
    return root->index;
}

// 在新数据到达时,可以调用类似 `update_loser_tree` 的函数更新相应叶节点及父节点
// 假设已经有一个已初始化的败者树 loser_tree 和当前数据大小 n,还有足够的空间存储新数据
void insert_new_data(LoserTreeNode loser_tree[MAX_TREE_SIZE], int new_value, int new_index) {
    // 将新数据先放到数组最后一个位置(作为临时存放区)
    loser_tree[2 * n - 1].value = new_value;
    loser_tree[2 * n - 1].index = new_index;

    // 更新路径从最后一个叶节点到根节点
    int current = 2 * n - 1;
    while (current > 0) {
        int parent = (current - 1) / 2; // 计算父节点的位置

        // 比较当前节点与父节点,决定谁是败者
        if (loser_tree[current].value <= loser_tree[parent].value) {
            // 如果当前节点值较小,则它成为新的胜者,向上更新
            loser_tree[parent] = loser_tree[current];
            current = parent;
        } else {
            // 如果当前节点值较大,则父节点维持不变,无需进一步更新
            break;
        }
    }

    // 当退出循环时,原数据位置被新数据覆盖,更新数组中的实际数据
    data[loser_tree[0].index] = new_value; // 假设 data 是原始数据数组
}

// 在调用 insert_new_data 之前,需要先从输入流获取旧数据并将其在败者树中的索引取出,例如:
int old_index = loser_tree[0].index;
int old_value = data[old_index]; // 存储旧数据以便后续处理或输出

// 然后插入新数据
insert_new_data(loser_tree, new_input_value, next_input_index);

// 注意:以上代码仅适用于求最小元素的情况。如果是求最大元素,则比较逻辑应改为“>”而非“<=”。

二 时空复杂度

A.空间复杂度

  • 败者树通常实现为完全二叉树,并且可以用数组来表示。对于包含 n个叶子节点的败者树,它需要的空间大小为2n - 1个元素,因为完全二叉树非叶子节点的数量等于叶子节点数量减一。因此,空间复杂度为 O(n)

B.时间复杂度

  • 初始化败者树的时间复杂度是线性的,因为每个节点只需要进行一次比较和更新操作,所以初始化整个败者树的时间复杂度为 O(n)
  • 查询当前败者(即最小/大值)的时间复杂度是常量级的,因为在败者树的根节点上直接存储了当前的最小/大值,所以查询时间复杂度为 O(1)
  • 插入新元素并重新调整败者树以保持其正确性的时间复杂度也是 O(log_2n)。这是因为每次插入后,从新插入的叶节点开始,最多只需要向上经过 log_2n 层就可以到达根节点,每层都需要一次比较和可能的交换操作。

C.总结

总结起来,在败者树中:

  • 空间复杂度:O(n)
  • 查询最小/大值的时间复杂度:O(1)
  • 插入新元素并更新败者树的时间复杂度:O(log_2n)

三 优缺点

A.优点:

  1. 高效查找最小或最大元素

    • 败者树可以在常数时间内找到当前已知数据中的最小或最大元素,这对于实时处理大量数据流和外排序等场景非常有用。
  2. 动态更新

    • 当新的数据项进入时,败者树能够快速地将新数据与现有数据进行比较,并通过自底向上的局部调整迅速反映最新的最小/大值,其更新时间复杂度为 O(log n)
  3. 空间效率

    • 由于败者树可以用数组实现并保持完全二叉树结构,因此空间占用相对较小,空间复杂度为 O(n),其中 n 是原始输入序列的长度。
  4. 缓存友好

    • 在实际硬件中,因为败者树的父节点与其子节点在内存中的位置通常相邻,这有助于提高缓存命中率,从而提升性能。
  5. 可扩展性

    • 败者树适用于多路归并排序算法,可以方便地处理多个输入流同时读取、比较和合并的过程,具有良好的可扩展性和并行性。

B.缺点:

  1. 初始化成本

    • 虽然单次插入的时间复杂度是 O(log n),但在构建初始败者树时需要对所有数据项进行比较,一次性建立整个败者树的时间复杂度为 O(n log n)
  2. 数据特性依赖

    • 如果输入数据已经是部分有序或者有特定分布,败者树的效果可能不如其他适应这些特性的数据结构,如堆。
  3. 不适用静态数据集

    • 对于不需要频繁更新的数据集,如果仅为了寻找最大或最小元素而使用败者树,则显得有些“杀鸡用牛刀”,不如直接对静态数据做一次排序或使用简单的数据结构如优先队列。
  4. 实现复杂性

    • 相比于简单数据结构如栈、队列或堆,败者树的实现和理解要复杂一些,特别是对于包含许多细节的正确性维护以及并发场景下的应用。

四 现实中的应用

  1. 外部排序

    • 当待排序的数据量太大以至于无法一次性装入内存时,败者树是多路归并排序(例如2-way至k-way归并排序)中非常关键的数据结构。它能有效地管理多个输入文件或缓冲区中的最小值,以实现高效的多路合并过程。通过使用败者树,可以在读取磁盘上的数据块时立即确定当前最小记录,并决定下一次应该从哪个数据源读取,从而减少不必要的磁盘I/O操作。
  2. 在线比赛系统

    • 在一些在线竞赛平台或游戏中,败者树可以用来高效地维护当前未决比赛中的胜者和败者队列。当新的比赛结果产生时,可以快速更新队列,并找出下一轮比赛的参赛者。
  3. 实时流处理

    • 在大数据流处理和实时分析中,败者树用于持续监控数据流中的最大/小值,例如监测股票市场的最高价、最低价,或者在传感器网络中寻找异常事件的最大或最小测量值。
  4. 分布式系统和数据库系统

    • 在分布式计算环境中,败者树可用于跨节点进行数据比较和选择,例如在MapReduce框架中作为中间阶段的合并策略。
  5. 算法竞赛和教育

    • 在ACM-ICPC等编程竞赛中,败者树常被用于解决与排序、优先级调度等相关的问题,因其简单而强大的功能受到选手们的青睐。
  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JJJ69

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值