- 考虑第三个参数:决策函数
决策函数的实现和KNN算法紧耦合,所以在KNN算法中用公共枚举类,将参暴露出来,并在类的内部给予实现。
public enum DecisionFunction {
MajorityVote,
WeightedMajorityVote;
}
如下图便是这次KNN算法实现的基本框架:
设计理由如下:
-
每一个KNN实例提供未知数据的分类或回归的功能;
-
每一个KNN实例是分类器或回归器。
-
每一个KNN实例都依赖一个KNN训练模型,KNN训练模型提供搜索未知数据点的 k k k个近邻的功能。
-
KNN训练模型可以有不同的实现方式,暴力的,KD树的,等等。
-
样本点规范了数据集的格式,易于操作。
下面三张图是详细的类的方法的设计。
k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索。
k-d树是一个k维二叉树,每个节点表示一个空间范围;
这种搜索有两种基本的方式:一种是范围查询(range searches),另一种是K近邻查询(K-neighbor searches)。
范围查询就是给定查询点和查询距离的阈值,从数据集中找出所有与查询点距离小于阈值的数据;
K近邻查询是给定查询点及正整数K,从数据集中找到距离查询点最近的K个数据,当K=1时,就是最近邻查询(nearest
neighbor searches)。
我们先以一个简单直观的2维实例来介绍k-d树算法。
二维空间划分举例
假设在二维平面上,有6个点,分别为:
( 2 , 3 ) 、 ( 4 , 7 ) 、 ( 5 , 4 ) 、 ( 7 , 2 ) 、 ( 8 , 1 ) 、 ( 9 , 6 ) (2,3)、(4,7)、(5,4)、(7,2)、(8,1)、(9,6) (2,3)、(4,7)、(5,4)、(7,2)、(8,1)、(9,6)
(怎样划分、以及划分空间的理由下面会细述。)
-
第一次划分是红色线对应的平面,这样空间被分成两个子空间。
-
第二次划分是蓝色线对应的平面,之后两个子空间又被各自分成两个子空间。
-
最后一次划分,是绿色线对应的平面,之后停止划分。(因为子空间中没有其他点了)
这样的划分,正好对应一颗二叉树,如下图。
而这样的一棵树,就是KD树了。
KD树快速搜索近邻的核心原理
上面讲述了KD树划分空间的实例,却没有阐述为什么这样划分。下面就这个问题作出解释,
实际上这也是为什么KD树能加快搜索的原理所在了。
如果按照暴力的思维,那就遍历整棵树,依次计算距离,然后更新最小值就好了。
可是KD树的奥妙就在于通过剪枝来减少无效的搜索。
如下图,如果我们通过粉色的搜索路径找到最近邻点,得到的距离的最小值,
树的遍历总要回溯到父亲节点的,然后在父亲节点的时候还需不需要的向上面的子空间继续搜索呢?
其实不需要的,因为发现已经得到的最短距离比未知点到划分子空间的超平面的距离还要小,在图上表现出来即为淡蓝色虚线圆没有和深蓝色的超平面相交,
这说明了及其重要的一点,父节点上面的子空间中不可能有更近的邻居了,而这是显而易见的。
KD树的实现细节
下面来介绍一下KD树实现的细节。
中位数算法
-
为了使二叉树的高度尽可能矮,它应该尽可能接近一颗平衡树,所以每次划分的时候都应该使左右两边的点的个数尽可能相等。
-
为了快速求出一个数组的中位数,并且将比它小的元素放在它的左边,比它大的元素放在它的右边。在cpp中可以使用
nth_element()
函数在 O ( n ) O(n) O(n)时间复杂度解决。算法实现其实就是快排。
java里面没有,我们自己实现一下。
package utils;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
public class MyArray {
private T[] list;
private Comparator comparator;
private Random random;
/**
-
@param list 数组
-
@param comparator 比较器
*/
public MyArray(T[] list, Comparator comparator) {
this.list = list;
this.comparator = comparator;
random = new Random();
}
public MyArray(List _list,Comparator comparator){
list = (T[]) new Object[_list.size()];
_list.toArray(list);
this.comparator = comparator;
random = new Random();
}
/**
-
@param l 数组左边界(包含)
-
@param r 数组右边界(不包含)
-
@param k 第 k 小
-
@return 第 k 小的元素
*/
public T getTopK(int l, int r, int k) {
if(l>=r) return null;
return solve(l,r,k);
}
public T getTopK(int k) {
return getTopK(0, list.length, k);
}
/**
-
@param l 数组左边界(包含)
-
@param r 数组右边界(不包含)
-
@param k 第 k 小
-
@return 第 k 小的元素
*/
private T solve(int l, int r, int k) {
if (l + 1 == r) return list[l];
int i = random.nextInt(r - l) + l;
// 交换 a[l],a[r-1]
T temp = list[i];
list[i] = list[r-1];
list[r-1] = temp;
int p = partion(list,l,r-1);
int cnt = p-l;
if(cnt>=k) return solve(l,p,k);
return solve(p,r,k-cnt);
}
/**
-
循环不变量法:
-
[l,i] 都小于等于x,
-
[i+1,j] 都大于x
-
[j+1,r-1] 尚未处理
-
a[r] 是基准值
-
@param a 待划分的数组
-
@param l 左边界
-
@param r 右边界
-
@return 基准值索引
*/
private int partion(T[] a, int l, int r) {
T x = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (comparator.compare(a[j], x) <= 0) {
i++;
T temp = a[j];
a[j] = a[i];
a[i] = temp;
}
}
T temp = a[i + 1];
a[i + 1] = a[r];
a[r] = temp;
return i + 1;
}
public void setComparator(Comparator comparator) {
this.comparator = comparator;
}
}
划分维度的选取
划分维度的选择一般有三种划分方式
-
随机选择一个维度进行划分空间
-
按照 k 个维度轮换作为划分维度
-
按方差最大的那个维度进行划分
最科学的方法自然是使用 第3种方法, 按照方差最大的那个维度来进行划分。
这样数据点可以更好地分散开来。
但是对于随机数据而言,第二种轮换换分的方式表现的同样很优秀!
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
负担。**
[外链图片转存中…(img-qv4CB75C-1715793014404)]
[外链图片转存中…(img-qVS7XB7a-1715793014404)]
[外链图片转存中…(img-rkaOwjNu-1715793014405)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!