diff --git a/src/main/java/azgracompress/kdtree/KDNode.java b/src/main/java/azgracompress/kdtree/KDNode.java new file mode 100644 index 0000000000000000000000000000000000000000..dfa09f3118d26d232a84c892289aafb520660821 --- /dev/null +++ b/src/main/java/azgracompress/kdtree/KDNode.java @@ -0,0 +1,44 @@ +package azgracompress.kdtree; + +public class KDNode { + private final int keyIndex; + private final int median; + + private final KDNode loSon; + private final KDNode hiSon; + + protected KDNode() { + keyIndex = -1; + median = -1; + loSon = null; + hiSon = null; + } + + + public KDNode(final int keyIndex, final int median, final KDNode loSon, final KDNode hiSon) { + this.keyIndex = keyIndex; + this.median = median; + this.loSon = loSon; + this.hiSon = hiSon; + } + + public final KDNode getLoSon() { + return loSon; + } + + public final KDNode getHiSon() { + return hiSon; + } + + public final int getKeyIndex() { + return keyIndex; + } + + public final int getMedian() { + return median; + } + + public boolean isTerminal() { + return false; + } +} diff --git a/src/main/java/azgracompress/kdtree/KDTreeBuilder.java b/src/main/java/azgracompress/kdtree/KDTreeBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..91ac7203b9287937996a3dc7e04db445b166a8f9 --- /dev/null +++ b/src/main/java/azgracompress/kdtree/KDTreeBuilder.java @@ -0,0 +1,128 @@ +package azgracompress.kdtree; + +import java.util.ArrayList; +import java.util.Arrays; + +public class KDTreeBuilder { + + private static class DividedRecords { + private final int[][] hiRecords; + private final int[][] loRecords; + + DividedRecords(int[][] hiRecords, int[][] loRecords) { + this.hiRecords = hiRecords; + this.loRecords = loRecords; + } + + public int[][] getHiRecords() { + return hiRecords; + } + + public int[][] getLoRecords() { + return loRecords; + } + } + + private final int bucketSize; + private final int dimension; + private int nodeCount = 0; + private int terminalNodeCount = 0; + + public KDTreeBuilder(final int dimension, final int bucketSize) { + this.bucketSize = bucketSize; + this.dimension = dimension; + } + + + public KDNode buildTree(final int[][] records) { + if (records.length <= bucketSize) { + return makeTerminalNode(records); + } + + double maxSpread = -1.0; + int keyIndex = 0; + + for (int j = 0; j < dimension; j++) { + // Find coordinate with greatest spread. + final double greatestSpread = calculateKeySpread(records, j); + if (greatestSpread > maxSpread) { + maxSpread = greatestSpread; + keyIndex = j; + } + } + final int median = calculateKeyMedian(records, keyIndex); + + + // Divide records in one method to hi and lo. + final DividedRecords dividedRecords = divideRecords(records, median, keyIndex); + return makeNonTerminalNode(keyIndex, median, dividedRecords); + } + + + private DividedRecords divideRecords(final int[][] records, final int median, final int keyIndex) { + ArrayList<int[]> loRecords = new ArrayList<>(); + ArrayList<int[]> hiRecords = new ArrayList<>(); + for (final int[] record : records) { + if (record[keyIndex] <= median) { + loRecords.add(record); + } else { + hiRecords.add(record); + } + } + return new DividedRecords(loRecords.toArray(new int[0][]), hiRecords.toArray(new int[0][])); + } + + private KDNode makeNonTerminalNode(final int keyIndex, final int median, final DividedRecords dividedRecords) { + final KDNode loSon = buildTree(dividedRecords.getLoRecords()); + final KDNode hiSon = buildTree(dividedRecords.getHiRecords()); + ++nodeCount; + return new KDNode(keyIndex, median, loSon, hiSon); + } + + public KDNode makeTerminalNode(final int[][] records) { + ++nodeCount; + ++terminalNodeCount; + return new TerminalKDNode(records); + } + + private int calculateKeyMedian(final int[][] records, final int keyIndex) { + assert (records.length > 1); + final int[] sortedArray = new int[records.length]; + for (int i = 0; i < records.length; i++) { + sortedArray[i] = records[i][keyIndex]; + } + Arrays.sort(sortedArray); + + final int midIndex = sortedArray.length / 2; + if ((sortedArray.length % 2) == 0) { + return (int) (((double) sortedArray[midIndex] + (double) sortedArray[(midIndex - 1)]) / 2.0); + } else { + return sortedArray[midIndex]; + } + } + + + private double calculateKeySpread(final int[][] records, final int keyIndex) { + double center = 0.0; + for (final int[] record : records) { + center += record[keyIndex]; + } + center /= (double) records.length; + + double spread = 0.0; + + for (final int[] record : records) { + spread += Math.pow(((double) center - (double) record[keyIndex]), 2); + } + + return Math.sqrt(spread); + } + + public int getNodeCount() { + return nodeCount; + } + + public int getTerminalNodeCount() { + return terminalNodeCount; + } +} diff --git a/src/main/java/azgracompress/kdtree/TerminalKDNode.java b/src/main/java/azgracompress/kdtree/TerminalKDNode.java new file mode 100644 index 0000000000000000000000000000000000000000..6dc239b1f0b1aef3a092fb246b807a4f22411a97 --- /dev/null +++ b/src/main/java/azgracompress/kdtree/TerminalKDNode.java @@ -0,0 +1,20 @@ +package azgracompress.kdtree; + +public class TerminalKDNode extends KDNode { + + private final int[][] bucket; + + public TerminalKDNode(final int[][] records) { + super(); + this.bucket = records; + } + + @Override + public boolean isTerminal() { + return true; + } + + public int[][] getBucket() { + return bucket; + } +}