1

I need an extension method that takes two arguments: a target IList<T> and a custom comparer.

public static void CustomSort<T>( IList<T> target, IComparer<T> comparer )
{
    ...
}

The extension method's job is to arrange target's elements in the order indicated by the custom comparer.

Here are some solutions that won't work:

  • The List<T>.Sort does in-place sorting, which is basically what I'm after. But I can't assume target's concrete type is List<T>.
  • LINQ offers some solutions but unfortunately they output their results as a new enumerable instead of modifying the target list.

Instead, I need the target's Remove and Insert methods to be called to put its elements into the right order. The target list may be observable so it is important that the solution does not do more removes and inserts than are necessary to obtain the desired order.

HappyNomad
  • 4,458
  • 4
  • 36
  • 55
  • 4
    Sorting in-place potentially uses **more** `Remove()`s and `Insert()`s than using LINQ, `Clear()` and `AddRange()`. (Sorting is `O(n log n)`, remove and reinsert is `O(n)` - as far as operations on the original list are concerned.) – millimoose Sep 24 '13 at 18:59
  • I don't necessary mind the downvotes but I'd like to know your reasons. Too easy? Then why not post an answer instead? – HappyNomad Sep 24 '13 at 19:01
  • 2
    If your primary goal is to minimize the number of changes to the `IList`, you'll probably want to sort the list into a new list with LINQ, and then calculate the optimal number of `Remove`s, `Insert`s, and indexed setting (something similar to a [Levenshtein distance](http://en.wikipedia.org/wiki/Levenshtein_distance)), and see if that beats the n+1 that `Clear()` and `Add()` gets you, then go forward with the optimal solution. – Tim S. Sep 24 '13 at 19:08
  • @HappyNomad I downvoted because this question is basically just says "Give me code". – Mansfield Sep 24 '13 at 19:11
  • @TimS. I'm reminded of the "then a miracle occurs" comic. – millimoose Sep 24 '13 at 19:13
  • @Mansfield The question is clearly stating my objective. I don't know what the problem with that is. – HappyNomad Sep 24 '13 at 19:14
  • @millimoose What are you talking about? – HappyNomad Sep 24 '13 at 19:16
  • @HappyNomad The fact that writing an algorithm to determine what the optimal "edits" between two given sequences are is probably less easy than the description "similar to the Levenshtein distance" (which only computes the distance itself) lets on. Much less writing one that's ultimately faster than a clear/reinsert. – millimoose Sep 24 '13 at 19:17
  • 1
    @HappyNomad Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results. – Mansfield Sep 24 '13 at 19:18
  • 1
    @Mansfield Of course it's not really a question asking for code if you notice the fact the starting sentence is really just a symptom of the XY problem, and the OP actually needs an efficient algorithm for changing the order of elements in a sequence in-place. (As further evidenced by the discussion in the comments.) That said, an edit to change the phrasing might be in order. – millimoose Sep 24 '13 at 19:19
  • @Mansfield (1) I clearly stated the objective which means I understand the problem. (2) I mentioned *two* solutions that I considered but won't work. – HappyNomad Sep 24 '13 at 19:27
  • @millimoose Yes, you are correct that the OP "needs an efficient algorithm for changing the order of elements in a sequence in-place". But that is just a rewording of the question's original title. And I don't see that your wording is any more precise or otherwise better than the original. – HappyNomad Sep 24 '13 at 20:46
  • @HappyNomad "I need an extension method" evokes that what you want is people to write your code for you. You should make the purpose of this code front and center. Also: you do realise I'm *defending* you here and suggesting what might get rid of the downvotes? Also: "How to reorder a list using the minimum amount of `Insert`s and `Remove`s" is a very very different question than "Somebody write me a quicksort that takes `IList` as an argument." (Which is what your first sentences read as.) – millimoose Sep 24 '13 at 20:54
  • @millimoose Yes, I noticed your effort so thank you. And I will add that phrase to my knee-jerk reaction invocation list. – HappyNomad Sep 24 '13 at 21:05
  • @HappyNomad It's not as much the phrase as the true nature of your problem being buried and needing some amount of discussion to come up. The advice is more general: start with what you're trying to ultimately accomplish, then move on to specifics and what you think might work. – millimoose Sep 24 '13 at 21:19
  • @millimoose (1) My question is only one paragraph (well, before Luis Filipe kindly inserted five line breaks) so I don't see how its true nature can be "buried" for a person with the slightest bit of patience. (2) Programmers write code, and most answers on SO include code, so I still don't understand what's wrong in that regard with my wording. (3) I think you nailed it with your latest rewording of the title. I fixed it which I hope helps a bit to combat the knee-jerk reactions. – HappyNomad Sep 25 '13 at 00:00
  • @HappyNomad With **(1)** and **(2)** you're still arguing against what I perceive is the SO groupthink, not an opinion of mine. A great deal of the motivation behind SO is to *not* be a forum where someone posts a vague "plz send me the codes to do X" and then twenty other people reply with "yes I need this codes too send asap" - the scars of looking for programming advice before SO probably inspire the kneejerkiness. It's unfortunate, but then again that's why there's the reopen process, and [meta] appeals, and question comments to clear things up. – millimoose Sep 25 '13 at 01:11

3 Answers3

2

I think something like this will do you.

We sort an array of offsets, then determine the set of deltas that need to be applied and finally apply them to produce the newly ordered IList.

public static void CustomSort<T>( IList<T> list , IComparer<T> comparer )
{
  int[] unsorted = Enumerable.Range(0,list.Count).ToArray() ;
  int[] sorted   = Enumerable.Range(0,list.Count).OrderBy( x => list[x] , comparer ).ToArray() ;
  var   moves    = sorted.Zip( unsorted , (x,y) => new{ Src = x , Dest = y , } ).Where( x => x.Src != x.Dest ).OrderBy( x => x.Src ).ToArray() ;

  // at this point, moves is a list of moves, from a source position to destination position.
  // We enumerate over this and apply the moves in order.
  // To do this we need a scratch pad to save incomplete moves, where an existing item has
  // been over-written, but it has not yet been moved into its final destination.

  Dictionary<int,T> scratchPad = new Dictionary<int, T>() ; // a parking lot for incomplete moves
  foreach ( var move in moves )
  {
    T value ;

    // see if the source value is on the scratchpad.
    // if it is, use it and remove it from the scratch pad.
    // otherwise, get it from the indicated slot in the list.
    if ( scratchPad.TryGetValue( move.Src , out value ) )
    {
      scratchPad.Remove( move.Src );
    }
    else
    {
      value = list[ move.Src ] ;
    }

    // if the destination is after the source position, we need to put
    // it on the scratch pad, since we haven't yet used it as a source.
    if ( move.Dest > move.Src )
    {
      scratchPad.Add( move.Dest , list[ move.Dest ]);
    }

    // finally, move the source value into its destination slot.
    list[ move.Dest ] = value ;

  }

  return ;
}
Nicholas Carey
  • 71,308
  • 16
  • 93
  • 135
  • +1 This is the best approach suggested so far and the only answer I'd consider. But, as currently implemented, it seems to lead to a large number of `moves` which pretty much defeats its purpose. It does gives me an interesting idea to employ my [FlatPlacementArgs](http://www.qnomad.com/wpf/corehelpers/) class in a similar fashion. – HappyNomad Sep 25 '13 at 02:03
1

The comments show that this can actually be a difficult problem, as stated. So what if we take this problem from different angles:

  1. Why are the observers of this collection take so long? They should only do fast things in response to individual changes. E.g. if it's for a UI change, it could wait until the next UI redraw before caring about the changes. That should let the whole reorder finish in one go. Or
  2. Use something like ObservableRangeCollection<T> so that you've got an AddRange on observable collections. Some simple code can determine if the collection is List<T> or ObservableRangeCollection<T> to use AddRange if possible.

E.g.

public static void SmartSort<T>(IList<T> source)
{
    var sortedList = source.OrderBy(x => x).ToList();
    if (source.SequenceEqual(sortedList))
        return;
    var list = source as List<T>;
    var collection = source as ObservableRangeCollection<T>;
    if (list != null)
    {
        list.Clear();
        list.AddRange(sortedList);
    }
    else if (collection != null)
        collection.ReplaceRange(sortedList);
    else
    {
        for (int i = 0; i < source.Count; i++)
            source[i] = sortedList[i];
    }
}
Community
  • 1
  • 1
Tim S.
  • 55,448
  • 7
  • 96
  • 122
  • This probably makes the most sense - I don't believe there's any point whatsoever in having the listeners fire on every insert/remove during a re-sort. More likely than not most of the elements will change position anyway so the GUI might as well redisplay completely. – millimoose Sep 24 '13 at 19:32
  • Great! One shortcoming I see, though, is that it could be heavy-handed to assume that a complete collection reset is necessary. At least, I think you should first check if the `sortedList` is already `SequenceEquals` to the "unsorted" one. – HappyNomad Sep 24 '13 at 20:22
  • @millimoose I don't agree with your assumptions. In my situation, already being sorted is actually the most common case. Also, the GUI is not the only potential listener. – HappyNomad Sep 24 '13 at 20:27
  • @HappyNomad added a `SequenceEqual` check. I didn't realize that being already sorted was such a common case. If it weren't, the check would be more trouble than it's worth. – Tim S. Sep 24 '13 at 20:31
  • Also, I changed the general case from a `Clear`/`Add`-loop to an indexer set-loop, so it works for arrays too, and won't make things like `List` resize their internal arrays needlessly. – Tim S. Sep 24 '13 at 20:37
  • @HappyNomad That precondition changes things **immensely** and you should've pointed it out before. In that case, it makes more sense to maintain the sort order all the time by inserting new items into the correct place, instead of re-sorting then going through contortions to reverse-engineer this. – millimoose Sep 24 '13 at 20:50
  • @TimS. `List.Clear()` doesn't change the internal buffer at all. – millimoose Sep 24 '13 at 20:51
  • @millimoose Well, I don't understand how that changes things immensely. The "reverse engineering" is an essential aspect of any acceptable solution. If you are suggesting that I shouldn't even need such an extension method, then I guess this question isn't for you. – HappyNomad Sep 25 '13 at 00:28
  • 1
    @HappyNomad What I'm saying is that there might be, in your current use case, a less fiddly of achieving your ultimate goal. Preferrable questions for SO are *practical* ones, and practical questions should accept a workaround as long as it gets your job done. "Inserting an element into a sorted list so that it remains sorted" is a much, much easier problem to solve than "determine the minimum set of changes to reorder a sequence"; so, if the actual problem you're facing can be solved by the former, everyone involved can get on with their lives faster than by trying to implement the latter. – millimoose Sep 25 '13 at 01:15
  • @millimoose I do have a practical need for an answer to the question as it is specifically stated. – HappyNomad Sep 25 '13 at 01:22
  • @HappyNomad Yet, you also say the collection will be mostly sorted? These are the two things I'm having difficulty reconciling. I imagine the most common way of arriving at a mostly sorted list would be making modifications to an already sorted one. – millimoose Sep 25 '13 at 01:24
1
  1. Use the LINQ extension method to sort and get a new List implicitly calling ToList() at the end;

  2. Then tell your observable list to suspend itself (do not care about changes)

  3. Clear your observable list

  4. Add items to the observable list from the linq-ordered list.

  5. Tell your observable list to resume watching for changes

It's efficient concerning the number of changes the observables watches; From the algorithm efficiency point of view i'd give a try to this solution. You might never need to optimize any further. Remember: premature optimization is the root of all evil

Luis Filipe
  • 8,488
  • 7
  • 48
  • 76