Sepa
Sepa
Abstract
Research in progress developing a fast algorithm which will find all possible
permutations of a data set without the use of "helping" data structures or recursion.
Three steps are needed to convert a data set to its next permutation. The first step is to
find the key field for this permutation. The first field which changes between the two
permutations is the key field. The key field in "abdca" as it becomes "acabd" (its next
sorted order permutation) is the field containing the value 'b'.
The key field can be determined by checking pairs of digits (or fields) starting from the
rear (or right side) of the set. For each pair, if the left value of the pair is smaller than the
right, then the field on the left is the key field. At this point all values to the right of the
key field must be in sorted order from right to left (if not a field to the right would be the
key field). If the key does not exist, the last permutation has been created. At this time,
the data set is in reverse sorted order (from left to right).
For clarification, those values to the right of the key field will be termed the "tail" of the
set, while those to the left will be the "head". Note that the head, key, and tail will be
different for each permutation.
To find the next permutation in order, the key field must be exchanged with the smallest
value in the tail which is larger than the key value. For example in the data set "abdca"
the field with the value 'b' is the key, and the field containing the value 'c' has the smallest
value in the tail which is larger than the key value. After swapping values, the set
becomes "acdba". It is possible to find this value by comparing each value with the key
value, starting at the end of the data set. The first value larger than the key value is the
value to be exchanged with the key value. Notice that the tail (the 'c' is the key now) is
still in reverse sorted order. This is important, because the last step is putting the tail into
sorted order.
Since the tail is in reverse sorted order, to sort it requires only a single loop, which works
from both ends of the tail at the same time, swapping each pair of items while moving to
the center of the tail. In this example, a character data set was used, but as stated before
the logic behind this algorithm will work with other data sets, as long as there is a less
than/greater than relationship among the data items. If all permutations are required, it is
necessary that the original data set be the first permutation (exist in sorted order).
A quick review of the algorithm shows it is compose of three loops (an example in C is
given as Listing 1). The first finds the key. The second finds the value to swap with the
key. The third reorders the tail from reverse order to sorted order. A short analysis shows
all three loops are bounded by the size of the data set, therefore the running time is
bounded by O(n).
References
Listing 1
#include<STDIO.H>
#include<STDLIB.H>
/* The key value is the first value from the end which
is smaller than the value to its immediate right */
key--;
newkey=len-1;
while( (newkey > key) && (str[newkey] <= str[key]) )
{
newkey--;
}
/* variables len and key are used to walk through the tail,
exchanging pairs from both ends of the tail. len and
key are reused to save memory */
len--;
key++;
while(len>key)
{
swap(str,len,key);
key++;
len--;
}
return 1;
}
void main()
{
/* test data */
do {
printf("%s\n",test_string);
} while( permute(test_string, strlen(test_string)) );
}
{menu}
Consider the following example regarding a prioritized list. Imagine that your house keys
were lost while surfing the net at a friend's house. Where would you begin your search?
Would your search begin
a.) near the computer?
b.) from an adjacent room?
c.) at your house?
d.) at a neighbor’s house?
Odds are you would retrace your steps by first searching your pockets and the
surrounding area repeatedly long before you would even search the neighbor’s house.
Here choice "a" is intentionally placed first immediately followed by an ordered list of
less likely choices (by degree). Choices very similar to these can be initially prioritized
within an array before a permutation is even attempted. Frequently permutations begin
with a known solution and attempt to improve upon agreeable ideas as permutable
subsets. Simply stated, time management is vital when dealing with very large arrays or
large character strings.
Furthermore, assume that a fast computer can complete a permutation set using a 50-
element target array in exactly 10 seconds. It's also reasonable to assume this computer
can complete a permutation set using a larger target array within a greater time period.
Using the presented scenario above and the known factorial properties of a permutation,
the following conclusions can be established:
A permutation over a new 51-element array would complete in approximately:
10 Seconds * 51 = 510 Seconds or 8.5 Minutes
A permutation over a new 52-element array would complete in approximately:
10 Seconds * 51 * 52 = 26520 Seconds or 442 Minutes or 7.37 Hours
OR 8.5 Minutes * 52 = 442 Minutes or 7.37 Hours
A permutation over a new 53-element array would complete in approximately:
10 Seconds * 51 * 52 * 53 = 1405560 Seconds or 16.27 Days
A permutation over a new 54-element array would complete in approximately:
10 Seconds * 51 * 52 * 53 * 54 = 75900240 Seconds or 2.41 Years
A permutation over a new 55-element array would complete in approximately:
10 Seconds * 51 * 52 * 53 * 54 * 55 = 4174513200 Seconds or 132.37 Years
In the above scenario, an absolute solution can probably be identified within a 53-element
array. Using an array with more than 53-elements would cause an unreasonable and
predictable wait. The QuickPerm algorithm presented below uses permutation subsets on
the head of the target array. Often the target array a[N] is simply used to index large
complex data structures. Here's the actual QuickPerm C++ algorithm for your review:
QuickPerm - Head Permutations Using a Linear Array Without Recursion
void QuickPerm(void)
{
unsigned int a[N], p[N+1];
register unsigned int i, j, tmp; // Upper Index i; Lower Index j
p[0] = 0
p[1] = 1
p[2] = 2
...
p[N] = N
Initially the default upper index variable i is set to one as a reference to the second least
significant odometer digit which in-turn is immediately reduced by one (a countdown
process). Since the upper index variable is clearly odd, the default lower index variable j
is assigned to the odometer digit referenced by the upper index which coincides to the
value now stored in p[i] or zero. Both index variables are properly set and the first
SWAP is called...
Before another swap can take place, upper and lower index values must be recalculated
based upon values stored in the base N odometer p[N]. Again the upper index begins at
the binary digit p[1] and resets each consecutive ZERO digit to a corresponding index
value (maximum digit). The upper index will settle upon the first odometer digit that is
not zero then reduce the referenced digit by one (again a true countdown process). If the
upper index is even, the lower index is assigned to the least significant odometer digit
which coincides to the value stored in p[0] (or simply the lower index will remain at
zero). Otherwise the lower index will be assigned to the actual odometer digit now
referenced by the upper index. A SWAP is executed and this process continues until the
upper index attempts to reference an odometer digit beyond the length of the linear
permutable target in question.
Notice the lower index always follows the upper index's lead. Observable modifications
compared to the counting QuickPerm algorithm include the following:
Using a nested while loop instead of a conditional if-else statement (as described in the
counting QuickPerm algorithm) clearly lead to the development of a Meta-permutation
class. Although this conditional optimization can be made regardless of the counting
process utilized, the Meta-permutation class precludes such an optimization in order to
return valid indices.
Fundamental Analysis of QuickPerm Above {Switch to Characters}
Number of Total Number of Unique
a[N] Display of a[N] Display of
Objects Combinations Produced
Initial Sequence Final Sequence
to Permute (N) (N!)
1 2 3 . . . (N - 1) N 3 4 . . . (N - 2) 1 2 (N -
if N is even (N - 1)! * N = N!
N 1)
1 2 3 . . . (N - 1)
if N is odd N 2 3 . . . (N - 1) 1 (N - 1)! * N = N!
N
A Permutation Ruler:
The graph below reflects how the value stored in the lower index variable j consistently
reduces by one when the upper index variable i is odd, otherwise j is assigned a zero
value. For each reference to j when i is odd, the lower index variable j will be assigned
to the upper index variable “i – 1” then “i – 2” until the lower index variable j
reaches zero. To control this iteration behavior, the value stored in the controller array
p[i] is initialized to the same value stored in the upper index variable i (as mentioned
above) then repeatedly reduced by one and assigned to the lower index variable j. This
process continues until the lower index variable j reaches 0 then repeats. (Please note,
the lower index variable j can also begin at zero and advance by one until the upper
index value is reached.) This behavior is strictly controlled by the following statement:
“IF i is odd then j = p[i] otherwise j = 0”
Which in-turn translates to the following C++ statement:
j = i % 2 * p[i];
(Click here to enlarge graph.)