📜  左派树左派堆(1)

📅  最后修改于: 2023-12-03 15:09:41.510000             🧑  作者: Mango

左派树 & 左派堆

简介

左派树和左派堆是两种常见的堆的数据结构,常常用于优先队列的实现。

左派堆是一种可以迅速合并的堆,它可以在 $O(\log n)$ 的时间复杂度下合并两个堆,也可以在 $O(\log n)$ 的时间内插入元素和删除堆顶。

左派树实现了左式堆,也是一种可以迅速合并的优先队列。

左派堆

左派堆是一种可以迅速合并的堆,它基于一个特殊的性质:左儿子的距离一定大于等于右儿子的距离(即左儿子一定比较“左”)。这个性质使得左派堆在合并的时候可以利用一些 clever 的技巧来达到 $O(\log n)$ 的合并时间复杂度。

数据结构

左派堆通常由一个树状结构组成,其中每个节点都有以下三个成员:

  • val:节点的值。
  • dist:节点的距离值。
  • leftright:左右儿子。
合并操作

为了方便描述,假设现在有两个左派堆 $H_1$ 和 $H_2$,我们需要将它们合并成一个新的左派堆 $H$。

当 $H_1$ 或 $H_2$ 为空堆时,直接返回另一个非空堆即可。

否则,根据左派堆的性质,我们需要让 $H$ 的根节点为 $H_1$ 和 $H_2$ 中距离值更小的那个节点。具体做法是将 $H_1$ 和 $H_2$ 的根节点分别作为新堆 $H$ 的左右儿子,然后再递归地合并 $H$ 的两个儿子得到新的 $H$。

struct Node {
    int val, dist;
    Node *left, *right;
    Node(int x) : val(x), dist(0), left(nullptr), right(nullptr) {}
};

Node* merge(Node* h1, Node* h2) {
    // 空堆情况
    if (h1 == nullptr) return h2;
    if (h2 == nullptr) return h1;
    // 让 h1 为距离较小的堆
    if (h1->val > h2->val) swap(h1, h2);
    // 将 h1->right 和 h2 合并到新堆 h1->right 上
    h1->right = merge(h1->right, h2);
    // 令 h1 的左儿子更加“左”
    if (h1->left == nullptr || h1->left->dist < h1->right->dist) swap(h1->left, h1->right);
    // 更新距离值
    h1->dist = h1->right == nullptr ? 0 : h1->right->dist + 1;
    return h1;
}
插入操作

插入操作相对简单,只需要将插入的元素看作一个单节点左派堆,然后将其与当前堆合并即可。

Node* insert(Node* h, int x) {
    auto node = new Node(x);
    return merge(h, node);
}
删除操作

删除操作比较复杂,但基本思路和合并是一样的。我们需要将根节点删去,然后将其左右儿子合并成一个新的堆。

Node* remove(Node* h) {
    auto left = h->left;
    auto right = h->right;
    delete h;
    return merge(left, right);
}
左派树

左派树是一种实现了左式堆的数据结构。它可以完成左派堆的所有操作(包括合并、插入和删除),并且在保证合并的时间复杂度 $O(\log n)$ 的同时,可以更灵活地应对优先队列的各种需求。

数据结构

左派树同样由一个树状结构组成,其中每个节点也有以下三个成员:

  • val:节点的值。
  • leftright:左右子树。
  • dist:节点的 siftdown 距离。
堆操作

左派树和左派堆的堆操作大致相同,只是堆节点的结构发生了一些变化。

插入操作

插入操作和左派堆非常相似,只需要插入一个新的节点,然后尝试将其左右子树用 siftdown 方法调整成“左倾树”即可。

node* insert(Node* root, int x) {
    auto node = new Node(x);
    return merge(root, node);
}
删除操作

删除操作和左派堆类似,只需要将根节点删除,然后将其左右儿子合并成一个新的堆。

Node* remove(Node* root) {
    return merge(root->left, root->right);
}
合并操作

合并操作是左派树最核心的操作之一,它主要利用了左派树的一些特殊性质。

具体做法是将左右两棵子树递归地合并,然后根据节点的 siftdown 距离和节点的大小进行一些 clever 的旋转操作,使得节点更加倾向于左子树的位置。

node* merge(Node* h1, Node* h2) {
    // 同左派堆
    if (h1 == nullptr) return h2;
    if (h2 == nullptr) return h1;
    // 让 h1 始终是较小堆
    if (h1->val > h2->val) swap(h1, h2);
    // 递归地合并 h1 和 h2 的子树
    auto right = merge(h1->right, h2);
    h1->right = right;
    // 旋转节点
    if (h1->left == nullptr || h1->left->dist < h1->right->dist) swap(h1->left, h1->right);
    h1->dist = h1->right == nullptr ? 0 : h1->right->dist + 1;
    return h1;
}
总结

左派树和左派堆是两种十分实用的数据结构,尤其适用于优先队列的实现。除此之外,它们也可以直接用于其他需要堆的场合。

左派堆和左派树对于刚开始接触它们的新手来说,可能并不好理解。但是只要掌握了其基本思想,就可以愉快地享用它们带来的好处啦!