Skip to content
K-d Tree Explorer, home

The rebuild · rebuilt in 2026, original lost

Rebuilding a lost program from its test kit.

The C source for this assignment no longer exists. What survived is the data, a saved Stage 1 output and my own experiment numbers. That turned out to be enough to pin down almost every design decision, and to show honestly where it was not.

The decisions are written up as records: DR-001 (what is certain and what is inferred), DR-002 (ties and the output format) and DR-003 (the balanced build). The methods page has the rest.

What happened to the code

  1. Sept–Oct 2020

    Assignment written in C and tested against the course's kit: the CLUE data in five orders and 3k–15k subsets. A Stage 1 output for five queries was saved in my testing folder as result/test1.txt (20 Sept).

  2. Nov 2020

    Code pushed to a private GitHub repository and synced to Google Drive. My experiment counts went into result1.csv and result2.csv for a plot.

  3. Later

    The repository was deleted. The Drive copy kept the git index but lost the two objects holding the assignment and lecture zips. No backup surfaced.

  4. Oct 2026

    Rebuilt in C99 from the evidence, ported to TypeScript, and checked against each other on every file of the test kit.

The evidence: one saved output

result/test1.txt, saved in my 2020 testing folder, holds 150 records (154 lines with the blank separators) for 5 queries, 57,807 bytes. Its first two lines:

144.9740079 -37.81499345 --> Census year: 2018 || Block ID: 18 || Property ID: 108975 || Base property ID: 108975 || CLUE small area: Melbourne (CBD) || Trading name: Besen Pty Ltd || Industry (ANZSIC4) code: 6419 || Industry (ANZSIC4) description: Other Auxiliary Finance and Investment Services || x coordinate: 144.97401 || y coordinate: -37.81499 || Location: (-37.81499345, 144.9740079) || 
144.9740079 -37.81499345 --> Census year: 2018 || Block ID: 18 || Property ID: 108975 || Base property ID: 108975 || CLUE small area: Melbourne (CBD) || Trading name: Department of Economic Development, Jobs, Transport and Resources || Industry (ANZSIC4) code: 7520 || Industry (ANZSIC4) description: State Government Administration || x coordinate: 144.97401 || y coordinate: -37.81499 || Location: (-37.81499345, 144.9740079) || 

The query is echoed as typed, every field is printed as Name: value || , and all seven businesses at that address follow in the order they appear in CLUEdata2018_original.csv. The rebuild's make test regenerates this file and compares it byte for byte.

Each design choice, and what it rests on

Key
x coordinate, y coordinate columns; x at even depths, y at odd
EvidenceThe median-first file builds a perfectly balanced tree (13 levels) only with x first.
Ties on the split axis
Strictly smaller goes left, equal or greater right
EvidenceStandard course convention; keeps the median file balanced.
Same location
One node, records in file order
Evidenceresult/test1.txt lists 7, 43, 1, 88, 11 businesses per answer in exactly the CSV's order.
Output
<query as typed> --> Field: value || … || , blank line between queries
EvidenceByte layout of result/test1.txt: LF endings, trailing space, quotes removed.
Distance
Euclidean in raw degrees, compared squared
EvidenceCoordinate units throughout; no projection in the spec-era code.
Comparisons
One per node whose key is compared with the query
EvidenceTextbook definition; the 2020 rule could not be recovered (below).
Stage 2
Inclusive radius search, NOTFOUND when empty
EvidenceInferred from result2.csv, which records a second, much wider search.

The heart of it, in C99

kdtree.c · kd_insert
void kd_insert(kd_tree_t *tree, record_t *rec) {
    double key[2] = {rec->x, rec->y};
    tree->records++;

    if (tree->root == NULL) {
        tree->root = node_new(rec);
        tree->nodes++;
        return;
    }

    kd_node_t *node = tree->root;
    int depth = 0;
    for (;;) {
        if (key[0] == node->key[0] && key[1] == node->key[1]) {
            /* Same location: append, keeping file order. */
            rec->next = NULL;
            node->tail->next = rec;
            node->tail = rec;
            node->count++;
            return;
        }
        int axis = depth % 2;
        int side = key[axis] < node->key[axis] ? 0 : 1;
        if (node->child[side] == NULL) {
            node->child[side] = node_new(rec);
            tree->nodes++;
            return;
        }
        node = node->child[side];
        depth++;
    }
}
kdtree.c · nearest_rec
static void nearest_rec(const kd_node_t *node, int depth, nn_state_t *st) {
    if (node == NULL) {
        return;
    }
    st->comparisons++;
    double d2 = dist2(st->q, node->key);
    if (d2 < st->best_d2) {
        st->best_d2 = d2;
        st->best = node;
    }

    int axis = depth % 2;
    double diff = st->q[axis] - node->key[axis];
    int near = diff < 0 ? 0 : 1;

    nearest_rec(node->child[near], depth + 1, st);
    /* The far side can only hold a closer point if the splitting line is
     * closer than the best distance so far. */
    if (diff * diff < st->best_d2) {
        nearest_rec(node->child[1 - near], depth + 1, st);
    }
}

Full source, Makefile and tests in coursework/rebuild-2026 in the repository. The TypeScript port in web/src/lib/kdtree.ts follows it line by line.

Parity: C and TypeScript agree exactly

The C programs ran over all ten files of the test kit with four query sets (40 runs). The TypeScript port must print the same comparison count for every query and an output file with the same SHA-256. The tree shapes below are the C kdstats output; the browser rebuilds the same trees.

Tree shape per test kit file, from the C rebuild
FileRecordsLocationsLevelsMean depthComparisons, 5 sample queries
testfile/CLUEdata2018_original.csv19,1174,1815217.4914 · 13 · 22 · 17 · 16
testfile/CLUEdata2018_random.csv19,1174,1812513.1611 · 12 · 10 · 9 · 16
testfile/CLUEdata2018_random_sorted.csv19,1174,1812714.0913 · 13 · 14 · 16 · 19
testfile/CLUEdata2018_sortx.csv19,1174,1816636.2341 · 35 · 25 · 33 · 27
testfile/CLUEdata2018_median.csv19,1174,1811310.0412 · 12 · 12 · 12 · 12
test1.csv3,0001,2962311.7910 · 14 · 19 · 14 · 19
test2.csv6,0002,0612612.2118 · 19 · 12 · 16 · 14
test3.csv9,0002,6352613.8213 · 21 · 16 · 18 · 13
test4.csv12,0003,1502413.1415 · 16 · 19 · 21 · 15
test5.csv15,0003,6032914.8915 · 21 · 13 · 28 · 20

Compiler for this fixture: Apple clang version 21.0.0 (clang-2100.3.34.2).

What could not be recovered

The 2020 comparison counts

For the balanced median-first tree, my 2020 program reported 34 to 82 comparisons for query points that sit exactly on a business. Any search that stops once it has an exact match needs about 12, the depth of the tree. I tried counting per visited node, per record, including empty children, every combination of strict and non-strict tests, left-first and far-first orders, both axis orders, single-precision keys, one node per record and two-phase searches. None matched more than a couple of the 65 numbers.

So the rebuild uses the textbook definition, and the experiment page shows the 2020 numbers next to the rebuilt ones rather than pretending they agree. The qualitative finding is the same in both: sorted-by-x is the most expensive order.