本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是机器学习的第16篇文章,我们来继续上周KD-Tree的话题。
如果有没有看过上篇文章或者是最新关注的小伙伴,可以点击一下下方的传送门:
旋转不可行分析
上周我们实现了KD-Tree建树和查询的核心功能,然后我们留了一个问题,如果我们KD-Tree的数据集发生变化,应该怎么办呢?
最朴素的办法就是重新建树,但是显然我们每次数据发生变动都把整棵树重建显然是不科学的,因为绝大多数数据是没有变化的,并且我们重新建树的成本很高,如果变动稍微频繁一些会导致大量的开销,这明显是不合理的。
另一个思路是借鉴平衡树,比如AVL或者是红黑树等树结构。在这些树结构当中,当我们新增或者是删除节点导致树发生不平衡的情况时,平衡树会进行旋转操作在不改变二叉搜索树性质的前提下维护树的平衡。看起来这是一个比较好的方法,但是遗憾的是,这并不太可行。因为KD-Tree和二叉搜索树不同,KD-Tree中的节点存储的元素都是高维的。每一棵子树的衡量的维度都不同,这会使得旋转操作变得非常麻烦,甚至是不可行的。
我们来看下面这张图:
这是平衡树当中经典的左旋操作,它旋转前后都满足平衡树的性质,即左子树上所有元素小于根节点,小于右子树上所有元素。通过旋转操作,我们可以变更树结构,但是不影响二叉搜索树的性质。
问题是KD-Tree当中我们在不同深度判断元素大小的维度不同,我们旋转之后节点的树深会发生变化,会导致判断标准发生变化。这样会导致旋转之后不再满足KD-Tree的性质。
我们用刚才的图举个例子:
我们给每个节点标上了数据,在树深为0的节点当中,划分维度是0,树深为1的节点划分维度是1。当我们旋转之后,很明显可以发现KD-Tree的性质被打破了。
比如D节点的第0维是2,B节点是1,但是D却放在了B的左子树。再比如A节点的第1维是3,E节点的第1维是7,但是E同样放在了A的左子树。
这还只是二维的KD-Tree,如果维度更高,会导致情况更加复杂。
通过这个例子,我们证明了平衡树旋转的方式不适合KD-Tree。
那么,除了平衡树旋转的方法之外,还有其他方法可以保持树平衡吗?
别说,还真有,这也是本篇文章的正主——替罪羊树。
替罪羊树
替罪羊树其实也是平衡二叉树,但是它和普通的平衡二叉树不同,它维护平衡的方式不是旋转,而是重建。
为什么叫替罪羊树呢,替罪羊是圣经里的一个宗教术语,原本指的是将山羊献祭作为赎罪的仪式,后来才衍生出了代人受过,背锅侠的意思。替罪羊树的意思是一个节点的变化可能会导致某一个子树或者是整棵树被摧毁并重建,相当于整棵子树充当了某一个节点的”替罪羊“。
替罪羊树的里非常简单粗暴,不强制保证所有子树完全平衡,允许一定程度的不平衡存在。当我们插入或者删除使得某一棵子树的节点超过平衡底线的时候,我们将整棵树拍平后重建。
比如下图红框当中表示一棵不平衡的子树:
很明显,它不平衡地十分严重,超过了我们的底线。于是我们将整棵子树拍平,拍平的意思是将子树当中所有的元素全部取出,然后重建该树。
拍平之后的结果是:
拍平之后重建该子树,得到:
我们把重建的这棵子树插回到原树上,代替之前不平衡的部分,这样就保证了树的平衡。
整个原理应该非常简单,底层的细节也只有一个,就是我们怎么衡量什么时候应该执行拍平重建的操作呢?
这一点在替罪羊树当中也非常简单粗暴,我们维护每一棵子树中的节点数,然后通过一个参数alpha来控制。当它的某一棵子树的节点数的占比超过alpha的时候,我们就认为不平衡性超过了限度,需要进行拍平和重建操作了。
一般alpha的取值在0.6-0.8之间。
删除
在替罪羊树当中删除节点有很多种方法,但是大都大同小异,核心的思想是我们删除节点并不是真的删除,而是给节点打上标记,标记这个节点在查询的时候不会被考虑进去。