PH-tree | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Type | tree, map | |||||||||||||||||||||||
Invented | 2014 | |||||||||||||||||||||||
|
The PH-tree [1] is a tree data structure used for spatial indexing of multi-dimensional data (keys) such as geographical coordinates, points, feature vectors, rectangles or bounding boxes. The PH-tree is space partitioning index [2] with a structure similar to that of a quadtree or octree. [3] However, unlike quadtrees, it uses a splitting policy based on tries and similar to Crit bit trees that is based on the bit-representation of the keys. The bit-based splitting policy, when combined with the use of different internal representations for nodes, provides scalability with high-dimensional data. The bit-representation splitting policy also imposes a maximum depth, thus avoiding degenerated trees and the need for rebalancing. [1]
The basic PH-tree is a spatial index that maps keys, which are d-dimensional vectors with integers, to user defined values. The PH-tree is a multi-dimensional generalization of a Crit bit tree in the sense that a Crit bit tree is equivalent to a PH-tree with -dimensional keys. Like the Crit bit tree, and unlike most other spatial indexes, the PH-tree is a map rather than a multimap . [1] [4]
A d-dimensional PH-tree is a tree of nodes where each node partitions space by subdividing it into quadrants (see below for how potentially large nodes scales with high dimensional data). Each quadrant contains at most one entry, either a key-value pair (leaf quadrant) or a key-subnode pair. For a key-subnode pair, the key represents the center of the subnode. The key is also the common prefix (bit-representation) of all keys in the subnode and its child subnodes. Each node has at least two entries, otherwise it is merged with the parent node. [1]
Some other structural properties of PH-trees are: [1]
Similar to most quadtrees, the PH-tree is a hierarchy of nodes where every node splits the space in all d dimensions. [1] Thus, a node can have up to subnodes, one for each quadrant.
The PH-tree uses the bits of the multi-dimensional keys to determine their position in the tree. All keys that have the same leading bits are stored in the same branch of the tree. [1]
For example, in a node at level L, to determine the quadrant where a key should be inserted (or removed or looked up), it looks at the L's bit of each dimension of the key. For a 3D node with 8 quadrants (forming a cube) the L's bit of the first dimension of the key determines whether the target quadrant is on the left or the right of the cube, the L's bit of the second dimension determines whether it is at the front or the back, and the L's bit of the third dimension determines bottom vs top, see picture.
Example with three 1D keys with 8bit values: , and . Adding and to an empty tree results in a single node. The two keys first differ in their 6th bit so the node has a level (starting with 0). The node has a 5bit prefix representing the common 5 bits of both keys. The node has two quadrants, each key is stored in one quadrant. Adding a third key results in one additional node at with one quadrant containing the original node as subnode and the other quadrant containing the new key .[ citation needed ]
With 2D keys every node has quadrants. The position of the quadrant where a key is stored is extracted from the respective bits of the keys, one bit from each dimension. The four quadrants of the node form a 2D hypercube (quadrants may be empty). The bits that are extracted from the keys form the hypercube address , for and for . is effectively the position of the quadrant in the node's hypercube.[ citation needed ]
The ordering of the entries in a node always follows Z-ordering. Entries in a node can, for example, be stored in fixed size arrays of size . h is then effectively the array index of a quadrant. This allows lookup, insert and remove with and there is no need to store h. Space complexity is however per node, so it is less suitable for high dimensional data. [1]
Another solution is to store entries in a sorted collection, such as dynamic arrays and/or B-trees. This slows down lookup operations to but reduces memory consumption to . [1]
The original implementation aimed for minimal memory consumption by switching between fixed and dynamic array representation depending on which uses less memory. [1] Other implementations do not switch dynamically but use fixed arrays for , dynamic arrays for and B-trees for high dimensional data.
Lookup, insertion and removal operations all work very similar: find the correct node, then perform the operation on the node. Window queries and k-nearest-neighbor searches are more complex.
The Lookup operation determines whether a key exists in the tree. It walks down the tree and checks every node whether it contains a candidate subnode or a user value that matches the key. [1]
function lookup(key) is entry ← get_root_entry() // if the tree is not empty the root entry contains a root node while entry != NIL && entry.is_subnode() do node ← entry.get_node() entry ← node.get_entry(key) repeatreturn entry // entry can be NIL
function get_entry(key) is node ← current node h ← extract_bits_at_depth(key, node.get_depth()} entry ← node.get_entry_at(h) return entry // entry can be NIL
The Insert operation inserts a new key-value pair into the tree unless they key already exists. The operation traverses the tree like the Lookup function and then inserts the key into the node. There are several cases to consider: [1]
function insert(node, key, value) level ← node.get_level() // Level is 0 for root h ← extract_bits_at_level(key, level) entry ← node.get_entry(h) if entry == NIL then // Case 1. entry_new ← create_entry(key, value) node.set_entry(h, entry_new) else if !entry.is_subnode() && entry.get_key() == key then // Case 2. Collision, there is already an entry return ← failed_insertion else // Case 3. level_diff ← get_level_of_difference(key, entry.get_key()) entry_new ← create_entry(key, value) // new subnode with existing entry and new entry subnode_new ← create_node(level_diff, entry, entry_new) node.set_entry(h, subnode_new) end ifreturn
Removal works inversely to insertion, with the additional constraint that any subnode has to be removed if less than two entries remain. The remaining entry is moved to the parent node. [1]
Window queries are queries that return all keys that lie inside a rectangular axis-aligned hyperbox. They can be defined to be two d-dimensional points and that represent the "lower left" and "upper right" corners of the query box. A trivial implementation traverses all entries in a node (starting with the root node) and if an entry matches it either adds it to the result list (if it is a user entry) or recursively traverses it (if it is a subnode). [1]
function query(node, min, max, result_list) isforeach entry ← node.get_entries() doif entry.is_subnode() thenif entry.get_prefix() >= min and entry.get_prefix() <= max then query(entry.get_subnode(), min, max, result_list) end ifelseif entry.get_key() >= min and entry.get_key() <= max then result_list.add(entry) end ifend ifrepeatreturn
In order to accurately estimate query time complexity the analysis needs to include the dimensionality . Traversing and comparing all entries in a node has a time complexity of because each comparison of -dimensional key with takes time. Since nodes can have up to entries, this does not scale well with increasing dimensionality . There are various ways how this approach can be improved by making use of the hypercube address h. [4]
The idea is to find minimum and maximum values for the quadrant's addresses such that the search can avoid some quadrants that do not overlap with the query box. Let be the center of a node (this is equal to the node's prefix) and and be two bit strings with bits each. Also, let subscript with indicate the 's bit of and and the 'th dimension of , and .
Let and . then has a `` for every dimension where the "lower" half of the node and all quadrants in it does not overlap with the query box. Similarly, has a `` for every dimension where the "upper" half does not overlap with the query box.
and then present the lowest and highest in a node that need to be traversed. Quadrants with or do not intersect with the query box. A proof is available in. [4] With this, the above query function can be improved to:
function query(node, min, max, result_list) is h_min ← calculate h_min h_max ← calculate h_max for each entry ← node.get_entries_range(h_min, h_max) do [ ... ] repeatreturn
Calculating and is . Depending on the distribution of the occupied quadrants in a node this approach will allow avoiding anywhere from no to almost all key comparisons. This reduces the average traversal time but the resulting complexity is still . [4]
Between and there can still be quadrants that do not overlap with the query box. Idea: and each have one bit for every dimensions that indicates whether the query box overlaps with the lower/upper half of a node in that dimension. This can be used to quickly check whether a quadrant overlaps with the query box without having to compare -dimensional keys: a quadrant overlaps with the query box if for every `` bit in there is a corresponding `` bit in and for every `` bit in there is a corresponding `` bit in . On a CPU with 64bit registers it is thus possible to check for overlap of up to -dimensional keys in . [4]
function is_overlap(h, h_min, h_max) isreturn (h | h_min) & h_max == h // evaluates to 'true' if quadrant and query overlap.
function query(node, min, max, result_list) is h_min ← calculate h_min h_max ← calculate h_max for each entry ← node.get_entries_range(h_min, h_max) do h ← entry.get_h(); if (h | h_min) & h_max == h then // evaluates to 'true' if quadrant and query overlap. [ ... ] end ifrepeatreturn
The resulting time complexity is compared to the of the full iteration. [4]
For higher dimensions with larger nodes it is also possible to avoid iterating through all and instead directly calculate the next higher that overlaps with the query box. The first step puts ``-bits into a given for all quadrants that have no overlap with the query box. The second step increments the adapted and the added ``-bits trigger an overflow so that the non-overlapping quadrants are skipped. The last step removes all the undesirable bits used for triggering the overflow. The logic is described in detail in. [4] The calculation works as follows:
function increment_h(h_input, h_min, h_max) is h_out = h_input | (~ h_max ) // pre - mask h_out += 1 // increment h_out = ( h_out & h_max ) | h_min // post - mask return h_out
Again, for this can be done on most CPUs in . The resulting time complexity for traversing a node is . [4] This works best if most of the quadrants that overlap with the query box are occupied with an entry.
k nearest neighbor searches can be implemented using standard algorithms. [5]
The PH-tree can only store integer values. Floating point values can trivially be stored as integers casting them as an integer. However, the authors also propose an approach without loss of precision. [1] [4]
Lossless converting of a floating point value into an integer value (and back) without loss if precision can be achieved by simply interpreting the 32 or 64 bits of the floating point value as an integer (with 32 or 64 bits). Due to the way that IEEE 754 encodes floating point values, the resulting integer values have the same ordering as the original floating point values, at least for positive values. Ordering for negative values can be achieved by inverting the non-sign bits. [1] [4]
Example implementations in Java:
longencode(doublevalue){longr=Double.doubleToRawLongBits(value);return(r>=0)?r:r^0x7FFFFFFFFFFFFFFFL;}
Example implementations in C++:
std::int64_tencode(doublevalue){std::int64_tr;memcpy(&r,&value,sizeof(r));returnr>=0?r:r^0x7FFFFFFFFFFFFFFFL;}
Encoding (and the inverse decoding) is lossless for all floating point values. The ordering works well in practice, including and . However, the integer representation also turns into a normal comparable value (smaller than infinity), infinities are comparable to each other and is larger than . [6] That means that, for example, a query range will not match a value of . In order to match the query range needs to be .[ citation needed ]
In order to store volumes (axis-aligned hyper-boxes) as keys, implementations typically use corner representation [7] which converts the two -dimensional minimum and maximum corners of a box into a single key with dimensions, for example by interleaving them: .
This works trivially for lookup, insert and remove operations. Window queries need to be converted from -dimensional vectors to -dimensional vectors. For example, for a window query that matches all boxes that are completely inside the query box, the query keys are: [7] [8]
For a window query operation that matches all boxes that intersect with a query box, the query keys are: [8]
In high dimensions with less than entries, a PH-tree may have only a single node, effectively “degenerating” into a B-Tree with Z-order curve. The add/remove/lookup operations remain and window queries can use the quadrant filters. However, this cannot avoid the curse of dimensionality, for high dimensional data with or a PH-tree is only marginally better than a full scan. [9]
Research has reported fast add/remove/exact-match operations with large and fast changing datasets. [10] Window queries have been shown to work well especially for small windows [11] or large dataset [12]
The PH-tree is mainly suited for in-memory use. [10] [13] [14] The size of the nodes (number of entries) is fixed while persistent storage tends to benefit from indexes with configurable node size to align node size with page size on disk. This is easier with other spatial indexes, such as R-Trees.
In computer science, binary search, also known as half-interval search, logarithmic search, or binary chop, is a search algorithm that finds the position of a target value within a sorted array. Binary search compares the target value to the middle element of the array. If they are not equal, the half in which the target cannot lie is eliminated and the search continues on the remaining half, again taking the middle element to compare to the target value, and repeating this until the target value is found. If the search ends with the remaining half being empty, the target is not in the array.
In computer science, a B-tree is a self-balancing tree data structure that maintains sorted data and allows searches, sequential access, insertions, and deletions in logarithmic time. The B-tree generalizes the binary search tree, allowing for nodes with more than two children. Unlike other self-balancing binary search trees, the B-tree is well suited for storage systems that read and write relatively large blocks of data, such as databases and file systems.
In computing, Chord is a protocol and algorithm for a peer-to-peer distributed hash table. A distributed hash table stores key-value pairs by assigning keys to different computers ; a node will store the values for all the keys for which it is responsible. Chord specifies how keys are assigned to nodes, and how a node can discover the value for a given key by first locating the node responsible for that key.
Kademlia is a distributed hash table for decentralized peer-to-peer computer networks designed by Petar Maymounkov and David Mazières in 2002. It specifies the structure of the network and the exchange of information through node lookups. Kademlia nodes communicate among themselves using UDP. A virtual or overlay network is formed by the participant nodes. Each node is identified by a number or node ID. The node ID serves not only as identification, but the Kademlia algorithm uses the node ID to locate values.
A quadtree is a tree data structure in which each internal node has exactly four children. Quadtrees are the two-dimensional analog of octrees and are most often used to partition a two-dimensional space by recursively subdividing it into four quadrants or regions. The data associated with a leaf cell varies by application, but the leaf cell represents a "unit of interesting spatial information".
In computing, a persistent data structure or not ephemeral data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure. The term was introduced in Driscoll, Sarnak, Sleator, and Tarjan's 1986 article.
R-trees are tree data structures used for spatial access methods, i.e., for indexing multi-dimensional information such as geographical coordinates, rectangles or polygons. The R-tree was proposed by Antonin Guttman in 1984 and has found significant use in both theoretical and applied contexts. A common real-world usage for an R-tree might be to store spatial objects such as restaurant locations or the polygons that typical maps are made of: streets, buildings, outlines of lakes, coastlines, etc. and then find answers quickly to queries such as "Find all museums within 2 km of my current location", "retrieve all road segments within 2 km of my location" or "find the nearest gas station". The R-tree can also accelerate nearest neighbor search for various distance metrics, including great-circle distance.
In computer science, a fusion tree is a type of tree data structure that implements an associative array on w-bit integers on a finite universe, where each of the input integers has size less than 2w and is non-negative. When operating on a collection of n key–value pairs, it uses O(n) space and performs searches in O(logwn) time, which is asymptotically faster than a traditional self-balancing binary search tree, and also better than the van Emde Boas tree for large values of w. It achieves this speed by using certain constant-time operations that can be done on a machine word. Fusion trees were invented in 1990 by Michael Fredman and Dan Willard.
A van Emde Boas tree, also known as a vEB tree or van Emde Boas priority queue, is a tree data structure which implements an associative array with m-bit integer keys. It was invented by a team led by Dutch computer scientist Peter van Emde Boas in 1975. It performs all operations in O(log m) time, or equivalently in O(log log M) time, where M = 2m is the largest element that can be stored in the tree. The parameter M is not to be confused with the actual number of elements stored in the tree, by which the performance of other tree data-structures is often measured.
A B+ tree is an m-ary tree with a variable but often large number of children per node. A B+ tree consists of a root, internal nodes and leaves. The root may be either a leaf or a node with two or more children.
In computer science, an interval tree is a tree data structure to hold intervals. Specifically, it allows one to efficiently find all intervals that overlap with any given interval or point. It is often used for windowing queries, for instance, to find all roads on a computerized map inside a rectangular viewport, or to find all visible elements inside a three-dimensional scene. A similar data structure is the segment tree.
In computer science, a k-d tree is a space-partitioning data structure for organizing points in a k-dimensional space. K-dimensional is that which concerns exactly k orthogonal axes or a space of any number of dimensions. k-d trees are a useful data structure for several applications, such as:
In mathematical analysis and computer science, functions which are Z-order, Lebesgue curve, Morton space-filling curve, Morton order or Morton code map multidimensional data to one dimension while preserving locality of the data points. It is named in France after Henri Lebesgue, who studied it in 1904, and named in the United States after Guy Macdonald Morton, who first applied the order to file sequencing in 1966. The z-value of a point in multidimensions is simply calculated by interleaving the binary representations of its coordinate values. Once the data are sorted into this ordering, any one-dimensional data structure can be used, such as simple one dimensional arrays, binary search trees, B-trees, skip lists or hash tables. The resulting ordering can equivalently be described as the order one would get from a depth-first traversal of a quadtree or octree.
An implicit k-d tree is a k-d tree defined implicitly above a rectilinear grid. Its split planes' positions and orientations are not given explicitly but implicitly by some recursive splitting-function defined on the hyperrectangles belonging to the tree's nodes. Each inner node's split plane is positioned on a grid plane of the underlying grid, partitioning the node's grid into two subgrids.
In peer-to-peer networks, Koorde is a distributed hash table (DHT) system based on the Chord DHT and the De Bruijn graph. Inheriting the simplicity of Chord, Koorde meets O(log n) hops per node, and hops per lookup request with O(log n) neighbors per node.
In computer science, M-trees are tree data structures that are similar to R-trees and B-trees. It is constructed using a metric and relies on the triangle inequality for efficient range and k-nearest neighbor (k-NN) queries. While M-trees can perform well in many conditions, the tree can also have large overlap and there is no clear strategy on how to best avoid overlap. In addition, it can only be used for distance functions that satisfy the triangle inequality, while many advanced dissimilarity functions used in information retrieval do not satisfy this.
In computer science, an x-fast trie is a data structure for storing integers from a bounded domain. It supports exact and predecessor or successor queries in time O(log log M), using O(n log M) space, where n is the number of stored values and M is the maximum value in the domain. The structure was proposed by Dan Willard in 1982, along with the more complicated y-fast trie, as a way to improve the space usage of van Emde Boas trees, while retaining the O(log log M) query time.
The Priority R-tree is a worst-case asymptotically optimal alternative to the spatial tree R-tree. It was first proposed by Arge, De Berg, Haverkort and Yi, K. in an article from 2004. The prioritized R-tree is essentially a hybrid between a k-dimensional tree and a r-tree in that it defines a given object's N-dimensional bounding volume as a point in N-dimensions, represented by the ordered pair of the rectangles. The term prioritized arrives from the introduction of four priority-leaves that represents the most extreme values of each dimensions, included in every branch of the tree. Before answering a window-query by traversing the sub-branches, the prioritized R-tree first checks for overlap in its priority nodes. The sub-branches are traversed by checking whether the least value of the first dimension of the query is above the value of the sub-branches. This gives access to a quick indexation by the value of the first dimension of the bounding box.
In computer science, the range query problem consists of efficiently answering several queries regarding a given interval of elements within an array. For example, a common task, known as range minimum query, is finding the smallest value inside a given range within a list of numbers.
A relaxed K-d tree or relaxed K-dimensional tree is a data structure which is a variant of K-d trees. Like K-dimensional trees, a relaxed K-dimensional tree stores a set of n-multidimensional records, each one having a unique K-dimensional key x=(x0,... ,xK−1). Unlike K-d trees, in a relaxed K-d tree, the discriminants in each node are arbitrary. Relaxed K-d trees were introduced in 1998.