Search code examples
pythonpermutationbacktracking

Why does the following code for finding out all possible permutations of an integer array work?


def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        perm = []

        def dfs(i):
            if len(nums) == 0:
                res.append(perm[:])
                return
            
            for j in range(len(nums)):
                perm.append(nums[j])
                n = nums.pop(j)
                dfs(i+1)
                perm.pop()
                nums.insert(j,n)
       
        dfs(0)
        return res

This function gives the correct result for some reason even though it shouldn't as nums[j] should be out of bounds. Can someone explain why this works?


Solution

  • To get an index out of bounds error, you would have to call nums[j], with j >= len(nums). Here's why that never happens in your code.

    You use j to identify a location within nums three times:

    • On first entering the for loop, you call nums[j] to append to perm. So long as j is a value between 0 and len(nums)-1 (because range(n) = (0, 1, 2, ..., n-2, n-1)), then you can't be out of range. The only way you'd end up being out of range is if the next time you called nums[j], the length of nums had changed. Let's see if that happens.
    • Your next opportunity to get an index out of bounds error is on the next line - n = nums.pop(j). Well there can be no problem here - you were able to grab the value of nums at j on the previous line, so why shouldn't you be able to pop that value out of nums now? But now you've mutated nums, and if you call nums[j] again, AND if j is greater than len(nums) - 2, you'd be in trouble - let's see if that happens.
    • Next, you call dfs(i+1), which opens up a new invocation of dfs, and an entirely new for loop, with a new j, that restarts at 0, and a new nums that's one shorter than it was in the previous function call. If nums is empty, you never enter the for loop, and if nums isn't empty, you enter the for loop with j = 0 - so no risk of being out of bounds here.
    • The above recursive process continues, restarting the for loop with j = 0 and with nums truncated by one, until you get to the bottom of the recursion hole and nums == 0. At this point, you climb back up out of the recursion hole and carry on where you left off - after the last dfs(i+1) call. When you jumped into the recursion hole, you'd just mutated nums, and were in imminent danger of an index out of bounds error...
    • The next line (perm.pop()) kinda doesn't count in the nums/j saga we're on - so we'll skip past it.
    • You then call nums.insert(j, n). nums.insert(j, n) places the value assigned to n at the index assigned to j - 1. e.g: taking a list lst with 4 items in it, calling lst.insert(4, <val>) would take <val> and place it at the end of nums - making your nums.insert(j, n) equivalent to saying nums.append(n). From the time you popped n out of nums, to now, you've basically just shuffled it from where it was to the end of nums. You WERE sailing perilously close to an index out of bounds error, but you've now undone the mutation you had previously inflicted on nums, so as you exit the for loop, nums is exactly as long as it was when you entered it. Going back to the start of the for loop and incrementing j before reentering the for loop poses no risk to you of an index out of bounds error.