前面推导为主,需要完整代码的直接拉倒最后面。
问题描述
需要从一个数据条数约120万-150万,大小约300M+的结构化文件构建一颗红黑树,然后做后续操作,该操作集成于某个大的函数里面,这个函数需要根据不同的文件将上述操作执行上百次。
由于公司的内网服务器是走物理机服务器上虚拟出来的玩意,同一台物理机虚拟出了不知道多少个服务器,每天下午基本都卡到爆炸,打条ls命令能给我响应二十几秒,看似4Ghz*8核的CPU,运行起来就是一坨屎。基本上述构建操作在单线程的情况下,在我笔记本上约2秒结束,而在公司的破机器上得三分钟左右。既然这样,那就不能便宜它了,上多线程干它。
(其实上面都是借口,运行三分钟是因为文件IO的瓶颈,而且已经通过切文件+多线程读取的方式解决,然而我依然想把这个构建过程用多线程给它干了 =w=)
思路
红黑树/排序树算法这里不多赘述,有兴趣的自己去看,这篇讲的就很不错。
本篇主要讲构建红黑树。主要分三个步骤:
- ①如何从有序数组构建完全二叉树
- ②如何将完全二叉树染色成红黑树
- ③如何多线程构建
以下一点一点来展开细说。
①如何从有序数组构建完全二叉树
我身边见过好几个这样的家伙:leetcode题目刷了不少,各种树形结构算法6的一批,然后给他一个简单的实际需求,从文件构建一棵树,结果懵逼,脱口而出的理由是,leetcode给你的都是写好的树,谁特么会自己去构建。
于是,现在来手把手教如何从有序数组构建一颗完全二叉树。
假设输入如下数组:
它对应的中序完全二叉树应该是:
从上述两个图基本就能看出,树在纵轴方向“压扁”之后,就变成了它对应的中序遍历的有序数组;而有序数组通过某种方式,往上“提拉”,就能变成它对应的中序完全二叉树。下面就来说如何把这个有序数组“提起来”。
原则相当简单,就一个步骤step:对于每棵子树
,它的中间节点就是根,左边的部分就是左子树
,右边的部分就是右子树
。
以上述数组为例。首先,它的中间节点是4
,那么对它进行上述step之后得到:
那么,现在这棵树的根节点就是4,左子树包含123,右子树包含567;那么再分别对这两颗子树进行上述step(只画123,剩下的就不画了):
然后就变成了上面那副完整的完全二叉树的图。
小问题:偶数个节点的中间节点怎么弄?
不管奇数偶数,假设你这棵子树对应的数组的起始索引为start,结束索引为end,那么中间索引
mid = (start + end) / 2
代码相当简单,基本就是套路:
/**
* 单线程创建子树.
*
* @param data 输入的有序数组
* @param start 该子树在数组中的起始索引
* @param end 该子树在数组中的结束索引
* @param color 该子树的根节点的颜色
* @param parent 该子树的父节点
*/
private Node<T> createTree(List<T> data, int start, int end, Color color, Node<T> parent) {
if (start > end) { //构建结束条件
return null;
}
int mid = (start + end) / 2; //求得中间节点索引
Node<T> node = new Node<>(data.get(mid), color, parent, null, null);
node.left = createTree(data, start, mid - 1, getAnotherColor(color), node); //求得左子树并挂到根节点
node.right = createTree(data, mid + 1, end, getAnotherColor(color), node); //求得右子树并挂到根节点
return node; //返回根节点
}
注意:关于颜色
和父节点
部分先跳过,这两个是操作红黑树要用的,去掉这两个,就能构建一棵普通的排序完全二叉树。
补充一下,父节点与红黑树的插入和删除操作有关,本篇全篇不会用到,于是忽略它吧~
②完全二叉树如何染色成红黑树
要给完全二叉树染色成红黑树,主要是要满足以下几个条件:
性质1:每个节点要么是黑色,要么是红色。
性质2:根节点是黑色。
性质3:每个叶子节点(NIL)是黑色。
性质4:每个红色结点的两个子结点一定都是黑色。
性质5:任意一结点到其每个叶子结点的路径都包含数量相同的黑结点。
第一条和忽略,第三条…基本忽略。重点注意第四条和第五条,红节点的子节点一定是黑,任一节点到其每个叶子结点的路径都包含数量相同的黑结点。再注意到,上一步构建的是完全二叉树,也是就是说,本篇构建的这棵树,除了最后一层,其他层都是满节点状态。每一层都是满节点意味着什么?意味着染色时,同一层的节点可以染一样的颜色。更进一步可以推出一种朴实无华的染色方案,对于同层颜色相同的完全二叉树而言:
红节点的儿子一定是黑节点,黑节点的儿子一定是红节点,最后一层节点一定要是红色。
当然,其实你可以不遵照这样的染色规则,但是实现起来真的很麻烦,如下图:
当然,并不排除在实际需求中有这样染色的,毕竟现实中可是有五彩斑斓的黑这种需求的嘛~
于是,朴实无华的正确染色方式应该如下图所示:
在看一遍朴实的染色方案:红节点的儿子一定是黑节点,黑节点的儿子一定是红节点,最后一层节点一定要是红色。
为什么最后一层节点一定要是红色?
这里用反例来说明。假设最后一层节点为黑色且最后一层不是满节点,如下图所示:
这里没画叶子结点,但是你要知道节点1、3、6下面挂的空节点就是叶子节点。显然这棵树是不满足性质5的,从根节点出发,到左边的叶子结点经历的黑节点(包括自己)数量为2,到右边经历的是1。反之,最后一层染红没什么问题。
其实上述推导说明了红黑树的一个重要性质:黑平衡性质。
由于写代码构建树的时候,是从上往下构建,所以你只需要定义好根节点的颜色,然后一层层红黑交替往下染就可以了。当然,最后注意需要把根节点涂黑。
首先是根据最后一层节点是红色节点倒推根节点的颜色。根据红黑树高度 h e i g h t = ⌊ l o g 2 N ⌋ + 1 height=⌊log_2{N}⌋+1 height=⌊log2N⌋+1,也就是节点数N对2取对数然后向下取整再加1。看起来花里胡哨的,实际代码很容易写,因为请注意,节点个数N的二进制与树高度对应的关系相当简单:
节点数 | 二进制 | 树高度 |
---|---|---|
1 | 1 | 1 |
2 | 10 | 2 |
4 | 100 | 3 |
7 | 111 | 3 |
8 | 1000 | 4 |
11 | 1011 | 4 |
多写几组你会发现啊,高度与节点数的二进制去掉前导零之后的位数一致,于是求高度的代码这么写:
/**
* 根据传入节点个数计算树的高度.
*/
private int getHeight(int nrNode) {
int height = 0;
while (nrNode != 0) {
++height;
nrNode >>= 1;
}
return height;
}
由于之前说过,这玩意是红-黑-红-黑交替染色的,于是由高度与最后一层为红可以简单推出:高度为奇数,根节点与最后一层节点同色,也就是红色;高度为偶数,根节点与最后一层节点异色,也就是黑色。代码如下:
/**
* 根据红黑树高度计算从什么颜色开始染色.
*/
private Color getColorFromHeight(int height) {
if ((height & 1) == 1) { // 习惯写C的可能看到这里的一行代码也用大括号很不舒服,比如我就是;
return Color.RED; // 但这是比较通用的代码规范,很多公司都会用,所以最好习惯一下;
} else { // 不鼓励使用三目运算符代替if else;
return Color.BLACK;
}
}
至此,染色问题说完了。
③如何多线程构建
首先,得知道一个问题,你用几个线程去构建它。为什么要想这么问题,因为它的数据是通过有序数组传进来的,这意味着它的数据已经在内存当中了,不需要进行IO,只用运算就行,也就是说,它是一个CPU密集型任务。那么对于这类任务,所用的线程数量,在不必要的情况下,不要超过CPU的核心数量。简单起见,本篇直接使用与CPU核心数相等的线程来构建:
//获取你的电脑的CPU核心数,这代码运行结果可能与预期不一致,至于为什么,去百度;如有需求,自己手动设置
int NCPU = Runtime.getRuntime().availableProcessors();
为了简化说明,使线程数为8
。
在知道了用几个线程去弄它之后,这里先详述一下单线程
的中序构建过程。如下图所示:
构建路线是先构建中间节点,再构建两边节点(本篇从左往右),更放宽一点说,就是先构建中间节点,再构建左子树和右子树。那么,对于一个中间节点来说,它其实并不关心你的左右子树是以何种方式构建的,遍历也好,递归也好,多线程也好,反正我最后都是你爹。
于是,多线程的思路来了,先扔给中间节点一个空,告诉它你的子树构建好了,让它继续去构建其他子树。然后,从中间节点拿到构建该子树的数据,去异步构建子树,用某种方法,将这异步得到的结果与中间节点关联起来。等执异步任务行完了,再认中间节点做爸爸即可。于是流程如下:
主要需要解决的问题有三个:
-
1)何时启动异步构建任务;
-
2)启动什么样的异步构建任务;
-
3)如何得到异步构建任务的结果并与相对应的节点关联。
下面一个个来解决。
1) 何时启动异步构建任务
说一句废话:在该启动的时候启动。。。
依然以上图为例,假设你准备用8
个线程来构建子树。这里本篇假设,你的CPU核心个数是2的幂
。本篇采用的是记录目前的构建层数,即记录当前节点是第几层,然后在指定层数的节点时开启异步构建。
那么这个指定层数怎么求呢,假设根节点是第1
层。于是,由上图可以看出,第二层是2个节点,第三层是4个节点,第4层是8个节点。那么你要用8个线程,也就是说,你要把第4层的8个节点对应的子树用多线程来构建。但是呢,启动异步构建应该是在第3层里,也就是那4个节点里开启。以下来细说。
构建某节点的伪代码:
step1: 构建该节点
step2: 构建子树(假设左优先)
- 构建左子树,完成后挂载到上面节点的左孩子
- 构建右子树,完成后挂载到上面节点的右孩子
也就是说,开启以某个节点为根的子树的构建任务其实是在其父节点的代码中进行的。那么,到达其父亲节点的层数的时候,就启动该子树的异步构建任务。
伪代码如下:
根据传入数据构建本层节点
if 未达到指定层数 then
正常构建左子树
正常构建右子树
else 达到了指定层数 then
对左子树开启异步构建任务,将异步任务与本层节点关联,并扔个本层节点一个null
对右子树开启异步构建任务,将异步任务与本层节点关联,并扔个本层节点一个null
2) 启动什么样的异步构建任务
直接说结论,启动Future
任务。多线程的创建方式主要有三种,分别是继承Thread
、实现Runnable
、实现Callable
,前两种区别不大,它们都是不带返回值的,但是这里是需要返回值的,返回所构建的子树的根节点,于是选择Callable
,对应接收这个返回值的玩意就是Future
了。
具体来说,上面八个线程构建八个子树,每个子树启动一个Future
任务,启动之后开始异步构建并扔给父节点一个null
,让主线程得以继续去构建其他上层节点和开启其他子树构建任务。
然后等子树构建好了,再从主线程取回结果,挂载到其父节点上。
3) 如何得到异步构建任务的结果并与相对应的节点关联
如何得到构建结果就一句话,用Future
执行get
就行了,不过如果被get
的线程没有执行完毕的话,是会阻塞调用线程的,这很基础,就不多说了。
重点是如何将异步任务与父节点关联上。本篇采用的是,在外面记录一个map
,key为启动异步任务的节点,value为一个List,该List包含两个值,index为0的是将来要挂到该节点的左子树上的任务,index为1是右边的。
然后所有子任务都开启之后,上层的节点也就都构建好了,然后将主线程阻塞,等待这些子树构建完毕。取结果的时候,直接遍历map
将结果进行挂载就行了。
好了,下面以实际代码来说明。
/**
* 多线程创建整棵树.
* @param data 传入的数据,有序数组
* @param start 构建本子树在数组中的开始index
* @param end 构建本子树在数组中的结束index
* @param color 本层节点的颜色
* @param parent 本层节点的父亲
* @param level 层数
*/
private Node<T> createTree(List<T> data, int start, int end, Color color, Node<T> parent, int level) {
//终止条件
if (start > end) {
return null;
}
//根据传入的参数构建出当前节点
int mid = (start + end) / 2;
Node<T> node = new Node<>(data.get(mid), color, null, null, parent);
//CASE1: 如果当前节点层数小于展开层数,用递归遍历[立即]获取左右节点
if (++level < multiLevel) {
node.left = createTree(data, start, mid - 1, getAnotherColor(color), node, level);
node.right = createTree(data, mid + 1, end, getAnotherColor(color), node, level);
//CASE2: 如果当前节点为展开层,则建立远期任务来绑定左右节点
} else {
futureMap.put(node, new ArrayList<Future<Node<T>>>(2) {{
//左半部分数据建立左子树
add(pool.submit(new ExecuteCreateTree(data, start, mid - 1, getAnotherColor(color), node, latch)));
//右半部分数据建立右子树
add(pool.submit(new ExecuteCreateTree(data, mid + 1, end, getAnotherColor(color), node, latch)));
}});
}
return node;
}
/**
* 红黑树的节点定义.
*/
private static class Node<T> {
T key;
Color color;
Node<T> left;
Node<T> right;
Node<T> parent;
public Node(T key, Color color, Node<T> left, Node<T> right, Node<T> parent) {
this.key = key;
this.color = color;
this.left = left;
this.right = right;
this.parent = parent;
}
}
以下来逐条讲解createTree
函数,先说传入数据和返回数据。
传入数据:
List data: 整个有序数组,别担心传整个会内存爆炸,这玩意就是个引用,64个字节(没记错?)
int start:要构建的子树对应在数组上的开始索引
int end:要构建的子树对应在数组上的结束索引
以上这两个,可以看出,是一段连续的空间。为什么是连续的空间呢?可以返回本篇顶部,看到树压扁
之后与将会与有序数组一一对应,那么你在这棵树中任意取一棵子树进行压扁
,你会发现它在数组上对应的是一段连续空间。Color color:该层节点的颜色,在外面计算出来的,前面已经详述了
Node parent:父节点,前面也详述了,不多详述int level:这就是在用多线程的时候,判断从哪一层开始展开异步任务
返回数据:
Node node = new Node<>(data.get(mid), color, null, null, parent);
返回的就是这个根据传入的数据构建的节点,只是在返回之前,将这个节点的左子树和右子树也进行了构建
//递归调用终止条件,start表示开始索引,end表示结束索引,开始大于结束了,返回一个空的叶子结点
if (start > end) {
return null;
}
//根据传入的参数构建出当前节点,中间节点将左右子树在升序数组上平均切成两半,于是中间节点的自然就是start到end这个范围的中间值
int mid = (start + end) / 2;
Node<T> node = new Node<>(data.get(mid), color, null, null, parent);
//构建当前节点的左右子树,分两种情况,第一种情况是非多线程展开层,直接递归构建;第二种情况是多线程展开层,开启异步多线程构建
//参数说明:multiLevel 在外部根据CPU核心数计算出的展开位置
// ExecuteCreateTree 异步构建任务内部类
// latch 在外部设定的CountDownLatch,用于同步,让主线程等待所有子线程构建完毕
//CASE1: 如果当前节点层数小于展开层数,用递归遍历[立即]获取左右节点
if (++level < multiLevel) { //level自增一,然后判断是否应该开启多线程
//mid的值已经被该节点用掉了,于是左子树用[start, mid-1]范围的值,右子树用[mid+1, end]范围的值
//getAnotherColor(color)是根据color获得相对的颜色,即如果color是红,那么得到黑,如果color是黑,那么得到红
node.left = createTree(data, start, mid - 1, getAnotherColor(color), node, level); //递归构建左子树
node.right = createTree(data, mid + 1, end, getAnotherColor(color), node, level); //递归构建右子树
//CASE2: 如果当前节点为展开层,则建立远期任务来绑定左右节点
} else {
//futureMap是外部定义的,是一个HashMap结构,用来绑定当前节点与其多线程子树任务Future
//ExecuteCreateTree()是一个内部类,继承了Callable接口,用来建立Future任务,用pool线程池启动它
futureMap.put(node, new ArrayList<Future<Node<T>>>(2) {{
//左半部分数据建立左子树
add(pool.submit(new ExecuteCreateTree(data, start, mid - 1, getAnotherColor(color), node, latch)));
//右半部分数据建立右子树
add(pool.submit(new ExecuteCreateTree(data, mid + 1, end, getAnotherColor(color), node, latch)));
}});
}
下面是完整代码,没有贴红黑树增删改查的函数,只贴构建
。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* @formatter:off
* Created with IntelliJ IDEA.
* Date: 2020-10-18 12:39
* Author: nullpo
* Description: 多线程从传入的有序数组构建红黑树
* 有一点设计的非常不好,就是在多线程构建子树的类里面调用了外部类的方法,形成了内部类和外部类的循环依赖,有待后续改进
* @formatter:on
*/
public class RBTree<T> {
private final Node<T> root;
private final Integer multiLevel;
private ExecutorService pool;
private CountDownLatch latch;
private Map<Node<T>, List<Future<Node<T>>>> futureMap;
public RBTree(List<T> sortedData) {
//获取CPU核心个数并初始化多线程展开层与线程池
int NCPU = Runtime.getRuntime().availableProcessors();
multiLevel = getMultiLevel(NCPU); //计算出展开层层数
pool = Executors.newFixedThreadPool(NCPU); //建立所需要的线程池
latch = new CountDownLatch(NCPU); //建立线程计数器
futureMap = new HashMap<>(NCPU >> 1, 1); //展开节点绑定Future任务
//CASE1: 输入无数据
if (null == sortedData || sortedData.size() == 0) {
root = null;
return;
}
//CASE2: 输入只有一个数据
if (sortedData.size() == 1) {
root = new Node<>(sortedData.get(0), Color.BLACK, null, null, null);
return;
}
//CASE3: 输入数据大于1
root = createTree(sortedData);
setBlack(root);
}
/**
* 以给定的数据构建出红黑树,并返回根节点.
*/
private Node<T> createTree(List<T> data) {
//1.初步建立头部框架,等待多线程构建NCPU棵子树
Node<T> node = createTree(data, 0, data.size() - 1, getColorFromHeight(getHeight(data.size())), null, 1);
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.多线程将NCPU棵子树构建完毕,取出子树,将子树挂载到相应的节点上
futureMap.forEach((tNode, futures) -> {
try {
tNode.left = futures.get(0).get(); //0位置是左子树
tNode.right = futures.get(1).get(); //1位置是右子树
} catch (Exception e) {
e.printStackTrace();
}
});
//3.释放掉线程池以及不再使用的对象的引用
release();
return node;
}
/**
* 构建完毕之后,释放掉不需要的引用,方便GC回收.
*/
private void release() {
pool.shutdown();
pool = null;
latch = null;
futureMap = null;
}
/**
* 多线程创建整棵树.
*
* @param data 传入的数据,有序数组
* @param start 构建本子树在数组中的开始index
* @param end 构建本子树在数组中的结束index
* @param color 本层节点的颜色
* @param parent 本层节点的父亲
* @param level 层数
*/
private Node<T> createTree(List<T> data, int start, int end, Color color, Node<T> parent, int level) {
//终止条件
if (start > end) {
return null;
}
//根据传入的参数构建出当前节点
int mid = (start + end) / 2;
Node<T> node = new Node<>(data.get(mid), color, null, null, parent);
//CASE1: 如果当前节点层数小于展开层数,用递归遍历[立即]获取左右节点
if (++level < multiLevel) {
node.left = createTree(data, start, mid - 1, getAnotherColor(color), node, level);
node.right = createTree(data, mid + 1, end, getAnotherColor(color), node, level);
//CASE2: 如果当前节点为展开层,则建立远期任务来绑定左右节点
} else {
futureMap.put(node, new ArrayList<Future<Node<T>>>(2) {{
//左半部分数据建立左子树
add(pool.submit(new ExecuteCreateTree(data, start, mid - 1, getAnotherColor(color), node, latch)));
//右半部分数据建立右子树
add(pool.submit(new ExecuteCreateTree(data, mid + 1, end, getAnotherColor(color), node, latch)));
}});
}
return node;
}
/**
* 单线程创建子树.
*/
private Node<T> createTree(List<T> data, int start, int end, Color color, Node<T> parent) {
if (start > end) {
return null;
}
int mid = (start + end) / 2;
Node<T> node = new Node<>(data.get(mid), color, parent, null, null);
node.left = createTree(data, start, mid - 1, getAnotherColor(color), node);
node.right = createTree(data, mid + 1, end, getAnotherColor(color), node);
return node;
}
/**
* 将节点颜色设置为黑色.
*/
private void setBlack(Node<T> node) {
node.color = Color.BLACK;
}
/**
* 将节点颜色设置为红色.
*/
private void setRed(Node<T> node) {
node.color = Color.RED;
}
/**
* 根据传入节点个数计算树的高度.
*/
private int getHeight(int nrNode) {
int height = 0;
while (nrNode != 0) {
++height;
nrNode >>= 1;
}
return height;
}
/**
* 根据红黑树高度计算从什么颜色开始染色.
*/
private Color getColorFromHeight(int height) {
if ((height & 1) == 1) {
return Color.RED;
} else {
return Color.BLACK;
}
}
/**
* 根据传入的颜色获取另一个颜色.
*/
private Color getAnotherColor(Color color) {
return color == Color.BLACK ? Color.RED : color;
}
/**
* 根据CPU核心数求得展开位置.
*/
private int getMultiLevel(int NCPU) {
int level = 0;
while (NCPU > 0) {
level++;
NCPU >>= 1;
}
return level;
}
/**
* 红黑树的节点类型.
*/
private static class Node<T> {
T key;
Color color;
Node<T> left;
Node<T> right;
Node<T> parent;
public Node(T key, Color color, Node<T> left, Node<T> right, Node<T> parent) {
this.key = key;
this.color = color;
this.left = left;
this.right = right;
this.parent = parent;
}
}
/**
* 颜色枚举.
*/
private enum Color {
RED {
String getName() {
return "RED";
}
},
BLACK {
String getName() {
return "BLACK";
}
};
abstract String getName();
}
/**
* 执行多线程的工具类.
*/
private class ExecuteCreateTree implements Callable<Node<T>> {
private final List<T> data;
private final int start;
private final int end;
private final Color color;
private final Node<T> parent;
private final CountDownLatch latch;
public ExecuteCreateTree(List<T> data, int start, int end, Color color, Node<T> parent, CountDownLatch latch) {
this.data = data;
this.start = start;
this.end = end;
this.color = color;
this.parent = parent;
this.latch = latch;
}
@Override
public Node<T> call() throws Exception {
Node<T> node = RBTree.this.createTree(data, start, end, color, parent);
latch.countDown();
return node;
}
}
}