C++ 实现红黑树

在这里插入图片描述

红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。

红黑树的性质

  1. 每个结点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,则它的两个孩子结点一定是黑色的。
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

红黑树第四条性质的理解:

在这里插入图片描述

如上图所示,该红黑树一共存在11条路径,每条路径上黑色节点的数量均为2
路径 1: 13 ----> 8 ----> 1 ----> NULL 黑色节点数量:2
路径 2: 13 ----> 8 ----> 1 ----> 6 ----> NULL (6这个节点的左孩子) 黑色节点数量:2
路径 3: 13 ----> 8 ----> 1 ----> 6 ----> NULL (6这个节点的右孩子) 黑色节点数量:2
··········

思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

最短路径的理解:红黑树的节点要么是红色要么是黑色,并且要求**从根节点到空节点路径(以下简称路径)**上的黑色节点的数量一样多,因此我们可以得出结论:当一颗红黑树路径上的黑色节点数量确定时,路径上的节点全部为黑色节点即为路径长度的最小值

最长路径的理解:因为红黑树要求路径上不能出现连续的红色节点,并且根节点必须为黑色,因此,最长路径的节点颜色变化一定是:黑 --> 红 --> 黑 --> 红 --> 黑 --> 红 --> ·······

综上所述:当一颗红黑树路径上的黑色节点数量确定时,最短路径 * 2 <= 最长路径
例如(如下图所示):一颗红黑树路径上的黑色节点数量为 2 ,最短路径:黑 --> 黑 最长路径:黑 --> 红 --> 黑 -->红。

在这里插入图片描述

红黑树节点定义

红黑树节点存储的数据类型我们依然选择 key-value 的结构,原因是 STL 库中的 map 和 set 底层的数据结构就是红黑树,到时候我们模拟实现 map 和 set 的时候,就要用我们自己实现的红黑树作为他们的底层数据结构。

我们先来思考一个问题:

红黑树应当插入什么颜色的节点?

红黑树的性质4:从根节点出发,到达每一个空节点的简单路径上黑色节点的数量是一样的。

性质4要求路径上的黑色节点数量相同,一条路径上黑色节点数量改变,那么我们必须调整其他路径的黑色节点数量,显然代价十分巨大。

因此,红黑树的应始终插入红色节点,这样就会有两种情况:

  • 新插入节点的父亲是黑色,新节点插入完成。
  • 新插入节点的父亲是红色,只需调整当前路径上节点的颜色使其满足红黑树的性质即可。(如下图所示)

在这里插入图片描述

有了上面的知识,我们就可以定义出红黑树的节点,以及怎么书写节点的构造函数了。

#pragma once

enum Color
{
    RED,
    BLACK
};

template<class K, class V>
struct RBTreeNode
{
    pair<K, V> _kv;

    RBTreeNode<K, V>* _parnet; //父节点指针
    RBTreeNode<K, V>* _left; //左孩子
    RBTreeNode<K, V>* _right; //右孩子

    Color _col; //节点的颜色
	
    //构造函数
    RBTreeNode(const pair<K, V>& kv)
        :_kv(kv)
        ,_parent(nullptr)
        ,_left(nullptr)
        ,_right(nullptr)
        _col(RED) //新插入的节点默认是红色的
    {}
};

template<class K, class V>
class AVLTree
{
    typedef RBTreeNode<K, V> Node;
public:
    AVLTree()
        :_root(nullptr) //初始时候根节点为为 nulptr
    {}

private:
    Node* _root;
};

红黑树的插入

插入一个新的节点,最重要的是插入之后经过变化,使得新插入一个节点之后,整棵树依然满足红黑树的性质。

1:父节点为黑色

这种情况在一开始就探讨过了,新插入的节点父亲为黑色,直接插入就完事儿了!新插入节点的位置在哪里?还是根据二叉搜索树的那里的插入一样。

在这里插入图片描述

2:父节点为红色,叔叔存在且为红

这个叔叔节点是什么呢?叔叔节点就是父节点的亲兄弟,你懂我那个意思吧!

在这里插入图片描述

这种情况应该怎么做呢?

将父节点,叔叔节点的颜色改为黑色,将祖父节点的颜色改为红色。

插入新的节点之前,该树是一颗红黑树,满足红黑树的性质,其中性质三:如果一个节点是红色的,则它的两个孩子结点一定是黑色的

这句话说明一条路径上不可能存在连续的两个红色节点。因此,如果父节点的颜色为红色,那么父节点的父节点的颜色一定为黑色。将父节点颜色改为红色,父节点颜色改为黑色,叔叔节点改为黑色,所有路径黑色节点的数量依旧相同。

在这里插入图片描述

  • 我们发现经过这样一次变化之后一条路径上出现了连续的红节点(17 和 25),任然不满足红黑树的性质

  • 我们将红黑树的插入情况进行了分类,每种插入情况都会对应一种调整方式(或者直接完成插入,情况1),调整后并不一定能完成插入,而是进入了红黑树插入分类的另一种情况(或者与上一次的情况一样,这个例子中就是与上一次的情况相同)。因此,我们需要更新父节点,叔叔节点,祖父以及新插入的节点(cur),判断更新后处于插入的哪种情况,直到完成插入。

  • 更新方式:令新插入的节点(cur)指向祖父节点,然后找到新的父节点,叔叔节点,祖父节点即可。

我们发现,我们的这个例子就是与上一次变换的方式相同,直接继续进行相同的变换即可。

在这里插入图片描述

我们可以看到在向上更新,变换颜色的过程中可能将根节点的颜色搞成红色。根据红黑树的性质二:**根节点是黑色的。**如果出现这种情况就需要将根节点的颜色改成黑色。这样修改之后每条路径上的黑色节点数量都会加一,该树依然满足红黑树的性质。

3:父节点为红色,叔叔存在且为黑

我们先来证明新插入一个节点之后的第一次调整颜色是不可能出现这种情况的!一定是另一种情况经过调整,更新 cur,parent,uncle,grandparent 之后出现的这种情况!

证明:如果新插入的节点的叔叔节点是黑色,因为父亲节点和叔叔节点到根节点的路径是相同的,如果叔叔节点是黑色的,那么,父亲节点必然也是黑色的(根据红黑树的性质思:不同路径上的黑色节点数量相同),因为是新插入一个节点嘛,父节点之下是不可能有黑色节点的,只能是父节点本身是黑色的。如果父节点是黑色的,在父节点下插入新节点时,我们的插入就直接成功了,不需要调整。

综上所述,在插入新节点的第一次调整节点颜色的时候是不可能出现这种情况的!只能是某种情况进行调整节点颜色之后,出现的这种情况!

我们来看下面的例子:

我们向下面的一颗红黑树中插入一个新节点 28,发现调整颜色的策略和 2 相同,我们就进行相应的颜色调整。

在这里插入图片描述

第一次调整之后的结果就是这个样子啦:是不是就满足我们的第三种情况啦!

在这里插入图片描述

那么这种情况应该怎么调整节点的颜色呢!!仔细观察你会发现无论怎么调整节点的颜色都比较麻烦,于是我们就要使用旋转啦!那进行什么类型的旋转呢?这就得根据 cur,parent,grandparent 的相对位置来看啦:

在这里插入图片描述

旋转想必你在 AVL 树的学习过程中已经了然于胸了!

我们上面的那个例子就是一个左单旋哈:以 grandparent 为旋转点进行左单旋。

在这里插入图片描述

第三种情况旋转之后都需要将 grandparent 改为红色,parent 改为黑色哈!

4:父节点为红色,叔叔不存在

在下面的例子中,我们向这颗红黑树中插入了 28 这个节点,满足情况四:父节点为红色,叔叔不存在,和情况三一样,同样需要根据 cur,parent,grandparent 的相对位置确定旋转方式。如下图所示:显然是一个左单旋哈!

在这里插入图片描述

左单旋之后的结果:

在这里插入图片描述

很明显仅仅是左单旋之后还不满足红黑树的性质,我们需要调整颜色,调整颜色的策略和情况三是一样的:将 grandparent 的颜色改为红色,将 parent 的颜色改为黑色即可。

聪明的你肯定已经发现了情况三和情况四其实是可以合并的啦!合并之后代码看起来就简单多了!

检查一棵树是不是红黑树

原理就是根据红黑树的性质来嘛!我们可以先求出一条路径中黑色节点的数量。然后根据这个基准值来对比其他路径黑色节点的数量!在求其他路径黑色节点的数量时,记得检查是否出现连续的红色节点哦!

代码

#pragma once

enum Color
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	Color _col; //节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED) //节点默认的颜色是红色
	{}
};

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
        //插入的时候如果根节点为 nullptr, 申请一个节点,将该节点的颜色改为黑色之后作为根节点就行啦
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
        //记录父节点
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur) //找到新节点的插入位置
		{
			if (cur->_kv.first < kv.first) //新插入的节点比根节点大,与右子树的根节点比较
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first) //新插入的节点比根节点小,与左子树的根节点比较
			{
				parent = cur;
				cur = cur->_left;
			}
			else //如果有相同的节点,插入失败
			{
				return false;
			}
		}
        //申请新的节点
		cur = new Node(kv);
        //解耦操作
		cur->_col = RED;
        //确定新的节点插入到那个位置,左孩子还是右孩子
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
        //向上链接父节点
		cur->_parent = parent;

        //调整节点的颜色,使之满足红黑树的性质,只有 parent 的颜色为红才会调整节点的颜色,parent 为黑就直接完成插入了嘛
		while (parent && parent->_col == RED)
		{
            //祖父节点
			Node* grandfather = parent->_parent;
            //这里根据父节点在祖父节点的位置进行分类,因为我们要确定 uncle 的位置嘛,这样分类代码比较简洁
            //parent 位于 grandparent 的左侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // uncle 不存在 或 uncle 存在且为黑
				{
					if (cur == parent->_left)
					{
						//     g
						//   p
						// c
                        //右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//     g
						//   p
						//		c
                        //左右双旋
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
			else // parent == grandfather->_right
			{
                //找到叔叔节点
				Node* uncle = grandfather->_left;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						// g
						//	  p
						//       c
                        //左单旋
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						// g
						//	  p
						// c
                        //右左双旭那
						RotateR(parent);
						RotateL(grandfather);
                        //调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
		}

		_root->_col = BLACK; // 无论根节点的颜色是否在调整的过程中变成了红色,最后我们都将根节点的颜色变为黑色,方便写代码

		return true;
	}

    //左单旋。逻辑和 AVL 树完全一样就不写注释
	void RotateL(Node* parent)
	{

		Node* cur = parent->_right;
		Node* curleft = cur->_left;

		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}

		cur->_left = parent;

		Node* ppnode = parent->_parent;

		parent->_parent = cur;


		if (parent == _root)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;

			}

			cur->_parent = ppnode;
		}
	}

    //右单旋。逻辑和 AVL 树完全一样就不写注释
	void RotateR(Node* parent)
	{

		Node* cur = parent->_left;
		Node* curright = cur->_right;

		parent->_left = curright;
		if (curright)
			curright->_parent = parent;

		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;

		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}

			cur->_parent = ppnode;
		}
	}
	
    //下面就是检查一颗树是不是红黑树的代码
    //benchmark 就是那个基准值。blacknum 就是用来统计路径中的黑色节点数量的,请体会传值的妙处
	bool CheckColour(Node* root, int blacknum, int benchmark)
	{
		if (root == nullptr)
		{
			if (blacknum != benchmark)
				return false;

			return true;
		}

		if (root->_col == BLACK)
		{
			++blacknum;
		}
        //检查是否出现连续的红色节点
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "出现连续红色节点" << endl;
			return false;
		}
        //递归左右子树
		return CheckColour(root->_left, blacknum, benchmark)
			&& CheckColour(root->_right, blacknum, benchmark);
	}

	bool IsBalance()
	{
		return IsBalance(_root);
	}

	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		if (root->_col != BLACK)
		{
			return false;
		}

		// 找到一条路径中的黑色节点数量
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++benchmark;

			cur = cur->_left;
		}

		return CheckColour(root, 0, benchmark);
	}

private:
	Node* _root = nullptr;
};

红黑树的删除

红黑树的删除比插入还要复杂一些,但是难度并没有上升多少!具体过程包含了普通的二叉搜索树的删除,懂的都懂!同样面试的过程中顶多考红黑树的插入呢!有兴趣可以了解了解!

红黑树与AVL树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数。所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

红黑树的删除

红黑树的删除比插入还要复杂一些,但是难度并没有上升多少!具体过程包含了普通的二叉搜索树的删除,懂的都懂!同样面试的过程中顶多考红黑树的插入呢!有兴趣可以了解了解!

红黑树与AVL树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是O( l o g 2 N log_2 N log2N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数。所以在经常进行增删的结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通过环境变量实现多个JDK切换

前文: 由于jdk版本需要升级为jdk17,因为jdk8比较常用且稳定,本人又不想卸载掉安装的jdk8,在经过查找资料后找到了可以通过修改环境变量在本地任意切换jdk版本 环境变量配置 网上教程一堆,直接跳过了,这里主要说明怎么通过配置环境变量切换 电脑->属性->高级系统设置-&g…

centos7中多版本go安装

安装go的方式 官网下载tar.gz包安装 # 1.下载tar包 wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz # 2.解压tar包到指定路径 tar -xvf go1.18.1.linux-amd64.tar.gz -C /usr/local/go1.18 # 3.配置环境变量&#xff0c;打开 /etc/profile 文件添加以下文件每次开机时…

【MATLAB源码-第65期】基于matlab的OFDM/OTFS通信系统性能对比,输处误码率曲线;对比是否采用LDPC编码。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 OTFS&#xff08;Orthogonal Time Frequency Space&#xff09;是一种无线通信调制技术&#xff0c;它利用时间、频率和空间的正交性来传输数据&#xff0c;目的是提高无线通信系统的性能&#xff0c;尤其是在多径和高移动性环…

selenium自动化测试入门 —— 定位frame和iframe中的元素对象

< frame> <iframe> 标签&#xff0c;浏览器会在标签中打开一个特定的页面窗口&#xff08;框架&#xff09;&#xff0c;它在本窗口中嵌套进入一个网页&#xff0c;当用selenium定位页面元素的时候会遇到定位不到frame框架内的元素的问题。 定位frame中的元素前我…

node插件express(路由)的插件使用(二)——body-parser和ejs插件的基本使用

文章目录 前言一、express使用中间件body-parser获取请全体的数据1. 代码2. 效果 二、express使用ejs&#xff08;了解即可&#xff09;1.安装2.作用3.基本使用&#xff08;1&#xff09;代码&#xff08;2&#xff09;代码分析和效果 4.列表渲染&#xff08;1&#xff09;代码…

同步网盘与云盘:哪个更好用?

同步网盘、同步云盘现在是热门的文件管理工具&#xff0c;在回答“同步网盘云盘哪个好用”这个问题之前&#xff0c;我们需要知道什么样的同步网盘、同步云盘算好用&#xff1f; 什么样的同步网盘云盘好用&#xff1f; 1、存储空间大 对于文件管理工具而言&#xff0c;存储空…

从零开始学习PX4源码0(固件下载及编译)

目录 文章目录 目录摘要1.重点学习网址2.固件下载1.下载最新版本固件2.下载之前版本固件 摘要 本节主要记录从零开始学习PX4源码1(固件下载)的过程&#xff0c;欢迎批评指正&#xff01;&#xff01;&#xff01; 下载固件主要分为两个版本&#xff0c;之前稳定版本和最新官网…

3、Sentinel 动态限流规则

Sentinel 的理念是开发者只需要关注资源的定义&#xff0c;当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则&#xff1a; • 通过 API 直接修改 (loadRules) • 通过 DataSource 适配不同数据源修改 通过 API 修改比较直观&#xff0c;可以通…

C#中LINQtoSQL只能在.NetFramework下使用,不能在.net 下使用

目录 一、在net7.0下无法实现LINQtoSQL 1.VS上建立数据库连接 2.VS上创建LINQtoSQL 二、在.NetFramework4.8下成功实现LINQtoSQL 1.VS上建立数据库连接 2.VS上创建LINQtoSQL 三、结论 四、理由 本文是个人观点&#xff0c;因为我百般努力在.net7.0下无法实现LINQtoSQL的…

如何将 XxlJob 集成达梦数据库

1. 前言 在某些情况下&#xff0c;你的项目可能会面临数据库选择的特殊要求&#xff0c;随着国产化的不断推进&#xff0c;达梦数据库是一个常见的选择。本篇博客将教你如何解决 XxlJob 与达梦数据库之间的 SQL 兼容性问题&#xff0c;以便你的任务调度系统能够在这个数据库中…

Idea去掉显示的测试覆盖率

一.启东时 误点击了 快捷键调出 【Ctrl 】【Alt】【F6】

npm ERR! code ELIFECYCLE

问题&#xff1a; 一个老项目&#xff0c;现在想运行下&#xff0c;打不开了 npm install 也出错 尝试1 、使用cnpm npm install -g cnpm --registryhttps://registry.npm.taobao.org cnpm install 还是不行 尝试2、 package.json 文件&#xff0c;去掉 那个插件 chorm…

Unity中Shader的GI的直接光实现

文章目录 前言一、在上一篇文章中&#xff0c;得到GI相关数据后&#xff0c;需要对其进行Lambert光照模型计算二、在准备好上面步骤后&#xff0c;我们需要准备缺少的数据1、准备上图中的 s.Normal2、准备上图中的 s.Albedo 前言 Unity中Shader的GI的直接光实现&#xff0c;基…

基于springboot实现在线考试平台项目【项目源码+论文说明】

基于springboot实现在线考试平台管理系统演示 摘要 网络的广泛应用给生活带来了十分的便利。所以把在线考试管理与现在网络相结合&#xff0c;利用java技术建设在线考试系统&#xff0c;实现在线考试的信息化。则对于进一步提高在线考试管理发展&#xff0c;丰富在线考试管理经…

【计算机网络】同源策略及跨域问题

1. 同源策略 同源策略是一套浏览器安全机制&#xff0c;当一个源的文档和脚本&#xff0c;与另一个源的资源进行通信时&#xff0c;同源策略就会对这个通信做出不同程度的限制。 同源策略对 同源资源 放行&#xff0c;对 异源资源 限制。因此限制造成的开发问题&#xff0c;称…

OkHttp库爬取百度云视频详细步骤

以下是使用OkHttp库的Kotlin爬虫程序&#xff0c;该爬虫用于爬取百度云的视频。 首先&#xff0c;我们需要导入OkHttp库和Kotlin库。import okhttp3.OkHttpClient和import kotlin.jvm.JVM。 import okhttp3.OkHttpClient import kotlin.jvm.JVM然后&#xff0c;我们需要创建一…

单片机温湿度-光照-DHT11-烟雾气体检测控制系统-proteus仿真-源程序

一、系统方案 本设计采用52单片机作为主控器&#xff0c;液晶1602显示&#xff0c;DHT11温湿度&#xff0c;光照、烟雾气体检测&#xff0c;按键设置报警阀值&#xff0c;蜂鸣器报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 // // …

docker部署Jenkins(Jenkins+Gitlab+Maven实现CI/CD)

GitLab介绍 GitLab是一个用于仓库管理系统的开源项目&#xff0c;使用Git作为代码管理工具&#xff0c;并在此基础上搭建起来的Web服务&#xff0c;可通过Web界面进行访问公开的或者私人项目。它拥有与Github类似的功能&#xff0c;能够浏览源代码&#xff0c;管理缺陷和注释。…

按键开发环境搭建

雷电模拟器 创建虚拟机 2.设置root权限 打开按键精灵连接虚拟机 开启悬浮 mumu模拟器操作 查找端口方法 adb connect 127.0.0.1:16416 设置-应用-所有应用-按键精灵-开启悬浮 步骤二&#xff1a;开启root 处理未root&#xff1a;中途如果有如下未root的情况&#x…

Perl安装教程

1. perl简介 Perl 是 Practical Extraction and Report Language 的缩写&#xff0c;可翻译为 “实用报表提取语言”。Perl 是高级、通用、直译式、动态的程序语言。Perl 最初的设计者为拉里沃尔&#xff08;Larry Wall&#xff09;&#xff0c;于1987年12月18日发表。Perl 借…