What is the best way to endlessly rotate 3 items in a list in C#

Viewed 75

I have a list with 3 members and I want to rotate them around on command. I created a method:

        public bool Rotate()
        {
            string a = this.doer[0];
            string b = this.doer[1];
            string c = this.doer[2];
            this.doer.Clear();
            this.doer.Add(c);
            this.doer.Add(a);
            this.doer.Add(b);
            return true;
        }

Is there a better and more efficient way to do it?

3 Answers

Ideally this would be achieved with a linked list, the advantage is that only node references are disconnected and no internal copying is needed. A C# Queue is actually based a linked list, so this is a fairly appropriate data structure.

However, this can also be done with a List at a small cost. When you do an Insert or Remove, it internally calls Array.Copy and although it's still O(n), providing it doesn't fail a capacity check there is no allocation and is extremely efficient and optimized.

Example Extensions

public static void RotateDown<T>(this IList<T> source)
{
   // ideally you would remove first to save a potential capacity expansion
   source.Add(source[0]);
   source.RemoveAt(0);
}
public static void RotateUp<T>(this IList<T> source)
{
   // ideally you would remove first to save a potential capacity expansion
   source.Insert(0,source[^1]);
   source.RemoveAt(source.Count-1);
}

Test

var list = new List<int>() {1, 2, 3, 4, 5};

Console.WriteLine(string.Join(", ",list));
list.RotateUp();
Console.WriteLine(string.Join(", ", list));
list.RotateUp();
Console.WriteLine(string.Join(", ", list));
list.RotateDown();
Console.WriteLine(string.Join(", ", list));
list.RotateDown();
Console.WriteLine(string.Join(", ", list));

Output

1, 2, 3, 4, 5
5, 1, 2, 3, 4
4, 5, 1, 2, 3
5, 1, 2, 3, 4
1, 2, 3, 4, 5

Note : Methods lack any range checking. Also ideally this would target a less derived interface, though it's only an example.

Here's a performance comparison of the accepted answer to one using a LinkedList just to quantify the difference. Extending that code:

public static class Extensions
{
    public static void RotateDown<T>(this IList<T> source)
    {
        // ideally you would remove first, to save a potential capacity expansion
        source.Add(source[0]);
        source.RemoveAt(0);
    }
    public static void RotateUp<T>(this IList<T> source)
    {
        // ideally you would remove first, to save a potential capacity expansion
        source.Insert(0, source[^1]);
        source.RemoveAt(source.Count - 1);
    }
    public static void RotateDown<T>(this LinkedList<T> source)
    {
        var first = source.First;
        source.RemoveFirst();
        source.AddLast(first);
    }
    public static void RotateUp<T>(this LinkedList<T> source)
    {
        var last = source.Last;
        source.RemoveLast();
        source.AddFirst(last);
    }
}

private static void ComparePerformance(int numberOfElements)
{
    var list = Enumerable.Range(1, numberOfElements).ToList();
    var linkedList = new LinkedList<int>(list);

    var t1 = Stopwatch.StartNew();
    for (int i = 0; i < numberOfElements; i++)
        list.RotateUp();

    Console.WriteLine($"List of {numberOfElements} took {t1.ElapsedMilliseconds} ms");

    var t2 = Stopwatch.StartNew();
    for (int i = 0; i < numberOfElements; i++)
        linkedList.RotateUp();

    Console.WriteLine($"LinkedList of {numberOfElements} took {t2.ElapsedMilliseconds} ms");
}

We see for small N there is no measurable difference. However, for say N=100000:

List of 100000 took 956 ms

LinkedList of 100000 took 2 ms

or N=10000

List of 10000 took 7 ms

LinkedList of 10000 took 0 ms

Choosing the right data structure can make quite a difference.

Note: For serious microbenchmarks, check out BenchmarkDotNet

If you are about default rotate [0,1,2] -> [1,2,0] -> [2,0,1], better to use Queue i think instead of List. Or convert it if list is a required.

In case of list, for example :

 var firstDoer = this.doer[0];
 this.doer.RemoveAt(0);
 this.doer.Add(firstDoer);

But in case if you will use Queue it much simplier :)

var doer = queue.Dequeue();
queue.Enqueue(doer);
Related