{'fraction_non_alphanumeric': 0.03866519700015129, 'fraction_numerical': 0.012492165380708466, 'mean_word_length': 4.274737802097583, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Building a Balanced k-d Tree in O(kn log n) Time

The original description of the k-d tree recognized that rebalancing techniques, such as are used to build an AVL tree or a red-black tree, are not applicable to a k-d tree. Hence, in order to build a balanced k-d tree, it is necessary to find the median of the data for each recursive subdivision of those data. The sort or selection that is used to find the median for each subdivision strongly influences the computational complexity of building a k-d tree. This paper discusses an alternative algorithm that builds a balanced k-d tree by presorting the data in each of k dimensions prior to building the tree. It then preserves the order of these k sorts during tree construction and thereby avoids the requirement for any further sorting. Moreover, this algorithm is amenable to parallel execution via multiple threads. Compared to an algorithm that finds the median for each recursive subdivision, this presorting algorithm has equivalent performance for four dimensions and better performance for three or fewer dimensions.


INTRODUCTION
Bentley introduced the k -d tree as a binary tree that stores k -dimensional data [4]. Like a standard binary tree, the kd tree subdivides data at each recursive level of the tree. Unlike a standard binary tree that uses only one key for all levels of the tree, the k -d tree uses k keys and cycles through these keys for successive levels of the tree. For example, to build a k -d tree from three-dimensional points that comprise (x, y, z) coordinates, the keys would be cycled as x, y, z, x, y, z... for successive levels of the k -d tree. A more elaborate scheme for cycling the keys chooses the coordinate that has the widest dispersion or largest variance to be the key for a particular level of recursion [8].
Due to the use of different keys at successive levels of the tree, it is not possible to employ rebalancing techniques, such as are used to build an AVL tree [1] or a red-black tree [3,10], when building a k -d tree. Hence, the typical approach to building a balanced k -d tree finds the median of the data for each recursive subdivision of those data. Bentley showed that if the median of n elements were found in O (n) time, it would be possible to build a depth-balanced k -d tree in O (n log n) time. However, algorithms that find the median in guaranteed O (n) time are somewhat complicated [6,7]. Quicksort [14] finds the median in O (n) time in the best case, but in O n 2 time in the worst case [23].
Merge sort [9] and heap sort [22] find the median in guaranteed O (n log n) time, which leads to O n log 2 n time for building a balanced k -d tree [21].
An alternative approach to building a balanced k -d tree presorts the data prior to building the tree and avoids resorting for each recursive subdivision. Two such algorithms have been reported that sort triangles for three-dimensional graphics ray tracing and that have best-case complexity of O (n log n) but undetermined worst-case complexity [12,21]. The algorithm that is described in the present article presorts points in each of k dimensions prior to building the k -d tree, then maintains the order of these k sorts when building a balanced k -d tree and thereby achieves a worst-case complexity of O (kn log n). A brief description of a similar algorithm has been presented previously [15].

The O (kn log n) Algorithm
Consider the 15 (x, y, z) tuples that are stored in elements 0 through 14 of the "Tuples" array that is shown at the left side of Figure 1. The k -d tree-building algorithm begins by presorting the tuples in their x-, y-and z-coordinates via three executions of merge sort. These three sorts do not in fact sort the x-, y-and z-coordinates by using these coordinates as sort keys entirely independently of one another; instead, x, y and z form the most significant portions of the respective super keys x:y:z, y:z:x and z:x:y that represent cyclic permutations of x, y and z. The symbols for these super keys use a colon to designate the concatenation of the individual x, y and z values. Hence, for example, the symbol z:x:y represents a super key wherein z is the most significant portion of the super key, x is the middle portion of the super key, and y is the least significant portion of the super key.
The merge sorts employ super keys, instead of keys that are merely the individual x-, y-and z-coordinates, in order to detect and remove duplicate tuples, as will be explained later. The merge sorts do not reorder the Tuples array; rather, they reorder three index arrays whose elements are indices into the Tuples array. The initial order of these index arrays is established by the merge sorts and is shown in Figure 1 in the xyz, yzx and zxy columns under "Initial Indices". In this figure, xyz, yzx and zxy are shorthand notations for the super keys x:y:z, y:z:x and z:x:y, respectively.
The xyz, yzx and zxy columns under "Initial Indices" represent the initial order of the xyz-, yzx-and zxy-index arrays that indicate the results of the three merge sorts.
This partitioning is accomplished for the yzx-index array as follows. The elements of the yzx-index array are retrieved in order of increasing address from 0 to 14. The x:y:z super key that is specified by each element of the yzx-index array is compared to the 7:2:6 super key that is specified by the median element of the xyz-index array. Each element of the yzx-index array is copied to either the lower or upper half of a temporary index array, depending on the result of this comparison. After all of the elements of the yzx-index array have been processed in this manner, the temporary index array replaces the yzx-index array and becomes the new yzx-index array that is depicted in Figure 1 under "After First Split." The partitioning of the first six elements of the yzx-index array is discussed below and provides insight into the details of the k -d tree-building algorithm.
The element at address 0 of the yzx-index array is 13 and specifies the tuple (2, 1, 3) for which the x:y:z super key is 2:1:3. This super key is less than the median super key 7:2:6; hence, the element at address 0 of the yzx-index array is copied to address 0 in the new yzx-index array, which is the lowest address in the lower half of the new yzx-index array. The element at address 1 of the yzx-index array is 4 and specifies the tuple (8,1,5) for which the x:y:z super key is 8:1:5. This super key is greater than the median super key 7:2:6; hence, the element at address 1 of the yzx-index array is copied to address 8 in the upper half of the new yzx-index array, which is the lowest address in the upper half of the new yzx-index array. The element at address 2 of the yzx-index array is 5 and specifies the tuple (7,2,6) for which the x:y:z super key is 7:2:6. This super key equals the median super key 7:2:6; hence, the element at address 2 in the yzx-index array is ignored and not copied to the new yzx-index array.
The element at address 3 of the yzx-index array is 9 and specifies the tuple (6, 3, 1) for which the x:y:z super key is 6:3:1. This super key is less than the median super key 7:2:6; hence, the element at address 3 of the yzx-index array is copied to address 1 in the lower half of the new yzx-index array, which is the second lowest address in the lower half of the new yzx-index array. The element at address 4 of the yzx-index array is 0 and specifies the tuple (2, 3, 3) for which the x:y:z super key is 2:3:3. This super key is less than the median super key 7:2:6; hence, the element at address 4 of the yzx-index array is copied to address 2 in the lower half of the new yzx-index array, which is the third lowest address in the lower half of the new yzx-index array. The element at address 5 of the yzx-index array is 6 and specifies the tuple (9, 4, 1) for which the x:y:z super key is 9:4:1. This super key is greater than the median super key 7:2:6; hence, the element at address 5 of the yzx-index array is copied to address 9 in the upper half of the new yzx-index array, which is the second lowest address in the upper half of the new yzx-index array.
Partitioning continues for the remaining eight elements of the yzx-index array in the manner that is described above. The partitioning of the first six elements of the yzx-index array reveals that the yzx-index array has been partitioned in x relative to the median element of the xyz-index array; this partitioning preserves the initial merge-sorted order in y:z:x within the lower and upper halves of the new yzx-index array.
Next, the zxy-index array is partitioned in x relative to the median element of the xyz-index array, which preserves the initial merge-sorted order in z:x:y for the lower and upper halves of the new zxy-index array. The reader is encouraged to audit the partitioning of the first few elements of the zxy-index array under "Initial Indices" in order to verify that these elements are correctly assigned to the lower and upper halves of the new zxy-index array that is shown in Figure 1 under "After First Split." Because the partitioning in x preserves the initial merge-sorted order for the lower and upper halves of the yzx-and zxy-index arrays, there is no requirement for any further sorting after the k initial merge sorts.
The lower and upper halves of the new xyz-, yzx-and zxy-index arrays in Figure 1 under "After First Split" reveal that the index value 5 is absent from the lower and upper halves of these index arrays. This value is absent from these index arrays because it is the value of the median element of the xyz-index array that specified the x:y:z super key 7:2:6 relative to which the yzx-and zxy-index arrays were partitioned in x. In order to record this partitioning, a reference to the tuple (7, 2, 6) is stored in the root of the nascent k -d tree, as shown in Figure 2 Figure 2: A k -d tree that is built from the (x, y, z) tuples of Figure 1.
Next, the lower and upper halves of the xyz-, yzx-and zxy-index arrays are processed recursively and partitioned in y to create the "less than" and "greater than" subtrees of the root of the k -d tree. Consider the lower half of the yzx-index array that is depicted in Figure 1 under "After First Split." The median element of this array is located at address 3; its value is 1 and specifies the tuple (5, 4, 2) for which the y:z:x super key is 4:2:5. The lower half of the yzxindex array is already sorted in y:z:x. However, the lower halves of the zxy-and xyz-index arrays require partitioning in y relative to the y:z:x super key 4:2:5 that is specified by the median element of the lower half of the yzx-index array. The reader is encouraged to verify the result of this partitioning by inspection of the first and second fourths of the new xyz-, yzx-and zxy-index arrays that are depicted in Figure 1 under "After Second Split." The upper halves of the zxy-and xyz-index arrays are partitioned in a similar manner relative to the y:z:x super key 5:3:9 that is formed from the tuple (9, 5, 3) that is specified by the value 12 of the median element at address 11 of the upper half of the yzxindex array. References to the tuples (5, 4, 2) and (9,5,3) are stored in the "less than" and "greater than" children of the root of the nascent k -d tree, as shown in Figure 2.
Recursion terminates when an index array comprises one, two or three elements. In the case of one element, a reference to the corresponding tuple is stored in a new node of the k -d tree. For two or three elements, the elements are already in sorted order in the index array, so the determi-nation of which tuple to reference from a new node of the k -d tree and which tuple or tuples to reference from children of that node is trivial. For example, consider the four fourths of the zxy-index arrays under "After Second Split" in Figure 1. Each fourth comprises three elements, so recursion terminates. The tuples (2,1,3) and (1,6,8) that are specified respectively by the median elements 13 and 11 at addresses 1 and 5 of the zxy-index array are referenced by the respective "less than" and "greater than" children of node (5, 4, 2) of the nascent k -d tree. Similarly, the tuples (8, 4, 2) and (9, 6, 7) that are specified respectively by the median elements 7 and 2 at addresses 9 and 13 of the zxyindex array are referenced by the respective "less than" and "greater than" children of node (9, 5, 3) of the nascent k -d tree. The children and grandchildren of nodes (5, 4, 2) and (9,5,3) are shown in Figure 2.
The foregoing discussion reveals that the k -d tree includes "less than" and "greater than" children but no "equal" children. For this reason, duplicate (x, y, z) tuples must be removed from the data prior to building the k -d tree. After the k initial merge sorts have reordered the xyz-, yzx-and zxy-index arrays, each index array is traversed once in order to discard all but one index from each set of contiguous indices that reference identical (x, y, z) tuples. In order that adjacent indices reference identical (x, y, z) tuples, the k initial merge sorts employ x:y:z, y:z:x and z:x:y super keys instead of keys that are merely the individual x-, y-and zcoordinates. If the k initial merge sorts employed keys that were only the individual x-, y-and z-coordinates, adjacent indices within an index array could reference non-identical (x, y, z) tuples for which one or more, but not all, of the x-, yand z-coordinates were identical. The x:y:z, y:z:x and z:x:y super keys guarantee that each group of identical (x, y, z) tuples is indexed by a set of contiguous indices within each index array. These super keys enable the removal of duplicate (x, y, z) tuples via one pass through each index array that discards adjacent indices that reference identical (x, y, z) tuples.
It is possible to optimize the use of the temporary index array such that only one temporary index array is required and such that the xyz-, yzx-and zxy-index arrays may be reused to avoid allocation of new index arrays at each level of recursion. This optimization operates as follows. The xyzindex array is copied to the temporary index array. Then the yzx-index array is partitioned in x and the result is stored in the two halves of the xyz-index array. Next, the zxy-index array is partitioned in x and the result is stored in the two halves of the yzx-index array. Finally, the temporary index array is copied to the zxy-index array. This optimization permutes the xyz-, yzx-and zxy-index arrays cyclically at each level of recursion, as is required to cycle the keys in the order x, y, z, x, y, z... for successive levels of the k -d tree. Moreover, it guarantees that the x:y:z, y:z:x or z:x:y super key that is required for partitioning at a particular level of the k -d tree is always specified by the median element of the xyz-index array. The additional computational cost of this index array optimization is the copying of one index array at each level of recursion. (The Appendix describes a more sophisticated approach to permuting the index arrays.) Recursive partitioning occurs for log 2 (n) levels of the nascent k -d tree. The computational complexity of this k -d treebuilding algorithm includes a O (kn log n) term for the k initial merge sorts plus a O (kn log n) term for partitioning or copying n elements of k index arrays at each of the log 2 (n) levels of recursion. This O (kn log n) k -d tree-building algorithm requires storage for a Tuples array of n k -dimensional tuples, plus an n-element temporary array, plus k n-element index arrays. The Tuples array is immutable. The index and temporary arrays are ephemeral and are no longer required after construction of the k -d tree.

Parallel Execution
The merge-sorting function [16] and the O (kn log n) k -d tree-building function both subdivide index arrays and process non-overlapping halves of each index array via recursive calls to these functions. Hence, these functions (or Java methods) are amenable to parallel execution via multiple threads that occurs as follows.
One thread executes a recursive call of the method; this thread is designated as the parent thread. The parent thread subdivides one or more index arrays, then calls the method recursively to process the lower and upper halves of each index array. The parent thread does not execute the recursive call that processes the lower half of each index array; instead, it launches a child thread to execute that recursive call. After launching the child thread, the parent thread executes the recursive call that processes the upper half of each index array, then waits for the child thread to finish execution of the recursive call that processes the lower half of each index array.
For a balanced k -d tree, the number of threads q that are required by this thread-allocation strategy is q = 2 d ≈ n/2 where d represents the deepest level of recursion and n represents the number of tuples. A large number of tuples would require a prohibitively large number of threads; hence, child threads are launched to only the maximum level of recursion that is allowed by the number of available threads. Beyond this maximum level of recursion, the parent thread processes both halves of each index array.
Two threads permit launching a child thread at the first level of recursion. Four threads permit launching child threads at the first two levels of recursion. Eight threads permit launching child threads at the first three levels of recursion, etc. Because child threads are launched at the highest levels of the tree (i.e., the lowest levels of recursion), each thread processes the maximum possible workload. Because index arrays are subdivided by their median elements at each level of recursion, all threads share the workload equally.
A disadvantage of this thread allocation strategy is that it limits the number of threads to an integer power of two. Because the level of recursion determines the number of threads, it is not possible to employ, for example, three or ten threads. An advantage of this thread allocation strategy is that it is simple and robust because synchronization involves only a parent thread and one child thread.

Results for the O (kn log n) Algorithm
The O (kn log n) k -d tree-building algorithm was implemented in the Java language, and the single-threaded performance of the merge sorting, duplicate tuple removal, and k -d tree-building methods was measured using a 2.3 GHz Intel i7 processor. Figure 3 shows the total time in seconds that was required to perform the initial merge sorts, remove the duplicate tuples, and build the k -d tree, plotted versus n log 2 (n) for 2 18 ≤ n ≤ 2 24 (x, y, z, w) tuples of randomly-generated 32-bit integers. The dashed line of Fig-ure 3 shows the least-squares fit of the total time t to the function t = mn log 2 (n) where m is the slope of the line. The correlation coefficient r = 0.998 indicates an adequate least-squares fit; hence, the execution times are proportional to n log 2 (n). The question of whether these execution times are proportional to k will be addressed in Section 4 of this article. 0e+00 1e+08 2e+08 3e+08 4e+08 The total of merge sorting, duplicate tuple removal, and k -d tree-building times (seconds) is plotted versus n log 2 (n) for the application of the O (kn log n) k -d tree-building algorithm to   ≤ n ≤   (x, y, z, w) tuples of randomlygenerated 32-bit integers.
The O (kn log n) k -d tree-building algorithm was parallelized via Java threads and its performance was measured for one to eight threads using a 2.3 GHz Intel quad-core i7 processor. Figure 4 shows the total time in seconds that was required to perform the initial merge sorts, remove the duplicate tuples, and build the k -d tree, plotted versus the number of threads q for n = 2 24 (x, y, z, w) tuples of randomlygenerated 32-bit integers. The dashed curve of Figure 4 shows the least-squares fit of the total time t to the equation This equation will be discussed in Section 4 of this article. The correlation coefficient r = 0.9867 indicates an acceptable least-squares fit.

The O (n log n) Algorithm
In order to understand the performance of the O (kn log n) k -d tree-building algorithm relative to that of other algorithms, the performance was compared to a O (n log n) k -d tree-building algorithm that incorporates a O (n) medianfinding algorithm [6,7]. Most of the complexity of the O (n log n) algorithm is limited to the O (n) median-finding algorithm. The application of this O (n log n) k -d tree-building algorithm to sort (x, y, z) tuples is described as follows. First, an index array is created and merge sorted in x, y or z via one of the x:y:z, y:z:x and z:x:y super keys; the choice of super key is arbitrary. The initial merge sort does not reorder the tuples array; instead, it reorders the index array whose elements are indices into the tuples array. Next, duplicate (x, y, z) tuples are removed via one pass through the index array, as discussed in Section 2.1 of this article.
The subsequent k -d tree-building step partitions the index array recursively. At each level of recursion, the median element of the index array is found in O (n) time using the x:y:z, y:z:x or z:x:y super key that is appropriate to that level of recursion. A convenient feature of the O (n) medianfinding algorithm is that the index array is partitioned relative to the median element during the search for the median element. Hence, once the median element has been found, a reference to the (x, y, z) tuple that the median element specifies is stored in the root of the nascent k -d tree, as shown in Figure 2. The lower and upper halves of the index array are processed recursively to create the "less than" and "greater than" subtrees of the root of the k -d tree. The O (n log n) k -d tree-building method processes non-overlapping halves of the index array via recursive calls to this method. Hence, this method is amenable to parallel execution via multiple threads in the manner that was explained in Section 2.2 of this article.
Recursion terminates when the index array comprises one, two or three elements. In the case of one element, a reference to the corresponding tuple is stored in a new node of the k -d tree. For two elements, a reference to the tuple that corresponds to the first element is stored in a new node of the k -d tree, then the super keys of the two elements are compared to decide whether to reference the tuple that corresponds to the second element from the "less than" or "greater than" child of that node. For three elements, the index array is sorted via insertion sort [5] to determine which tuple to reference from a new node of the k -d tree and which tuples to reference from the children of that node.
Recursive partitioning occurs for log 2 (n) levels of the nascent k -d tree. The computational complexity of this k -d treebuilding algorithm includes a O (n log n) term for the initial merge sort plus another O (n log n) term for partitioning n elements of the index array at each of the log 2 (n) levels of recursion. This O (n log n) k -d tree-building algorithm requires storage for a Tuples array of n k -dimensional tuples, plus an n-element index array, plus an n/2-element temporary array. The Tuples array is immutable. The index and temporary arrays are ephemeral and are no longer required after construction of the k -d tree.

Results for the O (n log n) Algorithm
The O (n log n) k -d tree-building algorithm was implemented in the Java language, and the single-threaded performance of the merge sorting, duplicate tuple removal, and k -d tree-building methods was measured using a 2.3 GHz Intel i7 processor. Figure 5 shows the total time in seconds that was required to perform the initial merge sort, remove the duplicate tuples, and build the k -d tree, plotted versus n log 2 (n) for 2 18 Figure 5: The total of merge sorting, duplicate tuple removal, and k -d tree-building times (seconds) is plotted versus n log 2 (n) for the application of the O (n log n) k -d tree-building algorithm to   ≤ n ≤   (x, y, z, w) tuples of randomlygenerated 32-bit integers.
The O (n log n) k -d tree-building algorithm was parallelized via Java threads and its performance was measured for one to eight threads using a 2.3 GHz Intel quad-core i7 processor. Figure 6 shows the total time in seconds that was required to perform the initial merge sort, remove the duplicate tuples, and build the k -d tree, plotted versus the number of threads q for n = 2 24 (x, y, z, w) tuples of randomly-generated 32bit integers. The dashed curve of Figure 6 shows the leastsquares fit of the total time t to Equation 1. The correlation coefficient r = 0.9958 indicates an acceptable least-squares fit.   Figures 4 and 6 show that the k -d tree-building algorithms scale for multiple threads. For either algorithm, the execution by eight threads on an Intel quad-core i7 processor, which supports concurrent execution of two threads per core, increases the execution speed by approximately three times relative to the speed of one thread. The execution time t does not adhere to the Amdahl [2] model t = ts + t1 q

DISCUSSION
but rather to the model that is expressed by Equation 1 in Section 2.3 of this article t = ts + t1 q + mc (q − 1) In this equation, q is the number of threads, ts represents the time required to execute the serial or non-parallelizable portion of the algorithm, t1 represents the time required to execute the parallelizable portion of the algorithm via one thread, and mc (q − 1) models an additional limitation to the performance of multi-threaded execution that the Amdahl model fails to capture. This additional limitation to performance may occur due to cache contention in a shared cache wherein multiple threads attempt to access the cache simultaneously. The cache miss term mc (q − 1) of Equation 1 models the performance limitation, which results from cache contention, as a linear function of q [11].
Differentiating Equation 1 with respect to q yields Setting dt/dq to zero in Equation 3 and solving for q predicts that the minimum execution time should occur at q = t1/mc threads. Substituting into this square root the respective values of t1 and mc that were obtained via least-squares fitting for the O (kn log n) and O (n log n) algorithms predicts minima at q = 5.87 and q = 6.15 threads, respectively. These minima are depicted in Figures 4 and 6, which predict decreased performance of both k -d tree building algorithms for greater than eight threads. The decreased performance for as few as eight threads is a consequence of the relatively large values for mc (1.96 and 1.75 seconds per thread, respectively) that were obtained via least-squares fitting. If execution time measurements were obtained using an Intel multi-core i7 processor that supports concurrent execution of more than eight threads, those measurements would likely confirm decreased performance for greater than eight threads.
The similar parallelizable times t1 for both algorithms (67.48 and 66.16 thread-seconds) indicate that their singlethreaded performance is about equal. This effect is due to a fortuitous choice of k = 4 that specifies test data that comprise (x, y, z, w) tuples. Because the execution time of the O (kn log n) algorithm should be proportional to k but the execution time of the O (n log n) algorithm should not, these two algorithms are expected to have unequal performance for a different choice of k. In order to test this hypothesis, each algorithm was utilized to build five different k -d trees. For each k -d tree, 2 24 k -dimensional tuples of randomly-generated 32-bit integers were created using a different value of k = 2, 3, 4, 5, 6. The performance of each algorithm was measured using a single thread of a 2.3 GHz Intel i7 processor. The results are shown in Figure 7. Figure 7 shows the total time in seconds that was required to perform the initial merge sorts, remove the duplicate tuples, and build the k -d tree via the O (kn log n) and O (n log n) algorithms for n = 2 24 k -dimensional tuples of randomly-generated 32-bit integers, plotted versus the number of dimensions k = 2, 3, 4, 5, 6. This figure demonstrates that the execution time of the O (kn log n) algorithm is proportional to k but the execution time of the O (n log n) algorithm is not. In this figure, the slope of the solid line (m SOLID = 18.07 seconds per dimension) indicates that for 2 24 tuples, each additional dimension increases the execution time of the O (kn log n) algorithm by 18 seconds. For k = 4, the two algorithms have equal performance.
In Figure 7, the slope of the dashed line (m DASHED = 0.55 seconds per dimension) suggests that the execution time of the O (n log n) algorithm might be proportional to k. How- ever, this apparent proportionality is related to the time required to generate random 32-bit integers. The storage requirements of the O (kn log n) and O (n log n) algorithms differ. Although both algorithms require storage for a tuples array of n k -dimensional tuples, the O (kn log n) algorithm requires storage for an n-element temporary array plus k n-element index arrays, whereas the O (n log n) algorithm requires storage for an n/2-element temporary array plus only one n-element index array.
The O (n) median-finding algorithm is complicated and requires careful implementation to achieve optimum performance. For example, the median-finding algorithm utilizes a sorting algorithm to sort large numbers of five-element arrays. For the initial implementation of the O (n) medianfinding algorithm, these small arrays were sorted via the merge sort algorithm that is used for the initial sort of the index array [16]. That merge sort algorithm is not optimized for sorting small arrays and hence resulted in poor performance of the O (n) median-finding algorithm and consequent poor performance of the O (n log n) k -d tree-building algorithm. Replacing the merge sort algorithm with an insertion sort algorithm that is optimized for sorting small arrays [5] conferred a 30 percent improvement to the performance of the O (n log n) k -d tree-building algorithm. (The Appendix describes a better alternative than insertion sort.)

CONCLUSION
The k -d tree-building algorithm proposed in this article guarantees a computational complexity of O (kn log n) for n points and k dimensions. For k = 4, the performance of this algorithm equals the performance of a O (n log n) k -d tree-building algorithm that employs a O(n) median-finding algorithm. For either algorithm, an improvement in performance by a factor of three relative to single-threaded performance is achieved via parallel execution by eight threads of an Intel quad-core i7 processor that supports concurrent execution of two threads per core.

Source Code
Source code that implemented the O (n log n) and O (kn log n) k -d tree-building algorithms and that was provided with prior versions of this article has been superseded by the source code described in the Source Code for Optimized Algorithms paragraphs at the end of the Appendix.

Russell A. Brown and John A. Robinson
Six modifications that optimize the performance of the O (n log n) and O (kn log n) k -d tree-building algorithms are described below.

Optimized Super Key Comparison
Both tree-building algorithms depend on the comparison of super keys. This comparison is performed by comparing portions of two super keys serially in a loop, beginning with the most significant portions of the super keys, and exiting the loop as soon as any two portions differ. A more efficient comparison may be accomplished by comparing the most significant portions of the super keys prior to entering the loop and avoiding execution of the loop altogether for the common case where the two super keys differ in their most significant portions. This simple change significantly improves the performance of both tree-building algorithms by eliminating the overhead of loop execution.

Optimized Merge Sort
Both tree-building algorithms depend on merge sort [16] that copies elements between arrays. An optimized merge sort minimizes copying via four variants of merge sort [17].
As discussed in Section 2.2 of this article, merge sort uses a separate thread to process each half of the index array recursively so that the two halves of the index array are processed concurrently by two threads. However, subsequent to this concurrent recursive processing, only one thread merges the two halves of the index array. Concurrent merging may be accomplished using two threads. One thread merges beginning at the lowest address of the lower half of the index array and at the highest address of the upper half of the index array [16]; the addresses move towards one another as they are updated. A second thread merges beginning at the highest address of the lower half of the index array and at the lowest address of the upper half of the index array; the addresses move away from one another as they are updated.
Concurrent merging and minimization of copying improve the performance of both tree-building algorithms.

Optimized Partitioning
As discussed in Section 2.2 of this article, the O (kn log n) tree-building algorithm uses a separate thread to process each half of the index array recursively so that the two halves of the index array are processed concurrently by two threads. However, prior to this concurrent recursive processing, only one thread partitions the index array into two halves. Concurrent partitioning may be performed using two threads. One thread begins at the lowest address of the the index array and ascends. A second thread begins at the highest address of the index array and descends.
As discussed in Section 2.1 of this article, the O (kn log n) algorithm partitions k − 1 index arrays and copies one index array at each level of the tree. An optimization avoids copying the index array and instead only partitions the k −1 index arrays to achieve O [(k − 1) n log n] performance. This optimization requires precomputing an array of log 2 n vectors, where each vector contains k + 1 elements that specify permutation of the index arrays for a given level of the tree.
Concurrent partitioning and avoidance of copying improve the performance of the O (kn log n) tree-building algorithm.

Precomputing the Modulus
Both tree-building algorithms select the most significant portion of the super key via a modulus during each recursive call of the tree-building function. This modulus may be precomputed and stored in an array of log 2 n elements, where each element contains the modulus for a given level of the tree, thus eliminating n/2 − log 2 n modulus computations.

Optimized Median Selection
The O (n log n) tree-building algorithm uses insertion sort [5] to select a median. The performance of this tree-building algorithm may be improved by median selection via an explicit decision tree [19] instead of via insertion sort that requires more comparisons and memory accesses.

Deferred Merge Sort and Partitioning
The O (kn log n) tree-building algorithm presorts the data in each of k dimensions prior to building the tree and then partitions the data in each of k − 1 dimensions about a median at each level of the tree. The performance of this algorithm may be improved by presorting in only one dimension prior to building the tree. Sorting in each additional dimension is deferred until the level of the tree where the sort is necessary. Moreover, partitioning the data in a given dimension is deferred until after that dimension has been sorted.
Deferred sorting and partitioning are illustrated by the following algorithm for building a k -d tree from a set of (x, y, z, w) tuples. First, the tuples are presorted using their x:y:z:w super keys to populate the xyzw index array.
At the first level of the nascent tree, the xyzw index array is split at its median element to create two half-index arrays. These half-index arrays are then sorted using a y:z:w:x super key to populate two yzwx half-index arrays.
At the second level of the tree, each yzwx half-index array is split at its median element to create four quarter-index arrays. These quarter-index arrays are then sorted using a z:w:x:y super key to populate four zwxy quarter-index arrays. Also at this second level of the tree, each xyzw halfindex array is partitioned about a zwxy-median to create four xyzw quarter-index arrays.
At the third level of the tree, each zwxy quarter-index array is split at its median element to create eight eighth-index arrays. These eighth-index arrays are then sorted using a w:x:y:z super key to populate eight wxyz eighth-index arrays. Also at this third level of the tree, each xyzw quarterindex array and each yzwx quarter-index array is partitioned about a wxyz-median to create eight xyzw eighthindex arrays and eight yzwx eighth-index arrays, respectively.
At this point, the data have been sorted in each of the k dimensions, so beginning with the fourth level of the tree, the O (kn log n) k -d tree-building algorithm proceeds as described in Section 2.1 of this article. The performance of this algorithm improves due to deferred sorting and partitioning to the degree indicated below.
Without deferred sorting, the computational complexity of presorting in k dimensions is O (kn log 2 n). With deferred sorting, the computational complexity is Hence, deferred sorting confers to the sorting performance a fractional improvement of Without deferred partitioning, the computational complexity of partitioning n elements of k − 1 index arrays at each of the log 2 n levels of the tree is O [(k − 1) n log 2 n]. With deferred partitioning, the computational complexity is

that sums and simplifies to
Hence, deferred partitioning confers to the partitioning performance a fractional improvement of Equations 4 and 5 predict that deferral improves the performance of sorting and partitioning by a factor that is inversely proportional to log 2 n and directly proportional to k − 1 and k, respectively.

Performance Measurements
The performance of the optimized O (n log n) and O (kn log n) k -d tree-building algorithms that benefit from the six abovedescribed modifications was compared to the performance of the non-optimized O (kn log n) algorithm described in Section 2.1 of this article. C++ implementations were benchmarked using an Intel quad-core i7 processor executing eight threads to process 2 24 4-dimensional randomly-generated tuples of 64-bit integers. The optimized O (n log n) and O (kn log n) algorithms achieved 28 percent and 26 percent performance improvements, respectively, relative to the nonoptimized O (kn log n) algorithm. For this benchmark test case, equations 4 and 5 predict performance improvements of 6 percent and 8 percent, respectively. Hence, the optimizations discussed above other than deferred sorting and partitioning significantly improve the performance of the O (kn log n) algorithm and presumably the performance of the O (n log n) algorithm as well.
A second benchmark used an Intel quad-core i7 processor executing eight threads to process 2 24 8-dimensional randomly-generated tuples of 64-bit integers. The optimized O (n log n) and O (kn log n) algorithms achieved 65 percent and 34 percent performance improvements, respectively, relative to the non-optimized O (kn log n) algorithm. The greater performance improvement of the O (n log n) algorithm relative to the O (kn log n) algorithm is due to the fact that the computational complexity of the O (kn log n) algorithm is proportional to the number of dimensions k whereas the computational complexity of the O (n log n) algorithm is independent of k, as demonstrated by Figure 7 of this article.

Scalability Curve Fitting
where t is the execution time, q is the number of threads, ts is the non-parallelizable execution time, t1 is the parallelizable execution time for a single thread, and mc models cache contention.
To facilitate least-squares curve fitting, the above equation is rearranged to yield where the right-hand side of Equation 6 is a linear function of ts and mc. A least-squares fit of the (qi, ti) pairs of measured data is accomplished by first averaging all singlethreaded execution times ti (where qi = 1) to obtain t1 and then by fitting ti − t1/qi versus qi for all (qi, ti) pairs via linear least squares [20] to obtain ts and mc. However, the values of qi that are used for linear least squares are not 1, 2, 4 and 8 threads but are calculated as described below.
A k -d tree built from 2 24 tuples requires 23 levels of recursion. Building the tree via a single thread executes one thread at all 23 levels of recursion. Building the tree via two threads executes two threads at all 23 levels of recursion. Building the tree via four threads executes two threads at the first level and four threads at the remaining 22 levels of recursion. Building the tree via eight threads executes two threads at the first level, four threads at the second level, and eight threads at the remaining 21 levels of recursion.
Given the execution by various numbers of threads at different levels of recursion, Equation 6 is modified to reflect the effective number of threads qe and the effective reciprocal of the number of threads (1/q) e t − t1 1 q e = ts + mc (qe − 1) In this equation, qe and (1/q) e are obtained by averaging q and 1/q respectively in Equation 6 over the various numbers of threads at different levels of recursion. For example, when building the tree via four threads, the effective number of threads qe4 to be used for fitting to Equation 7 is calculated as the average of the number of threads For the trivial cases of building the tree via one and two threads, qe1 = (1/q) e1 = 1, qe2 = 2, and (1/q) e2 = 1/2.
The execution time of the optimized O (kn log n) k -d treebuilding algorithm was measured for one to eight threads using a 2.3 GHz Intel quad-core i7 processor. Figure 8 shows the total time in seconds that was required to perform the initial merge sort, remove the duplicate tuples, and build the k -d tree, plotted versus the effective number of threads qe for n = 2 24 (x, y, z, w) tuples of randomly-generated 64-bit integers. The dashed curve of Figure 8 plots the equation using qe and the t1, ts and mc values obtained as explained above in conjunction with Equations 6 and 7. The correlation coefficient r = 0.9855 indicates an acceptable leastsquares fit. Equation 8 requires both qe and (1/q) e whereas the dashed curve in Figure 8 plots t versus only qe. The required values of (1/q) e are generated from qe values as follows. The four qe, (1/q) e pairs (1, 1), (2, 1/2); (3.91, 0.26087) and (7.57, 0.14674) are fit [13] via linear least squares to log (1/q) e = log b + m × log (qe) (9) that is derived by taking the logarithm of each side of The least-squares fit provides values for m and b. Then Equation 10 is used to generate a value of (1/q) e from each value of qe. Then the qe and (1/q) e values are substituted into Equation 8 to generate the dashed curve in Figure 8. Least-squares fits were also performed using execution time data obtained for the C++ implementations of the optimized O (n log n) and the non-optimized O (kn log n) k -d tree-building algorithms. These least-squares fits are similar to the fit depicted in Figure 8 for the optimized O (kn log n) algorithm and are omitted from this appendix.

Scalability
As explained in Section 4, the mc (q − 1) term of Equation 1 models a limitation to multi-threaded execution that the Amdahl model fails to capture and that may arise due to cache misses in a shared-memory architecture. Values for the coefficient mc that were obtained from least-squares fits to execution-time data for the C++ implementations of the tree-building algorithms are respectively 0.93, 1.05 and 1.60 for the optimized O (n log n), optimized O (kn log n), and non-optimized O (kn log n) algorithms. The value of mc for the non-optimized O (kn log n) algorithm is similar to the values of mc obtained for the Java implementations of the non-optimized algorithms, as seen from Figures 4 and 6. The values of mc for the optimized algorithms are significantly smaller due to fewer memory accesses during merge sort, partitioning, and median selection. Figure 9 plots via Equation 8 the least-squares fits for the optimized O (n log n), optimized O (kn log n), and nonoptimized O (kn log n) tree-building algorithms. Due to their smaller mc values that model fewer memory accesses, the optimized algorithms scale better than the non-optimized algorithm that scales less well due to a larger value of mc that models more memory accesses.

Source Code for Optimized Algorithms
Provided under the BSD 3-Clause License are C++ implementations of the optimized O (n log n) and O (kn log n) algorithms described as follows. The "kdTreeNlogn.cpp" and "kdTreeKnlogn.cpp" sourcecode files implement algorithms to build a k -d tree via the O (n log n) and O (kn log n) algorithms respectively. The "kdTreeMapNlogn.cpp" and "kdTreeMapKnlogn.cpp" sourcecode files implement algorithms to build a k -d tree-based key-to-value map via the O (n log n) and O (kn log n) algorithms respectively. The "kdTreeKmapNlogn.cpp" and "kdTreeKmapKnlogn.cpp" source-code files also implement algorithms to build a k -d tree-based key-to-value map via the O (n log n) and O (kn log n) algorithms respectively. The latter two implementations of a map execute twice as rapidly as the former two implementations. The improved execution efficiency of the latter two implementations is achieved by specifying the dimensionality k of the key during compilation instead of execution and thereby avoiding one degree of indirection when accessing the key.
All six implementations include algorithms that (1) search a k -d tree for all points that lie inside a k -dimensional hyperrectangular region [4] and (2) search a k -d tree for the n nearest neighbors to a query point 1 and sort those nearest neighbors according to their distances to the query point via a priority queue [18].
All six implementations optionally execute using multiple threads via the C++ future library.