最近研究了一下KD树,以及在此基础之上进行的改进BBF方法,以及如何利用BBF进行KNN。当然我还是主要参照很厉害的人物文章,代码利用C#实现了而已。
在这里对我帮助最大的网址如下:
http://blog.youkuaiyun.com/v_july_v/article/details/8203674 这篇文章主要讲的就是K近邻,距离度量,KD树,以及BBF算法。里面讲的很详细,里面大部分都是伪代码,可能实现起来还是有一定的难度。在我几天的研究之下,实现了文章中所说的方法,当然也同时借鉴了第三个网址中讲解的,主要是通过那个网址中的代码来实现。
http://www.cnblogs.com/eyeszjwang/articles/2437706.html 这个文章中主要讲解的是KD树的优化查找方法BBF,文章中也同时实现了K近邻的查找,虽然第一个网址中的代码页讲解了实现过程,但是看起来还是比较晦涩的。
http://www.leexiang.com/kd-tree 这个文章中讲解了KD的构造过程,以及最近邻查找的实现过程,这篇文章对于我前期实现工作帮助特别大。
当我我希望研究这个算法的还是要详细的阅读这几篇文章。
下面我就贴出我的代码:
1、准备工作
public class Node
{
public Train point{get;set;} //节点信息
public Node leftNode { get; set; } //左子树
public Node righNode { get; set; } //右子树
public int split { get; set; } //分割的方向轴序号
public Node parent { get; set; } //父节点
public List<Train> range { get; set; } //空间节点
}
public class Train
{
public float positionX { get; set; }
public float positionY { get; set; }
public float positionZ { get; set; }
public Int32 AvgRssi { set; get; }
}
public class PriorityList
{
public Node node { get; set; }
public float priority { get; set; }
}
2、初始化数据
private void GenerareData()
{
lsTrain.Add(new Train() { positionX = 2, positionY = 3, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 5, positionY = 4, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 9, positionY = 6, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 9, positionY = 8, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 4, positionY = 7, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 8, positionY = 1, positionZ = 0 });
lsTrain.Add(new Train() { positionX = 7, positionY = 2, positionZ = 0 });
}
private List<Train> lsTrain = new List<Train>();
Node root = CreatKDTree(lsTrain);
下面我们看看CreatKDTree()函数:
private Node CreatKDTree(List<Train> train)
{
//创建节点
Node node = new Node();
node.range = train;
if (train.Count == 1)
{
//只有一个节点时即为叶节点,直接返回叶节点
node.split = 0; //默认为X方向轴分割
node.point = train[0];
node.leftNode = null;
node.righNode = null;
return node;
}
int axis = GetAxis(train);
Train splitNode = GetSplitPoint(train, axis);
train.Remove(splitNode); //新的数据空间
//获取左子树的数据空间,即比splitNode在axis方向上小的数据
List<Train> leftTreeRange = this.LeftTreeRange(train, splitNode, axis);
//获取右子树的数据空间,即比splitNode在axis方向上大的数据
List<Train> rightTreeRange = this.RightTreeRange(train, splitNode, axis);
node.split = axis;
node.point = splitNode;
//子树不为空则进行下一层递归
if (leftTreeRange.Count == 0)
node.leftNode = null;
else
node.leftNode = this.CreatKDTree(leftTreeRange);
if (rightTreeRange.Count == 0)
node.righNode = null;
else
node.righNode = this.CreatKDTree(rightTreeRange);
return node;
}
创建KD树的过程主要使用的是递归的方法:
首先我们计算每个维度上的方差,方差的大小说明了在这个维度上的点的分散程度。我们选择维度中方差最大的作为我们的分裂维度。
private int GetAxis(List<Train> train)
{
//计算方差,选择方向轴
int axis = -1; //坐标轴,0表示X轴,1表示Y轴,2表示Z轴
float xDemonAvg = 0, yDemonAvg = 0, zDemonAvg = 0; //定义了三个个维度的平均值
foreach (var tmp in train)
{
xDemonAvg += tmp.positionX * 1.0f;
yDemonAvg += tmp.positionY * 1.0f;
zDemonAvg += tmp.positionZ * 1.0f;
}
//计算均值
xDemonAvg = xDemonAvg / train.Count;
yDemonAvg = yDemonAvg / train.Count;
zDemonAvg = zDemonAvg / train.Count;
//计算方差
double xS2 = 0, yS2 = 0, zS2 = 0; //初始化三个轴的方差
foreach (var tmp in train)
{
xS2 += Math.Pow(tmp.positionX - xDemonAvg, 2);
yS2 += Math.Pow(tmp.positionY - yDemonAvg, 2);
zS2 += Math.Pow(tmp.positionZ - zDemonAvg, 2);
}
xS2 = xS2 / train.Count;
yS2 = yS2 / train.Count;
zS2 = zS2 / train.Count;
if (xS2 >= yS2 && xS2 >= zS2)
{
axis = 0;
}
else if (yS2 > xS2 && yS2 > zS2)
{
axis = 1;
}
else
axis = 2;
return axis;
}
其次、当我选择好这个维度之后,我们需要在这个维度上将这个空间上的所有点按照升序排列。当然排序算法有很多,不同的排序的算法效率也是不同的,这里我们没有关注效率问题,选择的是快速排序算法,这个算法就是List.Sort()方法的内部实现。
private Train QuickSort(List<Train> train, int axis)
{
if (0 == axis)
{
train.Sort(this.CompareTrainX);
}
else if (1 == axis)
{
train.Sort(this.CompareTrainY);
}
else
train.Sort(this.CompareTrainZ);
return train[train.Count / 2];
}
#region 定义比较器
private int CompareTrainX(Train trainFirst, Train trainSecond)
{
if (trainFirst.positionX == trainSecond.positionX)
{
return 0;
}
else if (trainFirst.positionX < trainSecond.positionX)
{
return -1;
}
else
return 1;
}
private int CompareTrainY(Train trainFirst, Train trainSecond)
{
if (trainFirst.positionY == trainSecond.positionY)
{
return 0;
}
else if (trainFirst.positionY < trainSecond.positionY)
{
return -1;
}
else
return 1;
}
private int CompareTrainZ(Train trainFirst, Train trainSecond)
{
if (trainFirst.positionZ == trainSecond.positionZ)
{
return 0;
}
else if (trainFirst.positionZ < trainSecond.positionZ)
{
return -1;
}
else
return 1;
}
#endregion
当我们将选定的分裂维度上的控件数据进行了排序之后,我们选择最中间的数据点最为树或者子树的跟节点。即:
Train splitNode = GetSplitPoint(train, axis);
当我们我们确定了分裂点之后,下面要完成的就是子空间划分,即将空间中的划分为左右子空间,即:
//获取左子树的数据空间,即比splitNode在axis方向上小的数据
List<Train> leftTreeRange = this.LeftTreeRange(train, splitNode, axis);
//获取右子树的数据空间,即比splitNode在axis方向上大的数据
List<Train> rightTreeRange = this.RightTreeRange(train, splitNode, axis);
划分好子空间之后分别对子空间进行递归调用CreatKDTree()函数,进行子空间的KD树构造,递归的结束标志是子空间没有数据,即:
//子树不为空则进行下一层递归
if (leftTreeRange.Count == 0)
node.leftNode = null;
else
node.leftNode = this.CreatKDTree(leftTreeRange);
if (rightTreeRange.Count == 0)
node.righNode = null;
else
node.righNode = this.CreatKDTree(rightTreeRange);
这样我们就完成了KD树的构造过程。