Skip to content

Commit 0e154ef

Browse files
committed
Refactor "Permutations" into an iterator method
1 parent 4272b2b commit 0e154ef

File tree

4 files changed

+94
-211
lines changed

4 files changed

+94
-211
lines changed

MoreLinq.Test/PermutationsTest.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,14 @@ public void TestPermutationsAreIndependent()
184184
var permutedSets = set.Permutations();
185185

186186
var listPermutations = new List<IList<int>>();
187-
listPermutations.AddRange(permutedSets);
187+
foreach (var ps in permutedSets)
188+
{
189+
Assert.That(ps, Is.Not.All.Negative);
190+
listPermutations.Add(ps);
191+
for (var i = 0; i < ps.Count; i++)
192+
ps[i] = -1;
193+
}
194+
188195
Assert.That(listPermutations, Is.Not.Empty);
189196

190197
for (var i = 0; i < listPermutations.Count; i++)

MoreLinq/Extensions.g.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4644,7 +4644,6 @@ public static TResult Partition<TKey, TElement, TResult>(this IEnumerable<IGroup
46444644
[GeneratedCode("MoreLinq.ExtensionsGenerator", "1.0.0.0")]
46454645
public static partial class PermutationsExtension
46464646
{
4647-
46484647
/// <summary>
46494648
/// Generates a sequence of lists that represent the permutations of the original sequence.
46504649
/// </summary>

MoreLinq/NestedLoops.cs

Lines changed: 0 additions & 52 deletions
This file was deleted.

MoreLinq/Permutations.cs

Lines changed: 86 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -18,165 +18,11 @@
1818
namespace MoreLinq
1919
{
2020
using System;
21-
using System.Collections;
2221
using System.Collections.Generic;
23-
using System.Diagnostics.CodeAnalysis;
2422
using System.Linq;
2523

2624
public static partial class MoreEnumerable
2725
{
28-
/// <summary>
29-
/// The private implementation class that produces permutations of a sequence.
30-
/// </summary>
31-
32-
sealed class PermutationEnumerator<T> : IEnumerator<IList<T>>
33-
{
34-
// NOTE: The algorithm used to generate permutations uses the fact that any set
35-
// can be put into 1-to-1 correspondence with the set of ordinals number (0..n).
36-
// The implementation here is based on the algorithm described by Kenneth H. Rosen,
37-
// in Discrete Mathematics and Its Applications, 2nd edition, pp. 282-284.
38-
//
39-
// There are two significant changes from the original implementation.
40-
// First, the algorithm uses lazy evaluation and streaming to fit well into the
41-
// nature of most LINQ evaluations.
42-
//
43-
// Second, the algorithm has been modified to use dynamically generated nested loop
44-
// state machines, rather than an integral computation of the factorial function
45-
// to determine when to terminate. The original algorithm required a priori knowledge
46-
// of the number of iterations necessary to produce all permutations. This is a
47-
// necessary step to avoid overflowing the range of the permutation arrays used.
48-
// The number of permutation iterations is defined as the factorial of the original
49-
// set size minus 1.
50-
//
51-
// However, there's a fly in the ointment. The factorial function grows VERY rapidly.
52-
// 13! overflows the range of a Int32; while 28! overflows the range of decimal.
53-
// To overcome these limitations, the algorithm relies on the fact that the factorial
54-
// of N is equivalent to the evaluation of N-1 nested loops. Unfortunately, you can't
55-
// just code up a variable number of nested loops ... this is where .NET generators
56-
// with their elegant 'yield return' syntax come to the rescue.
57-
//
58-
// The methods of the Loop extension class (For and NestedLoops) provide the implementation
59-
// of dynamic nested loops using generators and sequence composition. In a nutshell,
60-
// the two Repeat() functions are the constructor of loops and nested loops, respectively.
61-
// The NestedLoops() function produces a composition of loops where the loop counter
62-
// for each nesting level is defined in a separate sequence passed in the call.
63-
//
64-
// For example: NestedLoops( () => DoSomething(), new[] { 6, 8 } )
65-
//
66-
// is equivalent to: for( int i = 0; i < 6; i++ )
67-
// for( int j = 0; j < 8; j++ )
68-
// DoSomething();
69-
70-
readonly IList<T> valueSet;
71-
readonly int[] permutation;
72-
readonly IEnumerable<Action> generator;
73-
74-
IEnumerator<Action> generatorIterator;
75-
bool hasMoreResults;
76-
77-
IList<T>? current;
78-
79-
public PermutationEnumerator(IEnumerable<T> valueSet)
80-
{
81-
this.valueSet = valueSet.ToArray();
82-
this.permutation = new int[this.valueSet.Count];
83-
// The nested loop construction below takes into account the fact that:
84-
// 1) for empty sets and sets of cardinality 1, there exists only a single permutation.
85-
// 2) for sets larger than 1 element, the number of nested loops needed is: set.Count-1
86-
this.generator = NestedLoops(NextPermutation, Generate(2UL, n => n + 1).Take(Math.Max(0, this.valueSet.Count - 1)));
87-
Reset();
88-
}
89-
90-
[MemberNotNull(nameof(generatorIterator))]
91-
public void Reset()
92-
{
93-
this.current = null;
94-
this.generatorIterator?.Dispose();
95-
// restore lexographic ordering of the permutation indexes
96-
for (var i = 0; i < this.permutation.Length; i++)
97-
this.permutation[i] = i;
98-
// start a new iteration over the nested loop generator
99-
this.generatorIterator = this.generator.GetEnumerator();
100-
// we must advance the nested loop iterator to the initial element,
101-
// this ensures that we only ever produce N!-1 calls to NextPermutation()
102-
_ = this.generatorIterator.MoveNext();
103-
this.hasMoreResults = true; // there's always at least one permutation: the original set itself
104-
}
105-
106-
public IList<T> Current
107-
{
108-
get
109-
{
110-
Debug.Assert(this.current is not null);
111-
return this.current;
112-
}
113-
}
114-
115-
object IEnumerator.Current => Current;
116-
117-
public bool MoveNext()
118-
{
119-
this.current = PermuteValueSet();
120-
// check if more permutation left to enumerate
121-
var prevResult = this.hasMoreResults;
122-
this.hasMoreResults = this.generatorIterator.MoveNext();
123-
if (this.hasMoreResults)
124-
this.generatorIterator.Current(); // produce the next permutation ordering
125-
// we return prevResult rather than m_HasMoreResults because there is always
126-
// at least one permutation: the original set. Also, this provides a simple way
127-
// to deal with the disparity between sets that have only one loop level (size 0-2)
128-
// and those that have two or more (size > 2).
129-
return prevResult;
130-
}
131-
132-
void IDisposable.Dispose() => this.generatorIterator.Dispose();
133-
134-
/// <summary>
135-
/// Transposes elements in the cached permutation array to produce the next permutation
136-
/// </summary>
137-
void NextPermutation()
138-
{
139-
// find the largest index j with m_Permutation[j] < m_Permutation[j+1]
140-
var j = this.permutation.Length - 2;
141-
while (this.permutation[j] > this.permutation[j + 1])
142-
j--;
143-
144-
// find index k such that m_Permutation[k] is the smallest integer
145-
// greater than m_Permutation[j] to the right of m_Permutation[j]
146-
var k = this.permutation.Length - 1;
147-
while (this.permutation[j] > this.permutation[k])
148-
k--;
149-
150-
(this.permutation[j], this.permutation[k]) = (this.permutation[k], this.permutation[j]);
151-
152-
// move the tail of the permutation after the jth position in increasing order
153-
for (int x = this.permutation.Length - 1, y = j + 1; x > y; x--, y++)
154-
(this.permutation[x], this.permutation[y]) = (this.permutation[y], this.permutation[x]);
155-
}
156-
157-
/// <summary>
158-
/// Creates a new list containing the values from the original
159-
/// set in their new permuted order.
160-
/// </summary>
161-
/// <remarks>
162-
/// The reason we return a new permuted value set, rather than reuse
163-
/// an existing collection, is that we have no control over what the
164-
/// consumer will do with the results produced. They could very easily
165-
/// generate and store a set of permutations and only then begin to
166-
/// process them. If we reused the same collection, the caller would
167-
/// be surprised to discover that all of the permutations looked the
168-
/// same.
169-
/// </remarks>
170-
/// <returns>Array of permuted source sequence values</returns>
171-
T[] PermuteValueSet()
172-
{
173-
var permutedSet = new T[this.permutation.Length];
174-
for (var i = 0; i < this.permutation.Length; i++)
175-
permutedSet[i] = this.valueSet[this.permutation[i]];
176-
return permutedSet;
177-
}
178-
}
179-
18026
/// <summary>
18127
/// Generates a sequence of lists that represent the permutations of the original sequence.
18228
/// </summary>
@@ -205,10 +51,93 @@ public static IEnumerable<IList<T>> Permutations<T>(this IEnumerable<T> sequence
20551

20652
return _(); IEnumerable<IList<T>> _()
20753
{
208-
using var iter = new PermutationEnumerator<T>(sequence);
54+
// The algorithm used to generate permutations uses the fact that any set can be put
55+
// into 1-to-1 correspondence with the set of ordinals number (0..n). The
56+
// implementation here is based on the algorithm described by Kenneth H. Rosen, in
57+
// Discrete Mathematics and Its Applications, 2nd edition, pp. 282-284.
58+
59+
var valueSet = sequence.ToArray();
60+
61+
// There's always at least one permutation: a copy of original set.
62+
63+
yield return (IList<T>)valueSet.Clone();
64+
65+
// For empty sets and sets of cardinality 1, there exists only a single permutation.
66+
67+
if (valueSet.Length is 0 or 1)
68+
yield break;
69+
70+
var permutation = new int[valueSet.Length];
71+
72+
// Initialize lexographic ordering of the permutation indexes.
73+
74+
for (var i = 0; i < permutation.Length; i++)
75+
permutation[i] = i;
76+
77+
// For sets larger than 1 element, the number of nested loops needed is one less
78+
// than the set length. Note that the factorial grows VERY rapidly such that 13!
79+
// overflows the range of an Int32 and 28! overflows the range of a Decimal.
80+
81+
ulong factorial = valueSet.Length switch
82+
{
83+
0 => 1,
84+
1 => 1,
85+
2 => 2,
86+
3 => 6,
87+
4 => 24,
88+
5 => 120,
89+
6 => 720,
90+
7 => 5_040,
91+
8 => 40_320,
92+
9 => 362_880,
93+
10 => 3_628_800,
94+
11 => 39_916_800,
95+
12 => 479_001_600,
96+
13 => 6_227_020_800,
97+
14 => 87_178_291_200,
98+
15 => 1_307_674_368_000,
99+
16 => 20_922_789_888_000,
100+
17 => 355_687_428_096_000,
101+
18 => 6_402_373_705_728_000,
102+
19 => 121_645_100_408_832_000,
103+
20 => 2_432_902_008_176_640_000,
104+
_ => throw new OverflowException("Too many permutations."),
105+
};
106+
107+
for (var n = 1UL; n < factorial; n++)
108+
{
109+
// Transposes elements in the cached permutation array to produce the next
110+
// permutation.
111+
112+
// Find the largest index j with permutation[j] < permutation[j+1]:
113+
114+
var j = permutation.Length - 2;
115+
while (permutation[j] > permutation[j + 1])
116+
j--;
117+
118+
// Find index k such that permutation[k] is the smallest integer greater than
119+
// permutation[j] to the right of permutation[j]:
120+
121+
var k = permutation.Length - 1;
122+
while (permutation[j] > permutation[k])
123+
k--;
209124

210-
while (iter.MoveNext())
211-
yield return iter.Current;
125+
(permutation[j], permutation[k]) = (permutation[k], permutation[j]);
126+
127+
// Move the tail of the permutation after the j-th position in increasing order.
128+
129+
for (int x = permutation.Length - 1, y = j + 1; x > y; x--, y++)
130+
(permutation[x], permutation[y]) = (permutation[y], permutation[x]);
131+
132+
// Yield a new array containing the values from the original set in their new
133+
// permuted order.
134+
135+
var permutedSet = new T[permutation.Length];
136+
for (var i = 0; i < permutation.Length; i++)
137+
permutedSet[i] = valueSet[permutation[i]];
138+
139+
yield return permutedSet;
140+
}
212141
}
213142
}
214143
}

0 commit comments

Comments
 (0)