← Back to DevBytes

Interview Guide: Octrees Problems and Solutions

What is an Octree?

An octree is a hierarchical spatial data structure that recursively subdivides three-dimensional space into eight octants. It's the 3D extension of a quadtree (which partitions 2D space into four quadrants). Each node in an octree represents a cubic bounding volume, and when a node contains more data points than a specified threshold, it splits into eight equally sized child cubes.

The key properties that define an octree include:

Why Octrees Matter in Technical Interviews

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Interviewers love octree problems because they test multiple competencies simultaneously:

Expect octree questions in interviews at companies working on 3D graphics, autonomous vehicles, game development, or AR/VR systems.

Octree Data Structure Representation

Let's build a complete octree implementation. First, we define the core structures:

#include <vector>
#include <array>
#include <limits>
#include <queue>
#include <cmath>

struct Point3D {
    double x, y, z;
    Point3D(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {}
};

struct AABB {
    Point3D min;  // corner with minimum coordinates
    Point3D max;  // corner with maximum coordinates
    
    AABB(Point3D _min, Point3D _max) : min(_min), max(_max) {}
    
    Point3D center() const {
        return Point3D(
            (min.x + max.x) / 2.0,
            (min.y + max.y) / 2.0,
            (min.z + max.z) / 2.0
        );
    }
    
    bool contains(const Point3D& p) const {
        return p.x >= min.x && p.x <= max.x &&
               p.y >= min.y && p.y <= max.y &&
               p.z >= min.z && p.z <= max.z;
    }
    
    bool intersects(const AABB& other) const {
        return max.x >= other.min.x && min.x <= other.max.x &&
               max.y >= other.min.y && min.y <= other.max.y &&
               max.z >= other.min.z && min.z <= other.max.z;
    }
};

class OctreeNode {
public:
    AABB bounds;
    std::vector<Point3D> points;
    std::array<OctreeNode*, 8> children;
    bool isLeaf;
    int maxCapacity;
    
    OctreeNode(const AABB& _bounds, int _maxCapacity)
        : bounds(_bounds), isLeaf(true), maxCapacity(_maxCapacity) {
        for (int i = 0; i < 8; i++) children[i] = nullptr;
    }
    
    ~OctreeNode() {
        for (int i = 0; i < 8; i++) {
            if (children[i] != nullptr) delete children[i];
        }
    }
    
    // Determine which octant a point belongs to (0-7)
    int getOctant(const Point3D& p) const {
        Point3D c = bounds.center();
        int octant = 0;
        if (p.x > c.x) octant |= 1;  // bit 0: x
        if (p.y > c.y) octant |= 2;  // bit 1: y
        if (p.z > c.z) octant |= 4;  // bit 2: z
        return octant;
    }
    
    // Compute child bounds for octant index 0-7
    AABB childBounds(int octant) const {
        Point3D c = bounds.center();
        double minX = (octant & 1) ? c.x : bounds.min.x;
        double maxX = (octant & 1) ? bounds.max.x : c.x;
        double minY = (octant & 2) ? c.y : bounds.min.y;
        double maxY = (octant & 2) ? bounds.max.y : c.y;
        double minZ = (octant & 4) ? c.z : bounds.min.z;
        double maxZ = (octant & 4) ? bounds.max.z : c.z;
        return AABB(Point3D(minX, minY, minZ), Point3D(maxX, maxY, maxZ));
    }
};

Problem 1: Building an Octree from a Point Cloud

This is the foundational interview question. Given a set of 3D points, construct an octree that partitions space until each leaf node contains at most maxCapacity points.

Interview Strategy

The interviewer evaluates whether you can handle the recursive subdivision correctly, avoid infinite recursion, and manage memory. Key edge cases: points exactly on the center plane (they go to the "lower" octant), empty nodes, and points outside the initial bounding box.

Solution

class Octree {
public:
    OctreeNode* root;
    int maxCapacity;
    
    Octree(const AABB& rootBounds, int _maxCapacity = 8)
        : maxCapacity(_maxCapacity) {
        root = new OctreeNode(rootBounds, maxCapacity);
    }
    
    ~Octree() { delete root; }
    
    void insert(const Point3D& p) {
        insertRecursive(root, p);
    }
    
    // Build from a vector of points at once
    void buildFromPoints(const std::vector<Point3D>& points) {
        for (const auto& p : points) {
            insert(p);
        }
    }
    
private:
    void subdivide(OctreeNode* node) {
        for (int i = 0; i < 8; i++) {
            AABB childBox = node->childBounds(i);
            node->children[i] = new OctreeNode(childBox, node->maxCapacity);
        }
        
        // Redistribute existing points to children
        std::vector<Point3D> existingPoints = std::move(node->points);
        node->isLeaf = false;
        node->points.clear();
        
        for (const auto& p : existingPoints) {
            int octant = node->getOctant(p);
            node->children[octant]->points.push_back(p);
        }
        
        // Recursively subdivide children that exceed capacity
        for (int i = 0; i < 8; i++) {
            if (node->children[i]->points.size() > maxCapacity) {
                subdivide(node->children[i]);
            }
        }
    }
    
    void insertRecursive(OctreeNode* node, const Point3D& p) {
        if (!node->bounds.contains(p)) {
            return;  // Point outside octree bounds, ignore or expand
        }
        
        if (node->isLeaf) {
            node->points.push_back(p);
            if (node->points.size() > maxCapacity) {
                subdivide(node);
            }
        } else {
            int octant = node->getOctant(p);
            insertRecursive(node->children[octant], p);
        }
    }
};

Interview Follow-up: Handling Out-of-Bounds Points

A common follow-up is: "What if a point lies outside the root bounding box?" Here's how to handle dynamic expansion:

void insertWithExpansion(OctreeNode*& root, const Point3D& p) {
    while (!root->bounds.contains(p)) {
        // Expand the root by doubling in the direction of the point
        Point3D c = root->bounds.center();
        Point3D newMin = root->bounds.min;
        Point3D newMax = root->bounds.max;
        double sizeX = newMax.x - newMin.x;
        double sizeY = newMax.y - newMin.y;
        double sizeZ = newMax.z - newMin.z;
        
        // Determine which direction to expand
        int octantForRoot = 0;
        if (p.x > c.x) { newMax.x += sizeX; octantForRoot |= 1; } else { newMin.x -= sizeX; }
        if (p.y > c.y) { newMax.y += sizeY; octantForRoot |= 2; } else { newMin.y -= sizeY; }
        if (p.z > c.z) { newMax.z += sizeZ; octantForRoot |= 4; } else { newMin.z -= sizeZ; }
        
        AABB newBounds(Point3D(newMin.x, newMin.y, newMin.z), 
                       Point3D(newMax.x, newMax.y, newMax.z));
        
        OctreeNode* newRoot = new OctreeNode(newBounds, root->maxCapacity);
        newRoot->isLeaf = false;
        
        // The old root becomes a child of the new root
        // Find the octant where the old root fits
        Point3D newCenter = newBounds.center();
        int oldRootOctant = 0;
        Point3D oldCenter = root->bounds.center();
        if (oldCenter.x > newCenter.x) oldRootOctant |= 1;
        if (oldCenter.y > newCenter.y) oldRootOctant |= 2;
        if (oldCenter.z > newCenter.z) oldRootOctant |= 4;
        
        newRoot->children[oldRootOctant] = root;
        root = newRoot;
    }
    insertRecursive(root, p);
}

Problem 2: Range Query and Nearest Neighbor Search

These are the most common operations interviewers ask you to implement on an existing octree. Range queries return all points within a bounding box; nearest neighbor finds the closest point to a query point.

Range Query Solution

std::vector<Point3D> rangeQuery(const AABB& queryBox) {
    std::vector<Point3D> results;
    rangeQueryRecursive(root, queryBox, results);
    return results;
}

void rangeQueryRecursive(OctreeNode* node, const AABB& queryBox, 
                         std::vector<Point3D>& results) {
    if (!node->bounds.intersects(queryBox)) {
        return;  // Prune: no overlap
    }
    
    if (node->isLeaf) {
        // Check each point in the leaf
        for (const auto& p : node->points) {
            if (queryBox.contains(p)) {
                results.push_back(p);
            }
        }
    } else {
        for (int i = 0; i < 8; i++) {
            if (node->children[i] != nullptr) {
                rangeQueryRecursive(node->children[i], queryBox, results);
            }
        }
    }
}

Nearest Neighbor Search

This is trickier. You need a priority queue to traverse nodes in order of distance, and maintain a running best distance for pruning.

#include <queue>
#include <tuple>

struct NodeDistance {
    OctreeNode* node;
    double distance;  // minimum possible distance from query point to node bounds
    
    bool operator<(const NodeDistance& other) const {
        return distance > other.distance;  // min-heap: smaller distance first
    }
};

// Squared distance from a point to the nearest point on an AABB
double pointToAABBDistanceSq(const Point3D& p, const AABB& box) {
    double dx = std::max(0.0, std::max(box.min.x - p.x, p.x - box.max.x));
    double dy = std::max(0.0, std::max(box.min.y - p.y, p.y - box.max.y));
    double dz = std::max(0.0, std::max(box.min.z - p.z, p.z - box.max.z));
    return dx * dx + dy * dy + dz * dz;
}

double pointDistanceSq(const Point3D& a, const Point3D& b) {
    double dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
    return dx * dx + dy * dy + dz * dz;
}

Point3D nearestNeighbor(const Point3D& query) {
    double bestDistSq = std::numeric_limits<double>::max();
    Point3D bestPoint(0, 0, 0);
    bool found = false;
    
    std::priority_queue<NodeDistance> pq;
    pq.push({root, pointToAABBDistanceSq(query, root->bounds)});
    
    while (!pq.empty()) {
        NodeDistance nd = pq.top();
        pq.pop();
        
        // Prune: if the node's minimum distance exceeds best known, skip
        if (nd.distance >= bestDistSq) continue;
        
        if (nd.node->isLeaf) {
            for (const auto& p : nd.node->points) {
                double distSq = pointDistanceSq(query, p);
                if (distSq < bestDistSq) {
                    bestDistSq = distSq;
                    bestPoint = p;
                    found = true;
                }
            }
        } else {
            for (int i = 0; i < 8; i++) {
                if (nd.node->children[i] != nullptr) {
                    double childDist = pointToAABBDistanceSq(query, 
                        nd.node->children[i]->bounds);
                    // Only push if it could contain a closer point
                    if (childDist < bestDistSq) {
                        pq.push({nd.node->children[i], childDist});
                    }
                }
            }
        }
    }
    
    return bestPoint;
}

K-Nearest Neighbors Extension

Interviewers often extend this to return the k nearest points. The approach uses a max-heap of size k to track the k closest found so far:

#include <queue>  // for std::priority_queue

struct PointDistance {
    Point3D point;
    double distSq;
    bool operator<(const PointDistance& other) const {
        return distSq < other.distSq;  // max-heap for k-best
    }
};

std::vector<Point3D> kNearestNeighbors(const Point3D& query, int k) {
    std::priority_queue<PointDistance> kBest;  // max-heap
    std::priority_queue<NodeDistance> pq;
    pq.push({root, pointToAABBDistanceSq(query, root->bounds)});
    
    while (!pq.empty()) {
        NodeDistance nd = pq.top();
        pq.pop();
        
        // Prune using the k-th best distance (largest in the max-heap)
        if (kBest.size() == k && nd.distance >= kBest.top().distSq) continue;
        
        if (nd.node->isLeaf) {
            for (const auto& p : nd.node->points) {
                double distSq = pointDistanceSq(query, p);
                if (kBest.size() < k) {
                    kBest.push({p, distSq});
                } else if (distSq < kBest.top().distSq) {
                    kBest.pop();
                    kBest.push({p, distSq});
                }
            }
        } else {
            for (int i = 0; i < 8; i++) {
                if (nd.node->children[i] != nullptr) {
                    double childDist = pointToAABBDistanceSq(query,
                        nd.node->children[i]->bounds);
                    if (kBest.size() < k || childDist < kBest.top().distSq) {
                        pq.push({nd.node->children[i], childDist});
                    }
                }
            }
        }
    }
    
    std::vector<Point3D> result;
    while (!kBest.empty()) {
        result.push_back(kBest.top().point);
        kBest.pop();
    }
    std::reverse(result.begin(), result.end());
    return result;
}

Problem 3: Collision Detection Between Two Octrees

This appears in game engine interviews. Given two octrees (representing two complex 3D objects), determine if they intersect. The naive O(n*m) comparison is unacceptable; octree traversal reduces it dramatically.

bool collisionBetweenOctrees(OctreeNode* nodeA, OctreeNode* nodeB) {
    // Early exit: bounding volumes don't intersect
    if (!nodeA->bounds.intersects(nodeB->bounds)) {
        return false;
    }
    
    // If both are leaves, check point-level intersection
    if (nodeA->isLeaf && nodeB->isLeaf) {
        // For point clouds: check if any points are close enough
        // For solid objects represented as occupied voxels: 
        // if both leaves have content, they collide
        return !nodeA->points.empty() && !nodeB->points.empty();
    }
    
    // If A is leaf but B is internal, recurse B's children against A
    if (nodeA->isLeaf) {
        if (nodeA->points.empty()) return false;
        for (int i = 0; i < 8; i++) {
            if (nodeB->children[i] != nullptr) {
                if (collisionBetweenOctrees(nodeA, nodeB->children[i])) {
                    return true;
                }
            }
        }
        return false;
    }
    
    // If B is leaf but A is internal
    if (nodeB->isLeaf) {
        if (nodeB->points.empty()) return false;
        for (int i = 0; i < 8; i++) {
            if (nodeA->children[i] != nullptr) {
                if (collisionBetweenOctrees(nodeA->children[i], nodeB)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    // Both are internal: check all child pairs that intersect
    for (int i = 0; i < 8; i++) {
        if (nodeA->children[i] == nullptr) continue;
        for (int j = 0; j < 8; j++) {
            if (nodeB->children[j] == nullptr) continue;
            if (collisionBetweenOctrees(nodeA->children[i], nodeB->children[j])) {
                return true;
            }
        }
    }
    
    return false;
}

Problem 4: Octree Serialization and Deserialization

This tests your ability to flatten a tree structure for network transmission or disk storage. The key insight: use a pre-order traversal encoding a byte per node indicating which children are present.

#include <vector>
#include <cstdint>

// Serialize octree to a compact byte stream
std::vector<uint8_t> serialize(OctreeNode* node) {
    std::vector<uint8_t> data;
    serializeNode(node, data);
    return data;
}

void serializeNode(OctreeNode* node, std::vector<uint8_t>& data) {
    // Format: [child_mask (1 byte)] [if leaf: point_count (4 bytes)] [points data]
    uint8_t mask = 0;
    
    if (node->isLeaf) {
        mask = 0xFF;  // Special marker: all bits set means leaf
        data.push_back(mask);
        
        // Write point count as 4 bytes (big-endian for portability)
        uint32_t count = node->points.size();
        data.push_back((count >> 24) & 0xFF);
        data.push_back((count >> 16) & 0xFF);
        data.push_back((count >> 8) & 0xFF);
        data.push_back(count & 0xFF);
        
        // Write each point as 3 doubles (24 bytes each)
        for (const auto& p : node->points) {
            // Pack doubles into bytes (simplified - use memcpy in practice)
            union { double d; uint64_t u; } converter;
            converter.d = p.x;
            uint64_t bits = converter.u;
            for (int shift = 56; shift >= 0; shift -= 8)
                data.push_back((bits >> shift) & 0xFF);
            converter.d = p.y;
            bits = converter.u;
            for (int shift = 56; shift >= 0; shift -= 8)
                data.push_back((bits >> shift) & 0xFF);
            converter.d = p.z;
            bits = converter.u;
            for (int shift = 56; shift >= 0; shift -= 8)
                data.push_back((bits >> shift) & 0xFF);
        }
    } else {
        // Internal node: child_mask bits indicate which children exist
        for (int i = 0; i < 8; i++) {
            if (node->children[i] != nullptr) {
                mask |= (1 << i);
            }
        }
        data.push_back(mask);
        
        // Recurse on present children in order
        for (int i = 0; i < 8; i++) {
            if (node->children[i] != nullptr) {
                serializeNode(node->children[i], data);
            }
        }
    }
}

// Deserialize from byte stream
OctreeNode* deserialize(const std::vector<uint8_t>& data, 
                        const AABB& bounds, int maxCapacity, 
                        size_t& offset) {
    if (offset >= data.size()) return nullptr;
    
    OctreeNode* node = new OctreeNode(bounds, maxCapacity);
    uint8_t mask = data[offset++];
    
    if (mask == 0xFF) {
        // Leaf node: read points
        node->isLeaf = true;
        uint32_t count = (data[offset] << 24) | (data[offset+1] << 16) |
                         (data[offset+2] << 8) | data[offset+3];
        offset += 4;
        
        for (uint32_t i = 0; i < count; i++) {
            union { uint64_t u; double d; } converter;
            converter.u = 0;
            for (int shift = 56; shift >= 0; shift -= 8)
                converter.u |= ((uint64_t)data[offset++] << shift);
            double x = converter.d;
            converter.u = 0;
            for (int shift = 56; shift >= 0; shift -= 8)
                converter.u |= ((uint64_t)data[offset++] << shift);
            double y = converter.d;
            converter.u = 0;
            for (int shift = 56; shift >= 0; shift -= 8)
                converter.u |= ((uint64_t)data[offset++] << shift);
            double z = converter.d;
            node->points.push_back(Point3D(x, y, z));
        }
    } else {
        // Internal node
        node->isLeaf = false;
        for (int i = 0; i < 8; i++) {
            if (mask & (1 << i)) {
                node->children[i] = deserialize(data, node->childBounds(i), 
                                                maxCapacity, offset);
            } else {
                node->children[i] = nullptr;
            }
        }
    }
    
    return node;
}

Problem 5: Octree for Dynamic / Moving Objects

Static octrees work well for fixed point clouds, but what about moving objects? Interviewers may ask about "loose octrees" — a variant where node boundaries are enlarged (typically by a factor of 2) so objects can move within a node without immediate reinsertion. Here's the concept:

class LooseOctreeNode {
public:
    AABB tightBounds;   // standard octant bounds
    AABB looseBounds;   // enlarged bounds for containment check
    std::vector<Point3D> objects;  // stores moving objects with IDs
    std::array<LooseOctreeNode*, 8> children;
    bool isLeaf;
    int maxCapacity;
    double loosenessFactor;  // typically 2.0
    
    LooseOctreeNode(const AABB& _tightBounds, double _loosenessFactor, int _maxCapacity)
        : tightBounds(_tightBounds), isLeaf(true), maxCapacity(_maxCapacity),
          loosenessFactor(_loosenessFactor) {
        // Loose bounds extend tight bounds by factor in each direction
        Point3D extent(tightBounds.max.x - tightBounds.min.x,
                       tightBounds.max.y - tightBounds.min.y,
                       tightBounds.max.z - tightBounds.min.z);
        double looseExtentX = extent.x * (loosenessFactor - 1.0) / 2.0;
        double looseExtentY = extent.y * (loosenessFactor - 1.0) / 2.0;
        double looseExtentZ = extent.z * (loosenessFactor - 1.0) / 2.0;
        looseBounds = AABB(
            Point3D(tightBounds.min.x - looseExtentX, 
                    tightBounds.min.y - looseExtentY,
                    tightBounds.min.z - looseExtentZ),
            Point3D(tightBounds.max.x + looseExtentX,
                    tightBounds.max.y + looseExtentY,
                    tightBounds.max.z + looseExtentZ)
        );
        for (int i = 0; i < 8; i++) children[i] = nullptr;
    }
    
    // Object is reinserted only when it exits the loose bounds
    bool objectStillFits(const Point3D& p) const {
        return looseBounds.contains(p);
    }
};

The loose octree trades spatial precision for temporal stability: objects can drift within the loose boundary without triggering costly reinsertions. This is widely used in game physics engines like Bullet and PhysX.

Problem 6: Octree Intersection with a Ray (Raycasting)

Critical for rendering and picking in 3D applications. Given a ray (origin + direction), find the first occupied voxel or point hit.

struct Ray {
    Point3D origin;
    Point3D direction;  // normalized
    Ray(Point3D o, Point3D d) : origin(o), direction(d) {}
};

// Ray-AABB intersection using slab method
bool rayIntersectsAABB(const Ray& ray, const AABB& box, double& tMin, double& tMax) {
    double tmin = 0.0;
    double tmax = std::numeric_limits<double>::max();
    
    // For each axis, compute slab intersection
    for (int axis = 0; axis < 3; axis++) {
        double originCoord, directionCoord, boxMin, boxMax;
        if (axis == 0) {
            originCoord = ray.origin.x; directionCoord = ray.direction.x;
            boxMin = box.min.x; boxMax = box.max.x;
        } else if (axis == 1) {
            originCoord = ray.origin.y; directionCoord = ray.direction.y;
            boxMin = box.min.y; boxMax = box.max.y;
        } else {
            originCoord = ray.origin.z; directionCoord = ray.direction.z;
            boxMin = box.min.z; boxMax = box.max.z;
        }
        
        if (std::abs(directionCoord) < 1e-9) {
            // Ray parallel to this axis
            if (originCoord < boxMin || originCoord > boxMax) return false;
        } else {
            double invD = 1.0 / directionCoord;
            double t1 = (boxMin - originCoord) * invD;
            double t2 = (boxMax - originCoord) * invD;
            if (t1 > t2) std::swap(t1, t2);
            tmin = std::max(tmin, t1);
            tmax = std::min(tmax, t2);
            if (tmin > tmax) return false;
        }
    }
    
    tMin = tmin;
    tMax = tmax;
    return true;
}

bool raycastOctree(OctreeNode* node, const Ray& ray, Point3D& hitPoint, double& hitDist) {
    double tMin, tMax;
    if (!rayIntersectsAABB(ray, node->bounds, tMin, tMax)) {
        return false;
    }
    
    if (node->isLeaf) {
        // Check points in leaf for exact intersection
        bool found = false;
        double closestDist = std::numeric_limits<double>::max();
        for (const auto& p : node->points) {
            // Simplified: check if point is near ray within tolerance
            // Vector from ray origin to point
            Point3D toPoint(p.x - ray.origin.x, p.y - ray.origin.y, p.z - ray.origin.z);
            // Project onto ray direction
            double t = toPoint.x * ray.direction.x + 
                       toPoint.y * ray.direction.y + 
                       toPoint.z * ray.direction.z;
            if (t < 0) continue;  // behind ray
            // Closest point on ray to the point
            Point3D closestOnRay(ray.origin.x + t * ray.direction.x,
                                 ray.origin.y + t * ray.direction.y,
                                 ray.origin.z + t * ray.direction.z);
            double distSq = (closestOnRay.x - p.x) * (closestOnRay.x - p.x) +
                            (closestOnRay.y - p.y) * (closestOnRay.y - p.y) +
                            (closestOnRay.z - p.z) * (closestOnRay.z - p.z);
            if (distSq < 0.01 && t < closestDist) {  // 0.01 is tolerance
                closestDist = t;
                hitPoint = p;
                found = true;
            }
        }
        if (found) {
            hitDist = closestDist;
            return true;
        }
        return false;
    }
    
    // Internal node: check children, front to back for first hit
    // Determine ray direction octant ordering (which children are hit first)
    Point3D c = node->bounds.center();
    int frontOctant = 0;
    if (ray.direction.x < 0) frontOctant |= 1;  // bit 0 flipped
    if (ray.direction.y < 0) frontOctant |= 2;  // bit 1 flipped
    if (ray.direction.z < 0) frontOctant |= 4;  // bit 2 flipped
    
    // Traverse children in approximate front-to-back order
    for (int i = 0; i < 8; i++) {
        int octant = i ^ frontOctant;  // XOR to get front-to-back ordering
        if (node->children[octant] != nullptr) {
            if (raycastOctree(node->children[octant],

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles