• No results found

Prove that if a rotation of n elements has a trivial cycle, then it has n trivial cycles (In other words, a rotation either moves all elements or no elements.)

It turns out that the number of cycles is gcd(k,n), so we should be able to do the rotation in n + gcd(k,n) assignments, 5 instead of the 3(n – gcd(n,k)) we needed for the Gries-Mills algorithm. Furthermore, in practice

GCD is very small; in fact, it is 1 (that is, there is only one cycle) about 60% of the time. So a rotate algorithm that exploits cycles always does fewer assignments.

5 See Elements of Programming, Section 10.4, for the proof.

There is one catch: Gries-Mills only required moving one step forward; it works even for singly linked lists. But if we want to take advantage of cycles, we need to be able to do long jumps. Such an algorithm requires stronger requirements on the iterators—namely, the ability to do random access.

To create our new rotate function, we’ll first write a helper function that moves every element in a cycle to its next position. But instead of saying “which position does the item in position x move to,” we’ll say “in which position do we find the item that’s going to move to position x.” Even though these two operations are symmetric mathematically, it turns out that the latter is more efficient, since it needs only one saved temporary variable per cycle, instead of one for every item that needs to be moved (except the last). Here’s our helper function:

Click here to view code image

template <ForwardIterator I, Transformation F> void rotate_cycle_from(I i, F from) {

ValueType<I> tmp = *i; I start = i;

for (I j = from(i); j != start; j = from(j)) { *i = *j;

i = j; }

*i = tmp; }

Note that we’re using the ValueType type function we defined near the end of Section 10.8. How does rotate_cycle_from know which position an item comes from? That information will be

encapsulated in a function object from that we pass in as an argument. You can think of from(i) as “compute where the element moving into position i comes from.”

The function object we’re going to pass to rotate_cycle_from will be an instance of rotate_transform:

Click here to view code image

template <RandomAccessIterator I> struct rotate_transform { DifferenceType<I> plus; DifferenceType<I> minus; I m1; rotate_transform(I f, I m, I l) : plus(m - f), minus(m - l), m1(f + (l - m)) {} // m1 separates items moving forward and backward I operator()(I i) const {

return i + ((i < m1) ? plus : minus); }

};

The idea is that even though we are conceptually “rotating” elements, in practice some items move forward and some move backward (because the rotation caused them to wrap around the end of our range). When rotate_transform is instantiated for a given set of ranges, it precomputes (1) how much to move forward for items that should move forward, (2) how much to move backward for things that move backward, and (3) what

items that should move forward, (2) how much to move backward for things that move backward, and (3) what

the crossover point is for deciding when to move forward and when to move backward.

Now we can write the cycle-exploiting version of algorithm for rotation, which is a variation of the algorithm discovered by Fletcher and Silver in 1965:

Click here to view code image

template <RandomAccessIterator I>

I rotate(I f, I m, I l, std::random_access_iterator_tag) { if (f == m) return l;

if (m == l) return f;

DifferenceType<I> cycles = gcd(m - f, l - m); rotate_transform<I> rotator(f, m, l);

while (cycles-- > 0) rotate_cycle_from(f + cycles, rotator); return rotator.m1;

}

After handling some trivial boundary cases, the algorithm first computes the number of cycles (the GCD) and constructs a rotate_transform object. Then it calls rotate_cycle_from to shift all the elements along each cycle, and repeats this for every cycle.

Let’s look at an example. Consider the rotation k = 2 for n = 6 elements that we used at the beginning of this section. For simplicity, we’ll assume that our values are integers stored in an array:

0 1 2 3 4 5

We also assume our iterators are integer offsets in an array, starting at 0. (Be careful to distinguish between the values at a position and the position itself.) To perform a k = 2 rotation, we’ll need to pass the three iterators f = 0, m = 4, and l = 6:

0 1 2 3 4 5 f m l

The boundary cases of our new rotate algorithm don’t apply, so the first thing it does is compute the number of cycles, which is equal to gcd(m – f, l – m) = gcd(4, 2) = 2. Then it constructs the rotator object, initializing its state variables as follows:

plus ← m – f = 4 – 0 = 4 minus ← m – l = 4 – 6 = – 2 m1 ← f + (l – m) = 0 + (6 – 4) = 2

The main loop of the function rotates all elements of a cycle, then moves on to the next cycle. Let us see what happens when rotate_cycle_from is called.

Initially, we pass f + d = 0 + 2 = 2 as the first argument. So inside the function, i = 2. We save the value at position 2, which is also 2, to our tmp variable and set start to our starting position of 2.

Now we go through a loop as long as a new variable, j, is not equal to start. Each time through the loop, we are going to set j by using the rotator function object that we passed in through the variable from. Basically, all that object does is add the stored values plus or minus to its argument, depending on whether the argument is less than the stored value m1. For example, if we call from(0), it will return 0 + 4, or 4, since 0 is less than 2. If we call from(4), it will return 4 + (–2), or 2, since 4 is not less than 2.

Here’s how the values in our array change as we go through the loop in rotate_cycle_from: i ← 2, j ← from(2) = 0 0 1 2 3 4 5 j i *i ← *j 0 1 0 3 4 5 j i i ← j = 0, j ← from(0) = 4 0 1 0 3 4 5 i j *i ← *j 4 1 0 3 4 5 i j

4 1 0 3 4 5 j i *i ← tmp 4 1 0 3 2 5 j i

This completes the first call to rotate_cycle_from in the while loop of our rotate function. Exercise 11.10. Continue to trace the preceding example until the rotate function finishes.

Notice that the signatures of this rotate function and the previous one differ by the type of the last argument. In the next section, we’ll write the wrapper that lets the fastest implementation for a given situation be automatically invoked.

When Is an Algorithm Faster in Practice?

We have seen an example where one algorithm does fewer assignments than another algorithm. Does that mean it will run faster? Not necessarily. In practice, the ability to fit relevant data in cache can make a dramatic difference in this speed. An algorithm that involves large jumps in memory—that is, one that has poor locality of reference—may end up being slower than one that requires more assignments but has better locality of reference.

11.5 Reverse

Another fundamental algorithm is reverse, which (obviously) reverses the order of the elements of a sequence. More formally, reverse permutes a k-element list such that item 0 and item k − 1 are swapped, item 1 and item k − 2 are swapped, and so on.

If we have reverse, we can implement rotate in just three lines of code:

Click here to view code image

template <BidirectionalIterator I>

void three_reverse_rotate(I f, I m, I l) { reverse(f, m);

reverse(m, l); reverse(f, l); }

For example, suppose we want to perform our k = 2 rotation on the sequence 0 1 2 3 4 5. The algorithm would perform the following operations:

Click here to view code image

f m l start 0 1 2 3 4 5 reverse(f, m) 3 2 1 0 4 5 reverse(m, l) 3 2 1 0 5 4 reverse(f, l) 4 5 0 1 2 3