Problem Statement: Given a list of integers that may contain duplicates, return all possible unique permutations of those integers, in any order.
A permutation is an arrangement of elements. For example, the three elements
[1,2,3] can be arranged into $3!=6$ unique permutations:
[3,2,1]. For this problem, the input list can have duplicate elements (that’s the extra requirement compared to the previous problem, LeetCode 46: Permutations), but the answer must not have any duplicate permutations.
The canonical algorithm for finding permutations is recursive backtracking, so we’ll take that approach.
The solution uses two functions: the main function, which sets up the recursion and returns the result, and the recursive backtracking function.
The main function accepts a list of integers. The first step is to sort this input array. This is a simple way to avoid duplicate permutations. Then we call the recursive function and return the result. The
0 argument in the
backtrack call means start the process at position
nums (the input list).
sort the input list (nums) backtrack(nums, 0) return result
In the backtrack function, we’ll need four variables, the first three of which are passed as parameters:
result: The list of permutations
nums: One unique permutation of the input list
i: The starting position in the current permutation
j: The current position in the current permutation
For the recursive base case, we check if our starting position is past the end of the current permutation. If it is, the current permutation is done, so we can add it to our list of results, and return:
if i is past the end of the current permutation add the current permutation to the result return
Now the real work begins. We’ll use a combination of iteration and recursion. The iteration loop on
j goes from
i (the starting position) to the end of the current permutation,
nums. For each
j (the current position), we’ll check the values at
nums[j]. If they’re different, we swap them and recursively start the process again at the next starting position
i+1. As a special case, we also make the recursive call when
i==j. To keep the code simple, we also do the swap in this case, though it has no effect since we’re swapping an element with itself.
for each j from i to end of nums if nums[i] and nums[j] are different or i==j swap nums[i] and nums[j] backtrack(copy of nums, i+1)
One implementation detail: When we make the recursive call, we need to send a copy of
nums, not just a reference to
nums. This is how we generate new permutations. If we sent a reference, we would overwrite the result of previous swaps.
Why does this process work? Since permutations are arrangements of the input values, it makes sense that we could generate these arrangements by swapping. The key is to make sure we use all possible swap positions (except where the swap would have no effect because the source and destination elements are the same). Notice that
i covers every position in
0 to the last element. And
j covers every position from
i to the last element. So (ignoring duplicates), we swap position
0 with positions
n-1, and so on until
i==n. So it seems reasonable that this would cover every possible swap.
As with last week’s recursive problem, instrumentation can help illuminate the flow of control in a recursive algorithm. Here’s what happens when the input is
1,1,2. Note that some swaps are skipped in this example, since the input contains a duplicate
i=0, j=0 1 1 2 i=1, j=1 1 1 2 i=2, j=2 1 1 2 i=3, adding to result: --> 1 1 2 Returned from backtrack at i=2, j=2 Returned from backtrack at i=1, j=1 i=1, j=2 1 2 1 i=2, j=2 1 2 1 i=3, adding to result: --> 1 2 1 Returned from backtrack at i=2, j=2 Returned from backtrack at i=1, j=2 Returned from backtrack at i=0, j=0 i=0, j=2 2 1 1 i=1, j=1 2 1 1 i=2, j=2 2 1 1 i=3, adding to result: --> 2 1 1 Returned from backtrack at i=2, j=2 Returned from backtrack at i=1, j=1 Returned from backtrack at i=0, j=2
LeetCode Model Solutions
I’m posting some LeetCode model solutions this year. For more about this, see A Project for 2021.