AlgoVerse

Splitting and joining binary search trees

Split a search tree at a key k into two trees (keys below and above k), then join the two again; both O(h). A split followed by a join deletes k.

SPLIT(t, k) cuts a search tree into three parts: a tree small with the keys below k, the node k (if present), and a tree big with the keys above k. Only k's search path is walked:

Read the full explanation ↓
Input:
A 1×10 · k=40PDF export

A tree of 10 keys with k = 40; the search path for 40 cuts the tree in two, then small's largest key, 35, becomes the joined tree's root.

  1. class Node:
  2. def __init__(self, key):
  3. self.key = key
  4. self.left = None
  5. self.right = None
  6. def split_join(A, k):
  7. root = build(A[0])
  8. small, mid, big = split(root, k)
  9. found = mid is not None
  10. root = None
  11. root = join(small, big)
  12. small = None
  13. big = None
  14. keys = []
  15. inorder(root, keys)
  16. return keys
  17. def split(t, k):
  18. # Returns the keys < k, the node with key k (or None), and the keys > k, as three search trees.
  19. if t is None:
  20. return None, None, None
  21. if k < t.key:
  22. s, m, b = split(t.left, k)
  23. t.left = b
  24. return s, m, t
  25. if k > t.key:
  26. s, m, b = split(t.right, k)
  27. t.right = s
  28. return t, m, b
  29. s, b = t.left, t.right
  30. t.left = None
  31. t.right = None
  32. return s, t, b
  33. def join(small, big):
  34. # Every key in small is below every key in big. The largest key of small becomes the new root.
  35. if small is None:
  36. return big
  37. parent = None
  38. m = small
  39. while m.right is not None:
  40. parent = m
  41. m = m.right
  42. if parent is None:
  43. small = m.left
  44. else:
  45. parent.right = m.left
  46. m.left = small
  47. m.right = big
  48. return m
  49. def inorder(t, keys):
  50. if t is not None:
  51. inorder(t.left, keys)
  52. keys.append(t.key)
  53. inorder(t.right, keys)
  54. def build(keys):
  55. root = None
  56. for key in keys:
  57. node = Node(key)
  58. if root is None:
  59. root = node
  60. continue
  61. p = root
  62. while True:
  63. if key < p.key:
  64. if p.left is None:
  65. p.left = node
  66. break
  67. p = p.left
  68. else:
  69. if p.right is None:
  70. p.right = node
  71. break
  72. p = p.right
  73. return root

Invariant: In a split, each node on k's search path goes to small or big with one of its subtrees intact; every key in small is below k and every key in big above it.

Start

root

∅

small

∅

big

∅

Recursion tree

Filled orange = running call · Orange outline = still on the stack · Grey = finished

Local variables

k40

Orange = changed in this step

A list

[50, 30, 70, 20, 40, 60, 80, 35, 45, 65]

Call stack

  1. split_join()line 8
Step 1 of 280
Black = split_done, joined · Orange = key stepbuilt: 0/1 · found: 0/1 · keep_left: 0/1 · keep_right: 0/1 · split_done: 0/1 · find_max: 0/1 · unlink_max: 0/1 · new_root: 0/1 · joined: 0/1
    Explanation

    Splitting

    SPLIT(t, k) cuts a search tree into three parts: a tree small with the keys below k, the node k (if present), and a tree big with the keys above k. Only k's search path is walked:

    • if k < t.key, node t goes to big with its right subtree, and the split goes on in the left subtree;
    • if k > t.key, the other way round: t goes to small with its left subtree.

    Joining

    JOIN(small, big) merges two trees where every key of the first is below every key of the second: take out small's largest node and make it the new root, with small on its left and big on its right. With a middle key m in hand (a three-way join) it is simpler still: m becomes the root.

    Cost

    Both walk a single path from the root down: O(h). These two operations underlie treaps, splay trees, and sets with union and intersection.

    The input

    A is the insertion order of the keys and k the key to split at.