The Quadtree: Solving the O(N^2) Spatial Nightmare

Back to Index
dev//21/02/2026//8 Min Read//Updated 21/02/2026

This analysis was built by a Software Engineer who once tried to simulate 10,000 particles and watched his CPU melt into a puddle of O(N2)O(N^2) regret. If the recursion depth makes your head spin, just imagine you're looking at a very organized square pizza.

Divide and Conquer the Map: A Deep Dive into Quadtrees


In game development and spatial computing, the "Proximity Problem" is the ultimate boss. If you have NN objects and you want to know which ones are colliding, the naive approach is to check every object against every other object.

As software engineers, we know that O(N2)O(N^2) is the "Abandon All Hope" complexity class. At 100 objects, it's 10,000 checks. At 10,000 objects, it's 100,000,000 checks per frame. Your 144Hz monitor just became a 1 frame-per-hour slideshow.

To solve this, we don't just need a faster loop; we need a smarter structure. Enter the Quadtree.

Part 1: The Engineering Foundation


A Quadtree is a tree data structure in which each internal node has exactly four children. It is the spatial equivalent of a Binary Search Tree, but instead of dividing a 1D line, it divides a 2D plane.

The Branching Factor: A Comparison


Tree TypeDimensionChildren per NodeUse Case
Binary Tree1D2Sorting, Searching (Values)
Quadtree2D4Collision Detection, Image Compression
Octree3D83D Physics, Voxel Engines, Ray Tracing

The Quadtree works on a simple principle: If two objects are in different parts of the world, they cannot possibly be colliding.

Part 2: The Architecture of Space


The Quadtree decomposes space recursively. We start with a single bounding box (the "Root"). If that box contains more than a certain number of objects (the "Capacity"), we split it into four equal quadrants: North-West (NW), North-East (NE), South-West (SW), and South-East (SE).

The Recursive Decomposition


Failed to render diagram. Check syntax.
graph TD
    Root[Root: Full Map] --> NW[North-West]
    Root --> NE[North-East]
    Root --> SW[South-West]
    Root --> SE[South-East]

    NW --> NW_NW[NW-NW]
    NW --> NW_NE[NW-NE]
    NW --> NW_SW[NW-SW]
    NW --> NW_SE[NW-SE]

This structure allows us to discard massive chunks of the map instantly. If a player is in the SE quadrant, we don't even look at objects in NW, NE, or SW.

Part 3: Implementation (The Rule of Three)


A robust Quadtree implementation relies on three core components:

  1. Boundary: A simple rectangle (x, y, width, height) defining the space.
  2. Capacity: The maximum number of points a node can hold before it MUST split.
  3. Points: The actual data (x, y coordinates and metadata) being stored.

The Algorithm Flow


StepActionLogic
1. InsertAdd a pointCheck if point is within Boundary. If not, return false.
2. Check CapacityRoom available?If points < Capacity, add to list. Return true.
3. SubdivideSplit!If points == Capacity, create 4 children. Move current points to children.
4. QueryFind neighborsDefine a "Search Range". Only check nodes that intersect with this range.

Part 4: The Lifecycle (The "Breathe" of the Tree)


If you watch a Quadtree simulation, you'll see the boxes constantly flickering. This is the tree "breathing" as it adapts to movement.

The Subdivision-Collapse Loop


A Quadtree is never static. As objects move, the tree must maintain its efficiency through two opposing forces:

  1. The Split (Expansion): When a node becomes too dense (exceeding capacity), it must subdivide. This prevents the "local O(N2)O(N^2)" problem within that specific box.
  2. The Merge (Contraction): If an area becomes sparse (e.g., a grenade explodes and clears out a room), the four child quadrants should be destroyed. Their remaining points are moved back up to the parent.

Why merge? Because traversing deep branches of an empty tree is a waste of CPU cycles. We want the tree to be as shallow as possible while still keeping individual node counts low. In many real-time simulations (like the one below), we simply rebuild the entire tree every frame. This implicitly handles both splitting and merging, as the new tree only exists where particles currently reside.

Part 5: The Go Implementation


I speak Go. Here is how you implement a production-ready, type-safe Quadtree in Golang:

go
package spatial // Point represents a 2D coordinate with optional metadata type Point struct { X, Y float64 Data interface{} } // Rectangle defines a boundary (Center X, Y and Half-Dimensions W, H) type Rectangle struct { X, Y, W, H float64 } func (r Rectangle) Contains(p Point) bool { return p.X >= r.X-r.W && p.X < r.X+r.W && p.Y >= r.Y-r.H && p.Y < r.Y+r.H } func (r Rectangle) Intersects(other Rectangle) bool { return !(other.X-other.W > r.X+r.W || other.X+other.W < r.X-r.W || other.Y-other.H > r.Y+r.H || other.Y+other.H < r.Y-r.H) } type Quadtree struct { Boundary Rectangle Capacity int Points []Point Divided bool NW, NE, SW, SE *Quadtree } func NewQuadtree(boundary Rectangle, capacity int) *Quadtree { return &Quadtree{ Boundary: boundary, Capacity: capacity, Points: []Point{}, } } func (qt *Quadtree) Subdivide() { x, y, w, h := qt.Boundary.X, qt.Boundary.Y, qt.Boundary.W/2, qt.Boundary.H/2 qt.NW = NewQuadtree(Rectangle{x - w, y - h, w, h}, qt.Capacity) qt.NE = NewQuadtree(Rectangle{x + w, y - h, w, h}, qt.Capacity) qt.SW = NewQuadtree(Rectangle{x - w, y + h, w, h}, qt.Capacity) qt.SE = NewQuadtree(Rectangle{x + w, y + h, w, h}, qt.Capacity) qt.Divided = true } func (qt *Quadtree) Insert(p Point) bool { if !qt.Boundary.Contains(p) { return false } if len(qt.Points) < qt.Capacity { qt.Points = append(qt.Points, p) return true } if !qt.Divided { qt.Subdivide() } return qt.NW.Insert(p) || qt.NE.Insert(p) || qt.SW.Insert(p) || qt.SE.Insert(p) } func (qt *Quadtree) Query(rangeRect Rectangle, found []Point) []Point { if !qt.Boundary.Intersects(rangeRect) { return found } for _, p := range qt.Points { if rangeRect.Contains(p) { found = append(found, p) } } if qt.Divided { found = qt.NW.Query(rangeRect, found) found = qt.NE.Query(rangeRect, found) found = qt.SW.Query(rangeRect, found) found = qt.SE.Query(rangeRect, found) } return found } // Example Main function for local execution func main() { // 1. Define the world boundary (Center 200, 200 with 200 half-width/height) boundary := Rectangle{200, 200, 200, 200} // 2. Initialize the Quadtree with a capacity of 4 points per node qt := NewQuadtree(boundary, 4) // 3. Insert some random points points := []Point{ {100, 100, "Point A"}, {105, 110, "Point B"}, {110, 105, "Point C"}, {120, 120, "Point D"}, {300, 300, "Point E"}, // This will be in a different quadrant } for _, p := range points { qt.Insert(p) } // 4. Query a specific area (Center 100, 100 with 50 half-width/height) searchRange := Rectangle{100, 100, 50, 50} found := qt.Query(searchRange, []Point{}) fmt.Printf("Found %d points in search range:\n", len(found)) for _, p := range found { fmt.Printf("- %s at (%.2f, %.2f)\n", p.Data, p.X, p.Y) } }

Part 6: The Deductions (The Performance Verdict)


By shifting from O(N2)O(N^2) to O(NlogN)O(N \log N) or even O(logN)O(\log N) for localized queries, the Quadtree transforms the impossible into the trivial.

Question A: Quadtree vs. Simple Grid?


The Data Verdict: Choose the Quadtree for Sparse environments and the Grid for Uniform environments.

  • The Reasoning:
    • A Simple Grid (dividing the map into 10×1010 \times 10 squares) is great if players are spread out evenly. But if 100 players crowd into a single square, that square becomes a O(N2)O(N^2) bottleneck.
    • A Quadtree is adaptive. It will only subdivide where the action is. If the map is empty, the tree is shallow. If there's a mosh pit in the corner, the tree grows deep only in that corner.
    • Deduction: Quadtrees are "Demand-Driven" structures.

Question B: What is the optimal "Capacity"?


The Data Verdict: 4 to 8 points is usually the "Goldilocks" zone.

  • The Trade-off:
    • Capacity = 1: The tree becomes incredibly deep. You spend more time navigating the tree (memory overhead) than checking collisions.
    • Capacity = 100: You're basically back to O(N2)O(N^2) within each node.
    • The Strategy: Set it high enough to handle small clusters without splitting, but low enough to keep the final intersection checks fast.

Question C: The "Dynamic" Problem


The Data Verdict: Moving objects require Re-insertion or Refitting.

  • The Challenge: Quadtrees are easy when points are static (like trees in a forest). When objects move (like bullets), you have two choices:
    1. Clear and Rebuild: Delete the whole tree and rebuild it every frame. (Surprisingly fast for N<5000N < 5000).
    2. Update: Move a point, check if it left its boundary, and "bubble it up" to the parent until it finds a new home.
  • Conclusion: If your objects are fast and numerous, rebuilding the tree is often more cache-friendly than complex pointer updates.

Conclusion


The Quadtree is a masterclass in Spatial Hashing. It teaches us that the best way to handle a massive problem isn't to work harder, but to categorize the problem until it becomes many tiny, manageable problems.

Whether you're building a 2D bullet hell, a map of the stars, or an image compression algorithm (where quadrants of the same color are merged), the Quadtree is your most reliable spatial ally.

You can check a visual implementation of this in my Quadtree Simulation app or any of my generative art projects that rely on particle proximity!

Analyzing data structures... Delicious.
Syntax error in textmermaid version 11.12.2