Tl;dr: Testing quad tree performance with different setups confirms that there is no single winner–takes–all solution that outperforms all others. Parameterising leaf bucket size based on data size and making the decision dynamic appear to be valid optimization techniques.

So this was basically for fun. Quad tree is a data structure that is useful for maps, game worlds, and other continuous 2D things. It is a rooted tree and a special case of k-d tree, which is related to R-tree.

A quad tree like all logical trees consists of branch nodes and leaf nodes.

Each branch has four tabularly aligned children (hence the quad; see picture below). A leaf node contains some items. Implementations abound: 1, 2, 3…. This java-quadtree has two additional features.

a tree autoexpands both inwards and outwards;

a configurable bucket size, which you can manually or automatically adjust (helps fine-tune the performance of trees of different size).

In my quad tree, dynamic bucket size means that buckets grow as a function time, specifically as some power of tree size. When the exponent is to cubic root (^0.333333), for instance, buckets are smaller than with the square root (^0.5). From the autoexpansion follows that dynamic trees are more “fruitful” than static ones in the sense that their smaller leaves bear more items than root-attending leaves. A static tree with massive buckets, on the other hand, is relatively flat – but it might also be suboptimal in searches.

A visual representation of a quad tree

Quad tree nodes are called Quads in the code. The basic node structure is like this:

public class Quad { public Quad parent = null, UL = null, // upper left corner child ... UR = null, LL = null, LR = null ; public double x1,y1, x2,y2 ;

Coordinates ( x1 , y1 ) and ( x2 , y2 ) create a 2D bounding box within which either child quads (UL, UR, LL, LR), or a number of items are stored. When an item is placed in a leaf, that leaf is checked for overflow and possibly replaced by four child quads:

if (items.size() == LEAF_MAX_OBJECTS) { expand(); } if (UL != null) { return place_(h, this); } else { h.quad = this; items.add(h); return h; }

A parent reference, which comes extra to a minimal implementation, enables intra-tree replace operations and growing the tree outwards.

In this implementation each item sits in a wrapper object. This creates some memory overhead, but the convenience is worth it, because an item and its coordinates now keep company. It also enables moving items in the tree with ease. The idea is simple: go upwards in the hierarchy until you find a node that contains the replaced item, then narrow down to a new leaf.

Moving an item in the quad tree

There is, of course, no single tree structure that would always outperform all others. Generally speaking search operations should be as fast as possible, and the number and distribution of items stored in a tree have effect on the outcome. I plotted some histograms to show how different settings contribute to the growth of a tree in this implementation (leaf depth on x axis; item count on y axis).

The deeper the tree, the slower the searches will be, but the same is also true of a big bucket size. So the performance boils down to how much data you have to crunch. It will depend on application how big buckets are tolerable, but a dynamic bucket size helps to make the tree grow evenly on all fronts, reducing cluttering as compared to a big static bucket size.

I did a little performance test with JMH. Java performance varies due to garbage collection, JIT optimization, and possible other dynamic assets. So it is a good idea to use an established testing tool. There are also extrinsic factors, like my machine could run faster one day and slower another or slow down during use, so these statistics don’t measure up to scientific scrutiny here. I feel reasonably confident with them though.

Bucket setup Data distribution Tree size (items) Bucket size Insert (M-item/sec) Replace † (M-item/sec) findAll() 100th †† (K-srch/s) findAll() 16th †† (K-srch/s) findAll() 4th †† (K-srch/s) Dyn. ^1/2 Uniform 1e6 7–1000 5.0 22.7 5.1 1.2 0.3 Static 100 Uniform 1e6 100 3.7 22.5 5.0 1.0 0.3 Static 10 Uniform 1e6 10 2.3 22.8 3.2 0.6 0.2 Dyn. ^3/4 Uniform 1e6 7–31622 8.0 22.7 1.6 0.7 0.3 Dyn. ^1/3 Uniform 1e6 7–100 4.8 22.0 1.8 0.6 0.3 Static 100 Uniform 1e5 100 6.0 22.6 46.9 11.8 3.6 Dyn. ^1/2 Uniform 1e5 7–316 6.8 22.4 44.3 11.6 3.8 Static 10 Uniform 1e5 10 3.9 22.7 43.0 9.7 3.1 Dyn. ^1/3 Uniform 1e5 7–46 6.6 22.5 25.0 7.0 3.1 Dyn. ^3/4 Uniform 1e5 7–5623 9.5 22.5 13.5 5.7 2.7 Static 100 Uniform 1e4 100 6.5 22.8 371.0 101.0 35.0 Static 10 Uniform 1e4 10 8.5 22.6 331.5 106.7 40.3 Dyn. ^1/2 Uniform 1e4 7–100 8.2 22.5 326.9 106.7 40.5 Dyn. ^1/3 Uniform 1e4 7–21 8.0 22.7 224.5 90.3 32.7 Dyn. ^3/4 Uniform 1e4 7–1000 10.6 22.8 124.2 60.1 29.4 Dyn. ^1/2 U, shifting frame ††† 1e6 7–1000 5.7 22.6 5.2 1.2 0.35 Static 100 U, shifting frame 1e6 100 3.7 22.7 5.0 1.0 0.3 Static 100 U, shifting frame 1e5 100 5.9 22.5 48.0 11.7 3.9 Dyn. ^1/2 U, shifting frame 1e5 7–316 7.2 22.8 44.2 11.6 4.0 Static 100 U, shifting frame 1e4 100 8.4 22.4 342.6 104.8 40.7 Dyn. ^1/2 U, shifting frame 1e4 7–100 8.8 22.6 326.4 108.3 41.3

(†) Replace here simulates a game world transition (at maximum 100th of world size at time). (††) Get all items from (x)th part of total area. (†††) A uniform distribution with a shifting frame that moves 100% along x-axis.

##Conclusion

Quad tree is a fun tree structure that is relatively easy to implement. Tree size and bucket size can affect tree performance, so optimizing the latter might pay off in critical contexts.

With a relatively large tree (1000.000 items) in typical searches, I found the best tested parameters over three times more performant than the worst. Insert and replace times varied too, but not as significantly.

Tree performance can be summarized by the following negative correlations.

faster inserts correlate with slower searches;

faster searches on restricted subareas slightly correlate with slower searches on larger subareas.

If your domain size is predictable, I recommend doing a binary search over static bucket sizes to determine an optimized solution (these probably follow a polynomial curve which could be induced from measurements, or deduced from tree properties). For a quick general solution, making bucket size dynamic with an exponent of 0.5 (square root of tree size) seems a good go.

##Benchmark details