diff --git a/README.md b/README.md index 5fbf182..109a1e5 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Gradle is used for development. * [Trie](src/main/java/dataStructures/trie) * [B-Tree](src/main/java/dataStructures/bTree) * Red-Black Tree (Not covered in CS2040s but useful!) - * Orthogonal Range Searching (**WIP**) + * [Orthogonal Range Searching](src/main/java/algorithms/orthogonalRangeSearching) * Interval Trees (**WIP**) 5. [Binary Heap](src/main/java/dataStructures/heap) (Max heap) 6. [Disjoint Set / Union Find](src/main/java/dataStructures/disjointSet) diff --git a/docs/assets/images/1DORS.jpg b/docs/assets/images/1DORS.jpg new file mode 100644 index 0000000..2ab8879 Binary files /dev/null and b/docs/assets/images/1DORS.jpg differ diff --git a/docs/assets/images/1DORSDynamicUpdates.jpg b/docs/assets/images/1DORSDynamicUpdates.jpg new file mode 100644 index 0000000..4c99165 Binary files /dev/null and b/docs/assets/images/1DORSDynamicUpdates.jpg differ diff --git a/docs/assets/images/1DORSQuery.jpeg b/docs/assets/images/1DORSQuery.jpeg new file mode 100644 index 0000000..b172e2e Binary files /dev/null and b/docs/assets/images/1DORSQuery.jpeg differ diff --git a/docs/assets/images/2DORS.jpg b/docs/assets/images/2DORS.jpg new file mode 100644 index 0000000..4f43453 Binary files /dev/null and b/docs/assets/images/2DORS.jpg differ diff --git a/docs/assets/images/2DORSQuery.jpg b/docs/assets/images/2DORSQuery.jpg new file mode 100644 index 0000000..710b1f3 Binary files /dev/null and b/docs/assets/images/2DORSQuery.jpg differ diff --git a/docs/assets/images/2DORSTrees.jpg b/docs/assets/images/2DORSTrees.jpg new file mode 100644 index 0000000..c8ce1cc Binary files /dev/null and b/docs/assets/images/2DORSTrees.jpg differ diff --git a/docs/team/profiles.md b/docs/team/profiles.md index 5fec48a..c9040cf 100644 --- a/docs/team/profiles.md +++ b/docs/team/profiles.md @@ -3,7 +3,7 @@ | Name | Description/About | Website (LinkedIn/GitHub/Personal) | Contributions | |-----------|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-------------------------------------------------------------| | Andre | Aspiring ML engineer. Developing this with wonderful ex-students. | You can find me [here](https://4ndrelim.github.io)! | Team lead | -| Kai ting | Likes algorithms and a committed TA! | [Hi](https://www.linkedin.com/in/kai-ting-ho-425181268/) | Cool sorting and obscure trees! B-Trees, ORS.. | +| Kai Ting | Likes algorithms and a committed TA! | [Linkedin](https://www.linkedin.com/in/kai-ting-ho-425181268/) | Cool sorting and obscure trees! B-Trees, ORS.. | | Changxian | DevOps is right up his alley! | ... | Hashing variants! BTS DevOps - configure Gradle & workflows | | Shu Heng | Interested in ML, aspiring researcher. | No website but here's my [Linkedin](https://www.linkedin.com/in/yeoshuheng), please give me a job :< | CS Fundamentals! Stacks and queues! RB-tree. | | Junneng | Aspiring tech entrepreneur. | [LinkedIn](https://www.linkedin.com/in/soo-jun-neng/) | Binary Search variants, Minimum Spanning Trees! | diff --git a/src/main/java/algorithms/orthogonalRangeSearching/RangeTreeNode.java b/src/main/java/algorithms/orthogonalRangeSearching/RangeTreeNode.java new file mode 100644 index 0000000..2a52b75 --- /dev/null +++ b/src/main/java/algorithms/orthogonalRangeSearching/RangeTreeNode.java @@ -0,0 +1,96 @@ +package algorithms.orthogonalRangeSearching; + +/** + * This class is the node class for building a range tree. + * @param Generic type of the node class + */ +public class RangeTreeNode { + private T val; + private int height; + private RangeTreeNode left = null; + private RangeTreeNode right = null; + private RangeTreeNode parent = null; + private RangeTreeNode yTree = null; + + public RangeTreeNode(T val) { + this.val = val; + } + + /** + * Constructor for range tree node + * @param val value of node + * @param left left child of node + * @param right right child of node + */ + public RangeTreeNode(T val, RangeTreeNode left, RangeTreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } + + public T getVal() { + return this.val; + } + + public int getHeight() { + return this.height; + } + + public RangeTreeNode getLeft() { + return this.left; + } + + public RangeTreeNode getRight() { + return this.right; + } + + public RangeTreeNode getParent() { + return this.parent; + } + + public RangeTreeNode getYTree() { + return this.yTree; + } + + public void setVal(T val) { + this.val = val; + } + + public void setLeft(RangeTreeNode left) { + this.left = left; + } + + public void setRight(RangeTreeNode right) { + this.right = right; + } + + public void setParent(RangeTreeNode parent) { + this.parent = parent; + } + + public void setHeight(int height) { + this.height = height; + } + + public void setYTree(RangeTreeNode yTree) { + this.yTree = yTree; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof RangeTreeNode)) { + return false; + } + RangeTreeNode node = (RangeTreeNode) other; + return this.val == node.val; + } + + @Override + public String toString() { + return String.valueOf(this.val); + } + +} diff --git a/src/main/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearching.java b/src/main/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearching.java new file mode 100644 index 0000000..bc5892a --- /dev/null +++ b/src/main/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearching.java @@ -0,0 +1,398 @@ +package algorithms.orthogonalRangeSearching.oneDim; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import algorithms.orthogonalRangeSearching.RangeTreeNode; + +/** + * This class performs 1 dimensional orthogonal range searching. + */ +public class OrthogonalRangeSearching { + /** + * Builds a Range Tree from an array of integers. + * + * @param inputs The array of integers. + * @param start The starting index of the input array. + * @param end The ending index of the input array. + * @return The root node of the constructed Range Tree. + */ + public static RangeTreeNode buildTree(int[] inputs, int start, int end) { + int mid = (end + start) / 2; + Arrays.sort(inputs); + + if (start > end) { + return null; + } else if (start == end) { + return new RangeTreeNode<>(inputs[start]); + } else { + return new RangeTreeNode<>(inputs[mid], buildTree(inputs, start, mid), + buildTree(inputs, mid + 1, end)); + } + } + + /** + * Finds the split node in the Range Tree based on a given range. + * + * @param root The root node of the Range Tree. + * @param low The lower bound of the range. + * @param high The upper bound of the range. + * @return The split node in the Range Tree. + */ + public static RangeTreeNode findSplit(RangeTreeNode root, int low, int high) { + RangeTreeNode v = root; + + while (true) { + if (v == null) { + return null; + } else { + if (high <= v.getVal()) { + if (isLeaf(v)) { + break; + } + v = v.getLeft(); + } else if (low > v.getVal()) { + v = v.getRight(); + } else { + break; + } + } + } + return v; + } + + /** + * Performs a recursive traversal of the Range Tree and adds leaf node values to the result list. + * + * @param v The current node being processed during traversal. + * @param result The list to store the values of leaf nodes encountered during traversal. + */ + public static void allLeafTraversal(RangeTreeNode v, List result) { + if (v != null) { + if (v.getLeft() != null) { + allLeafTraversal(v.getLeft(), result); + } + if (isLeaf(v)) { + result.add(v.getVal()); + } + if (v.getRight() != null) { + allLeafTraversal(v.getRight(), result); + } + } + } + + /** + * Performs a left traversal of the Range Tree to find nodes within a specified range. + * + * @param v The current node being processed. + * @param low The lower bound of the range. + * @param result A list to store the results of the traversal. + */ + public static void leftTraversal(RangeTreeNode v, int low, List result) { + if (v != null) { + if (isLeaf(v)) { + result.add(v.getVal()); + } else { + if (low <= v.getVal()) { + leftTraversal(v.getLeft(), low, result); + allLeafTraversal(v.getRight(), result); + } else { // definitely a qualifying leaf has to exist + leftTraversal(v.getRight(), low, result); + } + } + } + } + + /** + * Performs a right traversal of the Range Tree to find nodes within a specified range. + * + * @param v The current node being processed. + * @param high The upper bound of the range. + * @param result A list to store the results of the traversal. + */ + public static void rightTraversal(RangeTreeNode v, int high, List result) { + if (v != null) { + if (isLeaf(v) && v.getVal() <= high) { // leaf, need extra check + result.add(v.getVal()); + } else { + if (high > v.getVal()) { + allLeafTraversal(v.getLeft(), result); + rightTraversal(v.getRight(), high, result); + } else { // a qualifying leaf might or might not exist, we are just exploring + rightTraversal(v.getLeft(), high, result); + } + } + } + } + + /** + * Searches for elements within a specified range in the Range Tree. + * + * @param tree The root node of the Range Tree. + * @param low The lower bound of the range. + * @param high The upper bound of the range. + * @return An array of elements within the specified range. + */ + public static Object[] search(RangeTreeNode tree, int low, int high) { + RangeTreeNode splitNode = OrthogonalRangeSearching.findSplit(tree, low, high); + ArrayList result = new ArrayList<>(); + if (splitNode != null) { + if (isLeaf(splitNode) && splitNode.getVal() >= low + && splitNode.getVal() <= high) { + result.add(splitNode.getVal()); + } + leftTraversal(splitNode.getLeft(), low, result); + rightTraversal(splitNode.getRight(), high, result); + } + return result.toArray(); + } + + private static boolean isLeaf(RangeTreeNode node) { + return node.getLeft() == null && node.getRight() == null; + } + + // FUNCTIONS FROM HERE ONWARDS ARE DESIGNED TO SUPPORT DYNAMIC UPDATES. + + /** + * Configures the height and parent nodes for the nodes in the Range Tree. + * Note that this is only needed if we want to support dynamic updating of the range tree. + * + * @param node The root node of the Range Tree. + */ + public static void configureTree(RangeTreeNode node) { + if (node.getLeft() == null && node.getRight() == null) { + node.setHeight(0); + } else if (node.getLeft() == null) { + configureTree(node.getRight()); + node.setHeight(node.getRight().getHeight() + 1); + node.getRight().setParent(node); + } else if (node.getRight() == null) { + configureTree(node.getLeft()); + node.setHeight(node.getLeft().getHeight() + 1); + node.getLeft().setParent(node); + } else { + configureTree(node.getLeft()); + configureTree(node.getRight()); + node.setHeight(Math.max(node.getLeft().getHeight(), node.getRight().getHeight()) + 1); + node.getLeft().setParent(node); + node.getRight().setParent(node); + } + } + + /** + * Inserts a new element into the Range Tree while maintaining balance. + * + * @param node The root node of the Range Tree. + * @param val The value to be inserted. + * @return The root node of the updated Range Tree. + */ + public static RangeTreeNode insert(RangeTreeNode node, int val) { + if (val < node.getVal()) { + if (node.getLeft() != null) { + node.setLeft(insert(node.getLeft(), val)); + } else { + node.setLeft(new RangeTreeNode(val)); + node.getLeft().setParent(node); + node.setRight(new RangeTreeNode(node.getVal())); + node.getRight().setParent(node); + node.setVal(val); + } + } else if (val > node.getVal()) { + if (node.getRight() != null) { + node.setRight(insert(node.getRight(), val)); + } else { + node.setLeft(new RangeTreeNode(node.getVal())); + node.getLeft().setParent(node); + node.setRight(new RangeTreeNode(val)); + node.getRight().setParent(node); + node.setVal(node.getVal()); + } + } else { + throw new RuntimeException("Duplicate key not supported!"); + } + return rebalance(node); + } + + /** + * Calculates and returns the height of the given Range Tree node. + * + * @param node The Range Tree node for which to calculate the height. + * @return The height of the node, or -1 if the node is null. + */ + private static int height(RangeTreeNode node) { + return node == null + ? -1 + : node.getHeight(); + } + + /** + * Update height of node in range tree during rebalancing. + * + * @param node node whose height is to be updated + */ + private static void updateHeight(RangeTreeNode node) { + node.setHeight(1 + Math.max(height(node.getLeft()), height(node.getRight()))); + } + + /** + * Get balance factor to check if height-balanced property is violated. + * Note: negative value means tree is right heavy, + * positive value means tree is left heavy, + * 0 means tree is balanced in weight. + * + * @param node check balance factor of node + * @return int value representing the balance factor + */ + private static int getBalance(RangeTreeNode node) { + return node == null + ? 0 + : height(node.getLeft()) - height(node.getRight()); + } + + /** + * Performs a right rotation on the specified node. + * Note that function should be called only if the node has a left child since it will be the new root. + * + * @param n node to perform right rotation on. + * @return the new root after rotation. + */ + private static RangeTreeNode rotateRight(RangeTreeNode n) { + RangeTreeNode newRoot = n.getLeft(); + RangeTreeNode newLeftSub = newRoot.getRight(); + newRoot.setRight(n); + n.setLeft(newLeftSub); + + newRoot.setParent(n.getParent()); + n.setParent(newRoot); + + updateHeight(n); + updateHeight(newRoot); + return newRoot; + } + + /** + * Performs a left rotation on the specified node. + * Note that function should be called only if the node has a right child since it will be the new root. + * + * @param n node to perform left rotation on + * @return new root after rotation + */ + private static RangeTreeNode rotateLeft(RangeTreeNode n) { + RangeTreeNode newRoot = n.getRight(); // newRoot is the right subtree of the original root + RangeTreeNode newRightSub = newRoot.getLeft(); + newRoot.setLeft(n); + n.setRight(newRightSub); + + newRoot.setParent(n.getParent()); + n.setParent(newRoot); + + updateHeight(n); + updateHeight(newRoot); + return newRoot; + } + + /** + * Rebalances a node in the tree based on balance factor. + * + * @param n node to be rebalanced + * @return new root after rebalancing + */ + private static RangeTreeNode rebalance(RangeTreeNode n) { + updateHeight(n); + int balance = getBalance(n); + if (balance < -1) { // right-heavy case + RangeTreeNode rightChild = n.getRight(); + if (height(rightChild.getLeft()) > height(rightChild.getRight())) { + n.setRight(rotateRight(n.getRight())); + } + n = rotateLeft(n); + } else if (balance > 1) { // left-heavy case + RangeTreeNode leftChild = n.getLeft(); + if (height(leftChild.getRight()) > height(leftChild.getLeft())) { + n.setLeft(rotateLeft(n.getLeft())); + } + n = rotateRight(n); + } + return n; + } + + /** + * Deletes an element from the Range Tree while maintaining balance. + * + * @param node The root node of the Range Tree. + * @param val The value to be deleted. + * @return The root node of the updated Range Tree. + */ + public static RangeTreeNode delete(RangeTreeNode node, int val) { + RangeTreeNode leftChild = node.getLeft(); + RangeTreeNode rightChild = node.getRight(); + + if (leftChild.getLeft() == null && leftChild.getRight() == null + && val == leftChild.getVal()) { // left node is the leaf node + node.setVal(rightChild.getVal()); + node.setLeft(null); + node.setRight(null); + } else if (rightChild.getLeft() == null && rightChild.getRight() == null + && val == rightChild.getVal()) { // right node is the leaf node + node.setLeft(null); + node.setRight(null); + } else { + if (val <= node.getVal()) { + if (leftChild != null) { + node.setLeft(delete(leftChild, val)); + } + if (val == node.getVal()) { // duplicate node + node.setVal(getMostRight(leftChild).getVal()); // update the duplicate key + } + } else { + if (rightChild != null) { + node.setRight(delete(rightChild, val)); + } + } + } + return rebalance(node); + } + + /** + * Finds and returns the rightmost node in the Range Tree rooted at the given node. + * + * @param n The root node of a subtree to search in. + * @return The rightmost node in the subtree, or null if the input node is null. + */ + private static RangeTreeNode getMostRight(RangeTreeNode n) { + if (n.getRight() == null) { + return n; + } else { + return getMostRight(n.getRight()); + } + } + + /** + * Performs a level order traversal of the Range Tree and prints the elements. + * This is not a necessary function for orthogonal range searching, but merely a utility function for debugging + * and visualisation purposes. + * + * @param root The root node of the Range Tree. + */ + public static void levelOrderTraversal(RangeTreeNode root) { + if (root == null) { + return; + } + Queue> queue = new LinkedList<>(); + queue.add(root); + while (!queue.isEmpty()) { + RangeTreeNode current = queue.poll(); + System.out.print(current.getVal() + " "); + if (current.getLeft() != null) { + queue.add(current.getLeft()); + } + if (current.getRight() != null) { + queue.add(current.getRight()); + } + } + } + +} diff --git a/src/main/java/algorithms/orthogonalRangeSearching/oneDim/README.md b/src/main/java/algorithms/orthogonalRangeSearching/oneDim/README.md new file mode 100644 index 0000000..eeb04ed --- /dev/null +++ b/src/main/java/algorithms/orthogonalRangeSearching/oneDim/README.md @@ -0,0 +1,59 @@ +# 1D Orthogonal Range Searching + +1D orthogonal range searching is a computational problem where you search for elements or data points within a +specified 1D range (interval) in a collection of 1D data (e.g. Find me everyone between ages 22 and 27). Additionally, +we also want to support efficient insertions of new data points into the maintained set. + +One strategy would be to sort all the data points in O(nlogn) time, then insertion would take O(n). We can binary +search the low and high of the specified range to return the start and end indices of all the data points within +in O(logn) time. This would be a reasonable approach if the no. of queries >> no. of insertions. + +In cases where the no. of insertions >> no. of queries, we might want to further optimise the time complexity of +insertions to O(logn) using a 1D range tree. + +Strategy: +1. Use a binary search tree +2. Store all points in the leaves of the tree (internal nodes only store copies) +3. Each internal node v stores the MAX of any leaf in the left sub-tree (**range tree property**) + +![1DORS](../../../../../../docs/assets/images/1DORS.jpg) + +Say we want to find all the nodes between 10 and 50 i.e. query(10, 50). We would want to: +1. Find split node: highest node where search includes both left & right subtrees +=> we want to make use of the range tree property to perform binary search to find our split node. See findSplit(root, low, high) in code. +2. Left traversal & right traversal +- Left traversal covers the range within [low, splitNode] +- Right traversal covers the range within (splitNode, high] + +![1DORSQuery](../../../../../../docs/assets/images/1DORSQuery.jpeg) +Image Source: Lecture Slides + +## Complexity Analysis +**Time**: + +Build Tree (cost incurred once only): +- if given an unsorted array, O(nlogn) limited by sorting step +- if given a sorted array, O(n) + +Querying: O(k + logn) +- Find Split Node: O(logn) (binary search) +- Left Traversal: at every step, we either + 1. output all-right subtree (O(k) where k is no. of leaves) and recurse left + 2. recurse right (at most logn times) +- Right Traversal: similar to left traversal + +**Space**: S(n) = S(n / 2) + O(n) => O(nlogn) + +## Notes +### Dynamic Updates +If we need to dynamically update the tree, insertion and deletion is done in a manner similar to AVL trees +(insert/delete and rotate to maintain height balance), except now we need to ensure that we are adding the new node +as a leaf node, and we still need to adhere to the range tree property. + +Note how the ORS tree property enables efficient dynamic updating of the tree, as the value of the nodes do not need +to change after rotation. + +![1DORSDynamicUpdates](../../../../../../docs/assets/images/1DORSDynamicUpdates.jpg) + +For more implementation details, refer to the code below "// Functions from here onwards are designed to support +dynamic updates." diff --git a/src/main/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearching.java b/src/main/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearching.java new file mode 100644 index 0000000..a71ef5c --- /dev/null +++ b/src/main/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearching.java @@ -0,0 +1,306 @@ +package algorithms.orthogonalRangeSearching.twoDim; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import algorithms.orthogonalRangeSearching.RangeTreeNode; + +/** + * This class performs 2 dimensional orthogonal range searching. + */ +public class OrthogonalRangeSearching { + + /** + * Builds the X-tree, a data structure for the given range of 2D points. + * + * @param inputs List of 2D points in the format Integer[], where each array + * represents a point with two coordinates (x, y). + * @param start The starting index of the current range. + * @param end The ending index of the current range. + * @return The root node of the X-tree for the specified range. + */ + public static RangeTreeNode buildXTree(List inputs, int start, int end) { + + int mid = (end + start) / 2; + inputs.sort(Comparator.comparingInt(a -> a[0])); + + if (start > end) { + return null; + } else if (start == end) { + RangeTreeNode node = new RangeTreeNode<>(inputs.get(mid)); + node.setYTree(buildYTree(inputs, start, end)); + return node; + } else { + RangeTreeNode node = new RangeTreeNode<>(inputs.get(mid), buildXTree(inputs, start, mid), + buildXTree(inputs, mid + 1, end)); + node.setYTree(buildYTree(inputs, start, end)); + return node; + } + } + + /** + * Builds the Y-tree, a data structure for the given range of 2D points. + * + * @param inputs List of 2D points in the format Integer[], where each array + * represents a point with two coordinates (x, y). + * @param start The starting index of the current range. + * @param end The ending index of the current range. + * @return The root node of the Y-tree for the specified range. + */ + private static RangeTreeNode buildYTree(List inputs, int start, int end) { + + List ySortedSublist = inputs.subList(start, end + 1); + ySortedSublist.sort(Comparator.comparingInt(a -> a[1])); //sort by y-coordinate + + return buildYTreeHelper(ySortedSublist, 0, ySortedSublist.size() - 1); + } + + /** + * Helper function to build the Y-tree. + * + * @param inputs List of 2D points in the format Integer[], where each array + * represents a point with two coordinates (x, y). + * @param start The starting index of the current range. + * @param end The ending index of the current range. + * @return The root node of the Y-tree for the specified range. + */ + private static RangeTreeNode buildYTreeHelper(List inputs, int start, int end) { + + int mid = (end + start) / 2; + + if (start > end) { + return null; + } else if (start == end) { + return new RangeTreeNode<>(inputs.get(start)); + } else { + return new RangeTreeNode<>(inputs.get(mid), buildYTree(inputs, start, mid), + buildYTree(inputs, mid + 1, end)); + } + } + + /** + * Finds the X-split node in the X-tree based on the specified X-coordinate range. + * + * @param root The root node of the X-tree. + * @param xLow The lower bound of the X-coordinate range. + * @param xHigh The upper bound of the X-coordinate range. + * @return The X-split node or null if not found. + */ + public static RangeTreeNode findXSplit(RangeTreeNode root, int xLow, int xHigh) { + RangeTreeNode v = root; + + while (true) { + if (v == null) { + return null; + } else { + if (xHigh <= v.getVal()[0]) { + v = v.getLeft(); + } else if (xLow > v.getVal()[0]) { + v = v.getRight(); + } else { + break; + } + } + } + return v; + } + + /** + * Performs a left traversal of the X-tree to find points within the specified range. + * + * @param v The current node in the X-tree. + * @param xLow The lower bound of the X-coordinate range. + * @param xHigh The upper bound of the X-coordinate range. + * @param yLow The lower bound of the Y-coordinate range. + * @param yHigh The upper bound of the Y-coordinate range. + * @param result A list to store the results. + */ + public static void xLeftTraversal(RangeTreeNode v, int xLow, int xHigh, int yLow, int yHigh, + ArrayList result) { + if (v != null) { + if (isLeaf(v) && v.getVal()[0] >= xLow && v.getVal()[0] <= xHigh) { //leaf + ySearch(v, yLow, yHigh, result); + } else { + if (xLow <= v.getVal()[0]) { + xLeftTraversal(v.getLeft(), xLow, xHigh, yLow, yHigh, result); + ySearch(v.getRight(), yLow, yHigh, result); + } else { + xLeftTraversal(v.getRight(), xLow, xHigh, yLow, yHigh, result); + } + } + } + } + + /** + * Performs a right traversal of the X-tree to find points within the specified range. + * + * @param v The current node in the X-tree. + * @param xLow The lower bound of the X-coordinate range. + * @param xHigh The upper bound of the X-coordinate range. + * @param yLow The lower bound of the Y-coordinate range. + * @param yHigh The upper bound of the Y-coordinate range. + * @param result A list to store the results. + */ + public static void xRightTraversal(RangeTreeNode v, int xLow, int xHigh, int yLow, int yHigh, + ArrayList result) { + if (v != null) { + if (isLeaf(v) && v.getVal()[0] >= xLow && v.getVal()[0] <= xHigh) { //leaf + ySearch(v, yLow, yHigh, result); + } else { + if (xHigh >= v.getVal()[0]) { + ySearch(v.getLeft(), yLow, yHigh, result); + xRightTraversal(v.getRight(), xLow, xHigh, yLow, yHigh, result); + } else { + xRightTraversal(v.getLeft(), xLow, xHigh, yLow, yHigh, result); + } + } + } + } + + /** + * Finds the Y-split node in the Y-tree based on the specified Y-coordinate range. + * + * @param root The root node of the Y-tree. + * @param yLow The lower bound of the Y-coordinate range. + * @param yHigh The upper bound of the Y-coordinate range. + * @return The Y-split node or null if not found. + */ + public static RangeTreeNode findYSplit(RangeTreeNode root, int yLow, int yHigh) { + RangeTreeNode v = root; + + while (true) { + if (v == null) { + return null; + } else { + if (yHigh <= v.getVal()[1]) { + if (isLeaf(v)) { // extra check since ysplit might be leaf node + break; + } else { + v = v.getLeft(); + } + } else if (yLow > v.getVal()[1]) { + v = v.getRight(); + } else { + break; + } + } + } + return v; + } + + /** + * Performs a recursive traversal of the Range Tree and adds leaf node values to the result list. + * + * @param v The current node being processed during traversal. + * @param result The list to store the values of leaf nodes encountered during traversal. + */ + public static void allLeafTraversal(RangeTreeNode v, List result) { + if (v != null) { + if (v.getLeft() != null) { + allLeafTraversal(v.getLeft(), result); + } + if (isLeaf(v)) { + result.add(v.getVal()); + } + if (v.getRight() != null) { + allLeafTraversal(v.getRight(), result); + } + } + } + + /** + * Performs a left traversal of the Y-tree to find points within the specified range. + * + * @param v The current node in the Y-tree. + * @param low The lower bound of the Y-coordinate range. + * @param result A list to store the results. + */ + public static void yLeftTraversal(RangeTreeNode v, int low, List result) { + if (v != null) { + if (isLeaf(v)) { + result.add(v.getVal()); + } else { + if (low <= v.getVal()[1]) { + yLeftTraversal(v.getLeft(), low, result); + allLeafTraversal(v.getRight(), result); + } else { // definitely a qualifying leaf has to exist + yLeftTraversal(v.getRight(), low, result); + } + } + } + } + + /** + * Performs a right traversal of the Y-tree to find points within the specified range. + * + * @param v The current node in the Y-tree. + * @param high The upper bound of the Y-coordinate range. + * @param result A list to store the results. + */ + public static void yRightTraversal(RangeTreeNode v, int high, List result) { + if (v != null) { + if (isLeaf(v) && v.getVal()[1] <= high) { // leaf, need extra check + result.add(v.getVal()); + } else { + if (high > v.getVal()[1]) { + allLeafTraversal(v.getLeft(), result); + yRightTraversal(v.getRight(), high, result); + } else { // a qualifying leaf might or might not exist, we are just exploring + yRightTraversal(v.getLeft(), high, result); + } + } + } + } + + /** + * Searches for 2D points within the specified Y-coordinate range in the Y-tree. + * + * @param v The root node of the Y-tree. + * @param yLow The lower bound of the Y-coordinate range. + * @param yHigh The upper bound of the Y-coordinate range. + * @param result A list to store the results. + */ + public static void ySearch(RangeTreeNode v, int yLow, int yHigh, ArrayList result) { + if (v != null) { + RangeTreeNode splitNodeY = findYSplit(v.getYTree(), yLow, yHigh); + if (splitNodeY != null) { + if (isLeaf(splitNodeY) + && splitNodeY.getVal()[1] >= yLow && splitNodeY.getVal()[1] <= yHigh) { // if split node is leaf + result.add(splitNodeY.getVal()); + } + yLeftTraversal(splitNodeY.getLeft(), yLow, result); + yRightTraversal(splitNodeY.getRight(), yHigh, result); + } + } + } + + /** + * Searches for 2D points within the specified orthogonal range in the X-tree. + * + * @param tree The root node of the X-tree. + * @param xLow The lower bound of the X-coordinate range. + * @param xHigh The upper bound of the X-coordinate range. + * @param yLow The lower bound of the Y-coordinate range. + * @param yHigh The upper bound of the Y-coordinate range. + * @return A list of 2D points within the specified orthogonal range. + */ + public static List search(RangeTreeNode tree, int xLow, int xHigh, int yLow, int yHigh) { + RangeTreeNode splitNodeX = findXSplit(tree, xLow, xHigh); + ArrayList result = new ArrayList<>(); + if (splitNodeX != null) { + if (isLeaf(splitNodeX) + && splitNodeX.getVal()[0] >= xLow && splitNodeX.getVal()[0] <= xHigh) { // if split node is leaf + ySearch(splitNodeX, yLow, yHigh, result); + } + xLeftTraversal(splitNodeX.getLeft(), xLow, xHigh, yLow, yHigh, result); + xRightTraversal(splitNodeX.getRight(), xLow, xHigh, yLow, yHigh, result); + } + return result; + } + + private static boolean isLeaf(RangeTreeNode node) { + return node.getLeft() == null && node.getRight() == null; + } + +} diff --git a/src/main/java/algorithms/orthogonalRangeSearching/twoDim/README.md b/src/main/java/algorithms/orthogonalRangeSearching/twoDim/README.md new file mode 100644 index 0000000..73c436b --- /dev/null +++ b/src/main/java/algorithms/orthogonalRangeSearching/twoDim/README.md @@ -0,0 +1,71 @@ +# 2D Orthogonal Range Searching + +2D orthogonal range searching is a computational problem that involves efficiently answering queries about points +in a two-dimensional space that fall within a specified axis-aligned rectangular range (orthogonal range). In other +words, given a set of points in 2D, find me all the points that lie within a specific rectangle. + +To do so, we can extend the idea of 1D range trees to the 2D context. + +Strategy +- Each node in the x-tree has a set of points in its subtree +- Store a y-tree at each x-node containing all the points in the x-subtree + +![2DORS](../../../../../../docs/assets/images/2DORS.jpg) + +1. Build an x-tree using only x-coordinates. + - This should be done in the exact same way as you would for a 1D range tree. +2. For every node in the x-tree, build a y-tree out of nodes in the x-subtree using only y-coordinates. + +![2DORSTrees](../../../../../../docs/assets/images/2DORSTrees.jpg) + +Given the 2D range tree, we now want to query the points in a given rectangle +i.e. search(tree, xLow, xHigh, yLow, yHigh). + +We first want to find the points that will satisfy the x-condition i.e. find me all points whose x-coordinates are +between xLow and xHigh. To do so, we first need to find our split node in the x-tree, by performing binary search[^1] +while traversing the x-tree - similar to how we found the split node in a 1D range tree. This will give us our X-split. + +Now given our X-split, we want to find points that satisfy both our x-condition and y-condition. + +Let's first explore our x-left subtree (X-left traversal). If (xLow <= x-coordinate of curr node), we are confident +that the entire right subtree of the curr node satisfies our x-condition (because of BST property). + +Now we want to perform a Y-search to find the points that also satisfy the y-condition. To do so, we first need to +find our split node in the y-tree i.e. Y-split. Notice that the problem is now reduced to a 1D ORS problem. Once we +have found Y-split, we can simply perform left traversal and right traversal from Y-split, similar to how it is done +in 1D ORS. + +A similar symmetric logic applies in exploring our x-right subtree. + +![2DORSQuery](../../../../../../docs/assets/images/2DORSQuery.jpg) +Image Source: https://www.cse.wustl.edu/~taoju/cse546/lectures/Lecture21_rangequery_2d.pdf + +## Complexity Analysis +**Time**: + +Build Tree (cost incurred once only): O(nlog^2n) + +Querying: O(k + log^2n) +- O(logn) to find X-split +- O(logn) recursing steps in X-left traversal/X-right-traversal +- O(logn) y-tree searches of cost O(logn) => overall: O(log^2n) +- O(k) enumerating output + +**Space**: O(nlogn) +- Each point appears in at most 1 y-tree per level. (Refer to 1st image for visualisation) +- There are O(logn) levels in the x-tree. +- Therefore, each node appears in at most O(logn) y-trees. => overall: O(nlogn) +- The x-tree takes O(n) space. + +## Notes +### Dynamic Updates +Unlike 1D range trees, dynamic updates in a 2D range tree are inefficient as rotations in the x-tree might involve +entirely rebuilding the y-trees for the rotated notes. Therefore, 2D ORS is mainly used for static querying. + +### d-dimensional Range Trees +- Query cost: O(k + log^dn) +- Build tree cost: O(nlog^(d-1)n) +- Space: O(nlog^(d-1)n) + +[^1] This reference to binary search differs from our typical binary search formulation, but essentially refers +to the findSplit function where you walk down the balanced tree and remove roughly half of the nodes each time. \ No newline at end of file diff --git a/src/test/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearchingTest.java b/src/test/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearchingTest.java new file mode 100644 index 0000000..4de82f5 --- /dev/null +++ b/src/test/java/algorithms/orthogonalRangeSearching/oneDim/OrthogonalRangeSearchingTest.java @@ -0,0 +1,67 @@ +package algorithms.orthogonalRangeSearching.oneDim; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +import algorithms.orthogonalRangeSearching.RangeTreeNode; + +public class OrthogonalRangeSearchingTest { + @Test + public void test_orthogonalRangeSearching() { + + int[] firstInput = new int[]{7, 12, 25, 26, 40, 45}; + Object[] firstExpected = new Object[]{12, 25, 26, 40, 45}; + RangeTreeNode firstTree = OrthogonalRangeSearching.buildTree(firstInput, 0, firstInput.length - 1); + Object[] firstResult = OrthogonalRangeSearching.search(firstTree, 10, 50); + assertArrayEquals(firstExpected, firstResult); + + int[] secondInput = new int[]{-26, -7, -10, -40, -43}; + Object[] secondExpected = new Object[]{-26, -10, -7}; + RangeTreeNode secondTree = OrthogonalRangeSearching.buildTree(secondInput, 0, secondInput.length - 1); + Object[] secondResult = OrthogonalRangeSearching.search(secondTree, -30, -2); + assertArrayEquals(secondExpected, secondResult); + + int[] thirdInput = new int[]{26, 7, 12, 40, 45}; + Object[] thirdExpected = new Object[]{12, 26}; + RangeTreeNode thirdTree = OrthogonalRangeSearching.buildTree(thirdInput, 0, thirdInput.length - 1); + Object[] thirdResult = OrthogonalRangeSearching.search(thirdTree, 10, 35); + assertArrayEquals(thirdExpected, thirdResult); + + // for fourth input + // static queries + int[] fourthInput = new int[]{3, 19, 30, 49, 59, 70, 89, 100}; + Object[] fourthExpected = new Object[]{30, 49, 59, 70}; + RangeTreeNode fourthTree = OrthogonalRangeSearching.buildTree(fourthInput, 0, fourthInput.length - 1); + Object[] fourthResult = OrthogonalRangeSearching.search(fourthTree, 20, 71); + assertArrayEquals(fourthExpected, fourthResult); + + Object[] fifthExpected = new Object[]{49, 59, 70, 89, 100}; + Object[] fifthResult = OrthogonalRangeSearching.search(fourthTree, 31, 130); + assertArrayEquals(fifthExpected, fifthResult); + + Object[] sixthExpected = new Object[]{59}; + Object[] sixthResult = OrthogonalRangeSearching.search(fourthTree, 59, 59); + assertArrayEquals(sixthExpected, sixthResult); + + // dynamic updates then query + + OrthogonalRangeSearching.configureTree(fourthTree); + fourthTree = OrthogonalRangeSearching.insert(fourthTree, 101); + Object[] seventhExpected = new Object[]{49, 59, 70, 89, 100, 101}; + Object[] seventhResult = OrthogonalRangeSearching.search(fourthTree, 31, 130); + assertArrayEquals(seventhExpected, seventhResult); + + fourthTree = OrthogonalRangeSearching.insert(fourthTree, 46); + fourthTree = OrthogonalRangeSearching.insert(fourthTree, 32); + Object[] eighthExpected = new Object[]{30, 32, 46, 49, 59, 70}; + Object[] eighthResult = OrthogonalRangeSearching.search(fourthTree, 20, 71); + assertArrayEquals(eighthExpected, eighthResult); + + fourthTree = OrthogonalRangeSearching.delete(fourthTree, 32); + fourthTree = OrthogonalRangeSearching.delete(fourthTree, 59); + Object[] ninthExpected = new Object[]{30, 46, 49, 70}; + Object[] ninthResult = OrthogonalRangeSearching.search(fourthTree, 20, 72); + assertArrayEquals(ninthExpected, ninthResult); + } +} diff --git a/src/test/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearchingTest.java b/src/test/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearchingTest.java new file mode 100644 index 0000000..27e6ac6 --- /dev/null +++ b/src/test/java/algorithms/orthogonalRangeSearching/twoDim/OrthogonalRangeSearchingTest.java @@ -0,0 +1,48 @@ +package algorithms.orthogonalRangeSearching.twoDim; + +import static org.junit.Assert.assertArrayEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import algorithms.orthogonalRangeSearching.RangeTreeNode; + +public class OrthogonalRangeSearchingTest { + @Test + public void test_orthogonalRangeSearching() { + + ArrayList firstInput = new ArrayList<>(); + firstInput.add(new Integer[]{4, 5}); + firstInput.add(new Integer[]{3, 3}); + firstInput.add(new Integer[]{2, 4}); + firstInput.add(new Integer[]{1, 2}); + firstInput.add(new Integer[]{5, 2}); + firstInput.add(new Integer[]{6, 3}); + firstInput.add(new Integer[]{7, 1}); + firstInput.add(new Integer[]{8, 4}); + + ArrayList firstExpected = new ArrayList<>(); + firstExpected.add(new Integer[]{1, 2}); + firstExpected.add(new Integer[]{3, 3}); + firstExpected.add(new Integer[]{5, 2}); + firstExpected.add(new Integer[]{6, 3}); + + RangeTreeNode firstTree = OrthogonalRangeSearching.buildXTree(firstInput, + 0, firstInput.size() - 1); + List firstResult = OrthogonalRangeSearching.search(firstTree, 1, 6, 1, 3); + assertArrayEquals(firstExpected.toArray(), firstResult.toArray()); + + ArrayList secondExpected = new ArrayList<>(); + secondExpected.add(new Integer[]{6, 3}); + secondExpected.add(new Integer[]{7, 1}); + secondExpected.add(new Integer[]{8, 4}); + List secondResult = OrthogonalRangeSearching.search(firstTree, 6, 9, 0, 4); + assertArrayEquals(secondExpected.toArray(), secondResult.toArray()); + + ArrayList thirdExpected = new ArrayList<>(); + List thirdResult = OrthogonalRangeSearching.search(firstTree, 6, 9, 2, 2); + assertArrayEquals(thirdExpected.toArray(), thirdResult.toArray()); + } +}