如何为空间索引创建高效的数据结构?

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 7 天,点击查看活动详情

数据结构不仅能存储值,还可以帮助我们高效地对数据进行操作。比如,当我们想存储一些一维数据点、自然数或是字符串时,我们通常会使用一维数组。为了能快速地检索数据,我们一般会使用自然次序索引(1 < 2 < 3)或者是像字典树、二叉树这样的数据结构。

假如我们需要使用二维空间存储数据,那我们该如何设计一个快速索引?假如我们需要按邻近度排序(如找到某一点的所有临近点),那又该如何设计?

按自然次序检索是行不通的,因为我们将有两个不同的索引 —— 一个用于横坐标,另一个用于纵坐标。这样一来,我们就必须在数据库中搜索所有位置为 X + deltaY + delta 的点,之后再对两个集合进行交集操作。

由此一来,我们需要空间索引。

空间索引通常作为二维空间高效访问的手段。空间索引的实际应用场景包括但不限于:共享骑行(来福车、优步)、餐饮配送(DoorDash、Yelp)等。举例来说,DoorDash 通过空间索引来获取最靠近的配送点,Yelp 也借此帮助你找到离你最近的餐饮店的位置。

类似的应用还包括KNN 算法,用于搜索给定样本在特征空间中的所有邻近样本。

范围查询:查找一个包含给定坐标的对象(坐标查询),或者是与特定区域重叠的对象(窗口查询)。

空间连接:查找在空间上相互影响的对象对。我们能从空间方面了解对象之间的相交、邻近和包含关系。

现在你已经了解了什么是空间索引,那么什么数据结构能提高索引数据点的效率呢?如果你第一时间想到的是四叉树,那么恭喜你,你是正确的。在接下来的内容中,我们将讨论什么是四叉树,以及它是如何存储稀疏数据来实现索引的。

什么是四叉树?

四叉树通过划分空间来提高遍历和搜索的效率。它是一种将值划分为四个部分的树形数据结构。叶节点中保存的值取决于具体的应用场景。被细分的区域可以是正方形也可以是矩形。四叉树和字典树类似,不同之处在于四叉树只有四个子节点,并且子节点需要按照某些特征决定存储位置。比方说,如果某点的条件符合左象限的范围,那么就遍历左象限。

对于需要进行搜索的稀疏数据,四叉树可以是一个很好的选择。它可以保存化学反应中的数据片段,图像处理中的像素等等。

我将会在本中实现一个四叉树。

在开始之前,需要说明的是,四叉树的实现将包含三个部分。第一个是 Point,用于存储坐标(以 xy 表示)。第二个是 QuadNode,用于保存在四叉树中的节点。最后一个是 QuadTree,也就是四叉树本身。

Point

static class Point {
    int x;
    int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // 更像是一个深拷贝
    public Point(Point p) {
        this.x = p.x;
        this.y = p.y;
    }

    @Override
    public String toString() {
        return "x: " + x + " y: " + y;
    }
}

QuadNode

static class QuadNode<T> {
    T data;
    Point point;

    public QuadNode(Point p, T data) {
        this.data = data;
        this.point = p;
    }

    public QuadNode(int x, int y, T data) {
        this.data = data;
        this.point = new Point(x, y);
    }

    @Override
    public String toString() {
        return "data: " + data + " point: " + point;
    }
}

QuadTree 类

class QuadTree<P> {    
    Point topLeft;
    Point bottomRight;
    Set<QuadNode<P>> nodes;
    // 子类(作为四叉树的数组,也可以当做字典树使用)
    QuadTree<P> topLeftTree;
    QuadTree<P> topRightTree;
    QuadTree<P> bottomLeftTree;
    QuadTree<P> bottomRightTree;
    int maxLen;

    public QuadTree(Point topLeft, Point bottomRight, int maxLen) {
        this.topLeft = topLeft;
        this.bottomRight = bottomRight;
        this.maxLen = maxLen;
        nodes = new HashSet<>();
    }
}

插入

二叉搜索树一样,我们需要从根节点开始,查找当前坐标所属的节点,然后递归遍历当前节点,直到到达叶节点。

然后我们将坐标插入到四叉树中,检查是否需要进一步细分。如果需要进一步细分,我们需要将该区域细分为四个象限,并将内部值重新分配给子节点。

public void insert(Point p, P data) {
    QuadTree<P> curr = this;

    while (!curr.isLeaf()) {
        System.out.println("Inserting " + p + " data " + data);

        // 根据 x 来检查左上角和右下角的值
        if (p.x < (curr.topLeft.x + curr.bottomRight.x) / 2) {
            // 通过比对 y 来检查左上角和左下角的数据
            if (p.y < (curr.topLeft.y + curr.bottomRight.y) / 2) { // 是左上角
                System.out.println("Is within topLeftTree py: " + p.y + " " + " mid: " + ((curr.topLeft.y + curr.bottomRight.y) / 2));
                curr = curr.topLeftTree;
            } else { // 是左下角
                System.out.println("Is within bottomLeft");
                curr = curr.bottomLeftTree;
            }

        } else { // 检查右上角和右下角的数据
            // 通过比对 y 来检查右上角和右下角的数据
            if (p.y < (curr.topLeft.y + curr.bottomRight.y) / 2) { // 是右上角
                System.out.println("Is within topRight");
                curr = curr.topRightTree;

            } else { // 在右下角中
                System.out.println("Is within bottomRight");
                curr = curr.bottomRightTree;
            }
        }
    }

    // 当前是叶子
    QuadNode < P > quadNode = new QuadNode < > (p, data);
    curr.nodes.add(quadNode);
    // System.out.println("curr " + curr);
    // 如果当坐标是 maxLen,那我们就需要做进一步的划分
    if (curr.shouldSubDivide()) {
        // System.out.println("data " + data +  " need to be subdivide");
        curr.subDivide();
    }
}

搜索

从根节点开始,校验当前坐标所属的区域,然后递归遍历该区域,直到到达叶节点,接着返回该节点的值(也就是一个包含多个 Point 的数组)。

public Set<QuadNode<P>> search(Point p) {
    QuadTree<P> curr = this;

    while (!curr.isLeaf()) {
        // 通过检查是否在边界内来进行递归
        if (p.x < (curr.topLeft.x + curr.bottomRight.x) / 2) {
            if (p.y < (curr.topLeft.y + curr.bottomRight.y) / 2) {
                curr = curr.topLeftTree;
            } else {
                curr = curr.bottomLeftTree;
            }
        } else {
            if (p.y < (curr.topLeft.y + curr.bottomRight.y) / 2) {
                curr = curr.topRightTree;
            } else {
                curr = curr.bottomRightTree;
            }
        }
    }
    return curr.node;
}

总结

  • 在地理存储领域,自然次序搜索并不能满足实际的需求。因此我们一般使用空间索引来搜索二维空间。
  • 四叉树等效于一维空间中的二叉树。只要你对稀疏数据有搜索要求,你可以考虑使用四叉树。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

猜你喜欢

转载自juejin.im/post/7115063278060961799