C++红黑树

C++红黑树

  • 一.红黑树的概念和性质
    • 1.红黑树的概念和性质
    • 2.AVL树和红黑树的区别
  • 二.我们要实现的大致框架
    • 1.红黑树节点的定义
    • 2.为什么新节点默认是红色?
      • 1.共识
      • 2.新节点是黑色的坏处
      • 3.新节点是红色的好处
  • 三.红黑树的插入
  • 1.插入逻辑跟BST相同的那一部分
  • 2.分类讨论插入逻辑
    • 1.新插入节点的父亲是黑色
    • 2.新插入节点的父亲是红色
      • 1.具体分类的说明
      • 2.新插入节点的叔叔存在是红色
        • 1.说明:
        • 2.动图演示
        • 3.总结:
      • 3.新插入节点的叔叔不存在或者存在是黑色
        • 1.叔叔是祖父的右孩子
          • 1.说明
          • 2.旋转方案
            • 1.cur是parent的左孩子(右旋)
            • 2.cur是parent的右孩子(左右双旋)
        • 2.叔叔是祖父的左孩子
          • 1.cur是parent的右孩子(左旋)
          • 2.cur是parent的左孩子(右左双旋)
        • 3.叔叔不存在
        • 4.总结:
  • 3.插入代码
  • 四.红黑树的验证
  • 五.完整代码

前置说明:
需要学习AVL树的旋转之后再来看红黑树的旋转
因为红黑树的旋转是复用的AVL树的旋转的:
大家可以看我的这篇博客,里面详细介绍了AVL树的四种旋转
C++ AVL树(四种旋转,插入)

一.红黑树的概念和性质

1.红黑树的概念和性质

在这里插入图片描述

2.AVL树和红黑树的区别

在这里插入图片描述

二.我们要实现的大致框架

1.红黑树节点的定义

对于颜色我们采用枚举类型定义
对于红黑树节点,依旧采用Key-Value模型存储pair

enum Color
{RED,BLACK
};template<class K,class V>
struct RBTreeNode
{RBTreeNode<K,V>* _pLeft;RBTreeNode<K,V>* _pRight;RBTreeNode<K,V>* _pParent;Color _color;pair<K,V> _data;//新插入的节点默认是红色RBTreeNode(const pair<K,V>& data):_pLeft(nullptr),_pRight(nullptr),_pParent(nullptr),_color(RED),_data(data){}
};

2.为什么新节点默认是红色?

1.共识

首先我们要达成一个共识:
在这里插入图片描述
对于性质3和性质4,如果非要违反一个的话
我们选择违反性质3,而不是性质4
因为:
违反性质3还可以通过变色和旋转的方式来解决
而违法性质4的话,其他所有路径都要重新修改

因此违法性质3的损失更小,调整更简单
违法性质4…后果不言而喻…

2.新节点是黑色的坏处

插入之前:
在这里插入图片描述
插入过程:
在这里插入图片描述
插入之后:
在这里插入图片描述

3.新节点是红色的好处

插入之前:
在这里插入图片描述
插入过程:
在这里插入图片描述
插入之后:
在这里插入图片描述

三.红黑树的插入

1.插入逻辑跟BST相同的那一部分

下面是跟BST普通二叉搜索树的插入逻辑相同的那部分
唯一不太相同的是把根节点的颜色改成黑色了而已

bool Insert(const pair<K,V>& data)
{if (_pRoot == nullptr){_pRoot = new Node(data);//根节点是黑色_pRoot->_color = BLACK;return true;}Node* cur = _pRoot, * parent = nullptr;while (cur){//比我小,往左找if (data.first < cur->_data.first){parent = cur;cur = cur->_pLeft;}//比我大,往右找else if (data.first > cur->_data.first){parent = cur;cur = cur->_pRight;}//有重复元素,插入失败else{return false;}}//找到空位置,开始插入cur = new Node(data);//连接父亲和孩子if (cur->_data.first < parent->_data.first){parent->_pLeft = cur;}else{parent->_pRight = cur;}cur->_pParent = parent;//开始讨论节点颜色问题//.....return true;
}

2.分类讨论插入逻辑

介绍了新插入的节点必须是红色之后
我们就可以分情况讨论了

1.新插入节点的父亲是黑色

因为红黑树的性质3:所有红色节点的孩子不能是红色节点
这也就说明了红黑树不能存在连续的红色节点

在这里插入图片描述

2.新插入节点的父亲是红色

1.具体分类的说明

在这里插入图片描述
下面我们就对叔叔进行分类讨论

2.新插入节点的叔叔存在是红色

1.说明:

这里以叔叔是祖父的右孩子为例演示
其实叔叔是祖父的左孩子的话是一模一样的,就不再赘述了

2.动图演示

插入前:
在这里插入图片描述
调整过程:
在这里插入图片描述
调整之后:
在这里插入图片描述
刚才一开始时演示的是:
cur是新增节点时的情况
但是中间过程借由祖父向上继续调整修改时,我们就已经能看出即使cur不是新增节点,调整方式和逻辑也是一模一样的!!

3.总结:

在这里插入图片描述

3.新插入节点的叔叔不存在或者存在是黑色

刚才的那种情况只需要变色即可
现在就需要旋转+变色了
跟AVL树的旋转类似,依然是分为4种情况
依然是画抽象图来理解
画图里面规定:
p:代表parent,父亲
c:代表cur,孩子
g:grandParent,祖父
u:uncle,叔叔
a,b,c,d,e代表红黑树或者空节点
ar:ancestor:祖父的父亲

1.叔叔是祖父的右孩子
1.说明

1.uncle存在且为黑时:cur一定不是新增节点
在这里插入图片描述
2.为什么不能按照之前的方式只去修改颜色
在这里插入图片描述
在这里插入图片描述

2.旋转方案
1.cur是parent的左孩子(右旋)

修改之前:
在这里插入图片描述
修改过程:
在这里插入图片描述
这里是对g进行右旋,动图里面刚才写错了,抱歉
修改之后:
在这里插入图片描述
修改之后没有违反性质3

注意:因为此时祖父变成了p,而且p是黑色,所以就不会继续往上修改了,证明修改完毕

下面我们对照一下修改之前和修改之后,看看是否违反了性质4
在这里插入图片描述
没有违反,完美的一次修改

2.cur是parent的右孩子(左右双旋)

旋转之前:
在这里插入图片描述
旋转过程:
在这里插入图片描述
旋转之后:
在这里插入图片描述
修改之后没有违反性质3

注意:因为此时祖父变成了c而且c是黑色,所以无需继续往上修改了
但是因为此时p是红色,会继续进入循环,这样就会发生一些意想不到的错误,所以此时必须break

下面我们对照一下修改之前和修改之后,看看是否违反了性质4
在这里插入图片描述
完美修改

2.叔叔是祖父的左孩子

跟叔叔是祖父的右孩子就特别像了,直接给动图了

1.cur是parent的右孩子(左旋)

旋转之前:
在这里插入图片描述
旋转过程:
在这里插入图片描述
旋转之后:
在这里插入图片描述

2.cur是parent的左孩子(右左双旋)

旋转之前:
在这里插入图片描述
旋转过程:
在这里插入图片描述
旋转之后:
在这里插入图片描述

3.叔叔不存在

在这里插入图片描述
以右旋为例:
旋转之前:
在这里插入图片描述
旋转过程:
在这里插入图片描述
旋转之后:
在这里插入图片描述
以左右双旋为例:
旋转之前:
在这里插入图片描述
旋转过程:
在这里插入图片描述
旋转之后:
在这里插入图片描述

4.总结:

调整颜色的总结:
在这里插入图片描述

3.插入代码

// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
// 注意:为了简单起见,本次实现红黑树不存储重复性元素
bool Insert(const pair<K,V>& data)
{if (_pRoot == nullptr){_pRoot = new Node(data);//根节点是黑色_pRoot->_color = BLACK;return true;}Node* cur = _pRoot, * parent = nullptr;while (cur){//比我小,往左找if (data.first < cur->_data.first){parent = cur;cur = cur->_pLeft;}//比我大,往右找else if (data.first > cur->_data.first){parent = cur;cur = cur->_pRight;}//有重复元素,插入失败else{return false;}}//找到空位置,开始插入cur = new Node(data);//连接父亲和孩子if (cur->_data.first < parent->_data.first){parent->_pLeft = cur;}else{parent->_pRight = cur;}cur->_pParent = parent;//父亲是黑色,插入成功if (parent->_color == BLACK){return true;}//父亲是红色,需要调整//且此时爷爷一定是黑色//根据叔叔的颜色来调整while (parent && parent->_color == RED){Node* grandParent = parent->_pParent;//爸爸是爷爷的左孩子if (parent == grandParent->_pLeft){Node* uncle = grandParent->_pRight;//根据叔叔的颜色来调整//1.叔叔存在且为红if (uncle && uncle->_color == RED){//修改爸爸,叔叔,爷爷的颜色uncle->_color = parent->_color = BLACK;grandParent->_color = RED;//此时视爷爷为新插入的红色节点继续向上影响cur = grandParent;parent = cur->_pParent;}//2.叔叔不存在或者叔叔是黑else{//1.我是爸爸的左孩子if (parent->_pLeft == cur){//对爷爷进行一次右旋RotateR(grandParent);//把爷爷改成红色,爸爸改成黑色grandParent->_color = RED;parent->_color = BLACK;//此时爸爸是黑色,无需break,当然break也可以,因此爸爸是黑色,下次循环就不会进入了}//2.我是爸爸的右孩子else{//1.对爸爸进行左旋RotateL(parent);//2.对爷爷右旋RotateR(grandParent);//3.孩子改成黑色,爷爷改成红色cur->_color = BLACK;grandParent->_color = RED;//4.一定要break,如果不break的话,因为爸爸是红色,所以循环会继续,此时就乱套了break;}}}//爸爸是爷爷的右孩子else{Node* uncle = grandParent->_pLeft;//1.叔叔存在且为红if (uncle && uncle->_color == RED){uncle->_color = parent->_color = BLACK;grandParent->_color = RED;cur = grandParent;parent = cur->_pParent;}//2.叔叔不存在或者叔叔为黑else{//1.我是爸爸的右孩子if (parent->_pRight == cur){//对爷爷左旋RotateL(grandParent);//爷爷改成红色,爸爸改成黑色grandParent->_color = RED;parent->_color = BLACK;//此时爸爸是黑色,无需break,当然break也可以,因此爸爸是黑色,下次循环就不会进入了}//2.我是爸爸的左孩子else{//1.对爸爸右旋RotateR(parent);//2.对爷爷左旋RotateL(grandParent);//3.把孩子改成黑色,爷爷改成红色cur->_color = BLACK;grandParent->_color = RED;//4.一定要break,如果不break的话,因为爸爸是红色,所以循环会继续,此时就乱套了break;}}}}_pRoot->_color = BLACK;return true;
}

四.红黑树的验证

template<class K,class V>
class RBTree
{typedef RBTreeNode<K,V> Node;
public:// 检测红黑树中是否存在关键字为key的节点,存在返回该节点的地址,否则返回nullptrNode* Find(const K& key){Node* cur = _pRoot;while (cur){if (cur->_data.first > key){cur = cur->_pLeft;}else if (cur->_data.second < key){cur = cur->_pRight;}else{return cur;}}return nullptr;}// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测bool IsValidRBTRee(){//1.空树是红黑树if (_pRoot == nullptr){return true;}//2.根节点不能为红色if (_pRoot->_color == RED){return false;}//3.为了验证性质: 红黑树的任意一条路径上的黑色节点个数相同   找参考值size_t ReferenceCount = 0;Node* cur = _pRoot;while (cur){if (cur->_color == BLACK){ReferenceCount++;}cur = cur->_pLeft;}return _IsValidRBTRee(_pRoot, 0, ReferenceCount);}private:bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t& ReferenceCount){if (pRoot == nullptr){if (blackCount != ReferenceCount){cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}//验证性质: 红黑树中不能存在连续的红色节点//如果一个节点是红色,该节点一定不是根节点,因此该节点一定有父亲//只需要保证红色节点的父亲不是红色节点即可if (pRoot->_color == RED){if (pRoot->_pParent->_color == RED){cout << "存在连续的红色节点" << endl;return false;}}else{blackCount++;}return _IsValidRBTRee(pRoot->_pLeft, blackCount, ReferenceCount) && _IsValidRBTRee(pRoot->_pRight, blackCount, ReferenceCount);}private:Node* _pRoot = nullptr;
};

五.完整代码

1.RBTree.h

#pragma once
enum Color
{RED,BLACK
};template<class K,class V>
struct RBTreeNode
{RBTreeNode<K,V>* _pLeft;RBTreeNode<K,V>* _pRight;RBTreeNode<K,V>* _pParent;Color _color;pair<K,V> _data;//新插入的节点默认是红色RBTreeNode(const pair<K,V>& data):_pLeft(nullptr),_pRight(nullptr),_pParent(nullptr),_color(RED),_data(data){}
};template<class K,class V>
class RBTree
{typedef RBTreeNode<K,V> Node;
public:// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false// 注意:为了简单起见,本次实现红黑树不存储重复性元素bool Insert(const pair<K,V>& data){if (_pRoot == nullptr){_pRoot = new Node(data);//根节点是黑色_pRoot->_color = BLACK;return true;}Node* cur = _pRoot, * parent = nullptr;while (cur){//比我小,往左找if (data.first < cur->_data.first){parent = cur;cur = cur->_pLeft;}//比我大,往右找else if (data.first > cur->_data.first){parent = cur;cur = cur->_pRight;}//有重复元素,插入失败else{return false;}}//找到空位置,开始插入cur = new Node(data);//连接父亲和孩子if (cur->_data.first < parent->_data.first){parent->_pLeft = cur;}else{parent->_pRight = cur;}cur->_pParent = parent;//父亲是黑色,插入成功if (parent->_color == BLACK){return true;}//父亲是红色,需要调整//且此时爷爷一定是黑色//根据叔叔的颜色来调整while (parent && parent->_color == RED){Node* grandParent = parent->_pParent;//爸爸是爷爷的左孩子if (parent == grandParent->_pLeft){Node* uncle = grandParent->_pRight;//根据叔叔的颜色来调整//1.叔叔存在且为红if (uncle && uncle->_color == RED){//修改爸爸,叔叔,爷爷的颜色uncle->_color = parent->_color = BLACK;grandParent->_color = RED;//此时视爷爷为新插入的红色节点继续向上影响cur = grandParent;parent = cur->_pParent;}//2.叔叔不存在或者叔叔是黑else{//1.我是爸爸的左孩子if (parent->_pLeft == cur){//对爷爷进行一次右旋RotateR(grandParent);//把爷爷改成红色,爸爸改成黑色grandParent->_color = RED;parent->_color = BLACK;//此时爸爸是黑色,无需break,当然break也可以,因此爸爸是黑色,下次循环就不会进入了}//2.我是爸爸的右孩子else{//1.对爸爸进行左旋RotateL(parent);//2.对爷爷右旋RotateR(grandParent);//3.孩子改成黑色,爷爷改成红色cur->_color = BLACK;grandParent->_color = RED;//4.一定要break,如果不break的话,因为爸爸是红色,所以循环会继续,此时就乱套了break;}}}//爸爸是爷爷的右孩子else{Node* uncle = grandParent->_pLeft;//1.叔叔存在且为红if (uncle && uncle->_color == RED){uncle->_color = parent->_color = BLACK;grandParent->_color = RED;cur = grandParent;parent = cur->_pParent;}//2.叔叔不存在或者叔叔为黑else{//1.我是爸爸的右孩子if (parent->_pRight == cur){//对爷爷左旋RotateL(grandParent);//爷爷改成红色,爸爸改成黑色grandParent->_color = RED;parent->_color = BLACK;//此时爸爸是黑色,无需break,当然break也可以,因此爸爸是黑色,下次循环就不会进入了}//2.我是爸爸的左孩子else{//1.对爸爸右旋RotateR(parent);//2.对爷爷左旋RotateL(grandParent);//3.把孩子改成黑色,爷爷改成红色cur->_color = BLACK;grandParent->_color = RED;//4.一定要break,如果不break的话,因为爸爸是红色,所以循环会继续,此时就乱套了break;}}}}_pRoot->_color = BLACK;return true;}// 检测红黑树中是否存在关键字为key的节点,存在返回该节点的地址,否则返回nullptrNode* Find(const K& key){Node* cur = _pRoot;while (cur){if (cur->_data.first > key){cur = cur->_pLeft;}else if (cur->_data.second < key){cur = cur->_pRight;}else{return cur;}}return nullptr;}// 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测bool IsValidRBTRee(){//1.空树是红黑树if (_pRoot == nullptr){return true;}//2.根节点不能为红色if (_pRoot->_color == RED){return false;}//3.为了验证性质: 红黑树的任意一条路径上的黑色节点个数相同   找参考值size_t ReferenceCount = 0;Node* cur = _pRoot;while (cur){if (cur->_color == BLACK){ReferenceCount++;}cur = cur->_pLeft;}return _IsValidRBTRee(_pRoot, 0, ReferenceCount);}void InOrder(){_InOrder(_pRoot);}
private:bool _IsValidRBTRee(Node* pRoot, size_t blackCount, size_t& ReferenceCount){if (pRoot == nullptr){if (blackCount != ReferenceCount){cout << "存在黑色节点数量不相等的路径" << endl;return false;}return true;}//验证性质: 红黑树中不能存在连续的红色节点//如果一个节点是红色,该节点一定不是根节点,因此该节点一定有父亲//只需要保证红色节点的父亲不是红色节点即可if (pRoot->_color == RED){if (pRoot->_pParent->_color == RED){cout << "存在连续的红色节点" << endl;return false;}}else{blackCount++;}return _IsValidRBTRee(pRoot->_pLeft, blackCount, ReferenceCount) && _IsValidRBTRee(pRoot->_pRight, blackCount, ReferenceCount);}// 右单旋void RotateR(Node* pParent){Node* subL = pParent->_pLeft;Node* subLR = subL->_pRight;Node* grandParent = pParent->_pParent;subL->_pRight = pParent;pParent->_pParent = subL;pParent->_pLeft = subLR;if (subLR)subLR->_pParent = pParent;if (grandParent == nullptr){_pRoot = subL;_pRoot->_pParent = nullptr;}else{if (pParent == grandParent->_pLeft){grandParent->_pLeft = subL;}else{grandParent->_pRight = subL;}subL->_pParent = grandParent;}}// 左单旋void RotateL(Node* pParent){Node* subR = pParent->_pRight;Node* subRL = subR->_pLeft;Node* grandParent = pParent->_pParent;pParent->_pParent = subR;subR->_pLeft = pParent;pParent->_pRight = subRL;if (subRL)subRL->_pParent = pParent;//说明此时pParent是_pRootif (grandParent == nullptr){_pRoot = subR;_pRoot->_pParent = nullptr;}//说明此时pParent所在的树是一颗子树,需要跟父亲链接else{if (pParent == grandParent->_pLeft){grandParent->_pLeft = subR;}else{grandParent->_pRight = subR;}subR->_pParent = grandParent;}}void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_pLeft);cout << root->_data.first << " " << root->_data.second << " ";_InOrder(root->_pRight);}
private:Node* _pRoot = nullptr;
};

2.test.cpp

#include <iostream>
using namespace std;
#include <assert.h>
#include "RBtree1.h"
#include <vector>
void test1()
{int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };RBTree<int, int> t;for (auto e : a){t.Insert(make_pair(e, e));}t.InOrder();cout << endl;cout << t.IsValidRBTRee() << endl;}
void test2()
{const int N = 10000000;vector<int> v;v.reserve(N);srand(time(0));for (size_t i = 0; i < N; i++){v.push_back(rand() + i);}size_t begin2 = clock();RBTree<int, int> t;for (auto e : v){t.Insert(make_pair(e, e));}size_t end2 = clock();cout << t.IsValidRBTRee() << endl;
}int main()
{test2();return 0;
}

在这里插入图片描述
验证完毕

以上就是C++红黑树的全部内容,希望能对大家有所帮助!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pandas教程(一)—— 数据结构

前言 Pandas是贯穿数据分析的主要工具之一&#xff0c;它经常和其他数值计算工具一起使用&#xff08;例如&#xff1a;Numpy、SciPy和matplotlib&#xff09;。尽管pandas采用了很多NumPy的代码风格&#xff0c;但二者最大的区别是&#xff1a;pandas主要用于处理表格型或异质…

iPayLinks观察:从9月中国游戏厂商出海收入排行榜看游戏出海趋势

data.ai发布9月中国游戏厂商出海收入排行榜。分析指出&#xff0c;中国手游发行商正加大对日本市场投入。 日本市场有何特点&#xff1f;开发者在开拓日本市场时需要注意什么&#xff1f;本期iPayLinks观察&#xff0c;跟小贝一起走进日本游戏市场。 据data.ai数据&#xff0c…

Jenkins的邮箱配置和插件下载

启动&#xff1a;java -jar jenkins.war 一定在jenkins.war的目录下 进入cmd命令 浏览器输入网址&#xff1a;http://localhost:8080/login?from%2F 账号&#xff1a;admin 密码&#xff1a;123456 安装插件&#xff1a; 插件更新后重启下 配置邮箱账号&#xff1a; 3…

支持多医院使用的云HIS医院信息化管理系统源码 SaaS模式

一、什么是HIS系统 HIS系统&#xff08;Hospital InformationSystem&#xff09;是医院信息化建设的核心组成部分&#xff0c;它是为了管理和运营医院而设计和开发的一套综合性的信息系统。HIS系统通过整合医院各个部门和业务流程的数据和信息&#xff0c;实现了医院内部的信息…

Docker 安装 Nacos

文章目录 拉取镜像查看镜像前置工作数据库Java环境创建挂载目录改配置文件 运行访问 拉取镜像 docker pull nacos/nacos-server查看镜像 docker images 前置工作 数据库 运行命令前需要有 db-config 这个数据库。 数据库 /** Copyright 1999-2018 Alibaba Group Holding …

图像大变身:三款抠图换背景软件横评

在图像编辑和设计中&#xff0c;抠图是一项常见而关键的任务。它涉及将对象从其背景中精确分离&#xff0c;以便在不同的背景上自由放置或合成。为了使抠图工作更加高效和精确&#xff0c;那么怎么抠图换背景&#xff1f;以下是三个优秀的抠图软件&#xff0c;它们提供了各种工…

c语言:计算阶乘的和|练习题

一、题目 输入一个数n&#xff0c;计算1&#xff01;2&#xff01;……n&#xff01;的和 如图&#xff1a; 二、思路分析 设置两个函数 1、一个函数求阶乘 2、一个函数求多个数相加的总和 3、把求阶乘的函数&#xff0c;嵌套在求相加总和的函数里面 三、代码截图【带注释】 四…

SQL Server脚本根据开始时间和结束时间计算时分秒

废话不多说&#xff0c;直接上代码&#xff1a; SELECT下载时间,生产结束时间, CASE WHEN DATEDIFF(second, 下载时间, 生产结束时间) > 3600 THENCAST((DATEDIFF(second, 下载时间, 生产结束时间) / 3600) as varchar) 小时 CAST(((DATEDIFF(second, 下载时间, 生产结…

Neo4j 5.15 windows安装

1&#xff0c;什么是图数据库&#xff1f; 着社交、电商、金融、互联网那个等快速发展&#xff0c;现实社会织起了一张庞大复杂的关系网&#xff0c;传统数据库很难处理关系运算。大数据行业需要处理的数据之间的关系呈集合 数级增长&#xff0c;急需一种支持海量复杂数据关系…

Pytorch深度强化学习2-1:基于价值的强化学习——DQN算法

目录 0 专栏介绍1 基于价值的强化学习2 深度Q网络与Q-learning3 DQN原理分析4 DQN训练实例 0 专栏介绍 本专栏重点介绍强化学习技术的数学原理&#xff0c;并且采用Pytorch框架对常见的强化学习算法、案例进行实现&#xff0c;帮助读者理解并快速上手开发。同时&#xff0c;辅…

Java 将PDF 转为图片 工具 【Free Spire.PDF for Java】(免费版)

Java 将PDF 转为图片 使用工具&#xff1a;Free Spire.PDF for Java&#xff08;免费版&#xff09; Jar文件获取及导入&#xff1a; 方法1&#xff1a;通过官网下载jar文件包。下载后&#xff0c;解压文件&#xff0c;并将lib文件夹下的Spire.Pdf.jar文件导入Java程序。 方…

win10安装ffmpeg

1 ffmpeg官网下载 官网地址&#xff1a;https://ffmpeg.org/ ffmpeg可执行程序下载地址&#xff1a;https://www.gyan.dev/ffmpeg/builds/ ffmpeg官网文档&#xff1a;https://ffmpeg.org/documentation.html 选择对应的版本点解下载可执行程序包&#xff0c;比如6.1版本的…