Search code examples
c#sortingmergesortgeneric-programming

How do I make my MergeSort generic for different objects?


I currently have a merge sort, which was sorting a list of Nodes according to an integer within each Node called "F" (So Node.F).

However, I have come up with a need to use the MergeSort for another list of objects - Entities. However, I want to sort this according to an integer within each Entity called "AmountEaten" (So Entity.AmountEaten).

Now here lies my problem -> making the MergeSort class work for all objects. I have already replaced all references to Node within the sort with "object", but how do I allow a custom criteria for sorting? Is there a way to provide it as a parameter.

If that doesn't make sense, within the MergeSort, I compare two values (E.g. Left.F < Right.F). With a generic object, this doesn't work, because F doesn't exist. I would like to be able to compare anything within the object, inside my Sort (E.g. Left.AmountEaten < Right.AmountEaten). I can't figure out how to give this as a parameter. I immediately thought of Delegates, but I'm not sure how/if that is right.

As the sort deals with lists and not individual objects, I can't simply give a parameter of F/AmountEaten as I would like to access the variable, not the value.

If you need any more detail/don't understand, please ask.

Seem to have reached some form of a conclusion, but can you help me make it work?

MergeSort Class:

static class MergeSort
{
    public static IList<object> Sort(IList<object> input, Comparison<object> comparison /* Comparison is a delegate that works out the difference
                                                                                         * between 2 values - Same signature used by List<T>.Sort */)
    {
        List<object> Result = new List<object>();
        Queue<object> Left = new Queue<object>(); //Switched from lists to queues because removing at index 0 is more efficient
        Queue<object> Right = new Queue<object>();
        //Dequeue() and Count() have a time complexity of O(1)

        if (input.Count <= 1)
            return input;

        int midpoint = input.Count / 2;

        for (int i = 0; i < midpoint; i++)
            Left.Enqueue(input[i]);
        for (int i = midpoint; i < input.Count; i++)
            Right.Enqueue(input[i]);

        Left = new Queue<object>(Sort(Left.ToList(), comparison)); //Recursion! :o
        Right = new Queue<object>(Sort(Right.ToList(), comparison)); ; //This line and the one above split the list into smaller lists (left and right)

        Result = Merge(Left, Right, comparison); //This joins the lists together

        return Result;
    }

    private static List<object> Merge(Queue<object> Left, Queue<object> Right, Comparison<object> comparison)
    {
        int cmp = comparison(Left.Peek(), Right.Peek());
        //If cmp is less than 0, left is less. If it is greater, left is greater

        List<object> Result = new List<object>();
        while (Left.Count /* O(1) operation */ > 0 && Right.Count > 0)
        {
            if (cmp < 0)
                Result.Add(Left.Dequeue());
            //Left.RemoveAt(0) - Using a list to remove at a certain point is inefficient
            else
                Result.Add(Right.Dequeue());
        }

        while (Left.Count > 0)
            Result.Add(Left.Dequeue());

        while (Right.Count > 0)
            Result.Add(Right.Dequeue());

        return Result;
    }
}
}

Usage:

Entities = MergeSort.Sort(Entities, (p, q) => p.F.CompareTo(q.F)).ToList();

Solution

  • Normally the best signature is one similar to the one used by List<T>.Sort(...):

    static void MergeSort<T>(IList<T> coll, Comparison<T> comparison)
    {
        ...
        // < 0 coll[i] < coll[j], == 0 coll[i] == coll[j], > 0 coll[i] > coll[j]
        int cmp = comparison(coll[i], coll[j]);
        ...
    }
    

    use:

    MergeSort(coll, (p, q) => p.F.CompareTo(q.F));
    

    Note that if F is an integer, often you'll see comparisons like: (p, q) => p.F - q.F. This works because if p.F > q.F then p.F - q.F > 0 and so on.

    The other possible variant is the one used by LINQ OrderBy(...)

    Normally the best signature is one similar to the one used by List<T>.Sort(...):

    static void MergeSort<T, TKey>(IList<T> coll, Func<T, TKey> selector)
    {
        var comparer = Comparer<Tkey>.Default;
    
        ...
        // < 0 coll[i] < coll[j], == 0 coll[i] == coll[j], > 0 coll[i] > coll[j]
        int cmp = comparer(selector(coll[i]), selector(coll[j]));
        ...
    }
    

    use:

    MergeSort(coll, p => p.F);
    

    Here we pass to the method a delegate able to return the "sort key", like p => p.F. It's a little slower and more difficult to use (but it's probably used in LINQ because it's more similar to what is done in SQL)