{"id":"dsa-trees-search-trees","slug":"search-trees","title":"Binary search trees","text":"To find a value in the trees of the last step, you may have to look at every node: O(n). The recursion module's **sorted binary tree**, which interviews call a **binary search tree** (BST), keeps its values in an order that lets a search skip most of them. At every node:\n\n- every value in the left subtree is smaller than the node's value;\n- every value in the right subtree is greater.\n\n(In these lessons, a BST holds each value once.) In the recursion module you searched one, added values to one and printed one in order. This step adds what those operations cost, why the order of the inserts decides the shape of the tree, and how to build a tree that stays balanced.\n\n```text\n        8\n      /   \\\n     3     12\n    / \\      \\\n   1   6      15\n```\n\nTo look for 6, start at the root. 6 is smaller than 8, so it can only be on the left: the whole right subtree is out of the game. 6 is greater than 3, so go right, and there it is. Each comparison moves one level down and drops a whole subtree, as binary search drops half of a sorted list.\n\n### Search and insert\n\n```python\nclass Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef contains(node, value):\n    while node is not None:\n        if value == node.value:\n            return True\n        if value < node.value:\n            node = node.left\n        else:\n            node = node.right\n    return False\n\n\ndef insert(node, value):\n    if node is None:\n        return Node(value)\n    if value < node.value:\n        node.left = insert(node.left, value)\n    elif value > node.value:\n        node.right = insert(node.right, value)\n    return node\n\n\ndef smallest(node):\n    while node.left is not None:\n        node = node.left\n    return node.value\n\n\nroot = None\nfor value in [8, 3, 12, 1, 6, 15]:\n    root = insert(root, value)\nprint(contains(root, 6), contains(root, 7))\nprint(smallest(root), root.left.right.value)\n```\n\n```output\nTrue False\n1 6\n```\n\n`contains` needs no recursion: a loop walks down until it finds the value or falls off the tree at `None`. `insert` walks down in the same way and puts a new node where the search fell off. Unlike the recursion module's `add`, it returns the root of the tree it was given, so that its caller can store it, and so it works on an empty tree too: for `None`, the root is the new node itself. A value that's already in the tree isn't added again. The smallest value is as far left as you can go, and the largest as far right.\n\nThe classic mistake is to call `insert(node.left, value)` without the `node.left =` in front, or `insert(root, value)` without `root =`. The new node is made, returned and thrown away, and the tree never grows.\n\n### The shape depends on the order\n\nSearch, insert, smallest and largest each follow one path down: O(h) for a tree of height h. A balanced tree has a height of about log₂ n, so even a million values need only about 20 steps. But nothing keeps this tree balanced: its shape comes from the order of the inserts. This program builds a tree from the same seven values in two orders, then inserts 2,000 values in sorted order:\n\n```python\nclass Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef insert(node, value):\n    if node is None:\n        return Node(value)\n    if value < node.value:\n        node.left = insert(node.left, value)\n    elif value > node.value:\n        node.right = insert(node.right, value)\n    return node\n\n\ndef height(node):\n    if node is None:\n        return 0\n    return 1 + max(height(node.left), height(node.right))\n\n\nfor order in [[4, 2, 6, 1, 3, 5, 7], [1, 2, 3, 4, 5, 6, 7]]:\n    root = None\n    for value in order:\n        root = insert(root, value)\n    print(order, \"height\", height(root))\n\nroot = None\nfor value in range(1, 2001):\n    root = insert(root, value)\n```\n\n```output\n[4, 2, 6, 1, 3, 5, 7] height 3\n[1, 2, 3, 4, 5, 6, 7] height 7\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 32, in <module>\n    root = insert(root, value)\n  File \"main.py\", line 14, in insert\n    node.right = insert(node.right, value)\n                 ~~~~~~^^^^^^^^^^^^^^^^^^^\n  File \"main.py\", line 14, in insert\n    node.right = insert(node.right, value)\n                 ~~~~~~^^^^^^^^^^^^^^^^^^^\n  File \"main.py\", line 14, in insert\n    node.right = insert(node.right, value)\n                 ~~~~~~^^^^^^^^^^^^^^^^^^^\n  [Previous line repeated 996 more times]\nRecursionError: maximum recursion depth exceeded\n```\n\nIn sorted order, each new value is the greatest so far and goes to the right of everything: the tree becomes a **chain**, a list in disguise, with h = n. Every search is then O(n), and building the tree costs 1 + 2 + ... + n steps, O(n²). The recursive `insert` also goes one call deeper for each node, and Python gives up at about 1000 nested calls. A loop version of `insert` gets rid of the error, but not of the slowness: the chain is still 2,000 levels deep.\n\n### Keeping it balanced\n\nWhen the values come sorted, you can choose the order of the tree yourself. Make the middle value the root: half of the values are smaller and go to its left, half are greater and go to its right. Then build each half in the same way, around its own middle value. The height is about log₂ n, so the recursion stays shallow too. Pass the indexes `low` and `high` of the part to build instead of slices, and nothing gets copied: each value becomes one node, O(n) in all.\n\nJava's `TreeMap` and C++'s `std::map` are **self-balancing** trees (red-black trees, usually): they reshape themselves after every insert, so they stay O(log n) whatever the order. Python's standard library has no such tree. For \"is it there?\" questions, use a set or a dict, and for sorted data, a sorted list with `bisect`.\n","task":"What does this program print? Draw the tree first, inserting the values in order. Type each line it prints.\n","kind":"predict","seed":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef insert(node, value):\n    if node is None:\n        return Node(value)\n    if value < node.value:\n        node.left = insert(node.left, value)\n    elif value > node.value:\n        node.right = insert(node.right, value)\n    return node\n\n\ndef search(node, value):\n    while node is not None:\n        print(\"visit\", node.value)\n        if value == node.value:\n            return True\n        if value < node.value:\n            node = node.left\n        else:\n            node = node.right\n    return False\n\n\nroot = None\nfor value in [50, 30, 70, 20, 40, 60, 35]:\n    root = insert(root, value)\nprint(search(root, 38))\n","solution":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef insert(node, value):\n    if node is None:\n        return Node(value)\n    if value < node.value:\n        node.left = insert(node.left, value)\n    elif value > node.value:\n        node.right = insert(node.right, value)\n    return node\n\n\ndef search(node, value):\n    while node is not None:\n        print(\"visit\", node.value)\n        if value == node.value:\n            return True\n        if value < node.value:\n            node = node.left\n        else:\n            node = node.right\n    return False\n\n\nroot = None\nfor value in [50, 30, 70, 20, 40, 60, 35]:\n    root = insert(root, value)\nprint(search(root, 38))\n","hints":["Each value goes down from the root, left when it's smaller and right when it's greater, until it finds an empty place.","50 is the root, with 30 and 70 as its children. 40 is the right child of 30, and 35 goes left of 40.","The search prints every node it visits on its way down. 38 isn't in the tree, so the search falls off below 35 and returns `False`."],"tests":[{"name":"Output","stdin":"","expect":"visit 50\nvisit 30\nvisit 40\nvisit 35\nFalse","hidden":false}],"require":[],"forbid":[],"lines":["visit 50","visit 30","visit 40","visit 35","False"],"more":[{"task":"A shop keeps its prices in a binary search tree. Write `best_under(node, budget)` that returns the highest price that is at most `budget`, or `None` when every price is above it or the tree is empty. Walk one path down, as a search does: when a price is too high, the answer can only be on its left; when a price fits, it's the best so far, and a better one can only be on its right.\n\nFor example, this program:\n\n```python\n#        40\n#       /  \\\n#     20    60\n#    /  \\     \\\n#   10   30    80\nprices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\nprint(best_under(prices, 35))\nprint(best_under(prices, 60), best_under(prices, 5))\n```\n\nprints:\n\n```\n30\n60 None\n```\n","kind":"code","seed":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef best_under(node, budget):\n    pass\n\n\nif __name__ == \"__main__\":\n    prices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\n    print(best_under(prices, 35))\n    print(best_under(prices, 60), best_under(prices, 5))\n","solution":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef best_under(node, budget):\n    best = None\n    while node is not None:\n        if node.value <= budget:\n            best = node.value\n            node = node.right\n        else:\n            node = node.left\n    return best\n\n\nif __name__ == \"__main__\":\n    prices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\n    print(best_under(prices, 35))\n    print(best_under(prices, 60), best_under(prices, 5))\n","hints":["Keep a variable `best`, starting at `None`, and walk down from the root with a `while node is not None:` loop, as `contains` does.","When `node.value` is at most the budget, it's the best price so far: store it, then go right to look for a higher one that still fits.","When `node.value` is over the budget, everything on its right is even higher, so go left. When the loop falls off the tree, `best` is the answer."],"tests":[{"name":"Test 1","stdin":"","call":"prices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\nprint(best_under(prices, 35))\nprint(best_under(prices, 60), best_under(prices, 5))\n","expect":"30\n60 None","hidden":false},{"name":"Test 2","stdin":"","call":"prices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\nprint(best_under(prices, 100), best_under(prices, 79), best_under(prices, 10))\n","expect":"80 60 10","hidden":true},{"name":"Test 3","stdin":"","call":"prices = Node(40, Node(20, Node(10), Node(30)), Node(60, None, Node(80)))\nprint(best_under(prices, 25), best_under(prices, 59), best_under(prices, 40))\n","expect":"20 40 40","hidden":true},{"name":"print(best_under(None, 50))","stdin":"","call":"print(best_under(None, 50))\n","expect":"None","hidden":true},{"name":"print(best_under(Node(7), 6), best_under(Node(7), 7))","stdin":"","call":"print(best_under(Node(7), 6), best_under(Node(7), 7))\n","expect":"None 7","hidden":true}],"require":[],"forbid":[]},{"task":"A warehouse's item codes arrive sorted, and `build_tree(codes)` puts them into a binary search tree that `contains` then searches. It gives the right answers, but inserting sorted codes one by one makes the tree a chain, so building it is O(n²), and the hidden test has 80,000 codes and searches for codes all over that range. Rewrite `build_tree` so that it builds a balanced tree in O(n): the middle code is the root, the codes before it build the left subtree and the codes after it the right subtree, each in the same way. Keep `insert` and `contains` as they are.\n\nFor example, this program:\n\n```python\nroot = build_tree([3, 8, 15, 21, 40, 42, 57])\nprint(contains(root, 21), contains(root, 22), contains(root, 57))\n```\n\nprints:\n\n```\nTrue False True\n```\n","kind":"code","seed":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef insert(root, value):\n    new = Node(value)\n    if root is None:\n        return new\n    node = root\n    while True:\n        if value < node.value:\n            if node.left is None:\n                node.left = new\n                return root\n            node = node.left\n        else:\n            if node.right is None:\n                node.right = new\n                return root\n            node = node.right\n\n\ndef contains(node, value):\n    while node is not None:\n        if value == node.value:\n            return True\n        if value < node.value:\n            node = node.left\n        else:\n            node = node.right\n    return False\n\n\ndef build_tree(codes):\n    root = None\n    for code in codes:\n        root = insert(root, code)\n    return root\n\n\nif __name__ == \"__main__\":\n    root = build_tree([3, 8, 15, 21, 40, 42, 57])\n    print(contains(root, 21), contains(root, 22), contains(root, 57))\n","solution":"class Node:\n    def __init__(self, value, left=None, right=None):\n        self.value = value\n        self.left = left\n        self.right = right\n\n\ndef insert(root, value):\n    new = Node(value)\n    if root is None:\n        return new\n    node = root\n    while True:\n        if value < node.value:\n            if node.left is None:\n                node.left = new\n                return root\n            node = node.left\n        else:\n            if node.right is None:\n                node.right = new\n                return root\n            node = node.right\n\n\ndef contains(node, value):\n    while node is not None:\n        if value == node.value:\n            return True\n        if value < node.value:\n            node = node.left\n        else:\n            node = node.right\n    return False\n\n\ndef build(codes, low, high):\n    if low > high:\n        return None\n    middle = (low + high) // 2\n    return Node(codes[middle], build(codes, low, middle - 1), build(codes, middle + 1, high))\n\n\ndef build_tree(codes):\n    return build(codes, 0, len(codes) - 1)\n\n\nif __name__ == \"__main__\":\n    root = build_tree([3, 8, 15, 21, 40, 42, 57])\n    print(contains(root, 21), contains(root, 22), contains(root, 57))\n","hints":["Write a helper `build(codes, low, high)` that builds a tree from the codes at the indexes `low` to `high` and returns its root. `build_tree` calls it for the whole list, from 0 to `len(codes) - 1`.","In the helper, an empty part (`low > high`) is the empty tree, `None`. Otherwise, the middle index is `(low + high) // 2`.","The node for the middle code gets as its left child the tree built from `low` to `middle - 1`, and as its right child the tree built from `middle + 1` to `high`. The recursion is only about 17 levels deep for 80,000 codes."],"tests":[{"name":"Test 1","stdin":"","call":"root = build_tree([3, 8, 15, 21, 40, 42, 57])\nprint(contains(root, 21), contains(root, 22), contains(root, 57))\n","expect":"True False True","hidden":false},{"name":"print(build_tree([]))","stdin":"","call":"print(build_tree([]))\n","expect":"None","hidden":true},{"name":"Test 3","stdin":"","call":"root = build_tree([5])\nprint(contains(root, 5), contains(root, 4))\n","expect":"True False","hidden":true},{"name":"Test 4","stdin":"","call":"root = build_tree([1, 2])\nprint(contains(root, 1), contains(root, 2), contains(root, 3), contains(root, 0))\n","expect":"True True False False","hidden":true},{"name":"Test 5","stdin":"","call":"root = build_tree(list(range(10, 200, 10)))\nprint(sum(contains(root, v) for v in range(0, 300, 5)), contains(root, 190), contains(root, 15))\n","expect":"19 True False","hidden":true},{"name":"Test 6","stdin":"","call":"root = build_tree(list(range(0, 160_000, 2)))\nprint(sum(contains(root, v) for v in range(0, 160_000, 7)), contains(root, 159_998), contains(root, 159_999))\n","expect":"11429 True False","hidden":true,"big":true}],"require":[],"forbid":[]}]}