平衡二叉树、跳跃表

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 平衡二叉树、跳跃表


平衡二叉树介绍(AVL树、红黑树)

二叉搜索树Binary Search Tree

二叉搜索树(Binary Search Tree)是一棵满足如下性质(BST性质)的二叉树:

  • 任意结点的关键码≥它左子树中所有结点的关键码
  • 任意结点的关键码≤它右子树中所有结点的关键码

根据以上性质,二叉搜索树的中序遍历必然为一个有序序列

单旋转

AVL树

平衡因子Balance Factor :

  • 一个结点的左子树的高度减去它的右子树的高度。

AVL树

  • 任意结点的平衡因子的绝对值都不超过1,即 balance factor E{-1,0,1}
  • 每个结点需要保存:原始数据、左子结点、右子结点、子树高度

AVL树在插入、删除时,沿途更新结点的高度值

当平衡因子的绝对值大于1时,触发树结构的修正,通过旋转操作来进行平衡

AVL树–平衡因子

插入

旋转场景一:LL

旋转场景二:RR

旋转场景三:LR

旋转场景四:RL

旋转

再举个栗子

插入

RL-先右旋

RL-再左旋

平衡

红黑树

红黑树(Red-black Tree)是一种近似平衡的二叉搜索树

  • 从根到叶子的最长路径≤2*最短路径(简记:高度差在2倍以内)

规则:

  • 每个结点要么是红色,要么是黑色
  • 根结点是黑色
  • 最底层视作有一层空叶结点,是黑色的
  • 不能有两个相邻的红色结点
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点

规则被打破时,通过变色或旋转操作来修正

有非常多的情况需要讨论,这里就不再讲了

相比AVL树,红黑树插入删除更快(旋转少)、更省空间(颜色vs平衡因子)

查询稍慢(不如AVL树更加平衡)

红黑树是许多语言中有序集合、有序映射(例如C++ set, map)的内部实现方式

实战:跳表———Redis内部的数据结构

跳表

跳表(Skip List)是对元素有序的链表的优化,对标的是平衡树和二分查找

  • 二分查找:可以在数组上O(logN)查询,不可修改序列(不能用于链表)
  • 平衡树:支持高效的查询、插入、删除,但比较复杂,不容易实现

跳表是一种查询、插入、删除都是O(logN)的数据结构,

其特点是原理简单、容易实现、方便扩展、效率优秀,在 Redis、LevelDB等热门项目中用于代替平衡树。

链表插入、删除都是O(1),但查询很慢——O(N)

跳表的核心思想:如何提高有序链表的查询效率?

添加第一级索引

添加第二级索引

添加多级索引

查询

  • 从最高级索引、头元素起步
  • 沿着索引查找,直至找到一个大于或等于目标的元素,或者到达索引末尾
  • 如果该元素等于目标,则表明目标已被找到,算法结束
  • 如果该元素大于目标或已到达末尾,则回到当前索引的上一个元素,转入下一级索引,重复2

在一次查询中,每一层至多遍历3个结点

  • 最高级索引只有2个结点
  • 每一级索引遍历的第3个结点必然大于目标——不然的话在上一级索引中应该走得更远才对
  • 时间复杂度≤3*层数=O(logN)

空间复杂度

索引的层数:o(logN)

每层索引的结点数:N/2+N/4 +N/8+ …

整个跳表的结点总数大约为2N (N个原始数据+N个索引结点)

空间复杂度:O(N)

也可以每3个结点建一个索引

这样可以节省一点空间,但相应地造成查询效率略微下降(每层至多遍历3个结点→4个)

复杂度不变,常数有变化(时间和空间的平衡)

插入

先查询,再插入?

问题:插入很多次后,一个索引结点代表的结点数量会增多,如果不更新索引,时间复杂度会退化

解决方案:

重建?——效率太低!

在每个结点上记录它代表的下一级结点个数?——需要维护额外信息,实现复杂

跳表选择的方案是:利用随机+概率!

随机建立索引

现实中的跳表不限制“每2个结点建立一个索引”,而是:

  • 在原始数据中随机n/2个元素建立一级索引
  • 在一级索引中随机n/4个元素建立二级索引
  • 在二级索引中随机n/8个元素建立三级索引

当元素足够多时,可以期望随机出来的索引分布比较均匀

查询的时间复杂度依旧是O(logN)

删除

删除元素很简单,还是基于查询

在此过程中把原始链表和各级索引中对应的结点(如果有的话)都删掉就行了

时间复杂度O(logN)

现实中跳表的形态

https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

实战:树堆——最容易实现的平衡树之一

随机的艺术

在数据足够多的情况下,“随机”就是最自然、趋于平衡的

  • 快速排序“随机”选取中轴(pivot),按大小分成两半,期望层数O(logN)
  • 在一条数轴上随机撒点,点的期望分布是均匀的
  • 跳表通过随机来选取索引元素,以及决定是否更新索引
  • 把一组随机数据插入二叉搜索树(BST),期望是平衡的,即O(logN)层

对BST的结点进行旋转,不影响数据的有序性,可以让树变得更加平衡

问题:如何决定要不要旋转?

思路:产生一组额外的随机数据,让它们满足某种性质,从而形成一个趋于平衡的树结构

Treap = Tree + Heap

树堆(Treap)的每个结点保存两个值

  • 原始数据,也叫关键码
  • 额外的权值,是随机生成的

树堆首先是一棵二叉搜索树,结点的关键码(原始数据)满足BST性质:左≤根≤右

树堆也是一个堆,结点的额外权值满足大根堆形式:父≥子

Treap各项操作的时间复杂度均为O(logN)

Treap检索、求前驱、求后继的操作与普通BST一致——一次递归查找

BST–检索

BST–求前驱/后继

插入/删除

Treap先通过类似于BST的检索,找到需要插入新结点的位置

插入后,给新结点随机生成一个额外的权值

然后像二叉堆的插入过程一样,自底向上依次检查

当某个节点不满足大根堆性质时,就执行单旋转,使该点与其父节点的关系发生对换

删除时,首先通过检索找到需要删除的结点

由于Treap支持旋转,可以把需要删除的结点向下旋转成叶结点

最后直接删除

非常简便,避免了BST 删除操作对各种情况的讨论,也避免了维护堆性质

插入0,再删除0

插入

旋转

实战

1206.设计跳表

https://leetcode.cn/problems/design-skiplist/submissions/

constexpr int MAX_LEVEL = 32;
constexpr double SKIPLIST_P = 0.25;
struct Node {
    int val;
    vector<Node*> next;
    Node(int val, int level) : val(val), next(vector<Node*>(level, nullptr)) {}
};
class Skiplist {
private:
    int curLevel;
    Node* head;
    //生成1~maxLevel之间的数字. 且1/2概率返回2, 1/4概率返回3...
    int randomLevel() {
        int level = 1;
        while (((double)rand() / (RAND_MAX)) < SKIPLIST_P && level < MAX_LEVEL) ++level;
        return level;
    }
public:
    Skiplist() : curLevel(0), head(new Node(-1, MAX_LEVEL)) {   //根据题目中num的取值范围, 我们让head值为-1即可保证head不会被更新
    }
    bool search(const int target) {
        auto cur = head;
        for (int i = curLevel - 1; i >= 0; --i) {
            //找到第i层的最大的小于target的元素. 0层在下, max_level在上. 越下层的元素越多
            while (cur->next[i] && cur->next[i]->val < target) cur = cur->next[i];
        }
        //已经到第0层了
        cur = cur->next[0];
        //检查当前元素的值是否等于target
        return cur && cur->val == target;
    }
    void add(int num) {
        //存放每层需要更新的位置. 我们先假设都更新的是head
        vector<Node*> update(MAX_LEVEL, head);
        auto cur = head;
        for (int i = curLevel - 1; i >= 0; --i) {
            //找到所有层的值小于num的最后一个结点
            while (cur->next[i] && cur->next[i]->val < num) cur = cur->next[i];
            update[i] = cur;    //该节点即为num应当插入的位置的前驱节点
        }
        auto level = randomLevel(); //随机插入任意一层
        curLevel = max(curLevel, level);
        auto node = new Node(num, level); //创建要插入的节点, 其值为num, 其层级为randomLevel
        //在所有预期的层级中插入随机出来的node. 从第0层开始插入到其可能的最上层!
        for (int i = 0; i < level; ++i) {
            node->next[i] = update[i]->next[i]; //与其后缀节点建立联系
            update[i]->next[i] = node;  //与其前驱结点建立联系
        }
    }
    bool erase(int num) {
        //记录每层要更新的位置. 依然假定要更新的为head
        vector<Node*> update(MAX_LEVEL);
        auto cur = head;
        for (int i = curLevel - 1; i >= 0; --i) {
            while (cur->next[i] && cur->next[i]->val < num) cur = cur->next[i];
            update[i] = cur;
        }
        cur = cur->next[0]; //返回当前层的下一个节点
        if (!cur || cur->val != num) return false;  //若不存在num的节点, 则返回false
        for (int i = 0; i < curLevel; ++i) {
            if (update[i]->next[i] != cur) break;   //从最下层开始向上遍历, 若有一层的后面的节点不为cur, 则说明cur没能进入这一层(以及更上层). 则我们可以直接退出循环
            update[i]->next[i] = cur->next[i];  //更新当前层的节点. 在当前层中移除cur
        }
        delete cur; //我们可以将cur回收
        while (curLevel > 1 && !head->next[curLevel-1]) --curLevel; //若当前的最上层已经只有一个head了, 则我们可以直接将当前层移除掉
        return true;
    }
};


推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
存储 算法 数据库
树中枝繁叶茂:探索 B+ 树、B 树、二叉树、红黑树和跳表的世界
树中枝繁叶茂:探索 B+ 树、B 树、二叉树、红黑树和跳表的世界
65 0
|
11月前
|
存储 C++
二叉树搜索树的应用
二叉树搜索树的应用
55 1
|
6月前
|
数据格式
树结构练习——排序二叉树的中序遍历
树结构练习——排序二叉树的中序遍历
Java数据结构——平衡二叉树(AVL树)
Java数据结构——平衡二叉树(AVL树)
Java数据结构——平衡二叉树(AVL树)
|
存储 算法 关系型数据库
有了二叉树,平衡二叉树为什么还需要红黑树
有了二叉树,平衡二叉树为什么还需要红黑树
101 0
有了二叉树,平衡二叉树为什么还需要红黑树
数据结构-各种树(二叉树、二叉查找树、平衡二叉树、红黑树、B树、B+树)
数据结构-各种树(二叉树、二叉查找树、平衡二叉树、红黑树、B树、B+树)
|
算法
二叉搜索树、平衡二叉树
一、二叉搜索树 这里我们不用太多书面化的语言来定义,笔者认为在讨论数据结构、算法相关的内容时用太多书面化、学术化的语言是一种让人很烦的事情。咬文嚼字,不便于读者理解。 简单来说二叉树搜索树,其实就是用来做二分查找的一种二叉树。 特点是:根节点的左子树值均小于根节点的值,根节点的右子树值均大于根节点的值。 比如123 4 567建树的结果就是
56 0
|
存储
基于中序有序的二叉搜索树(上)
基于中序有序的二叉搜索树
|
存储
基于中序有序的二叉搜索树(下)
基于中序有序的二叉搜索树
|
存储 算法 C++
常见数据结构-二叉树(下)二叉查找树
常见数据结构-二叉树(下)二叉查找树
117 0