26

I need to sort some objects according to their contents (in fact according to one of their properties, which is NOT the key and may be duplicated between different objects).

.NET provides two classes (SortedDictionary and SortedList), and both are implemented using a binary tree. The only differences between them are

  • SortedList uses less memory than SortedDictionary.
  • SortedDictionary has faster insertion and removal operations for unsorted data, O(log n) as opposed to O(n) for SortedList.
  • If the list is populated all at once from sorted data, SortedList is faster than SortedDictionary.

I could achieve what I want using a List, and then using its Sort() method with a custom implementation of IComparer, but it would not be time-efficient as I would sort the whole List each time I want to insert a new object, whereas a good SortedList would just insert the item at the right position.

What I need is a SortedList class with a RefreshPosition(int index) to move only the changed (or inserted) object rather than resorting the whole list each time an object inside changes.

Am I missing something obvious ?

Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
Brann
  • 31,689
  • 32
  • 113
  • 162
  • 10
    Another major WTF for .Net. It has some great stuff going for it, but I'm completely shocked that they don't have a class for this, nor some other things that you'd think are quite common. – mpen May 09 '10 at 01:30
  • 1
    .NET now provides a `SortedSet`, but this *still* doesn't support duplicates. They must have some sort of an ideological issue with supporting sorted lists with duplicates. – Roman Starkov Nov 14 '12 at 11:39
  • possible duplicate of [Is there a sorted collection type in .NET?](http://stackoverflow.com/questions/196512/is-there-a-sorted-collection-type-in-net) – nawfal May 21 '14 at 20:24

5 Answers5

11

Maybe I'm slow, but isn't this the easiest implementation ever?

class SortedList<T> : List<T>
{
    public new void Add(T item)
    {
        Insert(~BinarySearch(item), item);
    }
}

http://msdn.microsoft.com/en-us/library/w4e7fxsh.aspx


Unfortunately, Add wasn't overrideable so I had to new it which isn't so nice when you have List<T> list = new SortedList<T>; which I actually needed to do.... so I went ahead and rebuilt the whole thing...

class SortedList<T> : IList<T>
{
    private List<T> list = new List<T>();

    public int IndexOf(T item)
    {
        var index = list.BinarySearch(item);
        return index < 0 ? -1 : index;
    }

    public void Insert(int index, T item)
    {
        throw new NotImplementedException("Cannot insert at index; must preserve order.");
    }

    public void RemoveAt(int index)
    {
        list.RemoveAt(index);
    }

    public T this[int index]
    {
        get
        {
            return list[index];
        }
        set
        {
            list.RemoveAt(index);
            this.Add(value);
        }
    }

    public void Add(T item)
    {
        list.Insert(~list.BinarySearch(item), item);
    }

    public void Clear()
    {
        list.Clear();
    }

    public bool Contains(T item)
    {
        return list.BinarySearch(item) >= 0;
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        list.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return list.Count; }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(T item)
    {
        var index = list.BinarySearch(item);
        if (index < 0) return false;
        list.RemoveAt(index);
        return true;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return list.GetEnumerator();
    }
}

Or perhaps something like this is a more appropriate Remove function...

    public bool Remove(T item)
    {
        var index = list.BinarySearch(item);
        if (index < 0) return false;
        while (((IComparable)item).CompareTo((IComparable)list[index]) == 0)
        {
            if (item == list[index])
            {
                list.RemoveAt(index);
                return true;
            }
            index++;
        }
        return false;
    }

Assuming items can compare equal but not be equal...

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • 6
    Thanks for the class. One edit: shouldn't the Add method be more like the following, to avoid the ArgumentOutOfRangeException exception? ` public virtual void Add(T item) { int index = list_.BinarySearch(item); list_.Insert(index >= 0 ? index : ~index, item); } ` – Suncat2000 Apr 09 '14 at 17:04
  • Probably. I guess I didn't test with duplicate items :-) – mpen Apr 14 '14 at 18:47
  • @Mark you need not write that `Remove` method. It's basically O(n). You could instead just pass the comparer to the `list.BinarySearch` method. It would give you the correct index to remove at. In your case: `index = list.BinarySearch(item, Comparer.Default)` and then `list.RemoveAt(index)`. It's O(n - index). Slight gain :) And ya Suncat is right. I will provide [an answer here](http://stackoverflow.com/questions/196512/is-there-a-sorted-collection-type-in-net) bringing both changes. – nawfal Jun 09 '14 at 04:54
  • @nawfal This was awfully long ago, but it doesn't look O(n) to me. It's O(m) where m is the number of elements that match your search; I imagine that would be 0 or 1 in most cases. – mpen Jun 09 '14 at 15:37
  • 1
    @Mark But actually removing the item is O(n), after you find it, because it's a list, so while you can find the item to remove in less than O(n) time, `Remove` is still O(n). – Servy Jun 09 '14 at 16:03
  • @Servy Oh... didn't realize `RemoveAt` was O(n). [Docs confirm](http://msdn.microsoft.com/en-us/library/5cw9x18z(v=vs.110).aspx). That's a shame. – mpen Jun 09 '14 at 16:22
  • 1
    @Mark That's the reason that sorted data structures are generally tree based data structures, rather than array/based data structures, making `SortedList` (or these homemade variants) almost never the proper data structure for any given task. You're generally better off with either a tree based structure, if you need the data sorted while constantly mutating it, or you want a regular `List` that you just sort every now and then when you're done mutating it (as discussed in Jon's answer). – Servy Jun 09 '14 at 16:24
  • @Servy That makes sense. Can't remember what I wanted this for any more :-) – mpen Jun 09 '14 at 16:27
  • @Mark Servy's comment is what I had in my mind. It's O(n) considering the removal as well. – nawfal Jun 09 '14 at 20:29
  • My main gripe with this is that it isn't semantically correct in the sense that it doesn't actually implement `IList` (the `NotImplementedException` on `Insert`). If you expose this as an `IList`, your consumers are in for a surprise: you are essentially lying. You may be limited to implementing `ICollection`. – MarioDS Aug 22 '16 at 14:05
  • @MDeSchaepmeester Yeah, I think you're right. I would be easy to swap out `IList` for `ICollection` and just delete a couple of the methods if that works for you. – mpen Aug 22 '16 at 19:12
3

I eventually decided to write it :

class RealSortedList<T> : List<T>
    {
        public IComparer<T> comparer;

        public int SortItem(int index)
        {
            T item = this[index];
            this.RemoveAt(index);
            int goodposition=FindLocation(this[index], 0, this.Count);
            this.Insert(goodposition, item);
            return goodposition;
        }

        public int FindLocation(T item, int begin, int end)
        {
            if (begin==end)
                return begin;
            int middle = begin + end / 2;
            int comparisonvalue = comparer.Compare(item, this[middle]);
            if (comparisonvalue < 0)
                return FindLocation(item,begin, middle);
            else if (comparisonvalue > 0)
                return FindLocation(item,middle, end);
            else
                return middle;
        }
    }
Brann
  • 31,689
  • 32
  • 113
  • 162
2

Don't forget that inserting an item into a list backed by an array can be an expensive operation - inserting a bunch of items and then sorting may well be quicker unless you really need to sort after every single operation.

Alternatively, you could always wrap a list and make your add operation find the right place and insert it there.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Yes, you're definitely right, in cases of mass inserts, sorting only once after the inserts is faster ! Still, I think a UpdatePosition(int index, IComparer comparer) which would assume that the list is sorted using comparer and would update the indexes accordingly would be quite useful ! – Brann Dec 23 '08 at 19:53
1

I've solved this problem in the past by writing an extension method that does a binary search on a IList, and another that does an insert. You can look up the correct implementation in the CLR source because there's a built-in version that works only on arrays, and then just tweak it to be an extension on IList.

One of those "should be in the BCL already" things.

Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
0

What I need is a SortedList class with a RefreshPosition(int index) to move only the changed (or inserted) object rather than resorting the whole list each time an object inside changes.

Why would you update using an index when such updates invalidate the index? Really, I would think that updating by object reference would be more convenient. You can do this with the SortedList - just remember that your Key type is the same as the return type of the function that extracts the comparable data form the object.

class UpdateableSortedList<K,V> {
    private SortedList<K,V> list = new SortedList<K,V>();
    public delegate K ExtractKeyFunc(V v);
    private ExtractKeyFunc func;

    public UpdateableSortedList(ExtractKeyFunc f) { func = f; }

    public void Add(V v) {
        list[func(v)] = v;
    }
    public void Update(V v) {
        int i = list.IndexOfValue(v);
        if (i >= 0) {
            list.RemoveAt(i);
        }
        list[func(v)] = v;
    }
    public IEnumerable<T> Values { get { return list.Values; } }
}

Something like that I guess.

Frank Krueger
  • 69,552
  • 46
  • 163
  • 208
  • As I stated into my post, different objects V can have the same "comparable value" K. I don't think SortedList supports duplicate Keys ; or am I mistaking ? – Brann Dec 23 '08 at 19:47
  • Ugh. I shouldn't have spent so much time in the SO editor. You know the solution: implement a binary tree yourself (to meet your requirements), or reuse one of the framework classes. If the framework classes don't work, then convert the SortedList into a MultiSortedList. – Frank Krueger Dec 23 '08 at 19:53
  • Yes ; that's the way to go. Too bad this functionnality is not built in into the .net framework ! – Brann Dec 23 '08 at 19:55