Back to Library

Triesort

String
Orders text by character prefixes or buckets.
Tree
Constructs a tree data structure to organize elements, then traverses it to produce a sorted sequence.
Out-of-Place
Requires auxiliary memory proportional to the input size (O(n)).
Stable
Preserves the relative order of elements with equal values.

Inserts strings into a character trie and emits them in lexicographic order.

Visualize Fullscreen

Enter data, then run the algorithm.

Did you know?

  • Words with a shared prefix reuse the same path through the trie rather than repeating those prefix nodes.
  • A word that is a prefix of another comes first because the traversal emits terminal words before visiting child edges.
  • Duplicate strings are kept together at one terminal trie node in their arrival order. Traversal follows Unicode code-point order, which can differ from a language-aware dictionary collation.

How it Works

  • Insert each string character by character into a trie.

  • Mark complete strings at terminal nodes.

  • Visit terminal strings before child edges in code-point order.

Complexity Analysis

Best Case
O(L+KlogK)O(L + K log K)
Average
O(L+KlogK)O(L + K log K)
Worst Case
O(L+KlogK)O(L + K log K)
Space
O(L)O(L)

Advantages

  • Shared prefixes occupy shared trie paths.
  • Output preserves equal-string order.

Disadvantages

  • Trie nodes and edges use extra memory.
  • Best suited to strings rather than numeric keys.

Implementation

TypeScript string-variants.ts
export interface StringSortEvent {
	text: string;
	highlight?: number;
	writes?: Record<number, string>;
	suffixOrder?: number[];
}

export class StringTrace {
	readonly events: StringSortEvent[] = [];

	constructor(readonly values: string[]) {}

	note(text: string, highlight?: number) {
		this.events.push({ text, highlight });
	}

	write(index: number, value: string) {
		this.values[index] = value;
		this.events.push({
			text: `Write “${value}” at index ${index}`,
			highlight: index,
			writes: { [index]: value }
		});
	}
}

type TrieNode = { terminal: string[]; children: Map<string, TrieNode> };

function compareChars(left: string, right: string) {
	return (left.codePointAt(0) as number) - (right.codePointAt(0) as number);
}

function compareStrings(left: string, right: string) {
	const a = Array.from(left);
	const b = Array.from(right);
	for (let index = 0; index < Math.min(a.length, b.length); index++) {
		const difference = compareChars(a[index], b[index]);
		if (difference !== 0) return difference;
	}
	return a.length - b.length;
}

export function trieSort(trace: StringTrace) {
	const root: TrieNode = { terminal: [], children: new Map() };
	for (let index = 0; index < trace.values.length; index++) {
		const value = trace.values[index];
		let node = root;
		for (const character of value) {
			let child = node.children.get(character);
			if (!child) {
				child = { terminal: [], children: new Map() };
				node.children.set(character, child);
			}
			node = child;
			trace.note(`Follow trie edge “${character}” for “${value}”`, index);
		}
		node.terminal.push(value);
		trace.note(`Mark “${value}” as a complete trie key`, index);
	}
	let output = 0;
	function visit(node: TrieNode) {
		for (const value of node.terminal) trace.write(output++, value);
		for (const character of [...node.children.keys()].sort(compareChars)) {
			visit(node.children.get(character) as TrieNode);
		}
	}
	visit(root);
}

type BurstNode = {
	depth: number;
	bucket: string[] | null;
	terminal: string[];
	children: Map<string, BurstNode>;
};

/** Burst threshold remains small so the split is visible with short inputs. */
export function burstSort(trace: StringTrace, threshold = 4) {
	const root: BurstNode = { depth: 0, bucket: [], terminal: [], children: new Map() };
	function child(node: BurstNode, character: string) {
		let result = node.children.get(character);
		if (!result) {
			result = { depth: node.depth + 1, bucket: [], terminal: [], children: new Map() };
			node.children.set(character, result);
		}
		return result;
	}
	function insert(node: BurstNode, value: string) {
		if (node.bucket !== null) {
			node.bucket.push(value);
			trace.note(`Place “${value}” in depth-${node.depth} burst bucket`);
			if (node.bucket.length > threshold) {
				const pending = node.bucket;
				node.bucket = null;
				trace.note(`Burst bucket at character depth ${node.depth}`);
				for (const entry of pending) insert(node, entry);
			}
			return;
		}
		const characters = Array.from(value);
		if (node.depth >= characters.length) {
			node.terminal.push(value);
			return;
		}
		insert(child(node, characters[node.depth]), value);
	}
	for (const value of [...trace.values]) insert(root, value);
	let output = 0;
	function visit(node: BurstNode) {
		for (const value of node.terminal) trace.write(output++, value);
		if (node.bucket) {
			// Insertion sort keeps equal strings in their arrival order.
			for (let index = 1; index < node.bucket.length; index++) {
				const value = node.bucket[index];
				let position = index;
				while (position > 0 && compareStrings(node.bucket[position - 1], value) > 0) {
					trace.note(`Compare “${node.bucket[position - 1]}” and “${value}” inside bucket`);
					node.bucket[position] = node.bucket[position - 1];
					position--;
				}
				node.bucket[position] = value;
			}
			for (const value of node.bucket) trace.write(output++, value);
		}
		for (const character of [...node.children.keys()].sort(compareChars)) {
			visit(node.children.get(character) as BurstNode);
		}
	}
	visit(root);
}