{"id":"recursion-examples-nested-structures","slug":"nested-structures","title":"When loops get clumsy","text":"A binary tree branches in two at each node, but recursion handles any kind of branching. The pattern stays the same: deal with this level, and call the function for everything nested inside it, however deep that goes.\n\n### Lists inside lists\n\nTake a list whose items can be numbers or lists, which again can hold numbers or lists: `[1, [2, 3], [4, [5, 6]], 7]`. One loop handles one level, and a loop inside it handles two, but how many loops would you need when nobody knows how deep the nesting goes? With recursion, you don't need to know. For each item, if it's a list, ask the same function about it. Otherwise it's a number:\n\n```python\ndef deep_sum(items):\n    total = 0\n    for item in items:\n        if type(item) == list:\n            total += deep_sum(item)\n        else:\n            total += item\n    return total\n\n\nprint(deep_sum([1, [2, 3], [4, [5, 6]], 7]))\nprint(deep_sum([[[[10]]]]))\n```\n\n```output\n28\n10\n```\n\n`type(item)` gives the type of the item, and `type(item) == list` is true when the item is a list. The loop goes across one level, and the recursive calls go down into the lists inside it. There's no separate base case: a list without lists inside it simply makes no recursive calls.\n\n### Any number of children\n\nIn a discussion thread, a comment can have any number of replies, and each reply can have replies of its own. A list of children takes the place of `left` and `right`:\n\n```python\nclass Comment:\n    def __init__(self, author, text):\n        self.author = author\n        self.text = text\n        self.replies = []\n\n    def add_reply(self, comment):\n        self.replies.append(comment)\n\n\ndef show(comment, depth):\n    print(\"  \" * depth + f\"{comment.author}: {comment.text}\")\n    for reply in comment.replies:\n        show(reply, depth + 1)\n\n\ndef count_comments(comment):\n    return 1 + sum([count_comments(reply) for reply in comment.replies])\n\n\nfirst = Comment(\"Ada\", \"Is recursion hard?\")\nanswer = Comment(\"Ben\", \"Only until it clicks.\")\nfirst.add_reply(answer)\nanswer.add_reply(Comment(\"Ada\", \"It clicked!\"))\nfirst.add_reply(Comment(\"Cy\", \"Draw the calls on paper.\"))\n\nshow(first, 0)\nprint(count_comments(first), \"comments\")\n```\n\n```output\nAda: Is recursion hard?\n  Ben: Only until it clicks.\n    Ada: It clicked!\n  Cy: Draw the calls on paper.\n4 comments\n```\n\n`count_comments` combines a comprehension with recursion: one comment, plus the count of each reply's whole thread. It has no `if` at all. For a comment without replies, the comprehension goes through an empty list and makes `[]`, `sum([])` is 0, and the function returns 1 without calling itself. That's the base case.\n\n### Loop or recursion?\n\nUse a loop to go through a flat series of items one by one. Reach for recursion when the data is nested to a depth you don't know in advance (trees, folders, menus inside menus, lists inside lists), or when a problem splits into smaller problems of the same kind, as in binary search. As the examples show, the two often work together: the loop goes across, and the recursion goes down.\n","task":"Fill in the blank so the function counts the numbers in a nested list, at any depth. The program should print:\n\n```\n7\n0\n```\n","kind":"fill","seed":"def count_numbers(items):\n    count = 0\n    for item in items:\n        if type(item) == list:\n            count += [[count_numbers(item)]]\n        else:\n            count += 1\n    return count\n\n\nprint(count_numbers([1, [2, 3], [4, [5, 6]], 7]))\nprint(count_numbers([]))\n","solution":"def count_numbers(items):\n    count = 0\n    for item in items:\n        if type(item) == list:\n            count += count_numbers(item)\n        else:\n            count += 1\n    return count\n\n\nprint(count_numbers([1, [2, 3], [4, [5, 6]], 7]))\nprint(count_numbers([]))\n","hints":["When the item is a list, the numbers inside it have to be counted too, at any depth.","Let the function count them: `count_numbers(item)`."],"tests":[{"name":"Output","stdin":"","expect":"7\n0","hidden":false}],"require":[],"forbid":[],"more":[{"task":"A folder holds files and other folders. The class `Folder` has a list of the sizes of its own files and a list of its subfolders. Write a recursive function `total_size(folder)` that returns the size of all the files in the folder and in all the folders inside it, at any depth.\n\nFor example, this program:\n\n```python\nhome = Folder(\"home\", [10, 20])\nphotos = Folder(\"photos\", [300, 450])\nhome.add(photos)\nphotos.add(Folder(\"trips\", [1200]))\nhome.add(Folder(\"empty\", []))\nprint(total_size(home))\nprint(total_size(photos))\n```\n\nprints:\n\n```\n1980\n1950\n```\n","kind":"code","seed":"class Folder:\n    def __init__(self, name, file_sizes):\n        self.name = name\n        self.file_sizes = file_sizes\n        self.subfolders = []\n\n    def add(self, folder):\n        self.subfolders.append(folder)\n\n\ndef total_size(folder):\n    pass\n\n\nif __name__ == \"__main__\":\n    home = Folder(\"home\", [10, 20])\n    photos = Folder(\"photos\", [300, 450])\n    home.add(photos)\n    photos.add(Folder(\"trips\", [1200]))\n    home.add(Folder(\"empty\", []))\n    print(total_size(home))\n    print(total_size(photos))\n","solution":"class Folder:\n    def __init__(self, name, file_sizes):\n        self.name = name\n        self.file_sizes = file_sizes\n        self.subfolders = []\n\n    def add(self, folder):\n        self.subfolders.append(folder)\n\n\ndef total_size(folder):\n    return sum(folder.file_sizes) + sum([total_size(sub) for sub in folder.subfolders])\n\n\nif __name__ == \"__main__\":\n    home = Folder(\"home\", [10, 20])\n    photos = Folder(\"photos\", [300, 450])\n    home.add(photos)\n    photos.add(Folder(\"trips\", [1200]))\n    home.add(Folder(\"empty\", []))\n    print(total_size(home))\n    print(total_size(photos))\n","hints":["The folder's own files add up to `sum(folder.file_sizes)`.","Each subfolder's total is `total_size(sub)`, a recursive call. Add those up too, with a loop or with `sum` and a comprehension.","A folder without subfolders makes no recursive calls, so it needs no separate base case."],"tests":[{"name":"Test 1","stdin":"","call":"home = Folder(\"home\", [10, 20])\nphotos = Folder(\"photos\", [300, 450])\nhome.add(photos)\nphotos.add(Folder(\"trips\", [1200]))\nhome.add(Folder(\"empty\", []))\nprint(total_size(home))\nprint(total_size(photos))\n","expect":"1980\n1950","hidden":false},{"name":"total_size(Folder(\"lonely\", [5, 5, 5]))","stdin":"","call":"total_size(Folder(\"lonely\", [5, 5, 5]))\n","expect":"15","hidden":true,"returns":true},{"name":"Test 3","stdin":"","call":"top = Folder(\"top\", [])\nmiddle = Folder(\"middle\", [])\ntop.add(middle)\nmiddle.add(Folder(\"a\", [7]))\nmiddle.add(Folder(\"b\", [8, 1]))\ntotal_size(top)\n","expect":"16","hidden":true,"returns":true}],"require":[{"pattern":"def total_size\\b[^\\n]*\\n(?:[ \\t]*\\n|[ \\t]+[^\\n]*\\n)*?[ \\t]+[^\\n]*\\btotal_size\\s*\\(","message":"Make `total_size` call itself inside its own body."}],"forbid":[]},{"task":"Write a recursive function `flatten(items)` that gets a list whose items can be lists, nested to any depth, and returns a new flat list of all the items that aren't lists, in their original order.\n\nFor example, this call:\n\n```python\nprint(flatten([1, [2, 3], [4, [5, 6]], 7]))\n```\n\nprints:\n\n```\n[1, 2, 3, 4, 5, 6, 7]\n```\n","kind":"code","seed":"def flatten(items):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(flatten([1, [2, 3], [4, [5, 6]], 7]))\n","solution":"def flatten(items):\n    result = []\n    for item in items:\n        if type(item) == list:\n            result += flatten(item)\n        else:\n            result.append(item)\n    return result\n\n\nif __name__ == \"__main__\":\n    print(flatten([1, [2, 3], [4, [5, 6]], 7]))\n","hints":["Start with an empty list `result` and go through the items with a loop, as in `deep_sum`.","An item that isn't a list goes into `result` with `append`.","For an item that is a list, `flatten(item)` gives its items as a flat list. Add them all to `result` with `+=`."],"tests":[{"name":"print(flatten([1, [2, 3], [4, [5, 6]], 7]))","stdin":"","call":"print(flatten([1, [2, 3], [4, [5, 6]], 7]))\n","expect":"[1, 2, 3, 4, 5, 6, 7]","hidden":false},{"name":"flatten([])","stdin":"","call":"flatten([])\n","expect":"[]","hidden":true,"returns":true},{"name":"flatten([[[[\"x\"]]], \"y\", [[], [\"z\"]]])","stdin":"","call":"flatten([[[[\"x\"]]], \"y\", [[], [\"z\"]]])\n","expect":"['x', 'y', 'z']","hidden":true,"returns":true},{"name":"flatten([5, 4, 3])","stdin":"","call":"flatten([5, 4, 3])\n","expect":"[5, 4, 3]","hidden":true,"returns":true}],"require":[{"pattern":"def flatten\\b[^\\n]*\\n(?:[ \\t]*\\n|[ \\t]+[^\\n]*\\n)*?[ \\t]+[^\\n]*\\bflatten\\s*\\(","message":"Make `flatten` call itself inside its own body."}],"forbid":[]}]}