The smaller tree hands it back unsorted
The listing apparatus is one array in one tree, and the tree’s shape is the last thing left to choose.
A balanced tree gives every document a code of ⌈log₂ d⌉ bits and puts its leaves in the alphabet’s order. A frequency-shaped tree gives short codes to documents that contributed many characters and puts its leaves in frequency order.
The first costs exactly what the plain array costs. The second costs the array’s entropy, which on a collection whose lengths fall as one over rank is sixteen per cent less.
And the second hands its answer back in the wrong order.
The two numbers
On the one-over-rank collection the balanced tree is 49,593 bits and the Huffman-shaped one is 41,589 — 83.9% of it. Both report the same four documents for the same query at almost the same operation count: twenty-six against twenty-four.
The balanced tree’s output is sorted. The Huffman tree’s is not.
Sorting the answer costs d log d comparisons on an answer of size d. On an answer of four that is eight comparisons; on an answer of four hundred it is about three and a half thousand. The descent that produced the answer cost twenty-four operations for the first and would cost several hundred for the second.
So for a full answer the trade is not close. Eight thousand bits against eight comparisons, or against three and a half thousand on a large answer — and even there the sort is comparisons on values already in registers while the saving is bits held for the life of the structure.
Take the smaller tree.
There is a hidden term in that comparison worth surfacing, because it is the reason the answer is not simply “always sort”. The sort is d log d comparisons of small integers, which on any real machine is a few nanoseconds for an answer of a hundred. The sixteen per cent is bits held for the life of the structure, paid on every page fault and every cache miss the structure causes over its lifetime.
Those are not the same kind of cost and comparing them by counting is the error the unit of cost is not one exists to prevent. What makes the conclusion safe anyway is the direction: the smaller structure is smaller and the sort is cheap, so both currencies point the same way and the incommensurability does not have to be resolved.
It would have to be resolved if they pointed differently — a representation ten per cent smaller that made every query twice as expensive, say — and this collection has no principled way to do that. It reports both and names the trade.
Why the order differs at all
A wavelet tree’s leaves are its code words read as paths. The descent reports symbols in the order it reaches their leaves, which is left to right, which is the order the code words sort as strings.
A code is order-preserving when reading its code words in that order gives the symbols in their own order. A balanced code assigns consecutive code words to consecutive symbols, so it is. A Huffman code assigns short words to frequent symbols wherever they fall in the alphabet, so it is not: a frequent document with a low number gets a short code and sorts early, and a rare document with a lower number gets a long one and sorts late.
That property has a name in this collection because a different operation needed it. The tree the operation insists on is where the compound walk turned out to require an ordered tree — “everything that went left is smaller” is false in a frequency-shaped tree — and where a plausible number came out wrong on 81% of queries.
Here the requirement is weaker. The listing descent needs no order at all to be correct; it needs one only if the caller wants sorted output.
Two things follow from the order being set by the frequencies. The order is a property of the collection rather than of the query, so it is stable across queries and a caller could in principle learn it — which is worse rather than better, because a caller that has learned it will break when a document is added.
And the order changes when the collection does. Adding one large document to a collection re-shapes the code, which permutes the leaf order, which changes the order every query’s answer comes back in. Nothing about the answers changes and everything about their presentation does.
Where the preference becomes a requirement
The trade above is settled for a full answer and it inverts completely for a truncated one.
A search interface showing ten results does not want all four hundred documents holding a pattern. The descent visits leaves in a definite order, so it can stop after ten — at a cost of about 2 × 10 × (1 + log₂(d/10)) operations rather than the full answer’s, which on ten thousand documents is a factor of twenty.
Stopping after ten on a Huffman tree reports the ten documents whose codes come first. That is not the ten smallest document numbers, not the ten with the most occurrences, and not any ordering a reader would recognise. It is decided by the collection’s length distribution, because the code shape is.
So a truncated listing on a frequency-shaped tree returns an arbitrary subset presented as an answer. Every count agrees, every document reported is genuinely there, and the ten shown are chosen by file sizes.
Which is the silent failure
That is worth separating from the ordinary case, because the two failures have different visibility.
On a full answer, reading the descent’s output as sorted is caught immediately by anything that checks: the first two documents come back out of order and a caller merging two lists gets a wrong merge on the first query.
On a truncated answer nothing is caught. Ten documents come back, all of them correct, and the only wrong thing is which ten. There is no assertion that fires, no count that disagrees, and no ordering to inspect — the caller asked for ten and got ten.
This collection has a habit for exactly this shape: an assertion that has never rejected anything proves nothing, so a check that cannot see a defect is a check that has to be replaced rather than trusted. The check here is on the set for the full answer, and on a truncated answer the set is not the thing that went wrong.
There is a second truncation shape with the same problem and it is more common than the first: reporting a count and a sample. A system saying “found in 412 documents, including these ten” is truncating, whether or not it calls it that, and the ten are chosen by the same arbitrary criterion.
The count is right. The sample is not a sample of anything a reader would recognise — it is the ten documents with the shortest codes, which is to say the ten largest documents that happen to hold the pattern, on a collection whose lengths vary. That is a biased sample with a bias nobody chose, and it is the kind of defect that looks like a feature: the large documents are often the interesting ones, so the output looks better than it should.
What a system should actually do
Three options, and the first two are the ones worth considering.
A balanced tree, and pay the sixteen per cent. Truncation is then safe, because the leaf order is the document order, and the ten reported are the ten lowest-numbered documents holding the pattern — which is at least an ordering somebody chose.
A frequency-shaped tree, and never truncate. Report the whole answer, sort it, and let the caller truncate. The sort costs d log d on an answer the descent already spent more than that on.
An alphabetic code — the minimum-cost prefix code among those that preserve order. That gets most of the size saving and the order, and it is a real construction: an interval dynamic program over the frequencies rather than Huffman’s merge.
The third is the right answer and this collection has already measured its cost in another strand: a factor of fourteen, for four per cent found an optimal alphabetic code costing 1.43% over Huffman on generated English and 3.5–5.0% on real text, while keeping the depth. Applied here, that is most of the sixteen per cent for a few per cent back, with sorted output.
It is named and not built in this apparatus, which is the honest state.
There is a fourth option that avoids the question by moving it, and it is worth naming because a real system might reach for it. Keep the frequency-shaped tree and store a permutation mapping code order to document order — d⌈log₂ d⌉ bits, which at a thousand documents is ten thousand bits against a saving of sixteen per cent of a much larger array. Truncation then reports the ten documents whose permuted positions come first, which is document order.
That works and it is a strange thing to build, because a permutation of d elements is exactly what a balanced code is. Storing the saving and then storing the thing the saving cost is a pattern worth being suspicious of, and here it is nearly a wash: the permutation is a fixed d log d while the saving is a fraction of n log d, so it pays when n is much larger than d and not otherwise.
The collection decides how big the choice is
The sixteen per cent is one collection’s number and the spread is large.
On equal-length documents the two shapes cost exactly the same, because the entropy is log₂ d and Huffman’s code is the balanced one. There is no choice to make.
On a collection whose lengths fall as one over rank, sixteen per cent.
On a collection where one document holds four fifths of the text, sixty per cent — the balanced tree spends five bits a symbol and the Huffman one spends two.
So the decision’s stakes are the length distribution’s entropy deficit, which is the array is the length distribution again, and on the collection where the stakes are highest the order problem is also worst — because a dominated collection’s code order is furthest from its document order.
What the operation count says
The two shapes differ in size and barely in work: twenty-four operations against twenty-six for the same query.
That is not obvious in advance and the direction is worth explaining. A Huffman tree is shallower for frequent documents and deeper for rare ones. The documents holding a pattern are roughly a uniform sample of the documents — a pattern is about as likely to be in a short document as a long one — so the descent’s leaves are uniform while the depths are frequency-weighted, which should make the Huffman descent deeper on average.
It comes out slightly cheaper here because the dominant document is in the answer and its one-bit code saves more than the rare documents’ long codes cost. On a query whose documents are all rare the ordering reverses.
So the operation count is a wash with a small, unsigned dependence on which documents are in the answer, and the choice is decided by the size and the order alone.
On the dominated collection the two shapes are furthest apart and the order is worst, which is the unhelpful combination. A code word is at least one bit is where that collection’s floor was measured, and it is the same collection: the one where the frequency-shaped tree is most worth having is the one where its leaf order is least like the document order, because one document has a one-bit code and everything else has five or six.
The general form
The pattern here is one this collection keeps meeting and it is worth stating once in general terms.
A representation chosen for its size may lose a property the caller was relying on, and the property is usually an order. A Huffman code loses the alphabet’s order. A hash table loses the key order. A frequency-sorted posting list loses the document order. In every case the structure is correct and something built on top of it silently is not.
What makes it hard to catch is that the property is rarely written down. Nobody specifies that a document listing returns its documents in order, because the obvious implementations do, and a caller written against those implementations depends on it without saying so.
The instruction is to write the property down when the representation changes, and to make the check test it. Here that means a check that the balanced tree’s output is sorted and the Huffman tree’s is not — a two-ended check, because one that only required the first would pass on a tree that happened to be ordered by accident, and one that only required the second would be testing nothing.
The same defect, three strands apart
It is worth putting the three instances side by side, because they are the same mistake made about three different objects and the third one is the one that would not have been caught.
In the tree the operation insists on, a bidirectional extension read a Huffman tree’s leaf order as the alphabet’s to accumulate “how many characters sort before this one”. The number came back plausible and wrong on 81% of queries, worst 706 against 3,058. Caught because a plate drew three tree shapes side by side.
In one set, three orders, the interval enumeration returns the same set on every shape and a different order, and a search reading the output as sorted computes the same wrong smaller-count. Caught by requiring the two shapes to disagree.
Here, the listing’s full answer is a set and the order is a presentation detail — until it is truncated, at which point the order chooses the answer. Not caught by anything, because the failure is in what a caller does with a correct result.
The three form a progression in how hard the defect is to see. In the first the structure computes a wrong number. In the second the structure computes a right number that a caller misreads. In the third the structure and the caller are both right and the composition is wrong.
Nothing in this collection’s checking habit reaches the third. An assertion lives inside a generator or a library and tests a claim about an object; a defect that only exists when two correct objects are composed is outside every one of them. The instruction that comes out is not a new kind of check but a writing rule: when a representation is chosen for its size, write down which properties it gave up, in the place a caller will read.
What the apparatus looks like with the choice made
Putting the pieces together, the apparatus a reader should build depends on one question: will the answer ever be truncated?
If no — a system that always consumes the whole document list, an offline analysis, a count — take the frequency-shaped tree, sort when a sorted answer is wanted, and the apparatus is the apparatus, three times smaller again’s 119,972 bits with sixteen per cent off on a collection whose lengths vary.
If yes, take the balanced tree and pay the sixteen per cent, or build the alphabetic code and pay a few per cent. The second is better and is more work, and the difference between them is a dynamic program over the document frequencies that runs once at construction time.
Either way the apparatus is one array in one tree and one descent, which is what three passes of removal left. The shape is the only parameter, it has two settings and a third that is better than both, and the setting is decided by a property of the interface rather than of the collection.
That last point is the unusual one. Every other decision in this strand — which structures to keep, how far the array compresses, whether the method beats reading every row — was decided by the collection. This one is decided by what the caller does with the answer, and no measurement of the corpus can settle it.
The state of the choice
The apparatus ships with the frequency-shaped tree and a sort, which is the second option above.
That is the right choice for a full answer and it forecloses truncation, which is the operation a real search system wants most. The alphabetic code would give both, at a few per cent of the saving, and it is the one remaining piece of this apparatus that is named and unbuilt.
Named alongside this one
Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.
- The last array in the apparatus document array · document listing · index size · wavelet tree
- A document already in the answer document array · document listing · index size
- A list of documents is not a list of occurrences document array · document listing · index size
- One separator, or one for each document array · index size · wavelet tree
- The apparatus that is smaller than its index document array · document listing · index size
- The array the walk never reads document array · document listing · index size
What links here
Every essay whose body links to this one.
The objects this essay names
Each one links to every other essay that touches it.
CorrectnessDescentDocument arrayDocument listingHuffman codeIndex sizeOrdered codeWavelet tree