how does this for loop work in C# for (; (p %= l) != 0; l -= p, index += p)

249 views Asked by At

I found this for loop in one of the solutions for Rotating array problem. I am not able to understand this line in the code.

    public void RotateArray(int[] nums, int p)
    {
        var l = nums.Length;
        var index = 0;
        for (; (p %= l) != 0; l -= p, index += p)
        {
            for (int i = 0; i < p; i++)
            {
                var temp = nums[l - p + i + index];
                nums[l - p + i + index] = nums[i + index];
                nums[i + index] = temp;
            }
        }
    }

this solution is working but not able to get the meaning of the condition in this for loop.

4

There are 4 answers

1
LWChris On BEST ANSWER

When a for loop is confusing, it's sometimes helpful to replace it with a while loop, because it has a more ordered depiction of the code execution order, i.e. you can read the code "from top to bottom" rather than having to jump around.

A for loop with a code of

code before loop;
for (initialization; condition; modifications) {
  loop body;
}
code after loop;

can be translated into this while:

code before loop;
initialization;
while (condition) {
  loop body;
  modifications;
}
code after loop;

Now let's modify your given code:

public void RotateArray(int[] nums, int p)
{
    var l = nums.Length;
    var index = 0;
    while ((p %= l) != 0)
    {
        for (int i = 0; i < p; i++)
        {
            var temp = nums[l - p + i + index];
            nums[l - p + i + index] = nums[i + index];
            nums[i + index] = temp;
        }
        l -= p;
        index += p;
    }
}

We can make it even easier on ourselves by changing a few more things:

  • replacing the abbreviated variables p and l with places and length
  • renamed index to offset (you will see why)
  • extracting the assignment from the while loop condition
  • Create variables a and b for the swap indices
  • Use tuple assignment and decomposition to swap the array contents
  • rewriting the compound assignments as normal assignments (this one is personal taste; I'd usually keep them in my code, but sometimes writing them explicitly helps, and when reading complicated code, every little bit might help)

All this is done to reduce the number of things we have to keep mental track of:

public void RotateArray(int[] nums, int places)
{
    var length = nums.Length;
    var offset = 0;
    places = places % length;

    while (places != 0)
    {
        for (int i = 0; i < places; i++)
        {
            var a = i + offset;
            var b = length - places + i + offset;
            (nums[a], nums[b]) = (nums[b], nums[a]);
        }

        length = length - places;
        offset = offset + places;
        places = places % length;
    }
}

To understand what this does, it's best to look at an example:

Example of rotating a length-7 array by 5 places

What we can see is that after each outer loop, a part of the array has been successfully rotated. This is what offset (or originally index) keeps track of. length (or l) keeps track of the length of the "rest" that needs a few rotations still.

We can also see that the modulus operator % is there to make sure that we keep the indices down within the array. It's clear that rotating a length-7 array by 4 would be the same as rotating it by 11, 18, 25, etc., or generally speaking, rotating an array of length n by (c * n) + x places is the same as rotating by just x places, which is what the modulus is for.

And lastly we see that offset/index is basically just use to shift all activities into the region we are working on.

The "difficult" part in understanding lies in proving that after each inner loop, the left "done" part has increased, and understanding why you can calculate the next places/p like this. But mathematically proving this algorithm goes beyond the scope of this post.

In general I recommend to create such visualizations as I did, which keep track of variable values and the manipulations. Do it for different values of length and places (e.g. 7/5) to get an even better understanding.

As others have pointed out, this code is very C#-unlike, where the order in which we value property of code is in general "clarity > brevity > performance" with exceptions only being made where warranted by the application.

5
Jeremy Thompson On

That is extremely unreadable code, borderline obfuscated. Using a While loop will make it easier to understand, all it's doing is swapping the items in the array by shuffling them with a rotation the number of steps:

public void RotateArray(int[] array, int steps)
{
    int length = array.Length;
    steps %= length;

    if (steps == 0)
        return; // No need to rotate if steps is 0

    Shuffle(array, 0, length - 1);
    Shuffle(array, 0, steps - 1);
    Shuffle(array, steps, length - 1);
}

public void Shuffle(int[] array, int start, int end)
{
    while (start < end)
    {
        int temp = array[start];
        array[start] = array[end];
        array[end] = temp;
        start++;
        end--;
    }
}

I'd recommend doing it with LINQ:

public int[] RotateArray(int[] array, int steps)
{
    steps %= array.Length;
    return array.Skip(steps).Concat(array.Take(steps)).ToArray();
}
0
Jeremy Lakeman On

Unravelling the syntactic sugar of that for loop. Trying to keep the rest of the code the same, but making it more readable by renaming variables;

public void RotateArray(int[] nums, int steps)
{
    var length = nums.Length;
    var start = 0;

    while (true)
    {
        steps = steps % length;
        if (steps == 0)
            break;

        var dest = start + length - steps;

        for (int i = 0; i < steps; i++)
        {
            var temp = nums[dest + i];
            nums[dest + i] = nums[start + i];
            nums[start + i] = temp;
        }

        length -= steps;
        start += steps;
    }
}

On each iteration of the loop, the first steps items in the array are swapped with the last steps items. Then we advance by steps and do it again.

If these two ranges don't overlap. Then you can consider that the last steps elements of the array are used as temporary space to shuffle each chunk of the array forwards.

Once these two ranges overlap, the result is a bit messy. eg for RotateArray({0,1,2,3,4,5,6,7,8,9}, 7) the first loop would end up with {3,4,5,6,7,8,9,1,2,0}. The last steps items were moved to the start, the first dest - start items were moved to the end. But the order of these items was also rotated since the elements were swapped a different number of times. Since (dest - start) % steps != 0.

But we're not done yet. We'll keep shuffling the remainder of the array until those last items have been fixed.

4
LWChris On

To understand your actual code from the question, see my other answer.

This answer is meant to guide you towards an implementation that is much more like what C# code would usually look like:

Improvement 1: Using LINQ and Array.Copy

While there is no need to use LINQ for collection manipulation, it can greatly reduce the amount of code you need to write. The named functions Skip and Take also help with understanding what the code does. Since LINQ will create a new array, you have to use Array.Copy to modify the content of the parameter in-place.

using System;
using System.Linq;

public void RotateArray(int[] nums, int places)
{
    var length = nums.Length;
    places %= length;
    var rotated = nums.Skip(length - places).Concat(nums.Take(length - places)).ToArray();
    Array.Copy(rotated, nums, length);
}

Improvement 2: Make the function pure

Over the years, C# has developed a tendency away from in-place modification to "pure" functions, at least for these functions on basic types such as an array. Pure functions do not alter the state of the object or the parameters. This is also called "having no side effects" and essentially means it can be called multiple times on the same object with the same arguments, and will always return the same result. Very well-known examples are string functions such as Substring or PadLeft.

Marking the method with the [Pure] attribute makes sure an editor or IDE with code analysis will show a warning for usages of the function where the return value is not used (try "1".PadLeft(3, '0'); without assigning the result to a variable).

using System;
using System.Linq;
using System.Diagnostics.Contracts;

[Pure]
public int[] RotateArray(int[] nums, int places)
{
    var length = nums.Length;
    places %= length;
    return nums.Skip(length - places).Concat(nums.Take(length - places)).ToArray();
}

Improvement 3: A generic extension

In an even better version, you write this code using generics. This means you can now use this function not only to rotate an array of int, but an array of any type.

Creating the function as an extension means you can simply type nums.Rotate(5); instead of RotateArray(nums, 5).

using System;
using System.Linq;
using System.Diagnostics.Contracts;

public static class ArrayExtensions
{
    [Pure]
    public static T[] Rotate<T>(this T[] source, int places)
    {
        var length = source.Length;
        places %= length;
        return source.Skip(length - places).Concat(source.Take(length - places)).ToArray();
    }
}

Improvement 4: Input validation and shifting backwards

You should also validate the parameters, e.g. checking for null and throwing an ArgumentNullException. We can also add the [NotNull] attribute to help the static null-state analysis know what variables can be null where in the code.

And while we're at it, we can deal with negative values of places by calling ArgumentOutOfRangeException.ThrowIfNegative, or we can rather elegantly use negative values to allow shifting in the opposite direction.

Note how we also added a shortcut for when we don't need to rotate anything. We still create a new array using the ToArray method to make sure the return value is always a new array, and not unexpectedly the input one but only in these special cases.

using System;
using System.Linq;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

public static class ArrayExtensions
{
    [Pure]
    [return: NotNull]
    public static T[] Rotate<T>([NotNull] this T[] source, int places)
    {
        ArgumentNullException.ThrowIfNull(source);
        // ArgumentOutOfRangeException.ThrowIfNegative(places);
        
        var length = source.Length;
        if (length == 0) {
            throw new InvalidOperationException("Cannot rotate empty array.");
        }
        places %= length;
        if (places == 0) {
            return source.ToArray();
        } else if (places < 0) {
            places += length;
        }
        return source.Skip(length - places).Concat(source.Take(length - places)).ToArray();
    }
}

Optional: Use ranges

And finally, if you want to make it even a bit shorter yet, you can use the Range feature instead of LINQ's Skip and Take. It's likely personal preference what is considered easier to read and understand.

using System;
using System.Linq;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

public static class ArrayExtensions
{
    [Pure]
    [return: NotNull]
    public static T[] Rotate<T>([NotNull] this T[] source, int places)
    {
        ArgumentNullException.ThrowIfNull(source);
        // ArgumentOutOfRangeException.ThrowIfNegative(places);
        
        var length = source.Length;
        if (length == 0) {
            throw new InvalidOperationException("Cannot rotate empty array.");
        }
        places %= length;
        if (places == 0) {
            return source.ToArray();
        } else if (places < 0) {
            places += length;
        }
        var wrap = length - places;
        return source[wrap..].Concat(source[..wrap]).ToArray();
    }
}