KdTree理解与实现(Java)

抛出问题

KdTree简介

原理简介

代码实现

Point.java

Rect.java

KdTree.java

复杂度比较

结语

抛出问题

如果让你设计一个外卖系统,你的数据库中有所有外卖商家所在的经纬度,那么如何能有效地根据用户的位置筛选出所有附近的商家?

最直接的方法是根据城市或者城市的每个区(如崂山区,市南区…)来对商家进行分类,然后根据用户所在的区返回同一区域下的所有商家。这个方法可以解决大部分问题,但是如果用户位于两个区的分界线周围怎么办?

KdTree简介

KdTree 是以二叉搜索树(Binary Search Tree)为原型的用于空间检索的数据结构,能够在随机分布的空间内以 O(log2N) 的时间复杂度实现对平面内点的搜索以及 O(log2N) + R 的复杂度查询平面内任意矩形内的所有点(R为矩形内点的个数)。 KdTree的应用十分广泛,包括且不限于范围搜索,最邻近点搜索,物理引擎中的碰撞检测以及地理节点(如外卖商家)数据库等。

原理简介

KdTree的实现方法与BST十分相似,以最常用的二维平面的KdTree为例,其每个节点存储一个二维的坐标点,并将平面空间以该点所在的横线/竖线递归地分割成两个子空间。

以width = 1.0, height = 1.0的单位平面为例,依次插入下列点

Note:

点对平面的分割方式是横向/纵向按照层次交替出现(根节点是哪个方向都可以)。

插入节点的方法类似于BST,即从根节点开始,(设要插入的节点为Pinsert,当前遍历的节点为Pcurrent)如果Pinsert在Pcurrent的左边或者下边,那么就访问Pcurrent的left child, 反之访问right child直到成为叶子节点。

本KdTree不支持删除操作。

代码实现

在介绍KdTree实现之前先定义两个辅助类Point(用来表示点)和Rect(用来表示矩形)

Point.java

用来表示一个坐标点,在本博客的语境下只需要两个方法:计算与另一点的距离(以平方和的形式) 和 判断两点是否相等。

// @file Point.java

// @author 王成昊

// @date 2018.10.14

public class Point {

public final double x;

public final double y;

// Point类是 immutable datatype

public Point(double x, double y) {

this.x = x;

this.y = y;

}

// 为了减少计算量,一般使用平方和来表示距离

public double distanceSquareTo(Point that) {

double dx = that.x - this.x;

double dy = that.y - this.y;

return dx * dx + dy * dy;

}

@Override

public boolean equals(Object that) {

if (this == that) return true;

if (that == null) return false;

if (that.getClass() != this.getClass()) return false;

Point point = (Point) that;

return (x == point.x) && (y == point.y);

}

}

Rect.java

用来表示一个矩形,在本例中使用四个坐标值来表示一个矩形。需要的方法是 判断矩形是否包含一个点 和 计算矩形和某点的距离(平方和的形式)

// @file Rect.java

// @author 王成昊

// @date 2018.10.14

public class Rect {

// 分别表示左下顶点和右上顶点

public final double minX;

public final double minY;

public final double maxX;

public final double maxY;

// Rect类是 immutable datatype

public Rect(double x0, double y0, double x1, double y1) {

minX = x0;

minY = y0;

maxX = x1;

maxY = y1;

}

// 判断该点是否位于该矩形之内

public boolean contains(Point point) {

return (point.x >= minX) && (point.x <= maxX)

&& (point.y >= minY) && (point.y <= maxY);

}

// 计算矩形到某一点的最近距离(以平方和的形式)

public double distanceSquareToPoint(Point point) {

double dx = 0.0;

double dy = 0.0;

if (point.x < minX) dx = minX - point.x;

else if (point.x > maxX) dx = point.x - maxX;

if (point.y < minY) dy = minY - point.y;

else if (point.y > maxY) dy = point.y - maxY;

return dx * dx + dy * dy;

}

}

KdTree.java

本例中KdTree将实现4个功能:

插入

判断是否包含某点

查询任意矩形内的所有点

查询距离某一点最近的点

// @file KdTree.java

// @author 王成昊

// @date 2018.10.14

import java.util.LinkedList;

public class KdTree {

// 节点类,其中 rect 成员表示该节点所分割的平面,

// 即它的左右孩子所表示的空间之和,该成员用于判断

// 最邻近点

private class Node {

Point point;

Rect rect;

Node left;

Node right;

Node (Point p, Rect r) {

point = p;

rect = r;

left = null;

right = null;

}

}

// 根节点

private Node root;

// 构造函数

public KdTree() {

root = null;

}

// 插入, 用同名私有方法递归实现, 默认根节点是纵向分割

public void insert(Point point) {

root = insert(point, root, false, 0.0, 0.0, 1.0, 1.0);

}

private Node insert(Point point, Node node, boolean isVertical,

double x0, double y0, double x1, double y1) {

if (node == null) {

return new Node(point, new Rect(x0, y0, x1, y1));

}

// 改变分割方向

isVertical = !isVertical;

// 判断要插入的点在当前点的左/下还是右/上

double value0 = isVertical ? point.x : point.y;

double value1 = isVertical ? node.point.x : node.point.y;

if (value0 < value1) {

node.left = insert(point, node.left, isVertical,

x0, y0, isVertical ? node.point.x : x1, isVertical ? y1 : node.point.y);

} else {

node.right = insert(point, node.right, isVertical,

isVertical ? node.point.x : x0, isVertical ? y0 : node.point.y, x1, y1);

}

return node;

}

// 判断是否包含该点, 用同名私有方法递归实现

public boolean contains(Point point) {

return contains(point, root, false);

}

private boolean contains(Point point, Node node, boolean isVertical) {

if (node == null) return false;

if (node.point.equals(point)) return true;

// 改变分割方向

isVertical = !isVertical;

// 判断要查询的点在当前点的左/下还是右/上

double value1 = isVertical ? point.x : point.y;

double value2 = isVertical ? node.point.x : node.point.y;

if (value1 < value2) {

return contains(point, node.left, isVertical);

} else {

return contains(point, node.right, isVertical);

}

}

// 返回矩形范围内的所有点, 用同名私有方法递归实现

public Iterable range(Rect rect) {

LinkedList result = new LinkedList();

range(rect, root, false, result);

return result;

}

private void range(Rect rect, Node node, boolean isVertical, LinkedList bag) {

if (node == null) return;

// 改变分割方向

isVertical = !isVertical;

Point point = node.point;

if (rect.contains(point)) bag.add(point);

// 判断当前点所分割的两个空间是否与矩形相交

double value = isVertical ? point.x : point.y;

double min = isVertical ? rect.minX : rect.minY;

double max = isVertical ? rect.maxX : rect.maxY;

if (min < value) {

range(rect, node.left, isVertical, bag);

}

if (max >= value) {

range(rect, node.right, isVertical, bag);

}

}

// 返回距离该点最近的点, 用同名私有方法递归实现

public Point nearest(Point target) {

return nearest(target, root, null, false);

}

private Point nearest(Point target, Node node, Point currentBest, boolean isVertical) {

if (node == null) return currentBest;

isVertical = !isVertical;

double value1 = isVertical ? target.x : target.y;

double value2 = isVertical ? node.point.x : node.point.y;

// 继续搜索目标点所在的半区

Node next = value1 < value2 ? node.left : node.right;

Node other = value1 < value2 ? node.right : node.left;

Point nextBest = nearest(target, next, node.point, isVertical);

double currentDistance = 0;

double nextDistance = nextBest.distanceSquareTo(target);

if (currentBest == null) {

currentBest = nextBest;

currentDistance = nextDistance;

} else {

currentDistance = currentBest.distanceSquareTo(target);

if (nextDistance < currentDistance) {

currentBest = nextBest;

currentDistance = nextDistance;

}

}

// 判断另一半区是否可能包含更近的点

if ((other != null) && (other.rect.distanceSquareToPoint(target) < currentDistance)) {

currentBest = nearest(target, other, currentBest, isVertical);

}

return currentBest;

}

public static void main(String[] args) {

// unit test

}

}

Note:

比较难理解的是nearest()方法,该方法为深度优先搜索,逻辑是:

1.从根节点开始向下搜索,递归搜索优势半区 (定义 目标点 为

public Point nearest(Point target)

中的target, 目标点所在的半区为优势半区,另一半区为劣势半区 ) ,并将当前点作为currentBest 参数传递给下层,直到叶子节点。

2.此时开始回溯,返回 nextBest ,获得 {该节点优势半区中的所有点,以及parent点} 中距离目标点最近的点 ,其最优距离为currentDistance。 此时考虑是否需要搜索劣势半区。

3.如果劣势半区所在的矩形与目标点的距离小于currentDistance,则搜索劣势半区。换句话说,如果矩形到目标点的距离小于currentDistance,说明劣势半区中有存在更近的点的可能。

复杂度比较

KdTree在最坏情况下的复杂度与暴力求解(用集合遍历所有元素)一样都是O(n), 但在随机分布的情况下可以达到O(log2N)。

以下为两个数据结构在随机分布的空间中的算法复杂度 (其中R表示矩形范围内点的个数)

数据结构

insert()

contains()

range()

nearest()

Set

1

N/2

N

N

KdTree

log2N

log2N

log2N + R

log2N

结语

之前在学数据库的时候大作业是做一个类似饿了么的外卖网站,其中的一个难点是如何根据用户所在的位置检索出所有附近的商家。当时想了半天也想不出来怎么能有效的进行区间搜索,用的是暴力方法(因为是demo所以数据量很小),现在学到了KdTree之后真的是大彻大悟啊

kdtree java_KdTree理解与实现(Java)相关推荐

  1. 循序渐进:带你理解什么是Java内存模型

    近期笔者在阅读<深入理解Java虚拟机:JVM高级特性与最佳实现(第3版)>,书中提到关于Java内存模型的知识点,但是看完之后还是感觉有些模糊,便查阅一些其他相关资料.本文是笔者经过对知 ...

  2. 理解和解决Java并发修改异常ConcurrentModificationException(转载)

    原文地址:https://www.jianshu.com/p/f3f6b12330c1 理解和解决Java并发修改异常ConcurrentModificationException 不知读者在Java ...

  3. java 句柄池_深入理解JVM之Java对象的创建、内存布局、访问定位详解

    本文实例讲述了深入理解JVM之Java对象的创建.内存布局.访问定位.分享给大家供大家参考,具体如下: 对象的创建 一个简单的创建对象语句Clazz instance = new Clazz();包含 ...

  4. Java异常处理深入理解_关于java异常处理机制的深入理解.doc

    关于java异常处理机制的深入理解.doc 关于JAVA异常处理机制的深入理解1引子TRYCATCHFINALLY恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解.不过 ...

  5. 【java】理解和运用Java中的Lambda

    1.概述 转载:理解和运用Java中的Lambda 前提 回想一下,JDK8是2014年发布正式版的,到现在为(2020-02-08)止已经过去了5年多.JDK8引入的两个比较强大的新特性是Lambd ...

  6. 深入理解JVM(1)——Java内存区域与Java对象

    最近放暑假在家,没有实验室的工作要做,正好趁这个时间恶补了之前一知半解的JVM知识,使用的主要学习资料是周志明大大的<深入理解Java虚拟机--JVM高级特性与最佳实践>,我仔细拜读了两遍 ...

  7. java的接口理解_原来Java的接口可以这样理解

    为什么写这篇文章 今天有人问了我这样一个问题 Java中为什么要使用接口呢? 还要先定义了一个接口,类还要实现接口的方法,还不如直接在这个类中写实现方法呢,根本没必要定义接口啊. 大概就是这样一个问题 ...

  8. 深入理解JMM(Java内存模型) --(三)顺序一致性

    数据竞争与顺序一致性保证 当程序未正确同步时,就会存在数据竞争.Java内存模型规范对数据竞争的定义如下: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序. 当代码 ...

  9. 要理解递归,得先理解递归--用Java语言由浅入深讲解汉诺塔游戏

    2019独角兽企业重金招聘Python工程师标准>>> 一.递归是什么? 定义:程序调用自身的编程技巧称为递归.它分为调用阶段和回退阶段,递归的回退顺序是它调用顺序的逆序. 递归使用 ...

最新文章

  1. js获取微信状态栏高度_人人商城打包app教程 方法 hbuilder打包支持支付宝微信原生支付...
  2. BZOJ 2038: [2009国家集训队]小Z的袜子(hose) 分块
  3. python层级抓取_python实现提取str字符串/json中多级目录下的某个值
  4. python整数因子_Python:通过非整数因子下采样2D numpy数组
  5. 5分绩点转4分_高考语文如何考上120分?衡中老师建议:这5点高中生必须重视
  6. Linux—vim/vi 翻页跳转命令快捷键
  7. linux挂载华为存储fc,linux 挂载存储设备
  8. mysql 存储过程 模糊查询_mysql 分页创建存储过程并实现模糊查询
  9. clion stfp 配置
  10. 图像处理1 高斯模糊
  11. [SDOI2016]储能表——数位DP
  12. 百度杀毒软件2013正式发布
  13. Python进行相关分析
  14. HCIE面试真题战报及点评系列12
  15. 【教程】 如何在linux/mac下安装pascal
  16. Wekan 2.23 发布,支持中文的 JavaScript kanban
  17. linux压缩到最小命令,Linux 压缩打包命令详细教程
  18. 'C 语言' | 全排列解桥本分数式问题
  19. TCP RTT测量妙计
  20. 【存储知识】存储基础知识(存储设备、HBA卡、硬盘接口类型、存储特性指标)

热门文章

  1. 输出高电平程序c语言,51用c语言怎么编程检测US-100超声波测距模块echo/rx引脚输出高电平时间长度,,谁能给我个模板...
  2. 全球首款5G手机出炉?其实只是一个笑话
  3. Vue笔记(四)——Vue动画路由
  4. 一片关于Bootstarp4的文章
  5. 微信小程序 --- 动态获取input的value
  6. 掰一掰GitHub上优秀的大数据项目
  7. alidata 手动挂载数据盘
  8. ORACLE 查看有多个执行计划的SQL语句
  9. 第二章 Javac编译原理
  10. 去除tableView表头悬浮