Finger tree
A persistent sequence with amortized constant-time access at both ends, and concatenation and splitting in logarithmic time. The ends are kept in buffers of one to four elements, and the middle is a finger tree of 2-3 nodes, one level deeper at each step down the spine.
The type, and adding at either end.
type 'a node = Node2 of 'a * 'a | Node3 of 'a * 'a * 'atype 'a digit = One of 'a | Two of 'a * 'a | Three of 'a * 'a * 'a | Four of 'a * 'a * 'a * 'a(* The middle of a Deep holds nodes of 'a: a nested type, one level deeperper spine step, so every function over it recurses polymorphically. *)type 'a t = Empty | Single of 'a | Deep of 'a digit * 'a node t * 'a digitlet rec cons : 'a. 'a -> 'a t -> 'a t =fun a -> function| Empty -> Single a| Single b -> Deep (One a, Empty, One b)| Deep (One b, m, sf) -> Deep (Two (a, b), m, sf)| Deep (Two (b, c), m, sf) -> Deep (Three (a, b, c), m, sf)| Deep (Three (b, c, d), m, sf) -> Deep (Four (a, b, c, d), m, sf)| Deep (Four (b, c, d, e), m, sf) -> Deep (Two (a, b), cons (Node3 (c, d, e)) m, sf)let rec snoc : 'a. 'a t -> 'a -> 'a t =fun t a ->match t with| Empty -> Single a| Single b -> Deep (One b, Empty, One a)| Deep (pr, m, One b) -> Deep (pr, m, Two (b, a))| Deep (pr, m, Two (b, c)) -> Deep (pr, m, Three (b, c, a))| Deep (pr, m, Three (b, c, d)) -> Deep (pr, m, Four (b, c, d, a))| Deep (pr, m, Four (b, c, d, e)) -> Deep (pr, snoc m (Node3 (b, c, d)), Two (e, a))let digit_list = function| One a -> [ a ] | Two (a, b) -> [ a; b ]| Three (a, b, c) -> [ a; b; c ] | Four (a, b, c, d) -> [ a; b; c; d ]let node_list = function Node2 (a, b) -> [ a; b ] | Node3 (a, b, c) -> [ a; b; c ]let rec to_list : 'a. 'a t -> 'a list = function| Empty -> []| Single a -> [ a ]| Deep (pr, m, sf) ->digit_list pr @ List.concat_map node_list (to_list m) @ digit_list sflet rec depth : 'a. 'a t -> int = function| Empty | Single _ -> 0| Deep (_, m, _) -> 1 + depth m
The middle of a Deep is 'a node t: a finger tree whose elements are nodes of the level above. At depth an element stands for between and elements of the sequence, so the spine is logarithmic in the length. Every function over the type calls itself at a different type, which OCaml accepts with an explicit polymorphic annotation, 'a. ....
Building a sequence 1 to 10 from both ends, and the spine depth for sequences built by snoc.
to_list -> [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]n = 10 spine depth 2n = 100 spine depth 4n = 1000 spine depth 6n = 10000 spine depth 8n = 100000 spine depth 10n = 1000000 spine depth 12
The depth grows by about 2 for each factor of 10, close to . cons does constant work unless the front digit is full, in which case it pushes a node of three one level down and leaves a digit of two. Two more conses are then needed before that level can overflow again, and charging each overflow to them gives amortized per operation, as with a counter in a redundant number system.
Concatenation joins the inner digits of the two trees into nodes and recurses down both spines, in . Splitting uses a measure: every node carries the monoidal sum of its elements' measures, and a search down the spine finds where a monotone predicate on the running sum changes, in . With size as the measure this is indexing; with the maximum priority it is a priority queue; with the largest key it is an ordered sequence. Haskell's Data.Sequence is a finger tree measured by size.
see also
- ZipperA zipper represents a data structure together with a focus, a position inside it: the substructure at the focus, and the path from the focus back to the root together with everything to either side of that path. Moving the focus one step and editing at the focus take constant time, and the structure is persistent, so edits share everything off the path. The type of paths is the derivative of the structure's type, in the sense of calculus.
- Red-black treeA red-black tree is a binary search tree whose nodes are coloured red or black so that no red node has a red child and every path from the root to a leaf passes through the same number of black nodes. The two rules keep the height at most 2 log2(n + 1), so search, insertion and deletion take O(log n) time. Red-black trees are an encoding of 2-3-4 trees as binary trees, and Okasaki's functional version reduces insertion to one rebalancing rule with four cases.
- MonoidA monoid is a set with an associative binary operation and an identity element for it: integers under addition with 0, strings under concatenation with the empty string, functions under composition with the identity. Associativity means a sequence can be combined with any bracketing, so a fold over a monoid can be split into parts and computed in parallel or incrementally. Lists are the free monoid, and every monoid-valued function on elements extends uniquely to lists.
- RopeA rope is a binary tree whose leaves are strings and whose internal nodes represent the concatenation of their children, with each node caching the length of its left subtree. Concatenation creates one node instead of copying, and indexing, splitting, insertion and deletion in the middle take time logarithmic in the length for a balanced rope. Ropes are immutable and share structure between versions, which makes them suited to text editors with undo and to programs that build long strings piece by piece.
- Braun treeA Braun tree is a binary tree in which, at every node, the left subtree has either the same number of nodes as the right subtree or exactly one more. The shape is therefore determined by the size alone, and the height is ⌊log₂ n⌋ + 1. Used as a flexible array, element 0 is at the root and the bits of an index choose the path, so indexing, updating, and adding or removing an element at the front all take logarithmic time without any rebalancing.
- Functional queueA functional queue is a first-in first-out queue built from immutable lists. The standard design keeps a front list to take elements from and a reversed rear list to add them to, and reverses the rear into the front when the front runs out; each operation then takes amortized constant time. When old versions of the queue are reused, the amortized bound fails, and Okasaki's banker's queue restores it by making the reversal lazy and memoized.
- Hash array mapped trieA hash array mapped trie (HAMT) is a trie over the bits of the keys' hash values: each level consumes a few bits, usually five, to choose one of 32 children, and each node stores only its occupied children in a compact array, located by counting bits in a 32-bit bitmap. Lookups follow about log32 n levels. Updated by copying the path to the changed leaf, a HAMT is a persistent hash map with small, cache-friendly nodes, and it is the standard implementation of immutable maps and sets in Clojure, Scala and Haskell's unordered-containers.
- Leftist heapA leftist heap is a priority queue represented as a heap-ordered binary tree in which, at every node, the right spine of the left child is at least as long as the right spine of the right child. The right spine of the whole tree then has at most log₂(n + 1) nodes, and two heaps can be merged in O(log n) time by merging their right spines like sorted lists. Insertion and deletion of the minimum are special cases of merging. Because merging copies only the right spines, leftist heaps are a standard persistent priority queue in functional languages.
- Skew binomial heapA skew binomial heap is a priority queue made of a list of heap-ordered trees whose sizes follow the skew binary number system, in which adding one never carries more than one digit. Insertion therefore takes constant time in the worst case, instead of the logarithmic worst case of an ordinary binomial heap, while merging, finding the minimum and deleting it take logarithmic time. Brodal and Okasaki used it as the base of a purely functional priority queue with optimal bounds.
- TrieA trie, or prefix tree, is a tree for a set of strings in which each edge is labelled with a character and each key is the path from the root to a node; keys with a common prefix share the nodes of that prefix. Lookup and insertion take time proportional to the length of the key, independent of the number of keys, and all keys with a given prefix are found in one subtree, in sorted order. Radix and Patricia trees compress chains of single-child nodes, and the same idea applies to integers, read bit by bit.
further reading
- [1]R. Hinze, R. Paterson, “Finger trees: a simple general-purpose data structure”, Journal of Functional Programming 16 (2006).
- [2]L. J. Guibas, E. M. McCreight, M. F. Plass, J. R. Roberts, “A new representation for linear lists”, STOC (1977).
- [3]C. Okasaki, Purely Functional Data Structures, Cambridge University Press (1998).
last updated