<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Master level
All lessons in this course

Lesson 47 of 50 15 minMaster

Spatial indexes: grids, quadtrees and BVH

Find points in an area, nearest neighbours and colliding objects without checking everything: uniform grids, quadtrees, k-d trees, R-trees and bounding volume hierarchies, and when to use each.

“Which sensors are inside this zone?”, “which crane is nearest to this load?”, “does this pipe clash with any beam?”. Questions about position cannot use an ordinary sorted list or hash table, because position has two (or three) dimensions. Checking every object is O(n) per question, and for clash detection between all pairs it is O(n²). A spatial index groups objects by where they are, so that whole regions can be skipped at once.

Uniform grid (spatial hashing)

Divide the area into equal square cells and keep a list of the objects in each cell, for example in a Dictionary keyed by the cell coordinates. A search looks only in the cells that overlap the search area.

Grid.cs
var sensors = new (string Id, double X, double Y)[]{    ("S1", 12, 8), ("S2", 47, 15), ("S3", 51, 22), ("S4", 55, 18), ("S5", 90, 75),    ("S6", 33, 61), ("S7", 58, 26), ("S8", 72, 40), ("S9", 5, 95), ("S10", 49, 30)}; const double Cell = 20;var grid = new Dictionary<(int, int), List<(string Id, double X, double Y)>>();foreach (var s in sensors){    var key = ((int)(s.X / Cell), (int)(s.Y / Cell));    if (!grid.TryGetValue(key, out var list)) grid[key] = list = new();    list.Add(s);} var (x1, y1, x2, y2) = (40.0, 10.0, 60.0, 28.0);     // the search zonevar found = new List<string>();int checkedCount = 0;for (int cx = (int)(x1 / Cell); cx <= (int)(x2 / Cell); cx++)    for (int cy = (int)(y1 / Cell); cy <= (int)(y2 / Cell); cy++)        if (grid.TryGetValue((cx, cy), out var list))            foreach (var s in list)            {                checkedCount++;                if (s.X >= x1 && s.X <= x2 && s.Y >= y1 && s.Y <= y2) found.Add(s.Id);            }Console.WriteLine($"Found {string.Join(", ", found)}");Console.WriteLine($"Checked {checkedCount} of {sensors.Length}");
Output
Found S2, S4, S3, S7
Checked 5 of 10

Grids are simple, fast to update (moving an object means moving it between two lists) and ideal when objects are spread evenly and similar in size: particles, game characters, delivery vans on a city map. Their weakness is clustering: if most objects sit in a few cells, those cells are scanned point by point and the grid is no better than a list.

Quadtree: adapt to the data

A quadtree starts with one square. When a square holds more than a few points, it splits into four quarters, and so on. Crowded areas end up with many small squares, empty areas with a few big ones. A search descends only into squares that overlap the search area, skipping every other subtree. In 3D the same idea is an octree (eight children).

Interactive

Searching an area: grid vs quadtree

Sensors on a site plan. Find every sensor inside the dashed box. An index lets you skip whole regions that cannot contain an answer.

Index
Click
Sensors checked 14 of 34Found 6Leaf regions visited 13 of 34
The quadtree splits any region holding more than 3 sensors into four, so crowded areas get small regions and empty areas stay big. Regions outside the box are skipped with their whole subtree: 14 sensors tested. Typical search: O(log n + answers).

The box starts at the edge of the dense cluster in the top right. Switch between the three indexes and compare “sensors checked”, then click elsewhere to move the box. The quadtree’s small squares there let it reject most of the cluster; the grid has to scan its crowded cells completely.

Quadtree.cs
var sensors = new (string Id, double X, double Y)[]{    ("S1", 12, 8), ("S2", 47, 15), ("S3", 51, 22), ("S4", 55, 18), ("S5", 90, 75),    ("S6", 33, 61), ("S7", 58, 26), ("S8", 72, 40), ("S9", 5, 95), ("S10", 49, 30)};var root = new QuadTree(0, 0, 100);foreach (var s in sensors) root.Insert(s.Id, s.X, s.Y); var found = new List<string>();int visited = root.Query(40, 10, 60, 28, found);Console.WriteLine($"Found {string.Join(", ", found)}");Console.WriteLine($"Visited {visited} squares"); class QuadTree(double x, double y, double size){    const int Capacity = 2;    readonly List<(string Id, double X, double Y)> points = new();    QuadTree[]? kids;     public void Insert(string id, double px, double py)    {        if (kids != null) { Child(px, py).Insert(id, px, py); return; }        points.Add((id, px, py));        if (points.Count > Capacity && size > 1)                 // too crowded: split into four        {            double h = size / 2;            kids = [new(x, y, h), new(x + h, y, h), new(x, y + h, h), new(x + h, y + h, h)];            foreach (var p in points) Child(p.X, p.Y).Insert(p.Id, p.X, p.Y);            points.Clear();        }    }     QuadTree Child(double px, double py) =>        kids![(px >= x + size / 2 ? 1 : 0) + (py >= y + size / 2 ? 2 : 0)];     public int Query(double x1, double y1, double x2, double y2, List<string> found)    {        if (x2 < x || x1 >= x + size || y2 < y || y1 >= y + size) return 0;   // no overlap: skip it all        int visited = 1;        foreach (var p in points)            if (p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2) found.Add(p.Id);        if (kids != null) foreach (var k in kids) visited += k.Query(x1, y1, x2, y2, found);        return visited;    }}
Output
Found S2, S3, S4, S7
Visited 7 squares

Nearest neighbour: k-d trees

A k-d tree is a binary search tree for points: the root splits the points by x at the median, its children split by y, their children by x again, and so on. To find the nearest point, descend to the leaf where the query point would go, then walk back up, exploring the other side of a split only if the circle around the best distance so far crosses the splitting line. On typical data this is O(log n) per query. In very high dimensions (hundreds of features, as in AI embeddings) every split gets crossed, and approximate methods such as HNSW graphs take over.

Objects with size: R-trees and BVH

Beams, pipes, rooms and parcels are not points. Wrap each object in an axis-aligned bounding box (AABB): its minimum and maximum x and y (and z). Two boxes overlap only if they overlap on every axis, a check of a few comparisons. If the boxes do not overlap, the objects certainly do not either; if they do, a precise (expensive) test decides.

BoundingBox.cs
var crane = new Box(10, 10, 30, 40);var scaffold = new Box(25, 35, 50, 60);var hut = new Box(60, 0, 80, 20);Console.WriteLine(crane.Overlaps(scaffold));Console.WriteLine(crane.Overlaps(hut));Console.WriteLine(Box.Union(crane, scaffold)); record struct Box(double MinX, double MinY, double MaxX, double MaxY){    public bool Overlaps(Box o) =>        MinX <= o.MaxX && o.MinX <= MaxX && MinY <= o.MaxY && o.MinY <= MaxY;   // overlap on both axes     public static Box Union(Box a, Box b) =>                                    // the box around both        new(Math.Min(a.MinX, b.MinX), Math.Min(a.MinY, b.MinY), Math.Max(a.MaxX, b.MaxX), Math.Max(a.MaxY, b.MaxY));}
Output
True
False
Box { MinX = 10, MinY = 10, MaxX = 50, MaxY = 60 }

A bounding volume hierarchy (BVH) groups nearby objects and puts a box around each group, then groups the groups, up to one box around everything. A query (a ray, a box, another object) tests the top box and descends only into children it touches. Game engines and ray tracers use BVHs for collisions and lighting; BIM software uses the same idea for clash detection between thousands of building elements. An R-tree is the database version of the same idea, kept balanced like a B-tree on disk: PostGIS spatial indexes are R-trees, while SQL Server uses a multi-level grid.

IndexGood forWatch out for
Uniform gridEvenly spread, similar-sized, moving objectsClusters; choosing the cell size
Quadtree / octreeUneven point data, zoomable mapsDeep trees when many points coincide
k-d treeNearest-neighbour queries on static pointsMany updates; very high dimensions
R-treeRectangles and shapes on disk (databases, GIS)More complex to implement
BVHCollisions, ray casting, clash detectionRebuilding when many objects move
Quick check

Most points are in one corner of the map. Which index adapts best?

Quick check

Two bounding boxes do not overlap. What can you conclude about the objects inside them?

Practice

Count the pairs of sensors closer than 10 m to each other, using a grid with 10 m cells so that each sensor is only compared with sensors in its own cell and the 8 neighbouring cells. For the given points, print the number of close pairs, the distance checks made and the number of all pairs.

Show a hint

Two points closer than the cell size are always in the same or adjacent cells. Compare each pair once with j > i, and compare squared distances to avoid square roots.

Show the solution
Solution.cs
const double R = 10;var pts = new (double X, double Y)[] { (1, 1), (4, 5), (12, 3), (25, 25), (27, 22), (40, 40), (9, 9), (45, 41) }; var grid = new Dictionary<(int, int), List<int>>();for (int i = 0; i < pts.Length; i++){    var key = ((int)Math.Floor(pts[i].X / R), (int)Math.Floor(pts[i].Y / R));    if (!grid.TryGetValue(key, out var list)) grid[key] = list = new();    list.Add(i);} int pairs = 0, checks = 0;for (int i = 0; i < pts.Length; i++){    int cx = (int)Math.Floor(pts[i].X / R), cy = (int)Math.Floor(pts[i].Y / R);    for (int dx = -1; dx <= 1; dx++)        for (int dy = -1; dy <= 1; dy++)            if (grid.TryGetValue((cx + dx, cy + dy), out var list))                foreach (int j in list)                    if (j > i)                                    // each pair once                    {                        checks++;                        double ex = pts[i].X - pts[j].X, ey = pts[i].Y - pts[j].Y;                        if (ex * ex + ey * ey < R * R) pairs++;                    }}Console.WriteLine($"{pairs} close pairs, {checks} checks, {pts.Length * (pts.Length - 1) / 2} pairs in total");
Output
6 close pairs, 8 checks, 28 pairs in total

Remember

  • Spatial indexes group objects by position so that whole regions can be skipped.
  • A uniform grid is simplest and great for evenly spread objects; clusters defeat it.
  • Quadtrees (octrees in 3D) subdivide only where data is dense; k-d trees answer nearest-neighbour queries.
  • Objects with size go in bounding boxes; BVHs and R-trees are hierarchies of boxes for collisions, clash detection and databases.
0 of 50 lessons completed