多线程从有序数组构建红黑树/排序树

本文详细介绍了如何从有序数组构建红黑树,包括从有序数组构建完全二叉树、染色成红黑树的策略以及如何利用多线程优化构建过程。文章通过实例展示了如何在特定层数启动异步任务,使用Future任务来构建子树,并最终将结果挂载到父节点。此外,还提供了完整的Java代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面推导为主,需要完整代码的直接拉倒最后面。

问题描述

需要从一个数据条数约120万-150万,大小约300M+的结构化文件构建一颗红黑树,然后做后续操作,该操作集成于某个大的函数里面,这个函数需要根据不同的文件将上述操作执行上百次。

由于公司的内网服务器是走物理机服务器上虚拟出来的玩意,同一台物理机虚拟出了不知道多少个服务器,每天下午基本都卡到爆炸,打条ls命令能给我响应二十几秒,看似4Ghz*8核的CPU,运行起来就是一坨屎。基本上述构建操作在单线程的情况下,在我笔记本上约2秒结束,而在公司的破机器上得三分钟左右。既然这样,那就不能便宜它了,上多线程干它。

(其实上面都是借口,运行三分钟是因为文件IO的瓶颈,而且已经通过切文件+多线程读取的方式解决,然而我依然想把这个构建过程用多线程给它干了 =w=)

思路

红黑树/排序树算法这里不多赘述,有兴趣的自己去看,这篇讲的就很不错。

本篇主要讲构建红黑树。主要分三个步骤:

  • ①如何从有序数组构建完全二叉树
  • ②如何将完全二叉树染色成红黑树
  • ③如何多线程构建

以下一点一点来展开细说。

①如何从有序数组构建完全二叉树

我身边见过好几个这样的家伙:leetcode题目刷了不少,各种树形结构算法6的一批,然后给他一个简单的实际需求,从文件构建一棵树,结果懵逼,脱口而出的理由是,leetcode给你的都是写好的树,谁特么会自己去构建。

于是,现在来手把手教如何从有序数组构建一颗完全二叉树。

假设输入如下数组:
样例数组

它对应的中序完全二叉树应该是:
中序二叉树

从上述两个图基本就能看出,树在纵轴方向“压扁”之后,就变成了它对应的中序遍历的有序数组;而有序数组通过某种方式,往上“提拉”,就能变成它对应的中序完全二叉树。下面就来说如何把这个有序数组“提起来”。

原则相当简单,就一个步骤step:对于每棵子树,它的中间节点就是根,左边的部分就是左子树,右边的部分就是右子树

以上述数组为例。首先,它的中间节点是4,那么对它进行上述step之后得到:

转换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

在知道了用几个线程去弄它之后,这里先详述一下单线程的中序构建过程。如下图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值