trie: one node per rune, children in rune order, and the last path kept

The trie walked a key byte by byte, so a Hangul syllable cost three
nodes and every lookup first spelled its key back into UTF-8.  It walks
runes now: a third of the nodes for Korean data, and no buffer in the
lookup.  Children are kept in rune order, so both the search and the
insert stop early, and a trie remembers the path of the last key put,
which a file sorted by key — hanja.dict is one — walks straight down.
This commit is contained in:
2026-08-17 12:50:05 +09:00
parent 6a6749e824
commit 3527bcd489
3 changed files with 41 additions and 24 deletions

5
dat.h
View File

@@ -74,7 +74,7 @@ struct Tnode
int vlen;
int child;
int sibling;
char c;
Rune c;
};
typedef struct Trie Trie;
@@ -83,6 +83,9 @@ struct Trie
Tnode *nodes;
int n;
int cap;
/* the last key put and its path, so a sorted file walks it once */
Str last;
int path[Maxrunes];
};
enum

2
dict.c
View File

@@ -24,7 +24,7 @@ words(Tnode *nd, Str *out, int n, int max)
return n;
}
/* The entries at and below a node, in the dictionary's order. */
/* The entries at and below a node, the shortest keys first. */
static int
below(Trie *t, int ni, Str *out, int n, int max)
{

62
trie.c
View File

@@ -3,9 +3,9 @@
#include "fn.h"
/*
* A byte trie over UTF-8 keys. Maps need prefix matches while composing;
* dictionaries need exact matches; both are small enough for one structure.
* Node 0 is the root.
* A trie over the runes of a key. Maps need prefix matches while
* composing; dictionaries need exact matches; both are small enough for
* one structure. Node 0 is the root.
*/
static int
@@ -24,32 +24,37 @@ newnode(Trie *t)
return i;
}
/* The children of a node are kept in rune order, so both end early. */
static int
find(Trie *t, int ni, char c)
find(Trie *t, int ni, Rune c)
{
int pi;
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling)
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling){
if(t->nodes[pi].c == c)
return pi;
if(t->nodes[pi].c > c)
break;
}
return -1;
}
/* Appends, so that a walk of the children keeps the file's order. */
static int
add(Trie *t, int ni, char c)
add(Trie *t, int ni, Rune c)
{
int last, pi;
int next, pi, prev;
last = -1;
for(pi = t->nodes[ni].child; pi >= 0; pi = t->nodes[pi].sibling)
last = pi;
prev = -1;
for(next = t->nodes[ni].child; next >= 0 && t->nodes[next].c < c;
next = t->nodes[next].sibling)
prev = next;
pi = newnode(t);
t->nodes[pi].c = c;
if(last < 0)
t->nodes[pi].sibling = next;
if(prev < 0)
t->nodes[ni].child = pi;
else
t->nodes[last].sibling = pi;
t->nodes[prev].sibling = pi;
return pi;
}
@@ -69,15 +74,26 @@ trienew(void)
void
trieput(Trie *t, char *key, int klen, char *val, int vlen)
{
int ni, ci, i;
Str k;
int ci, i, ni, same;
if(!sinit(&k, key, klen))
return;
ni = 0;
for(i = 0; i < klen; i++){
ci = find(t, ni, key[i]);
if(ci < 0)
ci = add(t, ni, key[i]);
ni = ci;
same = 1;
for(i = 0; i < k.n; i++){
if(same && i < t->last.n && t->last.r[i] == k.r[i]){
ni = t->path[i];
continue;
}
same = 0;
ci = find(t, ni, k.r[i]);
if(ci < 0)
ci = add(t, ni, k.r[i]);
ni = ci;
t->path[i] = ni;
}
t->last = k;
free(t->nodes[ni].val);
t->nodes[ni].val = emalloc(vlen + 1);
memmove(t->nodes[ni].val, val, vlen);
@@ -163,15 +179,13 @@ trieclose(Trie *t)
int
trienode(Trie *t, Str *key)
{
char buf[Maxutf];
int i, klen, ni;
int i, ni;
if(t == nil)
return -1;
klen = stoutf(key, buf, sizeof buf);
ni = 0;
for(i = 0; i < klen && ni >= 0; i++)
ni = find(t, ni, buf[i]);
for(i = 0; i < key->n && ni >= 0; i++)
ni = find(t, ni, key->r[i]);
return ni;
}