Algorithm summary for generating permutations

Time：2020-2-18

Summary

I think my algorithm thinking ability is a little weak, so I will basically do 1-2 to leetcode algorithm problems every night. In the past two days, we have encountered a problem of permutation – next permutation. Then I went to search for the algorithm to generate the permutation. Here is a summary.

algorithm

At present, the following algorithms are commonly used to generate a sequence arrangement:

• Brute force
• Insert method
• Lexicographic method
• SJT algorithm (Steinhaus Johnson Trotter)
• Heap algorithm

Next, we will introduce the content, implementation, advantages and disadvantages of the algorithm.

Before introducing these algorithms, let’s do some examples and code conventions:

• My code implementation is to use go language, and only to achieve the`int`It is not difficult for other types of slices to expand themselves.
• Unless otherwise specified, I assume that`int`There are no duplicate elements in, and some duplicate elements can be de duplicated by themselves. Some algorithms can deal with the problem of duplicate elements.

The full code is on GitHub.

Violence law

describe

Violence lawIt’s a very direct method of divide and Conquer: the arrangement of n-1 elements can be obtained by adding the nth element. The algorithm steps are as follows:

• Exchange the nth element to the last position in turn
• Permutation of n-1 elements before recursive generation
• Adding the last element is the arrangement of n elements

Realization

The algorithm is also very simple. Two auxiliary functions are introduced here, copy and reverse slice. The following code will use:

``````func copySlice(nums []int) []int {
n := make([]int, len(nums), len(nums))
copy(n, nums)
return n
}

//Invert the [I, J] range of slice nums
func reverseSlice(nums []int, i, j int) {
for i < j {
nums[i], nums[j] = nums[j], nums[i]
i++
j--
}
}``````

The algorithm code is as follows:

``````func BruteForce(nums []int, n int, ans *[][]int) {
if n == 1 {
*ans = append(*ans, copySlice(nums))
return
}

n := len(nums)
for i := 0; i < n; i++ {
nums[i], nums[n-1] = nums[n-1], nums[i]
BruteForce(nums, n-1, ans)
nums[i], nums[n-1] = nums[n-1], nums[i]
}
}``````

As an interface, it needs to be as simple as possible. The second parameter initial value is the length of the previous parameter slice. Optimize interface:

``````func bruteForceHelper(nums []int, n int, ans *[][]int) {
//Generating permutation logic
...
}

func BruteForce(nums []int) [][]int{
ans := make([][]int, 0, len(nums))
bruteForceHelper(nums, len(nums), &ans)
return ans
}``````

Advantages: simple and direct logic, easy to understand.

Disadvantage: the number of permutations returned must be`n!`, the key of performance is the size of coefficient. Since each cycle of violence method needs to exchange elements at two positions, it needs toExchange it backIn`n`In large cases, the performance is poor.

Insertion method

describe

As the name implies, the insertion method is to insert elements into all possible positions in a sequence to generate a new sequence. Start with 1 element. For example, to generate`{1,2,3}`Arrangement:

• Starting from sequence 1, insert element 2. There are two positions to insert, generating two sequences 12 and 21
• Insert 3 into all possible positions of the two sequences to generate the final six sequences
``````          1
12          21
123 132 312  213 231 321``````

Realization

The implementation is as follows:

``````func insertHelper(nums []int, n int) [][]int {
if n == 1 {
return [][]int{[]int{nums[0]}}
}

var ans [][]int
for _, subPermutation := range insertHelper(nums, n-1) {
//Insert on position 0-n in turn
for i := 0; i <= len(subPermutation); i++ {
permutation := make([]int, n, n)
copy(permutation[:i], subPermutation[:i])
permutation[i] = nums[n-1]
copy(permutation[i+1:], subPermutation[i:])
ans = append(ans, permutation)
}
}

return ans
}

func Insert(nums []int) [][]int {
return insertHelper(nums, len(nums))
}``````

Advantages: it is also simple, direct and easy to understand.

Disadvantages: due to a lot of data movement in the algorithm, the performance is lower than that of violence method16%

Dictionary method

describe

The premise of this algorithm is that the sequence must be in ascending orderOf course, you can also fine tune the use of other sequences. It gets the next sequence by modifying the current sequence. We define one for each sequenceweight, the size of the numbers formed by the analogy sequence, when the sequence is arranged in ascending order“Weight”Minimum, in descending order“Weight”Maximum. Here is the 1234 arrangement from small to large according to * * weight:

``````1234
1243
1324
1342
1423
1432
2134
...``````

We have observed that the highest order is 1 at the beginning, and we can adjust the order of the next three elements slightly to make the whole“Weight”Increase, analogy integer. When the last three elements are already in reverse order, the highest order of the next sequence must be 2, because only the last three elements can’t be adjusted“Weight”Increased. The core steps of the algorithm are:

• For the current sequence, find the index`i`The following elements are completely reversed.
• Index at this time`i`The element at needs to be changed to a later element that is greater than the minimum value of that element.
• Then the remaining elements are arranged in ascending order, that is, the next sequence of the current sequence.

This algorithm is used in C + + standard library`next_permutation`For the implementation of the algorithm, see GNU C + + STD:: next < permutation.

Realization

``````func NextPermutation(nums []int) bool {
if len(nums) <= 1 {
return false
}

i := len(nums) - 1
for i > 0 && nums[i-1] > nums[i] {
i--
}

//It's all in reverse order. It's at its maximum
if i == 0 {
reverse(nums, 0, len(nums)-1)
return false
}

//Find the element with larger element at Peso index I
j := len(nums) - 1
for nums[j] <= nums[i-1] {
j--
}

nums[i-1], nums[j] = nums[j], nums[i-1]
//Reverse the following elements
reverse(nums, i, len(nums)-1)
return true
}

func lexicographicHelper(nums []int) [][]int {
ans := make([][]int, 0, len(nums))
ans = append(ans, copySlice(nums))
for NextPermutation(nums) {
ans = append(ans, copySlice(nums))
}

return ans
}

func Lexicographic(nums []int) [][]int {
return lexicographicHelper(nums)
}``````

`NextPermutation`The function can be used to solve the previous leetcode algorithm problem. Its return`false`Indicates that the last sequence has been reached.

Advantage:`NextPermutation`It can be used alone and has good performance.

SJT algorithm

describe

SJT algorithm passes the previous permutationExchange only two adjacent elementsTo generate the next spread. For example, an arrangement of 123 is generated in the following order:

``````123 (exchange 23) - >
132 (exchange 13) - >
312 (exchange 12) - >
321 (exchange 32) - >
231 (exchange 31) - >
213``````

A simple scheme is to generate N element permutations through n-1 element permutations. For example, we now use the arrangement of 2 elements to generate the arrangement of 3 elements.

There are only two permutations of the two elements: 1 2 and 2 1.

By inserting 3 at all different positions in the arrangement of 2 elements, we can get the arrangement of 3 elements.

Insert 3 at different positions of 1 2 to obtain: 1 23，1 32 sum312. Insert 3 at different positions of 2 1 to get: 2 13，2 31 sum3 2 1。

Above is the logic of insert method, but the performance of insert method is poor due to a large number of data movements. The SJT algorithm does not require the generation of all n-1 element permutations. It records the direction of each element in the arrangement. The algorithm steps are as follows:

• Finds the largest element that can be moved in a sequence. Moving an element means that its value is greater than the adjacent element it points to.
• Exchange the element with the adjacent element it points to.
• Modify the orientation of all elements whose values are greater than the element.
• Repeat the above steps until there are no movable elements.

Suppose we need to generate all permutations of sequence 1 23 4. First, initialize all elements from right to left. The first arrangement is the initial sequence:

``<1 <2 <3 <4``

All movable elements are 2, 3 and 4. The maximum is 4. We exchange 3 and 4. Since 4 is the largest element at this time, there is no need to change the direction. Get the next permutation:

``<1 <2 <4 <3``

4 is also the largest mobile element, swapping 2 and 4 without changing direction. Get the next permutation:

``<1 <4 <2 <3``

4 is also the largest movable element, exchanging 1 and 4 without changing direction. Get the next permutation:

``<4 <1 <2 <3``

Currently 4 cannot be moved, 3 becomes the largest movable element, exchanging 2 and 3. Note that element 4 is larger than 3, so change the direction of element 4. Get the next permutation:

``>4 <1 <3 <2``

At this time, element 4 becomes the largest movable element, exchanging 4 and 1. Notice that the direction of element 4 has changed. Get the next permutation:

``<1 >4 <3 <2``

Exchange 4 and 3 to get the next permutation:

``<1 <3 >4 <2``

Exchange 4 and 2:

``<1 <3 <2 >4``

At this time, element 3 is the largest element that can be moved. Exchange 1 and 3 to change the direction of element 4:

``<3 <1 <2 <4``

Continue this process, and the final order is（It is strongly recommended that you try）：

``<2 <1 >3 >4``

There are no moving elements. The algorithm is over.

Realization

``````func getLargestMovableIndex(nums []int, dir []bool) int {
maxI := -1
l := len(nums)
for i, num := range nums {
if dir[i] {
if i > 0 && num > nums[i-1] {
if maxI == -1 || num > nums[maxI] {
maxI = i
}
}
} else {
if i < l-1 && num > nums[i+1] {
if maxI == -1 || num > nums[maxI] {
maxI = i
}
}
}
}

return maxI
}

func sjtHelper(nums []int, ans *[][]int) {
l := len(nums)
//True means the direction is right to left
//False means the direction is from left to right
dir := make([]bool, l, l)
for i := range dir {
dir[i] = true
}

maxI := getLargestMovableIndex(nums, dir)
for maxI >= 0 {
maxNum := nums[maxI]
//Exchange the maximum moveable element with the element it points to
if dir[maxI] {
nums[maxI], nums[maxI-1] = nums[maxI-1], nums[maxI]
dir[maxI], dir[maxI-1] = dir[maxI-1], dir[maxI]
} else {
nums[maxI], nums[maxI+1] = nums[maxI+1], nums[maxI]
dir[maxI], dir[maxI+1] = dir[maxI+1], dir[maxI]
}

*ans = append(*ans, copySlice(nums))

//Change the direction of all elements larger than the currently moving element
for i, num := range nums {
if num > maxNum {
dir[i] = !dir[i]
}
}

maxI = getLargestMovableIndex(nums, dir)
}
}

func Sjt(nums []int) [][]int {
ans := make([][]int, 0, len(nums))
ans = append(ans, copySlice(nums))
sjtHelper(nums, &ans)
return ans
}``````

Advantages: as an algorithm thinking can learn from.

Heap algorithm

describe

Heap algorithm is elegant and efficient. It evolved from violence law. We mentioned earlier that the poor performance of violence law is mainly due to multiple exchanges. Heap algorithm improves efficiency by reducing exchanges.

The algorithm steps are as follows:

• If the number of elements is odd, exchange the first and last elements.
• If the number of elements is even, exchange the i-th and last elements in turn.

There are detailed proofs on Wikipedia. You can have a look if you are interested.

Realization

``````func heapHelper(nums []int, n int, ans *[][]int) {
if n == 1 {
*ans = append(*ans, copySlice(nums))
return
}

for i := 0; i < n-1; i++ {
heapHelper(nums, n-1, ans)
if n&1 == 0 {
//If it's even, exchange the I and last elements
nums[i], nums[n-1] = nums[n-1], nums[i]
} else {
//If it's odd, swap the first and last elements
nums[0], nums[n-1] = nums[n-1], nums[0]
}
}
heapHelper(nums, n-1, ans)
}

//Heap uses heap algorithm to generate permutation
func Heap(nums []int) [][]int {
ans := make([][]int, 0, len(nums))
heapHelper(nums, len(nums), &ans)
return ans
}``````

Heap algorithm is very difficult to understand and easy to write wrong. Now I just memorize it It is.

Advantages: simple and efficient code implementation.