{"id":"recursion-binary-search","slug":"binary-search","title":"Binary search","text":"To find out whether a value is in a list, `in` (or a loop) looks at the items one by one from the start. That's a **linear search**. In a list of a million items, it may have to look at all million.\n\nIf the list is sorted, there's a much faster way. Think of guessing a number between 1 and 100 when you're told \"higher\" or \"lower\" after each guess. You start with 50, and whatever the answer, half of the numbers are out of the game. **Binary search** does the same with a sorted list:\n\n1. Look at the item in the middle of the search area.\n2. If it's the one you're looking for, you're done.\n3. If it's smaller than what you're looking for, the target can only be in the right half. Search there.\n4. If it's greater, search the left half.\n5. If the search area is empty, the value isn't in the list.\n\nSteps 3 and 4 are the same search on a smaller area, so recursion fits well. The function gets the list, the target and the edges of the search area, `low` and `high`, as indexes. It returns the target's index, or -1 when the target isn't there. A `print` in each call shows how the area shrinks:\n\n```python\ndef find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    print(f\"searching {low}..{high}, middle item {numbers[middle]}\")\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, middle + 1, high)\n    return find(numbers, target, low, middle - 1)\n\n\nnumbers = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]\nprint(find(numbers, 23, 0, len(numbers) - 1))\nprint(find(numbers, 10, 0, len(numbers) - 1))\n```\n\n```output\nsearching 0..9, middle item 16\nsearching 5..9, middle item 56\nsearching 5..6, middle item 23\n5\nsearching 0..9, middle item 16\nsearching 0..3, middle item 5\nsearching 2..3, middle item 8\nsearching 3..3, middle item 12\n-1\n```\n\nThe first call searches the whole list, from index 0 to `len(numbers) - 1`. `(low + high) // 2` is the index in the middle, rounded down. When the middle item is too small, the new area starts at `middle + 1`: the middle item has been checked already, so it's left out. In the second search, the last call gets `low` 3 and `high` 2. The area is empty, and that's the base case that returns -1.\n\n### Why it's fast\n\nEvery call halves the search area. A list of 10 items needs at most 4 looks, and a list of a million items needs at most 20, because a million can be halved only about 20 times before a single item is left. A linear search might need a million looks.\n\n### A wrapper, and a warning\n\nSo that the people who use the search don't need to think about `low` and `high`, you can wrap it in a function of its own. Here `contains` starts the search on the whole list and turns the index into `True` or `False`. Binary search works on anything sorted, such as words in alphabetical order, since `<` compares strings alphabetically:\n\n```python\ndef find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, middle + 1, high)\n    return find(numbers, target, low, middle - 1)\n\n\ndef contains(items, target):\n    return find(items, target, 0, len(items) - 1) != -1\n\n\nprint(contains([2, 5, 8, 12, 16], 12))\nprint(contains([\"ant\", \"bee\", \"cat\", \"dog\"], \"cat\"))\nprint(contains([9, 2, 7, 4, 1], 9))\n```\n\n```output\nTrue\nTrue\nFalse\n```\n\nThe last answer is wrong: 9 is in the list. But the list isn't sorted, and binary search trusts the order. It looked at 7 first, decided that 9 could only be on the right, and never saw the 9 at the start. Binary search only works on a sorted list.\n","task":"Fill in the blanks with the new edges of the search area: where the right half starts and where the left half ends. The program should print:\n\n```\n5\n1\n-1\n```\n","kind":"fill","seed":"def find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, [[middle + 1]], high)\n    return find(numbers, target, low, [[middle - 1]])\n\n\nnumbers = [1, 4, 9, 16, 25, 36, 49]\nprint(find(numbers, 36, 0, 6))\nprint(find(numbers, 4, 0, 6))\nprint(find(numbers, 20, 0, 6))\n","solution":"def find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, middle + 1, high)\n    return find(numbers, target, low, middle - 1)\n\n\nnumbers = [1, 4, 9, 16, 25, 36, 49]\nprint(find(numbers, 36, 0, 6))\nprint(find(numbers, 4, 0, 6))\nprint(find(numbers, 20, 0, 6))\n","hints":["The middle item has been checked already, so neither half includes it.","The right half starts just after the middle, at `middle + 1`, and the left half ends just before it, at `middle - 1`."],"tests":[{"name":"Output","stdin":"","expect":"5\n1\n-1","hidden":false}],"require":[],"forbid":[],"more":[{"task":"This binary search has a bug. A search for 4 in `[1, 3, 5, 7]` never ends, and Python stops it with a `RecursionError`. Find out why and fix it, so the function returns the target's index, or -1 when the target isn't in the list.\n\nFor example, this call:\n\n```python\nprint(find([1, 3, 5, 7], 4, 0, 3))\n```\n\nshould print:\n\n```\n-1\n```\n","kind":"code","seed":"def find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, middle, high)\n    return find(numbers, target, low, middle)\n\n\nif __name__ == \"__main__\":\n    print(find([1, 3, 5, 7], 4, 0, 3))\n","solution":"def find(numbers, target, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if numbers[middle] == target:\n        return middle\n    if numbers[middle] < target:\n        return find(numbers, target, middle + 1, high)\n    return find(numbers, target, low, middle - 1)\n\n\nif __name__ == \"__main__\":\n    print(find([1, 3, 5, 7], 4, 0, 3))\n","hints":["Follow the calls on paper: `low` 0 and `high` 3 give `middle` 1. What are `low` and `high` in the next call, and the one after it?","The search area stops shrinking, because the middle item, which has been checked already, stays in it. So the same call repeats forever.","Leave the middle item out of both halves: `middle + 1` and `middle - 1`."],"tests":[{"name":"print(find([1, 3, 5, 7], 4, 0, 3))","stdin":"","call":"print(find([1, 3, 5, 7], 4, 0, 3))\n","expect":"-1","hidden":false},{"name":"find([1, 3, 5, 7], 7, 0, 3)","stdin":"","call":"find([1, 3, 5, 7], 7, 0, 3)\n","expect":"3","hidden":true,"returns":true},{"name":"find([1, 3, 5, 7], 1, 0, 3)","stdin":"","call":"find([1, 3, 5, 7], 1, 0, 3)\n","expect":"0","hidden":true,"returns":true},{"name":"find([2, 4, 6, 8, 10], 11, 0, 4)","stdin":"","call":"find([2, 4, 6, 8, 10], 11, 0, 4)\n","expect":"-1","hidden":true,"returns":true}],"require":[],"forbid":[]},{"task":"Write a recursive function `find_name(names, name, low, high)` that searches a list of names in alphabetical order with binary search. It returns the index of `name` in the list, or -1 if the name isn't there. Don't use loops, `in` or `index`. A search that steps through the names one at a time won't do either: the tests also search a list of 3000 names, and a call for each name ends in a `RecursionError`.\n\nFor example, this program:\n\n```python\nnames = [\"Ada\", \"Ben\", \"Cleo\", \"Dan\", \"Eva\", \"Finn\", \"Gus\"]\nprint(find_name(names, \"Eva\", 0, len(names) - 1))\nprint(find_name(names, \"Bob\", 0, len(names) - 1))\n```\n\nprints:\n\n```\n4\n-1\n```\n","kind":"code","seed":"def find_name(names, name, low, high):\n    pass\n\n\nif __name__ == \"__main__\":\n    names = [\"Ada\", \"Ben\", \"Cleo\", \"Dan\", \"Eva\", \"Finn\", \"Gus\"]\n    print(find_name(names, \"Eva\", 0, len(names) - 1))\n    print(find_name(names, \"Bob\", 0, len(names) - 1))\n","solution":"def find_name(names, name, low, high):\n    if low > high:\n        return -1\n    middle = (low + high) // 2\n    if names[middle] == name:\n        return middle\n    if names[middle] < name:\n        return find_name(names, name, middle + 1, high)\n    return find_name(names, name, low, middle - 1)\n\n\nif __name__ == \"__main__\":\n    names = [\"Ada\", \"Ben\", \"Cleo\", \"Dan\", \"Eva\", \"Finn\", \"Gus\"]\n    print(find_name(names, \"Eva\", 0, len(names) - 1))\n    print(find_name(names, \"Bob\", 0, len(names) - 1))\n","hints":["Start with the base case: when `low > high`, the search area is empty and the name isn't there.","Find the middle index with `(low + high) // 2`. If the name there is the one you want, return the index.","`<` compares strings alphabetically. If the middle name comes before `name`, search from `middle + 1` to `high`, otherwise from `low` to `middle - 1`."],"tests":[{"name":"Test 1","stdin":"","call":"names = [\"Ada\", \"Ben\", \"Cleo\", \"Dan\", \"Eva\", \"Finn\", \"Gus\"]\nprint(find_name(names, \"Eva\", 0, len(names) - 1))\nprint(find_name(names, \"Bob\", 0, len(names) - 1))\n","expect":"4\n-1","hidden":false},{"name":"Test 2","stdin":"","call":"names = [\"Ada\", \"Ben\", \"Cleo\", \"Dan\", \"Eva\", \"Finn\", \"Gus\"]\nprint(find_name(names, \"Ada\", 0, 6), find_name(names, \"Gus\", 0, 6), find_name(names, \"Dan\", 0, 6))\n","expect":"0 6 3","hidden":true},{"name":"find_name([\"Kai\", \"Lea\", \"Max\", \"Noa\"], \"Zoe\", 0, 3)","stdin":"","call":"find_name([\"Kai\", \"Lea\", \"Max\", \"Noa\"], \"Zoe\", 0, 3)\n","expect":"-1","hidden":true,"returns":true},{"name":"find_name([], \"Ann\", 0, -1)","stdin":"","call":"find_name([], \"Ann\", 0, -1)\n","expect":"-1","hidden":true,"returns":true},{"name":"Test 5","stdin":"","call":"names = [f\"name{i:04d}\" for i in range(3000)]\nprint(find_name(names, \"name2999\", 0, 2999), find_name(names, \"name0000\", 0, 2999), find_name(names, \"zzz\", 0, 2999))\n","expect":"2999 0 -1","hidden":true}],"require":[{"pattern":"def find_name\\b[^\\n]*\\n(?:[ \\t]*\\n|[ \\t]+[^\\n]*\\n)*?[ \\t]+[^\\n]*\\bfind_name\\s*\\(","message":"Make `find_name` call itself inside its own body."}],"forbid":[{"pattern":"\\b(?:while|for)\\b","message":"Use recursion instead of a loop."},{"pattern":"\\bin\\b|\\.index\\s*\\(","message":"Search with binary search, without `in` or `index`."}]}]}