Leftist heap
A leftist heap is a priority queue that supports merging two queues in logarithmic time[1][2]. It is a binary tree in heap order, every node's element being no larger than its children's, with an additional shape condition: at every node, the right spine of the left child, the path that keeps going right, is at least as long as the right spine of the right child. The tree is therefore unbalanced towards the left, possibly very much so, but its right spine is short: it has at most nodes. Merging two heaps walks down their right spines, as when merging two sorted lists, and restores the shape condition on the way back up. Insertion merges a one-element heap, and deleting the minimum merges the root's two children. The operations never modify a tree in place and copy only the nodes on the right spines, which makes leftist heaps a standard persistent priority queue in functional programming[3].
Definition
The rank of a tree, called dist in Knuth's presentation and the s-value in some texts, is the number of nodes on its right spine, with for the empty tree. A leftist heap is a binary tree that is heap-ordered and satisfies, at every node with children and ,
Every node stores its rank so that the condition can be checked in constant time. A node with one child always has it on the left.
The right spine is short
A tree of rank has at least nodes. For this holds trivially. A node of rank has a right child of rank and, by the leftist condition, a left child of rank at least , so by induction it has at least
nodes. Hence a leftist heap of elements has rank at most . Nothing bounds the height: the left spine can have nodes[2][9].
Operations
To merge two non-empty heaps, compare their roots. The smaller root becomes the root of the result; its left subtree is kept, and its right subtree is merged recursively with the other heap. On the way back, the helper make puts the child of larger rank on the left and sets the rank. Each recursive step moves one node down the right spine of one of the two heaps, so merging heaps of and elements takes at most steps[3].
A leftist heap in OCaml.
(* A leftist heap: a heap-ordered binary tree in which every node's leftchild has rank at least that of its right child. The rank of a tree isthe length of its right spine. *)type 'a heap = E | T of int * 'a * 'a heap * 'a heaplet rank = function E -> 0 | T (r, _, _, _) -> r(* Build a node, putting the child of larger rank on the left. *)let make x a b =if rank a >= rank b then T (rank b + 1, x, a, b) else T (rank a + 1, x, b, a)(* Merge walks down the two right spines, like merging sorted lists. *)let rec merge h1 h2 =match h1, h2 with| E, h | h, E -> h| T (_, x, a1, b1), T (_, y, a2, b2) ->if x <= y then make x a1 (merge b1 h2) else make y a2 (merge h1 b2)let singleton x = T (1, x, E, E)let insert x h = merge (singleton x) hlet find_min = function E -> None | T (_, x, _, _) -> Some xlet delete_min = function E -> E | T (_, _, a, b) -> merge a b(* Build a heap from a list in linear time by merging in pairs. *)let of_list xs =let rec pass acc = function| a :: b :: rest -> pass (merge a b :: acc) rest| [h] -> h :: acc| [] -> accinlet rec go = function [] -> E | [h] -> h | hs -> go (pass [] hs) ingo (List.map singleton xs)let rec to_sorted_list h =match find_min h with None -> [] | Some x -> x :: to_sorted_list (delete_min h)
Insertion is merging with a single node, so it takes time, and delete_min merges the two children of the root in the same time. Building a heap by repeated insertion takes ; merging the singletons in pairs, then the results in pairs, and so on, takes because the merges of round each cost [3].
Merging two small heaps, and checking that the originals survive.
open Leftistlet rec show indent = function| E -> ()| T (r, x, a, b) ->Printf.printf "%s%d (rank %d)\n" indent x r;show (indent ^ " ") a; show (indent ^ " ") b
Running it. Children are listed below their parent, left first.
h1:4 (rank 2)9 (rank 1)5 (rank 1)7 (rank 1)h2:1 (rank 1)3 (rank 1)8 (rank 1)merge h1 h2:1 (rank 2)4 (rank 2)9 (rank 1)5 (rank 1)7 (rank 1)3 (rank 1)8 (rank 1)sorted: 1 3 4 5 7 8 9h1 is unchanged: 4 5 7 9
Here the merge allocated a single node, the new root 1. Merging h1 with the empty right child of 1 returned h1 itself, and make then swapped the children because h1 has the larger rank. In general a merge allocates one node per step along the right spines and shares every other node with its arguments, which remain valid. This is what makes the structure persistent at no extra asymptotic cost[3].
Rank and height for 100,000 elements inserted in random, ascending and descending order, and the cost of merging two large heaps.
open Leftistlet rec size = function E -> 0 | T (_, _, a, b) -> 1 + size a + size blet rec height = function E -> 0 | T (_, _, a, b) -> 1 + max (height a) (height b)(* Merge again, counting the recursive steps. *)let steps = ref 0let rec merge_counted h1 h2 =incr steps;match h1, h2 with| E, h | h, E -> h| T (_, x, a1, b1), T (_, y, a2, b2) ->if x <= y then make x a1 (merge_counted b1 h2) else make y a2 (merge_counted h1 b2)
Running it.
random size 100000 rank 11 height 31ascending size 100000 rank 16 height 17descending size 100000 rank 1 height 100000log2(n + 1) = 16.61merging two heaps of 100000: 21 steps, result rank 12heap sort of the random input sorted: true
Ascending insertion reaches the bound exactly: . Descending insertion puts each new, smaller element at the root with the old heap as its left child, which gives a path of height 100,000 with rank 1; every operation on it is still cheap, because operations only follow right spines. Merging two heaps of 100,000 elements took 21 steps.
Comparison with other heaps
The array-based binary heap of heapsort has the same insertion and deletion of the minimum and better constant factors, but merging two binary heaps requires rebuilding one of them, in time[10]. Later designs have improved on leftist heaps in different directions:
Binomial queues also merge in and insert in amortized constant time[8]. Skew heaps drop the rank and swap the children at every step of a merge unconditionally; this gives amortized time with less bookkeeping, but amortization breaks down when old versions are reused, so skew heaps are not efficiently persistent[4]. Weight-biased leftist trees use the size of a subtree instead of its rank; since sizes are known before the recursive call returns, merging can be done top-down in a single pass[5]. Fibonacci heaps add constant amortized time for decreasing a key, which speeds up Dijkstra's and Prim's algorithms[6], and Brodal and Okasaki gave a purely functional priority queue with worst-case constant-time insertion and merging[7].
History
Clark Allan Crane introduced the structure in his Stanford thesis of 1972 as a way to represent priority queues as balanced binary trees that support merging[1]. Knuth presented it in the third volume of The Art of Computer Programming in 1973 under the name leftist trees[2], and Tarjan's Data Structures and Network Algorithms of 1983 used leftist heaps as its basic meldable heap[9]. Sleator and Tarjan's skew heaps were introduced in 1986 as a self-adjusting analogue[4]. Okasaki's Purely Functional Data Structures of 1998 made leftist heaps the first example of a functional data structure with efficient persistent operations, and the pairwise construction in linear time is one of its exercises[3].
see also
- 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.
- Finger treeA 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.
- 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.
further reading
- [1]C. A. Crane, Linear Lists and Priority Queues as Balanced Binary Trees, PhD thesis, Stanford University, report STAN-CS-72-259 (1972).
- [2]D. E. Knuth, The Art of Computer Programming, vol. 3: Sorting and Searching, §5.2.3, Addison-Wesley (1973; 2nd ed. 1998).
- [3]C. Okasaki, Purely Functional Data Structures, §3.1, Cambridge University Press (1998).
- [4]D. D. Sleator, R. E. Tarjan, “Self-adjusting heaps”, SIAM Journal on Computing 15 (1986).
- [5]S. Cho, S. Sahni, “Weight-biased leftist trees and modified skip lists”, ACM Journal of Experimental Algorithmics 3 (1998).
- [6]M. L. Fredman, R. E. Tarjan, “Fibonacci heaps and their uses in improved network optimization algorithms”, Journal of the ACM 34 (1987).
- [7]G. S. Brodal, C. Okasaki, “Optimal purely functional priority queues”, Journal of Functional Programming 6 (1996).
- [8]J. Vuillemin, “A data structure for manipulating priority queues”, Communications of the ACM 21 (1978).
- [9]R. E. Tarjan, Data Structures and Network Algorithms, ch. 3 “Heaps”, SIAM (1983).
- [10]J. W. J. Williams, “Algorithm 232: Heapsort”, Communications of the ACM 7 (1964).
last updated