diff --git a/Source/Collections/IListExtensions.Test.cs b/Source/Collections/IListExtensions.Test.cs index 097e9ba..a6c70e6 100644 --- a/Source/Collections/IListExtensions.Test.cs +++ b/Source/Collections/IListExtensions.Test.cs @@ -32,6 +32,27 @@ namespace Nuclex.Support.Collections { [TestFixture] internal class IListExtensionsTest { + /// Tests whether the insertion sort algorithm works on big lists + [Test] + public void InsertionSortCanSortBigList() { + const int ListSize = 16384; + + var testList = new List(capacity: ListSize); + { + var random = new Random(); + for(int index = 0; index < ListSize; ++index) { + testList.Add(random.Next()); + } + } + + var testListAsIList = (IList)testList; + testListAsIList.InsertionSort(); + + for(int index = 1; index < ListSize; ++index) { + Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); + } + } + /// Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected [Test] public void InsertionSortCanSortWholeList() { @@ -60,6 +81,27 @@ namespace Nuclex.Support.Collections { ); } + /// Tests whether the quicksort algorithm works on big lists + [Test] + public void QuickSortCanSortBigList() { + const int ListSize = 16384; + + var testList = new List(capacity: ListSize); + { + var random = new Random(); + for(int index = 0; index < ListSize; ++index) { + testList.Add(random.Next()); + } + } + + var testListAsIList = (IList)testList; + testListAsIList.QuickSort(); + + for(int index = 1; index < ListSize; ++index) { + Assert.LessOrEqual(testListAsIList[index - 1], testListAsIList[index]); + } + } + /// Tests whether the insertion sort algorithm can be applied to 'Text' property works as expected [Test] public void QuickSortCanSortWholeList() { diff --git a/Source/Collections/IListExtensions.cs b/Source/Collections/IListExtensions.cs index ba46a0d..8f79aa3 100644 --- a/Source/Collections/IListExtensions.cs +++ b/Source/Collections/IListExtensions.cs @@ -118,21 +118,38 @@ namespace Nuclex.Support.Collections { this IList list, int startIndex, int count, IComparer comparer ) { var remainingPartitions = new Stack(); - remainingPartitions.Push(new Partition(startIndex, startIndex + count - 1)); - while(remainingPartitions.Count > 0) { - Partition current = remainingPartitions.Pop(); - int leftEnd = current.LeftmostIndex; - int rightEnd = current.RightmostIndex; + int lastIndex = startIndex + count - 1; + for(; ; ) { + int pivotIndex = quicksortPartition(list, startIndex, lastIndex, comparer); - int pivotIndex = quicksortPartition(list, leftEnd, rightEnd, comparer); - if(pivotIndex - 1 > leftEnd) { - remainingPartitions.Push(new Partition(leftEnd, pivotIndex - 1)); - } - if(pivotIndex + 1 < rightEnd) { - remainingPartitions.Push(new Partition(pivotIndex + 1, rightEnd)); - } - } + // This block just queues the next partitions left of the pivot point and right + // of the pivot point (if they contain at least 2 elements). It's fattened up + // a bit by trying to forego the stack and adjusting the startIndex/lastIndex + // directly where it's clear the next loop can process these partitions. + if(pivotIndex - 1 > startIndex) { // Are the elements to sort right of the pivot? + if(pivotIndex + 1 < lastIndex) { // Are the elements left of the pivot as well? + remainingPartitions.Push(new Partition(startIndex, pivotIndex - 1)); + startIndex = pivotIndex + 1; + } else { // Elements to sort are only right of the pivot + lastIndex = pivotIndex - 1; + } + } else if(pivotIndex + 1 < lastIndex) { // Are elements to sort only left of the pivot? + startIndex = pivotIndex + 1; + } else { // Partition was fully sorted + + // Did we process all queued partitions? If so, the list is sorted + if(remainingPartitions.Count == 0) { + return; + } + + // Pull the next partition that needs to be sorted from the stack + Partition current = remainingPartitions.Pop(); + startIndex = current.LeftmostIndex; + lastIndex = current.RightmostIndex; + + } // if sortable sub-partitions exist left/right/nowhere + } // for ever (termination inside loop) } /// @@ -156,32 +173,45 @@ namespace Nuclex.Support.Collections { QuickSort(list, 0, list.Count, Comparer.Default); } + /// + /// Moves an element downward over all elements that precede it in the sort order + /// + /// Type of elements stored in the sorted list + /// List that is being sorted + /// Index of the first element in the partition + /// Index of hte last element in the partition + /// + /// Comparison function that decides the ordering of elements + /// + /// The index of the next pivot element private static int quicksortPartition( IList list, int firstIndex, int lastIndex, IComparer comparer ) { - TElement pivot = list[lastIndex]; - - // Set the high index element to its proper sorted position - int nextIndex = firstIndex; + + // Step through all elements in the partition and accumulate those that are smaller + // than the last element on the left (by swapping). At the end 'firstIndex' will be + // the new pivot point, left of which are all elements smaller than the element at + // 'lastIndex' and right of it will be all elements which are larger. for(int index = firstIndex; index < lastIndex; ++index) { - if(comparer.Compare(list[index], pivot) < 0) { - TElement temp = list[nextIndex]; - list[nextIndex] = list[index]; + if(comparer.Compare(list[index], list[lastIndex]) < 0) { + TElement temp = list[firstIndex]; + list[firstIndex] = list[index]; list[index] = temp; - ++nextIndex; + ++firstIndex; } } - - // Set the high index value to its sorted position + + // The element at 'lastIndex' as a sort value that's in the middle of the two sides, + // so we'll have to swap it, too, putting it in the middle and making it the new pivot. { - TElement temp = list[nextIndex]; - list[nextIndex] = list[lastIndex]; + TElement temp = list[firstIndex]; + list[firstIndex] = list[lastIndex]; list[lastIndex] = temp; } - // Returns the next sorting element location - return nextIndex; + // Return the index of the new pivot position + return firstIndex; }