KD-Tree及希爾伯特空間填充曲線的應用

林學徒發表於2022-05-08

引言

我們可能會有這樣的一種需求,像是叫車軟體中呼叫附近的車來接送自己,或者是在qq中檢視附近的人。我們都需要知道距離自己一定範圍內的其它目標的集合。如果將上面舉例的功能抽象出來,就是要實現以某個點為中心,以一定的距離為半徑,在空間中查詢其它點所構成的集合。誠然,當空間中點的數目較少時,我們可以採用遍歷所有點的方式來計算出當前點與其它點之間的距離的方式來得到對應的結果集,但是空間中的點數目較多(假如達到千萬級別),且存在多個點要計算出距離當前點一定範圍內的點所構成的集合時,這個計算的時間複雜度便達到了O(\(mn\))級別,為此,我們需要改進該實現方式。

如何實現

我們先不考慮多維空間中的點,先考慮一維平面上的數,假如為1,92,8,11,18,91,7,47這幾個數。如果我們想得到某個數一定距離範圍內的那幾個數,如數字11,想得到距離不超過為7的數(<=7)。那麼我們可以將這些數按照從小到大的順序進行排序,使其有序,然後找到對應的數字,以該數為中心,分別往左和往右遍歷對應的點並計算距離,如果是在符合要求的範圍內,則將其納入結果集中。以上面例子為例,將數字進行排序,得到 1,7,8,11,18,47,91,92的數字序列。然後找到數11,分別往左計算各點的距離,得到符合條件的數7,8(11-1=10>7不符合條件)。往右計算各點的距離,得到符合條件的數18(47-11=36>7不符合條件)。為此,可以得到符合條件的結果集7,8,18。

除了將數進行排序這種方式,我們也可以採用二叉樹這一資料結構,通過直接定位到節點7然後搜尋查詢以節點7為根的子樹來縮小需要進行查詢的資料範圍。需要注意的是,如果需要得到一定距離範圍內的數為新加入的數,那麼可以採用先找到該數最臨近的數的方式並以該數為中心點進行查詢的方式,也可以採用將該數加入到集合中,並進行重新排序然後以該數位中心點的方式進行查詢。

對於空間中的點,我們也可以採用類似的方式進行考慮,由於是多維的,我們就得考慮將其降維,使其成為一維,並使其在空間中相鄰的點,在一維空間中也大體保持相鄰。除了降維之外,我們也可考慮將空間根據某種方式劃分為多個子空間,以減少需要進行搜尋查詢的資料範圍。為此,我們有如下兩種方式。

1.kd-tree資料結構方式

基本原理: 其基本原理是將空間按照各個維度依次對其進行分割成多個子空間,使得空間中的點集均勻的分佈在按照對應維度分割的子空間中,從而達到在搜尋目標點時減少在空間中搜尋的資料範圍的目的。

介紹: kd-tree是一種多維空間劃分的的二叉樹,其可以看成是將線段樹擴充到多維的變體。二叉樹上的每個節點都對應了一個k維的超矩形區域。對於將空間進行劃分的維度的選擇其可以有多種方式,一般就是按照某個維度中方差較大的那個維度或者輪流交替維度選擇的方式。空間進行分割維度的選擇方式會由於資料的分佈情況不同而影響查詢的效率。

kd-tree構建過程: 建樹的過程為一個遞迴的過程,其非葉子節點不儲存任何座標值,只儲存劃分的維度以及對應的劃分值,葉子節點儲存對應的節點值,該方式便於後續的查詢過程的實現。相關虛擬碼如下:

function buildTree(dataSet):
    if(len(dataSet) == 0):
        return none;
    //只剩下一個點座標或者所有座標值都相同,則其為葉子節點
    if(len(dataSet) == 1 || getMaxVariance(dataSet) == 0):
        return buildNode(dataSet);
    //得到分隔的維度以及分隔維度所對應的值
    maxVarianceSplitDimension <- getSplitDimension(dataSet);
    sort(dataSet,sortedBy maxVarianceSplitDimension);
    medianData <- len(dataSet)//2;
    splitValue <- dataSet[medianData].getValue(maxVarianceSplitDimension);
    //將點劃分為兩部分,一部分為小於劃分值的,一部分為大於等於劃分值的
    for data in dataSet:
        if(data.getValue(maxVarianceSplitDimension) < medianData.getValue(maxVarianceSplitDimension)):
            leftDataSet.add(data);
        else:
            rightDataSet.add(data);
    root <- buildNode(splitValue,maxVarianceSplitDimension);
    left <- buildTree(leftDataSet);
    right <- buildTree(rightDataSet);
    root.left <- left;
    root.right <- right;
    left.parent <- root;
    right.parent <- root;
    return root;

kd-tree查詢過程: kd-tree的查詢過程是一個二分搜尋加回溯的過程,每次二分搜尋到葉子節點之後,會回溯回另一個未曾訪問過的分支,去判斷該分支下是否有更接近的點。相關虛擬碼如下:

// point為要查詢臨近點的座標,m為要查詢與點point最臨近的點的數目
function search(kdTree,point,m):
    stack.push(kdTree.root);
    while !stack.isEmpty():
        node <- stack.pop();
        if(isLeaf(node)):
            distance <- getDistance(point,node);
            if(priorityQueue.size() < m):
                priorityQueue.push(saveNode(node,distance));
            else if(priorityQueue.peek(sortedBy distance) > distance):
                priorityQueue.pop(sortedBy distance);
                priorityQueue.push(saveNode(node,distance));
            continue;
        
        // 查詢到的節點數目小於m個時,需要回溯該分支下的相關節點進行查詢,如果節點數等於m個時,需要拿出當前查詢到的點中距離最遠的點進行判斷,判斷是否要進行回溯查詢
        currentMaxDistance <- priorityQueue.size() < m ? MAX_VALUE:priorityQueue.peek(sortedBy distance).distance;
        //當為根節點或者和當前節點的父節點判斷發現存在超球面和超矩形相交時,進入對應的分支
        if (node == root || Math.abs(point.getValue(node.parent.splitDimension) - node.parent.splitValue) < currentMaxDistance):
            while !isLeaf(node):
                if(point.getValue(node.splitDimension) < node.splitValue):
                    stack.push(node.right);
                    node <- node.left;
                else:
                    stack.push(node.left);
                    node <- node.right;
            stack.push(node);

    return priorityQueue;

實現:

package com.example.nearest;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.*;

/**
 * @author 學徒
 */
@Data
public class KDTree {
    /**
     * 根節點
     */
    private Node root;

    public KDTree(List<double[]> dataSet) {
        if (Objects.isNull(dataSet) || dataSet.size() == 0) {
            return;
        }
        this.root = this.buildTree(dataSet);
    }

    public KDTree(double[][] dataSet) {
        if (Objects.isNull(dataSet) || dataSet.length == 0 || dataSet[0].length == 0) {
            return;
        }
        List<double[]> data = new ArrayList<>(dataSet.length);
        for (double[] d : dataSet) {
            data.add(d);
        }
        this.root = this.buildTree(data);
    }

    /**
     * 樹節點
     */
    private class Node {
        /**
         * 分割維度
         */
        private int splitDimension;

        /**
         * 分割值
         */
        private double splitValue;

        /**
         * 點座標值
         */
        private double[] value;

        private Node left;

        private Node right;

        private Node parent;

        public Node(int splitDimension, double splitValue, Node left, Node right) {
            this.splitDimension = splitDimension;
            this.splitValue = splitValue;
            this.left = left;
            this.right = right;
        }

        public Node(double[] value) {
            this.value = value;
        }

        public boolean isLeaf() {
            return Objects.isNull(left) && Objects.isNull(right);
        }
    }

    /**
     * 用於查詢資料的輔助點
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    private class SearchNode {
        /**
         * 中心點到目標點的距離
         */
        private double distance;

        /**
         * 目標點節點
         */
        private Node node;
    }

    /**
     * 用於根據資料集建立對應的kd樹
     *
     * @param dataSet 點構成的資料集
     */
    private Node buildTree(List<double[]> dataSet) {
        if (dataSet.size() == 1) {
            return new Node(dataSet.get(0));
        }

        // 最大方差及其相關維度
        int dimensions = dataSet.get(0).length;
        double maxVariance = Double.MIN_VALUE;
        int splitDimension = -1;
        for (int dimension = 0; dimension < dimensions; dimension++) {
            double tempVariance = Utils.getVariance(dataSet, dimension);
            if (maxVariance < tempVariance) {
                maxVariance = tempVariance;
                splitDimension = dimension;
            }
        }
        // 最大方差各個維度相同。且均為0,則表示各個點座標相同
        if (Objects.equals(maxVariance, 0)) {
            return new Node(dataSet.get(0));
        }

        // 分割為左右兩個子樹
        double splitValue = Utils.getMedian(dataSet, splitDimension);
        int size = (dataSet.size() + 1) / 2;
        List<double[]> left = new ArrayList<>(size);
        List<double[]> right = new ArrayList<>(size);
        for (double[] data : dataSet) {
            if (data[splitDimension] < splitValue) {
                left.add(data);
            } else {
                right.add(data);
            }
        }

        Node leftTree = buildTree(left);
        Node rightTree = buildTree(right);
        Node root = new Node(splitDimension, splitValue, leftTree, rightTree);
        leftTree.parent = root;
        rightTree.parent = root;
        return root;
    }


    /**
     * 用於查詢該KDTree中距離指定點最近的m個點
     *
     * @param point  需要進行查詢的點
     * @param number 進行查詢的點的數量
     * @return 距離最近的點的數量構成的列表
     */
    public List<double[]> search(double[] point, int number) {
        Queue<SearchNode> searchNodes = assistSearch(point, number);
        List<double[]> result = new ArrayList<>(searchNodes.size());
        for (SearchNode node : searchNodes) {
            result.add(node.getNode().value);
        }
        return result;
    }

    /**
     * 用於得到某個點中的最接近點的座標
     *
     * @param point 查詢點
     * @return 最接近的點座標
     */
    public double[] searchNearest(double[] point) {
        List<double[]> result = search(point, 1);
        if (result.size() == 1) {
            return result.get(0);
        }
        return null;
    }

    /**
     * 用於查詢該KDTree中距離指定點最近的m個點
     *
     * @param point  需要進行查詢的點
     * @param number 進行查詢的點的數量
     * @return 距離最近的點的數量構成的佇列
     */
    private Queue<SearchNode> assistSearch(double[] point, int number) {
        Stack<Node> stack = new Stack<>();
        Queue<SearchNode> result = new PriorityQueue<>(number, (o1, o2) -> Double.compare(o2.distance, o1.distance));
        stack.push(this.root);
        while (!stack.isEmpty()) {
            Node node = stack.pop();
            if (node.isLeaf()) {
                double distance = Utils.getDistance(point, node.value);
                if (result.size() < number) {
                    result.add(new SearchNode(distance, node));
                } else if (result.peek().distance > distance) {
                    result.poll();
                    result.add(new SearchNode(distance, node));
                }
                continue;
            }

            // 查詢到的節點數目小於m個時,需要回溯該分支下的相關節點進行查詢,
            // 如果節點數等於m個時,需要拿出當前查詢到的點中距離最遠的點進行判斷,
            // 判斷是否要進行回溯查詢
            double currentMaxDistance = result.size() < number ? Double.MAX_VALUE : result.peek().distance;
            //當為根節點或者和當前節點的父節點判斷發現存在超球面和超矩形相交時,進入對應的分支
            if (Objects.equals(node, root) || Math.abs(point[node.parent.splitDimension] - node.parent.splitValue) < currentMaxDistance) {
                while (!node.isLeaf()) {
                    if (point[node.splitDimension] < node.splitValue) {
                        stack.push(node.right);
                        node = node.left;
                    } else {
                        stack.push(node.left);
                        node = node.right;
                    }
                }
                stack.push(node);
            }
        }
        return result;
    }
}



package com.example.nearest;

import java.util.List;

/**
 * @author 學徒
 */
public class Utils {
    /**
     * 用於獲取資料集中的某個資料維度的方差
     *
     * @param data      資料集
     * @param dimension 維度
     */
    public static double getVariance(List<double[]> data, int dimension) {
        double vsum = 0;
        double sum = 0;
        for (double[] d : data) {
            sum += d[dimension];
            vsum += Math.pow(d[dimension], 2);
        }
        int n = data.size();
        return vsum / n - Math.pow(sum / n, 2);

    }

    /**
     * 用於獲取資料集中某個資料維度的中間值
     *
     * @param data      資料集
     * @param dimension 維度
     * @return 對應維度的中間值
     */
    public static double getMedian(List<double[]> data, int dimension) {
        double[] numbers = new double[data.size()];
        for (int i = 0; i < data.size(); i++) {
            numbers[i] = data.get(i)[dimension];
        }
        return findPositionValue(numbers, 0, numbers.length - 1, numbers.length / 2);
    }

    /**
     * 三向切分的快排思想,用於得到對應位置的資料值
     *
     * @param data     資料
     * @param low      左邊界
     * @param high     右邊界
     * @param position 在邊界內進行查詢的位置
     * @return 對應位置的值
     */
    private static double findPositionValue(double[] data, int low, int high, int position) {
        //第一個相等元素的下標,第一個未訪問元素的下標,最後一個未訪問元素的下標
        int equalityFirstIndex = low, unvisitedFirstIndex = low + 1, unvisitedEndIndex = high;
        double number = data[low];
        while (unvisitedFirstIndex <= unvisitedEndIndex) {
            if (data[unvisitedFirstIndex] < number) {
                exchange(data, unvisitedFirstIndex, equalityFirstIndex);
                unvisitedFirstIndex++;
                equalityFirstIndex++;
            } else if (data[unvisitedFirstIndex] == number) {
                unvisitedFirstIndex++;
            } else {
                exchange(data, unvisitedFirstIndex, unvisitedEndIndex);
                unvisitedEndIndex--;
            }
        }
        // low ~ equalityFirstIndex-1 為小於基準元素的部分
        // equalityFirstIndex ~ unvisitedFirstIndex-1 為等於基準元素的部分
        // unvisitedFirstIndex ~ high 為大於基準元素的部分
        if (position <= unvisitedFirstIndex - 1 && position >= equalityFirstIndex) {
            return data[position];
        }
        if (position <= equalityFirstIndex - 1 && position >= low) {
            return findPositionValue(data, low, equalityFirstIndex - 1, position);
        }
        return findPositionValue(data, unvisitedFirstIndex, high, position);
    }


    /**
     * 用於交換兩個元素
     *
     * @param data 資料集
     * @param i    元素1下標
     * @param j    元素2下標
     */
    private static void exchange(double[] data, int i, int j) {
        double temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    /**
     * 用於得到兩個點之間的距離(此處計算歐式距離)
     *
     * @param point1 點1
     * @param point2 點2
     * @return 兩點之間的距離
     */
    public static double getDistance(double[] point1, double[] point2) {
        if (point1.length != point2.length) {
            return Double.MAX_VALUE;
        }
        double sum = 0;
        for (int i = 0; i < point1.length; i++) {
            sum += Math.pow(point1[i] - point2[i], 2);
        }
        return Math.sqrt(sum);
    }
}

測試程式碼:
以下為一點測試程式碼,用於供除錯,測試使用

以下程式碼位於KDTree類中
/**
    * 列印樹,測試時用
    */
public void print() {
    printRec(root, 0);
}

private void printRec(KDTree.Node node, int lv) {
    if (!node.isLeaf()) {
        for (int i = 0; i < lv; i++)
            System.out.print("--");
        System.out.println(node.splitDimension + ":" + node.splitValue);
        printRec(node.left, lv + 1);
        printRec(node.right, lv + 1);
    } else {
        for (int i = 0; i < lv; i++)
            System.out.print("--");
        StringBuilder s = new StringBuilder();
        s.append('(');
        for (int i = 0; i < node.value.length - 1; i++) {
            s.append(node.value[i]).append(',');
        }
        s.append(node.value[node.value.length - 1]).append(')');
        System.out.println(s);
    }
}

以下測試程式碼位於單獨的測試類中

public static void correct(){
    int count = 10000;
    while(count-->0){
        int num = 1000;
        double[][] input = new double[num][2];
        for(int i=0;i<num;i++){
            input[i][0]=Math.random()*10;
            input[i][1]=Math.random()*10;
        }
        double[] query = new double[]{Math.random()*50,Math.random()*50};

        KDTree tree=new KDTree(input);
        double[] result = tree.searchNearest(query);
        double[] result1 = nearest(query,input);
        if (result[0]!=result1[0]||result[1]!=result1[1]) {
            System.out.println(count);
            System.out.println("查詢點:"+query[0]+"\t"+query[1]);
            System.out.println("tree找到的:"+result[0]+"\t"+result[1]);
            System.out.println("線性找到的"+result1[0]+"\t"+result1[1]);
            System.out.println("tree找到的距離:"+Utils.getDistance(result,query));
            System.out.println("線性找到的距離:"+Utils.getDistance(result1,query));
            tree.print();
            break;
        }
    }
}

public static void performance(int iteration,int datasize){
    int count = iteration;

    int num = datasize;
    double[][] input = new double[num][2];
    for(int i=0;i<num;i++){
        input[i][0]=Math.random()*num;
        input[i][1]=Math.random()*num;
    }

    KDTree tree=new KDTree(input);

    double[][] query = new double[iteration][2];
    for(int i=0;i<iteration;i++){
        query[i][0]= Math.random()*num*1.5;
        query[i][1]= Math.random()*num*1.5;
    }

    long start = System.nanoTime();
    for(int i=0;i<iteration;i++){
        double[] result = tree.searchNearest(query[i]);
    }
    long timekdtree = System.nanoTime()-start;

    start = System.nanoTime();
    for(int i=0;i<iteration;i++){
        double[] result = nearest(query[i],input);
    }
    long timelinear = System.nanoTime()-start;

    System.out.println("datasize:"+datasize+";iteration:"+iteration);
    System.out.println("kdtree:"+timekdtree);
    System.out.println("linear:"+timelinear);
    System.out.println("linear/kdtree:"+(timelinear*1.0/timekdtree));
}

分析: 構建一棵kd-tree的時間複雜度根據劃分維度的選擇方式不同,時間複雜度也不同。採用方差最大的那個維度作為空間劃分的依據,其時間複雜度為O(nklogn),採用輪流交替維度作為空間劃分依據的,其時間複雜度為O(nlogn)。建樹的空間複雜度為O(n)。查詢過程的時間複雜度為O(\(n^{1-\tfrac{1}{k}}\)+m),其中m為每次要進行查詢的最近點個數,k為空間的維度。

適用情況: 該方式適用於點座標變動較少甚至基本不變而且維度k不大的情況,其可以精確的獲得所需的一定距離的點。對於每次空間中的點座標發生變化的情況,其可以採用重新建立kd-tree資料結構的方式或者採用替罪羊方式實現的改進後的kd-tree來實現。對於維度k小於20時能夠獲得較高的效率,當空間維度較高時,其接近於線性掃描。假設資料集的維數為D,一般來說要求資料的規模N滿足N>>\(2^D\),才能達到高效的搜尋。

2.降維編碼方式

在介紹降維編碼方式之前,我們可以先看下這個視訊讓自己對如何降維多維空間有個直觀的認識。

空間填充曲線

空間填充曲線從數學的角度上看,可以看成是一種把N維空間資料轉換到1維連續空間上的對映函式。實際上,儲存磁碟(常見的關係型資料庫)是一維的儲存裝置,而空間資料大多都是多維資料,很少存在一維順序。因此,為了使多維空間上鄰近的元素對映也儘可能是一維直線上鄰近的點,專家們提出了許多的對映方法。其中最常用的方法包括Z Ordering、Hilbert、Peano 曲線等等。

為此,對於需要查詢某個多維空間中的臨近點,我們可以先採用一定的方式對其進行降維,使其在空間中臨近的點,在一維中也儘可能的臨近。然後再在一維空間中查詢臨近值從而達到查詢空間中臨近點的目的。

對於空間填充曲線的實現方式,我們在此只考慮實現二維下的希爾伯特空間填充曲線。

實現:

package com.example.nearest;

import com.google.common.collect.Lists;
import lombok.Data;

import java.util.*;

/**
 * @author 學徒
 */
public class HilbertCode {

    @Data
    private class HilbertNode {
        /**
         * 原始資料
         */
        private double[] value;

        /**
         * 編碼值
         */
        private long codeValue;

        /**
         * 計算座標點保留的精度
         */
        private final static int PRECISION = 10000;

        /**
         * 計算的資料維度
         */
        private final static int ALLOW_DIMENSION = 2;


        public HilbertNode(double[] value) {
            this.value = value;
            this.codeValue = this.encode(value);
        }

        /**
         * 用於將空間點座標進行編碼
         *
         * @param value 空間點座標
         * @return 編碼後的值
         */
        private long encode(double[] value) {
            if (Objects.isNull(value) || !Objects.equals(value.length, ALLOW_DIMENSION)) {
                throw new IllegalArgumentException("點引數存在錯誤");
            }

            long x = new Double(value[0] * PRECISION).longValue();
            long y = new Double(value[1] * PRECISION).longValue();

            long n = 1 << 62;
            long rx, ry, s, d = 0;
            boolean rxb, ryb;
            for (s = n >> 1; s > 0; s >>= 1) {
                rxb = (x & s) > 0;
                ryb = (y & s) > 0;
                rx = rxb ? 1 : 0;
                ry = ryb ? 1 : 0;
                d += s * s * ((3 * rx) ^ ry);
                long[] xy = rot(s, x, y, rxb, ryb);
                x = xy[0];
                y = xy[1];
            }
            return d;
        }

        private long[] rot(long n, long x, long y, boolean rxb, boolean ryb) {
            if (ryb) {
                return new long[]{x, y};
            }

            if (rxb) {
                x = n - 1 - x;
                y = n - 1 - y;
            }

            return new long[]{y, x};
        }
    }

    @Data
    private class HilbertSearchNode {

        private HilbertNode hilbertNode;

        private double distance;

        public HilbertSearchNode(HilbertNode node, double[] point) {
            if (Objects.isNull(point) || !Objects.equals(point.length, HilbertNode.ALLOW_DIMENSION)) {
                throw new IllegalArgumentException("點引數存在錯誤");
            }

            this.hilbertNode = node;
            this.distance = Utils.getDistance(point, node.value);
        }
    }

    /**
     * 維護希爾伯特編碼序列
     */
    private TreeSet<HilbertNode> hilbertSequence;

    public HilbertCode() {
        // 得到按照編碼值從小到大進行編碼的序列
        this.hilbertSequence = new TreeSet<>((o1, o2) -> {
            if (o1.codeValue > o2.codeValue) {
                return 1;
            } else if (o1.codeValue == o2.codeValue) {
                return 0;
            }
            return -1;
        });
    }


    /**
     * 新增點座標
     *
     * @param value 點座標
     * @return 新增結果
     */
    public boolean add(double[] value) {
        return hilbertSequence.add(new HilbertNode(value));
    }

    /**
     * 儘可能的獲取當前點前後2m個元素,總共4m個,最少20個
     *
     * @param point 要得到對應位置的點
     * @param m     元素個數
     * @return 結果列表
     */
    private List<HilbertNode> getNear2MNode(double[] point, int m) {
        SortedSet<HilbertNode> headNodes = hilbertSequence.headSet(new HilbertNode(point));
        SortedSet<HilbertNode> tailNodes = hilbertSequence.tailSet(new HilbertNode(point));

        List<HilbertNode> result = new ArrayList<>(m << 2);
        // 查詢的點越多,精度越高
        final int MINIMUM_POINTS = 100;
        int count = 4 * m < MINIMUM_POINTS ? MINIMUM_POINTS : 4 * m;

        while (result.size() < count) {
            if (headNodes.size() == 0 && tailNodes.size() == 0) {
                break;
            }

            if (headNodes.size() != 0) {
                HilbertNode node = headNodes.last();
                headNodes.remove(node);
                result.add(node);
            }

            if (tailNodes.size() != 0) {
                HilbertNode node = tailNodes.first();
                tailNodes.remove(node);
                result.add(node);
            }
        }
        // 結束之後,要將從維護佇列中是刪除的節點重新新增回去
        hilbertSequence.addAll(result);

        return result;
    }


    /**
     * 查詢某個點的臨近點
     *
     * @param point 查詢點
     * @param m     臨近點的個數
     * @return 臨近點座標集合
     */
    public List<double[]> search(double[] point, int m) {
        List<double[]> result = new ArrayList<>(m);
        Queue<HilbertSearchNode> queue = new PriorityQueue<>((o1, o2) -> {
            if (o2.getDistance() > o1.getDistance()) {
                return 1;
            } else if (o1.getDistance() == o2.getDistance()) {
                return 0;
            }
            return -1;
        });

        List<HilbertNode> nearNode = getNear2MNode(point, m);

        for (HilbertNode node : nearNode) {
            queue.add(new HilbertSearchNode(node, point));
            if (queue.size() > m) {
                queue.poll();
            }
        }

        for (HilbertSearchNode node : queue) {
            result.add(node.getHilbertNode().getValue());
        }
        return result;
    }


    /**
     * 用於得到某個點中的最接近點的座標
     *
     * @param point 查詢點
     * @return 最接近的點座標
     */
    public double[] searchNearest(double[] point) {
        List<double[]> result = search(point, 1);
        if (result.size() == 1) {
            return result.get(0);
        }
        return null;
    }
}

分析: 將空間點降維編碼成一維資料,其時間複雜度為O(1)。維護有序資料結構,其時間複雜度為O(logn),查詢時間複雜度為O(logn+m),其中m為查詢的點的數目。

適用情況: 空間填充曲線適用於空間中點座標經常變動甚至點數目經常變化的情況,但是其查詢效果受填充曲線選擇的影響,為此在查詢一定距離的點的時候,可以考慮多查詢幾個點臨近點,以獲得更準確的結果。

總結

在生產中,我們一定要根據專案的實際情況來靈活的組合實現方式去高效的完成功能。當多維空間中點集數目不多且對該功能的呼叫頻率較低時,由於沒有建立和維護對應資料結構的成本,通過採用遍歷所有點的方式並不一定比採用資料結構或者降維編碼的方式實現的效率更低。為此我們可以根據實際的專案情況,在一定資料規模和功能使用頻率作為指標來衡量採用哪種實現方式的方法,通過多種功能組合實現,可以達到更高效的使用情況。

像博主本人之前遇到過的一個問題,需要首先得到某個執行可能異常的機器人的最臨近點,之後得到與最臨近點之間的距離以此來斷定機器人是否脫軌的功能。由於機器人脫軌執行的可能性很低,而且地圖普遍較小且不輕易改動。為此,採用遍歷判斷的方式去得到最臨近點效率會比維護各種資料結構等的實現更高效且開銷更小。但為了相容可能的大地圖以及異常情況,為此設定了一個閾值,當滿足情況的時候,會切換到採用維護KD-Tree的方式進行實現。

相關文章