wiki

Braun tree

A Braun tree is a binary tree balanced as tightly as possible: at every node the size of the left subtree is equal to that of the right one or one greater[1][3]. Because the condition fixes the shape for each size, there is never anything to rebalance, and a Braun tree of nodes has height . Its main use is as a functional flexible array, a sequence that supports indexing and growth at the front in time[2][4].

§ 01

Indexing

Element 0 is at the root. The remaining elements are dealt alternately to the two subtrees, the odd indices to the left and the even ones to the right, recursively:

Dealing alternately is what keeps the sizes within one of each other.

0123456789odd index: go lefteven index: go rightget 9: 9 odd → left, (9−1)/2 = 4 even → right, (4−2)/2 = 1 odd → left, 0: here
A Braun tree holding the indices 0 to 9, each at the node where it is stored. The highlighted path is the search for index 9.

Adding an element at the front shifts every index up by one, which moves the old odd elements to even positions and the even ones to odd. cons therefore puts the new element at the root, conses the old root onto the old right subtree to make the new left one, and makes the old left subtree the new right one. Removing the front element reverses this. Both follow one path, so they take time[3].

A Braun tree as a flexible array.

(* A Braun tree: at every node the left subtree has the same size as the
right one, or one more. Element 0 is the root, odd indices are in the
left subtree and even ones in the right. *)
type 'a t = Empty | Node of 'a * 'a t * 'a t
let rec cons x = function
| Empty -> Node (x, Empty, Empty)
| Node (y, l, r) -> Node (x, cons y r, l)
let rec uncons = function
| Empty -> None
| Node (x, l, r) -> (
match uncons l with
| None -> Some (x, Empty)
| Some (y, l') -> Some (x, Node (y, r, l')))
let rec get t i =
match t with
| Empty -> invalid_arg "get"
| Node (x, l, r) ->
if i = 0 then x else if i mod 2 = 1 then get l ((i - 1) / 2) else get r ((i - 2) / 2)
let rec set t i v =
match t with
| Empty -> invalid_arg "set"
| Node (x, l, r) ->
if i = 0 then Node (v, l, r)
else if i mod 2 = 1 then Node (x, set l ((i - 1) / 2) v, r)
else Node (x, l, set r ((i - 2) / 2) v)
(* diff t m is size t - m, for a tree of size m or m + 1. *)
let rec diff t m =
match (t, m) with
| Empty, 0 -> 0
| Node _, 0 -> 1
| Node (_, l, _), m when m mod 2 = 1 -> diff l ((m - 1) / 2)
| Node (_, _, r), m -> diff r ((m - 2) / 2)
| Empty, _ -> invalid_arg "diff"
(* The right subtree's size determines the left's up to one. *)
let rec size = function
| Empty -> 0
| Node (_, l, r) -> let m = size r in 1 + 2 * m + diff l m
let of_list l = List.fold_left (fun t x -> cons x t) Empty (List.rev l)

The size is not stored. Given the size of the right subtree, the left one has or elements, and diff finds out which by following one path. Okasaki's size runs in time this way[3].

Operations on a small tree, persistence, and a tree of a million elements.

open Braun
let rec show = function
| Empty -> "."
| Node (x, Empty, Empty) -> string_of_int x
| Node (x, l, r) -> Printf.sprintf "%d(%s %s)" x (show l) (show r)
let rec height = function Empty -> 0 | Node (_, l, r) -> 1 + max (height l) (height r)
let rec braun_ok = function
| Empty -> true
| Node (_, l, r) -> let d = size l - size r in (d = 0 || d = 1) && braun_ok l && braun_ok r

Running it. A tree prints as root(left right), with . for empty.

0(1(3(7 .) 5(9 .)) 2(4(8 .) 6))
get 7 = 7, size = 10
after set: get 7 = 70, old version still 7
uncons: 0, then 1(2(4(8 .) 6) 3(5(9 .) 7))
1,000,000 elements: height 20, balanced true, get 765432 = 765432

A million elements gives height 20, which is . set copies only the path to the updated element, so the old version still holds the old value.

§ 02

Other uses

Building a Braun tree from a list in linear time, and finding its size in less than linear time, are the subjects of Okasaki's paper on Braun trees[3]. A Braun tree that is heap-ordered instead of indexed is a priority queue with logarithmic insertion and deletion and no balance information to maintain. Braun trees are a common teaching example in verified functional programming, where their simple invariant makes proofs short[5].

§ 03

History

W. Braun and Martin Rem described the trees in a 1983 Eindhoven memorandum on flexible arrays[1], and Rob Hoogerwoord published the design and its derivation in 1992[2]. Paulson presented flexible arrays as Braun trees in ML for the Working Programmer[4], and Okasaki gave linear-time construction and sublinear size in 1997[3].

see also

further reading

  1. [1]W. Braun, M. Rem, “A logarithmic implementation of flexible arrays”, Memorandum MR83/4, Eindhoven University of Technology (1983).
  2. [2]R. R. Hoogerwoord, “A logarithmic implementation of flexible arrays”, Mathematics of Program Construction (MPC), LNCS 669 (1992).
  3. [3]C. Okasaki, “Three algorithms on Braun trees”, Journal of Functional Programming 7 (1997).
  4. [4]L. C. Paulson, ML for the Working Programmer, 2nd ed., ch. 4, Cambridge University Press (1996).
  5. [5]T. Nipkow et al., Functional Algorithms, Verified!, ch. “Braun Trees” (2021).

last updated