The well-known solution
The most widely spread solution to this problem is the Dijkstra algorithm for generating the next permutation. It is based on the assumption that there is a total order between the elements of the array and the elements of the array are unique (there are no repetitions). If these conditions are met, the algorithm always generates the next permutation in lexicographical order.
The algorithm is very straight forward and goes as follow:
def get_next(a):
N = len(a)
i = len(a)-1
while a[i-1] >= a[i]:
i -= 1
j = N
while a[j-1] <= a[i-1]:
j -= 1
a[i-1], a[j-1] = a[j-1], a[i-1]
i += 1
j = N
while i < j:
a[i-1], a[j-1] = a[j-1], a[i-1]
i+=1
j-=1
return a
To use this algorithm to generate every possible permutation we just call it n! times:
def dijkstra(a):
import operator
fact = reduce(operator.mul, range(1,len(a)+1))
the_list = [copy(a)]
for i in range(fact-1):
a = get_next(a)
the_list.append(copy(a))
return the_list
the only possibly uncommon thing here is line 3 which is a functional-style factorial function.
This works very well for generating the permutations of n (the permutations of the array [0..n-1]) but it won't work for arrays of arbitrary elements. So we explore other options.
The known recursive solution
There's a (not-so-well-)known recursive solution to solving the permutation problem. It is known as the HeapPermute algorithm or Heap's algorithm for permuting an array.
My python implementation of the algorithm goes as follow:
def heap_aux(a, n, the_list):
if n == 1:
the_list.append(copy(a))
else:
for i in range(n):
heap_aux(a, n-1, the_list);
if n % 2 == 1:
a[0], a[n-1] = a[n-1], a[0]
else:
a[i], a[n-1] = a[n-1], a[i]
def heap(a):
the_list = []
heap_aux(a, len(a), the_list)
return the_list
This algorithm works very well and it is very efficient too but it generates the permutations in an order that I find particularly disturbing.
Also it is not as easy to understand as I would like.
Enter my recursive solution
This is probably the most straight forward solution I could come up with. It is a clasical recursive design.
Want to permute an array of n elements? How would you do it if you already had a function to solve the problem for a smaller array?
Simple. Select each possible element of the array to be "the first element of the permutation" and then permute the rest of the array:
A permutation of 1,2,3,4 would be:
1 + permutation_of(2,3,4)
2 + permutation_of(1,3,4)
3 + permutation_of(2,1,4)
4 + permutation_of(2,3,1)
Simple, right?
Let's code it:
for j in range(i, len(a)):
a[i], a[j] = a[j], a[i]
permutation(a, i+1, the_list)
a[i], a[j] = a[j], a[i]
This loop captures the essence of the procedure described above. We have to remember to put the elements back where they were after the recursive call to allow next iteration to work. Not doing so would result in swapping the wrong element in the next iteration.
Puting it all together (and adding a small optimization):
def perm_aux(a, i, the_list):
if i == len(a):
the_list.append(copy(a))
else:
perm_aux(a, i+1, the_list)
for j in range(i+1, len(a)):
a[i], a[j] = a[j], a[i]
perm_aux(a, i+1, the_list)
a[i], a[j] = a[j], a[i]
def permutate(a):
the_list = []
perm_aux(a, 0, the_list)
return the_list
This algorithm generates the list in a nicer order (though it's not lexicographical). And as its predecessor is quite efficient (taking into account the complexity of the problem).
Performance
Normally recursive algorithms are put down for performance reasons: They keep all local variables stored in the stack while the other recursive calls are made and have to incur in function calling overhead. In the case of these two algorithms the good thing is that the recurtion depth is not proportional to the size of the problem solution but to the size of the array (actually it is exactly as deep as the array's length). Also, if the list variables are passed by reference (this is programming language specific), only the reference to the variable needs to be stored in the stack and there is no overhead of coping the variables on every call.
It will probably never be as efficient as their iterative counterparts but for me readability (with small performance penalties) definitely pays off.
To measure the times I used the following python decorator:
def timing(func):
def inside(*args):
t1 = time.time()
res = func(*args)
t2 = time.time()
print '%s took %0.3f ms' % (func.func_name, (t2-t1)*1000.0)
return res
return inside
Given the complexity of the problem, the graph grows rapidly out of scale. So a graph in logarithmic scale is in order
As you can see the algorithms behave very similarly. The largest difference (for an array of size 9) between the fastest (heap) and the slowest (permute) is 0.6 seconds. This of course will grow with the array size, but in a linear instead of exponential manner.
Have another solution to this problem? comment on! =)
|