AVL 树实现

news2024/11/22 16:18:19

AVL 树的概念

也许因为插入的值不够随机,也许因为经过某些插入或删除操作,二叉搜索树可能会失去平衡,甚至可能退化为单链表,造成搜索效率低。

二叉树不平衡

AVL Tree 是一个「加上了额外平衡条件」的二叉搜索树,其平衡条件的建立是为了确保整棵树的深度为 O ( l o g 2 N ) O(log_2N) O(log2N)

AVL Tree 要求任何节点的左右子树高度相差最多为 1。当违反该规定时,就需要进行旋转来保证该规定。

AVL 树的实现

节点的定义

AVL 树节点的定义比一般的二叉搜索树复杂,它需要额外一个 parent 指针,方便后续旋转。并在每个节点中引入平衡因子,便于判断是否需要旋转。

/// @brief AVL 树节点结构
/// @tparam K 节点的 key 值
/// @tparam V 节点的 value 值
template <class K, class V>
struct AVLTreeNode {
	AVLTreeNode(const pair<K, V>& kv) 
		: _kv(kv)
		, _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		, _bf(0)
	{}

	pair<K, V> _kv;
	AVLTreeNode<K, V>* _parent;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
    // 左右子树高度相同平衡因子为:0
    // 左子树高平衡因子为负
    // 右子树高平衡因子为正
	int _bf;
};

接口总览

template<class K, class V>
class AVLTree {
	typedef AVLTreeNode<K, V> Node;
public:
	Node* Find(const K& key);
	bool Insert(const pair<K, V>& kv);

private:
	void RotateR(Node* parent);
	void RotateL(Node* parent);
	void RotateLR(Node* parent);
	void RotateRL(Node* parent);
private:
	Node* _root = nullptr;
};

查找

AVL 树的查找和普通的搜索二叉树一样:

  • 若 key 值大于当前节点的值,在当前节点的右子树中查找
  • 若 key 值小于当前节点的值,在当前节点的左子树中查找
  • 若 key 值等于当前节点的值,返回当前节点的地址
  • 若找到空,查找失败,返回空指针
/// @brief 查找指定 key 值
/// @param key 要查找的 key
/// @return 找到返回节点的指针,没找到返回空指针
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur != nullptr) {
        // key 值与当前节点值比较
        if (key > cur->_kv.first) {
            cur = cur->_right;
        } else if (key < cur->_kv.first) {
            cur = cur->_left;
        } else {
            return cur;
        }
    }
    return nullptr;
}

插入

AVL 的插入整体分为两步:

  1. 按照二叉搜索树的方式将节点插入
  2. 调整节点的平衡因子

平衡因子是怎么调整的?

设新插入的节点为 pCur,新插入节点的父节点为 pParent。在插入之前,pParent 的平衡因子有三种可能:0、-1、1。

插入分为两种:

  • pCur 插入到 pParent 的左侧,将 pParent 的平衡因子减 1
  • pCur 插入到 pParent 的右侧,将 pParent 的平衡因子加 1

此时,pParent 的平衡因子可能有三种情况:0、正负 1、正负 2。

  1. 0:说明插入之前是正负 1,插入后被调整为 0,满足 AVL 性质插入成功
  2. 正负 1:说明插入之前是 0,插入后被调整为正负 1,此时 pParent 变高,需要继续向上更新
  3. 正负 2:说明插入之前是正负 1,插入后被调整为正负 2,此时破坏了规定,需要旋转处理
/// @brief 插入指定节点
/// @param kv 待插入的节点
/// @return 插入成功返回 true,失败返回 false
bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        return true;
    }

    // 先找到要插入的位置
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur != nullptr) {
        if (kv.first > cur->_kv.first) {
            parent = cur;
            cur = cur->_right;
        } else if (kv.first < cur->_kv.first) {
            parent = cur;
            cur = cur->_left;
        } else {
            // 已经存在,插入失败
            return false;
        }
    }

    // 将节点插入
    cur = new Node(kv);
    if (kv.first > parent->_kv.first) {
        parent->_right = cur;
        cur->_parent = parent;
    } else {
        parent->_left = cur;
        cur->_parent = parent;
    }

    // 更新平衡因子,直到正常
    while (parent != nullptr) {
        // 调整父亲的平衡因子
        if (parent->_left == cur) {
            --parent->_bf;
        } else {
            ++parent->_bf;
        }

        if (parent->_bf == 0) {
            // 此时不需要再继续调整了,直接退出
            break;
        } else if (parent->_bf == 1 || parent->_bf == -1) {
            // 此时需要继续向上调整
            cur = parent;
            parent = parent->_parent;
        } else if (parent->_bf == 2 || parent->_bf == -2) {
            // 此时需要旋转处理
            if (parent->_bf == -2 && cur->_bf == -1) {
                RotateR(parent);
            } else if (parent->_bf == 2 && cur->_bf == 1) {
                RotateL(parent);
            } else if (parent->_bf == -2 && cur->_bf == 1) {
                RotateLR(parent);
            } else if (parent->_bf == 2 && cur->_bf == -1) {
                RotateRL(parent);
            } else {
                assert(false);
            }
            // 旋转完了就平衡了,直接退出
            break;
        } else {
            // 此时说明之前就处理错了
            assert(false);
        } // end of if (parent->_bf == 0)
    } // end of while (parent != nullptr)
    return true;
}

旋转

假设平衡因子为正负 2 的节点为 X,由于节点最多拥有两个子节点,因此可以分为四种情况:

  1. 插入点位于 X 的左子节点的左子树——左左:右单旋
  2. 插入点位于 X 的左子节点的右子树——左右:左右双旋
  3. 插入点位于 X 的右子节点的右子树——右右:左单旋
  4. 插入点位于 X 的右子节点的左子树——右左:右左双旋

AVL 破坏

右单旋

单旋

假设平衡因子为正负 2 的节点为 parent,parent 的父节点为 pParent,parent 的左子树为 subL,subL 的右子树为 subLR。

右单旋的操作流程:

  1. 让 subLR 作为 parent 的左子树
  2. 让 parent 作为 subL 的右子树
  3. 让 subL 作为整个子树的新根
  4. 更新平衡因子
/// @brief 进行右单旋
/// @param parent 平衡因子为正负 2 的节点
void RotateR(Node* parent) {
    Node* pParent = parent->_parent;
    Node* subL = parent->_left;
    Node* subLR = parent->_left->_right;

    // 更改链接关系
    // 1. subLR 作为 parent 的左子树
    parent->_left = subLR;
    if (subLR != nullptr) {
        subLR->_parent = parent;
    }
    // 2. parent 作为 subL 的右子树
    subL->_right = parent;
    parent->_parent = subL;

    // 3. subL 作为整个子树的新根
    if (parent == _root) {
        // parent 为 _root,此时令 subL 为 _root
        _root = subL;
        subL->_parent = nullptr;
    } else {
        // parent 不为 _root,pParent 也就不为空
        if (parent == pParent->_left) {
            pParent->_left = subL;
        } else {
            pParent->_right = subL;
        }
        subL->_parent = pParent;
    }

    // 4. 更新平衡因子
    // 观察上图明显可知
    subL->_bf = 0;
    parent->_bf = 0;
}

左单旋

左单旋与右单旋类似,只是方向不同。

假设平衡因子为正负 2 的节点为 parent,parent 的父节点为 pParent,parent 的右子树为 subR,subR 的左子树为 subRL。

左单旋的操作流程:

  1. 让 subRL 作为 parent 的右子树
  2. 让 parent 作为 subR 的左子树
  3. 让 subR 作为整个子树的新根
  4. 更新平衡因子
/// @brief 进行左单旋
/// @param parent 平衡因子为正负 2 的节点
void RotateL(Node* parent) {
    Node* pParetn = parent->_parent;
    Node* subR = parent->_right;
    Node* subRL = parent->_right->_left;

    // 更改链接关系
    // 1. subRL 作为 parent 的右子树
    parent->_right = subRL;
    if (subRL != nullptr) {
        subRL->_parent = parent;
    }
    // 2. parent 作为 subR 的左子树
    subR->_left = parent;
    parent->_parent = subR;

    // 3. subR 作为整个子树的新根
    if (parent == _root) {
        _root = subR;
        subR->_parent = nullptr;
    } else {
        if (parent == pParetn->_left) {
            pParetn->_left = subR;
        } else {
            pParetn->_right = subR;
        }
        subR->_parent = pParetn;
    }

    // 4. 更新平衡因子
    subR->_bf = 0;
    parent->_bf = 0;
}

左右双旋

双旋1

假设平衡因子为正负 2 的节点为 parent,parent 的左子树为 subL,subL 的右子树为 subLR。

左右双旋就是对 subL 进行一次左单旋,对 parent 进行一次右单旋。双旋也就完成了,要注意的是双旋后平衡因子的更新。

此时分三种情况:

  1. 新插入的节点是 subLR 的右子树

双旋更新1

  1. 新插入的节点是 subLR 的左子树

双旋更新2

  1. 新插入的是 subLR

双旋更新3

结合上述情况,写出如下代码:

/// @brief 进行左右双旋
/// @param parent 平衡因子为正负 2 的节点
void RotateLR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = parent->_left->_right;
    int bf = subLR->_bf;

    RotateL(subL);
    RotateR(parent);

    if (bf == 1) {
        // 新插入节点是 subLR 的右子树
        parent->_bf = 0;
        subL->_bf = -1;
        subLR->_bf = 0;
    } else if (bf == -1) {
        // 新插入的节点是 subLR 的左子树
        parent->_bf = 1;
        subL->_bf = 0;
        subLR->_bf = 0;
    } else if (bf == 0) {
        // 新插入的节点是 subLR
        parent->_bf = 0;
        subL->_bf = 0;
        subLR->_bf = 0;
    } else {
        assert(false);
    }
}

右左双旋

假设平衡因子为正负 2 的节点为 parent,parent 的右子树为 subR,subR 的左子树为 subRL。

右左双旋就是对 subR 进行一次右单旋,对 parent 进行一次左单旋。流程和左右双旋一样,这里就不过多介绍了。

void RotateRL(Node* parent) {
    Node* subR = parent->_right;
    Node* subRL = parent->_right->_left;
    int bf = subRL->_bf;

    RotateR(subR);
    RotateL(parent);

    if (bf == 1) {
        // 新插入节点是 subRL 的右子树
        parent->_bf = -1;
        subR->_bf = 0;
        subRL->_bf = 0;
    } else if (bf == -1) {
        // 新插入的节点是 subRL 的左子树
        parent->_bf = 0;
        subR->_bf = 1;
        subRL->_bf = 0;
    } else if (bf == 0) {
        // 新插入的节点是 subRL
        parent->_bf = 0;
        subR->_bf = 0;
        subRL->_bf = 0;
    } else {
        assert(false);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/384475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

buu [MRCTF2020]Easy_RSA 1

题目描述&#xff1a; import sympy from gmpy2 import gcd, invert from random import randint from Crypto.Util.number import getPrime, isPrime, getRandomNBitInteger, bytes_to_long, long_to_bytes import base64from zlib import * flag b"MRCTF{XXXX}" …

【基础算法】单链表的OJ练习(1) # 反转链表 # 合并两个有序链表 #

文章目录前言反转链表合并两个有序链表写在最后前言 上一章讲解了单链表 -> 传送门 <- &#xff0c;后面几章就对单链表进行一些简单的题目练习&#xff0c;目的是为了更好的理解单链表的实现以及加深对某些函数接口的熟练度。 本章带来了两个题目。一是反转链表&#x…

Springboot怎么实现restfult风格Api接口

前言在最近的一次技术评审会议上&#xff0c;听到有同事发言说&#xff1a;“我们的项目采用restful风格的接口设计&#xff0c;开发效率更高&#xff0c;接口扩展性更好...”&#xff0c;当我听到开头第一句&#xff0c;我脑子里就开始冒问号&#xff1a;项目里的接口用到的是…

Django实践-03模型-01表生成模型

文章目录Django实践-03模型Django MTV之模型投票案例1.创建应用1.创建应用2.配置模板文件2.配置关系型数据库MySQL1.创建数据库2.创建表3.按照MySQL依赖4.修改settings.py文件 添加应用 配置数据库5. 基于数据库生成实体类3.使用ORM完成模型的CRUD操作1.新增2.删除3.更新4.查询…

代数小课堂:向量代数(方向比努力更重要)

文章目录 引言I 数字的方向性1.1 箱子受力1.2 爆破逃离方向II 向量的表示法2.1 极坐标方法对向量表示2.2 终点的坐标表示向量III 向量的计算3.1 计算向量的长度和方向3.2 平行四边形法则(计算向量的长度)引言 代数学除了带来了方程和函数工具,还揭示了关于数字的另一个规律,…

C++——特殊类设计

目录 不能被拷贝的类 只能在堆上创建对象的类 只能在栈上创建对象的类 不能被继承的类 只能创建一个对象的类(单例模式) 饿汉模式 懒汉模式 单例对象释放问题 不能被拷贝的类 C98&#xff1a;将拷贝构造函数与赋值运算符重载只声明不定义&#xff0c;并且将其访问权…

React Native学习笔记(2.基本语法-类组件)

1. 基本语法 (1). 引入组件。(2). 继承共通。(3). 定义render函数。(4). 返回文本。(5). export导出 2. 自定义组件&#xff08;引用&#xff09; 将上面定义的"cat“组件引用到当前文件里 (1). inprot引入。(2). 使用 3. 自定义组件&#xff08;参数定义与传参&#x…

【Linux】项目自动化构建工具——make/Makefile

目录 1.make与Makefile的关系 Makefile make 项目清理 clean .PHONY 当我们编写一个较大的软件项目时&#xff0c;通常需要将多个源文件编译成可执行程序或库文件。为了简化这个过程&#xff0c;我们可以使用 make 工具和 Makefile 文件。Makefile 文件可以帮助我们自动…

你知道Java中的JCP, JEP, JLS, JSR是什么意思吗?

目录 一、JCP 二、JSR 三、JLS 四、JEP 公众号&#xff1a;MCNU云原生&#xff0c;欢迎微信搜索关注&#xff0c;更多干货&#xff0c;及时掌握。 JCP, JEP, JLS, JSR这些概念是Java社区中的一些概念&#xff0c;但是没有没有经常关注社区的童鞋们未必知道这些缩写所代表的…

centos7搭建FTP

1.简介文件传输协议&#xff08;File Transfer Protocol&#xff0c;FTP&#xff09;是用于在网络上进行文件传输的一种协议&#xff0c;工作于OSI&#xff0c;TCP的应用层&#xff0c;客户端和服务端之前连接要经过一次TCP的三次握手&#xff0c;其作用就是可以使用户以文件操…

第十二章 实现shallowReadonly功能

实现shallowReadonly功能 shallowReadonly&#xff1a; 让一个响应式数据变为只读的(浅只读) 接下来附上测试用例&#xff1a; import { isReadonly,shallowReadonly } from "../reactive"describe(shallowReadonly,()>{test(should not make non-reactive pro…

Session会话管理

会话管理Web会话管理概述常见的Web应用会话管理方式基于Server端的Session的管理方式基于Cookie的Session的管理方式Cookie与Session最大的区别Cookie-Based的管理方式基于Token-Based的管理方式Web会话管理的安全问题Web会话管理概述 会话管理&#xff1a;在进行人机交互的时…

java高级篇之三大性质总结:原子性、可见性以及有序性

1. 三大性质简介 在并发编程中分析线程安全的问题时往往需要切入点&#xff0c;那就是两大核心&#xff1a;JMM抽象内存模型以及happens-before规则&#xff08;在这篇文章中已经经过了&#xff09;&#xff0c;三条性质&#xff1a;原子性&#xff0c;有序性和可见性。关于sy…

JavaSE:常用类

前言从现在开始进入高级部分的学习&#xff0c;鼓励自己一下&#xff01;画个大饼&#xff1a; 常用类->集合框架->IO流->多线程->网络编程 ->注解与反射->GUI很重要的东西&#xff0c;不能不会&#xff01;Object类祖宗类&#xff0c;主要方法&#xff1a;t…

接口测试简介

接口测试简介 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。 测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑依赖关系等。 ——百度百科&#xff01; …

低代码开发与传统开发有什么不同?有什么价值?

低代码开发与传统开发有些什么不同&#xff1f;有什么价值&#xff1f; 自2014年Forrester明确提出低代码&#xff08;Low-Code&#xff09;概念以来&#xff0c;这一领域已经逐步升温。近年来&#xff0c;低代码凭借其低开发门槛和易用性等优点赢得了众多投资研究机构和企业用…

设计模式(十四)----结构型模式之组合模式

1 概述 对于这个图片肯定会非常熟悉&#xff0c;上图我们可以看做是一个文件系统&#xff0c;对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树&#xff0c;当我们找到某个叶子节点后&#xff0c;就可以对叶子节点进行相关的操作。可以将这颗树…

Cookie原理及JAVA端关于Cookie的增删改查操作

什么是Cookie 在java中&#xff0c;Cookie是来自于Servlet规范中一个工具类&#xff0c;存在于Tomcat提供servlet-api.jar中Cookie存放当前用户的私人数据 Cookie原理 用户打开浏览器第一次&#xff08;指每次重新打开浏览器的第一次&#xff0c;而非指历来第一次&#xff0…

ChatGPT可以作为一个翻译器吗?

论文地址&#xff1a;https://arxiv.org/abs/2301.08745.pdf 背景 自从OpenAI2022年11月30日发布ChatGPT以来&#xff0c;基本上把NLP所有任务大统一了&#xff0c;那么在机器翻译的表现到底如何呢&#xff1f;腾讯AI Lab在翻译Prompt、多语言翻译以及翻译鲁棒性三方面做了一…

365天深度学习训练营-第J4周:ResNet与DenseNet结合探索

目录 一、前言 二、论文解读 三、DPN代码复现 四、总结 一、前言 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制● 难度&#xff1a;夯实基础⭐⭐ ● 语言&#xff1a;Python3、Pytorc…