承接上文,探讨kd二叉查找树的平衡、删除改进以及运用。
不管是普通的二叉查找树仍是kd二叉查找树,频繁的添加以及删除操做均可能破坏整棵树的平衡,怎么办呢?对于普通的二叉查找树,能够经过DSW算法或者AVL算法进行平衡,相关内容能够看这里,数据结构与算法-二叉查找树(DSW)和数据结构与算法-二叉查找树(AVL)。以上算法的核心都是旋转,经过旋转来调整左右子树的高度来平衡树,可是旋转对于kd二叉树是不合适的,由于kd二叉树不一样层次之间比较的维度是不一样的。好比说存在节点P,比较的维度是x吧,那么节点P的左子树中任意节点的x维度的值小于P节点x维度值,右子树中任意节点的x维度的值大于P节点x维度值。若是将P节点调高或者下降层次,那么P节点和左右子树比较的维度就会改变,假如是y吧。不能保证节点P的左子树中任意节点的y维度的值小于P节点y维度值,右子树中任意节点的y维度的值大于P节点y维度值。因此,旋转不能用于kd二叉树。
答案是删除原有的树,从新建立一颗平衡的kd二叉查找树。这种方法很是暴力,若是用于生产环境,必须选择服务器压力较小的一个时间点来从新平衡树。而且,要控制从新建立kd树的频率,这个频率须要在生产环境中,根据具体的数据量来调整。
接下来探讨改进删除算法。删除算法的效率不高,缘由在于须要不停的遍历被删除节点下的某颗子树,不只耗费时间并且浪费计算资源。怎么改进呢?
有一种叫作替罪羊的算法能够用到这里。就是说,每次删除节点的时候,不是真正的删除,而是作个标记代表这个节点已删除,这样就不会影响kd树的平衡。可是被标记的节点太多也很差,怎么处理呢?能够在每次删除的时候进行检查,统计被标记节点在整棵树中的比例,若是比例大于阈值,就从新平衡树。删除掉被标记的节点,使用剩余节点建立新的平衡的kd二叉树。因为从新建立kd树是由某个节点删除引发的,可是显然责任不是它一个节点的,它就成了替罪羊,所以,算法就以此命名了。替罪羊算法效率仍是能够的,牺牲较小的查找效率来获取整棵树的平衡是可取的,生产环境能够考虑使用。
假设咱们有一系列点处于k维空间中,如今有个需求,须要知道符合x1<x<xn,y1<y<yn,...,k1<k<kn的节点有哪些。若是没有kd二叉树,只能经过遍历筛选出符合要求的节点,显然,这种方式效率不高。kd二叉树能够帮助咱们跳过一些节点来提升查找效率。首先用天然语言描述算法逻辑:
假设存在节点P,节点P所处层次须要比较维度x,首先判断x(P)处于x1<x<xn的哪一个范围。若是x1<x(P)<xn,显然,须要遍历P节点的全部子树,若是x(P)<x1,那么只须要遍历P节点的右子树,若是x(P)>xn,那么只须要遍历P节点的左子树便可。
searchRange(range[][]){
if root != 0
search(root, 0, range);
}
search(p, i, range[][]){
found = true;
for j = 0 到 k - 1
if !(range[j][0] <= p-el.keys[j] <= range[j][1])
found = false;
bread;
if found
输出 p->el;
if p->left != 0 && range[i][1] <= p->el.keys[i]
search(p->left, (i + 1) mod k, range);
if p->right != 0 && range[i][0] >= p->el.keys[i]
search(p->right, (i + 1) mod k, range);
}复制代码
使用kd二叉树查找特定范围的节点相比于直接遍历要快的多。
kNN的全称是k-Nearest-Neighbor,k个最近邻点。也就是说,假设如今有n个m维空间的点,给定点P,须要找到与定点P最靠近的k个点,这就是kNN算法。这里距离的求法用的是欧式距离:
最直观的算法逻辑是遍历n个节点,计算出每一个节点与定点P的距离,而后排序,获取距离最小的k个邻点。算法的时间复杂度是0(n),还能够接受,可是太占用计算资源了,每两个节点距离的计算须要n次减法,n次平方,不只仅占用大量时间,还会占用大量计算资源,得不偿失。
当前有不少kNN解决方案,咱们来探讨基于kd二叉树的kNN方案。
在探讨以前,咱们须要理解kd二叉树的几何意义,就从一维kd二叉树开始。
当k为1时,kd二叉树就退化为普通的二叉查找树,咱们能够将二叉树上的节点想象成横坐标上的定点。就像这样:
横坐标被分解成8个部分,若是给出定点P,那么该定点最终会落入这8个部分之一。
当k为2时,kd二叉树中的节点能够想象成坐标系上的点,就像这样:
其中每一个点都表明着一部分区域,首先A节点表明整个矩形,G节点表明被A分开的左边的矩形,B节点表明被A分开的右边的矩形,以此类推,每一个节点表明着某块区域的同时将该区域分割成两部分,分别被左右子树占据。
也就是说,若是给出一个定点P,那么P必然落入以上8个区域之一。
在kd二叉树上实现kNN算法之因此效率较高,是由于kd二叉树能够将k维空间分割成m个区域,给出的查找定点必然落入某个区域之中,该区域必然被某个节点P的左子树或者右子树占据,假设定点落入了左子树中,咱们能够计算出在该区域中距离定点最近的节点,假设距离为l1。下面是核心逻辑,咱们已经获取了节点P左子树中距离定点最近的节点,那么节点P的右子树有必要遍历吗?假设节点P比较的是x维度的值,咱们能够计算出定点和x=x(P)超平面之间的距离l2。若是l1<l2,那么就不必遍历右子树了,由于定点距离右子树中节点的距离只会比l2更大,也就是说l1确定是最小的距离。若是l1>l2,那么就有必要遍历右子树,由于右子树中可能存在距离定点更近的节点。到目前为止,咱们已经获取了节点P所表明的的子树中距离定点最近的节点,下一步怎么走?咱们知道,节点P必然是某个节点R的左子树或者右子树,假设是左子树吧,那么问题来了,节点R的右子树须要遍历吗?这就回到了上面的问题,你会发现,经过不断回溯父节点,咱们能够不断的跳过某些子树,最终整棵树里的节点所有遍历完毕。由于kd树在查找距离定点最近节点时,能够跳过不少子树,所以效率会大大提升。
-
给出定点P,首先获取定点P落入了哪块区域,假设是区域Q吧,计算出定点P和区域Q中节点最小的距离l1
-
回溯到父节点R,计算定点P和节点R之间的距离l2,取l1和l2之间的最小值为新的l1
-
假设节点R比较的是x维度的值,计算定点P和x=x(p)超平面之间的距离l3
-
若是l1<l3,跳过R节点的另外一颗子树,继续回溯到R节点的父节点,若是R节点为Root,遍历结束,不然从第二步开始
-
若是l1>l3,遍历R节点的另外一颗子树,获取定点和该子树中节点最小的距离l4
-
取l1和l4之间的最小值为新的l1,当前,R节点所在子树已经遍历完毕,那就继续回溯到R节点的父节点,若是R节点为Root,遍历结束,不然从第二步开始
算法中,也许有人会疑惑l4的大小如何获取,也就是说,不知道如何遍历R节点的另外一颗子树。答案是使用递归,由于R节点的子树也是一颗kd二叉树,咱们对该子树使用递归便可获取l4的大小。
到目前为止,咱们已经探讨了kd二叉树的平衡,删除算法改进,输出符合特定范围的节点以及最后的kNN算法。kd二叉树还有不少细节须要去理解,这些须要读者在实践中获取了。