Easy way to add stable sorting to TList and TStringList
Asked Answered
H

4

10

I use TList/TObjectList and TStringList (with associated objects) for a multitude of tasks, either as-is, or as basis for more complex structures. While the sort functionality is usually good enough, I sometimes need to do a stable sort, and both lists use quicksort.

What's the easiest way to implement stable sorting for TList and/or TStringList? Do I have to write my own sorting routine, or can it be done by using some clever trick with TStringListSortCompare/TListSortCompare?

Hyphenated answered 26/10, 2011 at 18:32 Comment(1)
See also: #9303988Hourihan
C
5

You'll have to write your own sorting routine.

You can read the current QuickSort implementation, and write your own stable version (e.g. a Merge sort or any other stable variant).

Some tricks:

  • If you are sure that an index is < Count, you can use the fast pointer array (TList.List[]) instead of slower Items[] or GetItem(): sub-method calling and range checking slow down the execution;
  • The comparison function is most of the time the speed bottleneck of the search - so take care of this part;
  • If you need speed, use a real profiler over real (e.g. random) data - but make it right before making it fast;
  • Start from an existing implementation of the sort;
  • To minimize stack space, you may use a temporary record to store and share variables among recursive calls.
Coleencolella answered 26/10, 2011 at 18:57 Comment(2)
I thought I had to write my own, but had to ask. I was hoping for some "magic" trick :-) Come to think of it, isn't it strange Borland/Embarcadero didn't make TList.Sort stable? I'm no expert, but I believe there are sorting algorithms with the same performance as quicksort and still quite modest memory usage. Ah, well.Hyphenated
As far as all book tell, QuickSort is a well known sort algorithm at the same time fast, easy to code and debug, and low memory use. Stable variations (like merge or insertion sorts) are more complex, and do not provide better performance. Sorting in the VCL did not require to be stable, therefore QuickSort was the best candidate for a general-purpose sorting implementation.Coleencolella
A
3

This question is rather old, but here goes my answer for future readers: I also needed this recently and adapted the implementation found in the book "The Tomes of Delphi Algorithms and Data Structures" by Julian Bucknall. Implementation for TList, TObjectList and descendant classes. It works with Delphi 2009 to XE7 and probably other versions as well: http://alexandrecmachado.blogspot.com.br/2015/02/merge-sort-for-delphi.html

Andromada answered 26/2, 2015 at 12:10 Comment(8)
Nice! Thanks, man. I note however that there are no unit tests. How confident are you that the code is solid? Would you use it in production code?Chak
I wrote some unit tests, and unfortunately the sort doesn't appear to be completely stable :( I will post the test to your blog.Chak
There was a bug in the original code. Thanks for finding it and informing. The code is fixed and yes, I will use it in production. Here is the information about the problem and the fix: alexandrecmachado.blogspot.com.br/2015/03/…Andromada
Link only answers are not to be encouraged.Wenger
@DavidHeffernan I think you woke up in the wrong side of the bed? Do you spend your time going through all questions of every user that points some incomplete information you provided in your answer, or is just in my case? LOLAndromada
How about concentrating on the content of my comment instead of making it personal.Wenger
"Oh so I will downvote all his answers just to show who is in charge here" LOL. You are patheticAndromada
No. Once again concentrate on the content and don't take it personally. Link only answers are discouraged. This has been discussed on meta and the guidelines are clear. Speak to that.Wenger
V
0

From similar question How Can I Replace StringList.Sort with a Stable Sort in Delphi?, linked here in comment by lkessler I need to copy to here really easy trick as mentioned in question.

You can easily make quick sort behave stable just by adding initial order numbers into data to sort and adding last comparation condition in CustomSort compare function to compare this initial order numbers.

Easy, fast and smart. Costs only one extra integer (or byte, or use some reserved storage like TComponent.Tag if You sort TComponents) on each sortable item and one initialization loop over them.

TObjectToSort = class
  ...
  Index: Integer;
end;

function MyStableSortComparer(List: TStringList; Index1, Index2: Integer): Integer;
var
  o1, o2: TObjectToSort; 
begin
  o1 := TObjectToSort(List.Objects[Index1]);
  o2 := TObjectToSort(List.Objects[Index2]);
  ...
  if Result = 0 then
    Result := o1.Index - o2.Index;
end;


for i := 0 to MyStrtingList.Count - 1 do
  TObjectToSort(MyStrtingList.Objects[i]).Index := i;
MyStrtingList.CustomSort(MyStableSortComparer);
Vauntcourier answered 15/3, 2017 at 7:24 Comment(0)
G
0

For anyone using generics here is a ready-to-use implementation of insertion and merge sort, both stable sorting algorithms.

uses Generics.Defaults, Generics.Collections;

type
  TMySort = class
  public
    class procedure InsertionSort<T>(AArray: TArray<T>; FirstIndex, LastIndex: Integer; const AComparer: IComparer<T>); static;
    class procedure MergeSort<T>(AArray: TArray<T>; FirstIndex, LastIndex: Integer; const AComparer: IComparer<T>); static;
  end;

implementation

class procedure TMySort.InsertionSort<T>(AArray: TArray<T>; FirstIndex, LastIndex: Integer; const AComparer: IComparer<T>);
var
  UnsortedIdx, CompareIdx: Integer;
  AItem: T;
begin
  for UnsortedIdx := Succ(FirstIndex) to LastIndex do begin
    AItem := AArray[UnsortedIdx];
    CompareIdx := UnsortedIdx - 1;
    while (CompareIdx >= FirstIndex) and (AComparer.Compare(AItem, AArray[CompareIdx]) < 0) do begin
      AArray[CompareIdx + 1] := AArray[CompareIdx]; { shift the compared (bigger) item to the right }
      Dec(CompareIdx);
    end;
    AArray[CompareIdx + 1] := AItem;
  end;
end;

class procedure TMySort.MergeSort<T>(AArray: TArray<T>; FirstIndex, LastIndex: Integer; const AComparer: IComparer<T>);
const
  MinMergeSortLimit = 16;
var
  LeftLast, RightFirst: Integer;
  LeftIdx, RightIdx, SortedIdx: Integer;
  LeftCount: Integer;
  TmpLeftArray: TArray<T>;
begin
  if (LastIndex - FirstIndex) < MinMergeSortLimit then
    { sort small chunks with insertion sort (recursion ends here)}
    TMySort.InsertionSort<T>(AArray, FirstIndex, LastIndex, AComparer)
  else begin
    { MERGE SORT }
    { calculate the index for splitting the array in left and right halves }
    LeftLast := (FirstIndex + LastIndex) div 2;
    RightFirst := LeftLast + 1;
    { sort both halves of the array recursively }
    TMySort.MergeSort<T>(AArray, FirstIndex, LeftLast, AComparer);
    TMySort.MergeSort<T>(AArray, RightFirst, LastIndex, AComparer);
    { copy the first half of the array to a temporary array }
    LeftCount := LeftLast - FirstIndex + 1;
    TmpLeftArray := System.Copy(AArray, FirstIndex, LeftCount);
    { setup the loop variables }
    LeftIdx := 0;  { left array to merge -> moved to TmpLeftArray, starts at index 0 }
    RightIdx := RightFirst; { right array to merge -> second half of AArray }
    SortedIdx := FirstIndex - 1; { range of merged items }
    { merge item by item until one of the arrays is empty }
    while (LeftIdx < LeftCount) and (RightIdx <= LastIndex) do begin
      { get the smaller item from the next items in both arrays and move it
        each step will increase the sorted range by 1 and decrease the items still to merge by 1}
      Inc(SortedIdx);
      if AComparer.Compare(TmpLeftArray[LeftIdx], AArray[RightIdx]) <= 0 then begin
        AArray[SortedIdx] := TmpLeftArray[LeftIdx];
        Inc(LeftIdx);
      end else begin
        AArray[SortedIdx] := AArray[RightIdx];
        Inc(RightIdx);
      end;
    end;
    { copy the rest of the left array, if there is any}
    while (LeftIdx < LeftCount) do begin
      Inc(SortedIdx);
      AArray[SortedIdx] := TmpLeftArray[LeftIdx];
      Inc(LeftIdx);
    end;
    { any rest of the right array is already in place }
  end;
end;

The implementation is made for arrays and applicable for TList/TObjectList too (as their Items property is an array).

var
  AList: TList<T>;
  AComparer: IComparer<T>;
begin
  ...
  TMySort.MergeSort<T>(AList.List, 0, AList.Count-1, AComparer);
  ...
end;

Besides being stable, in my experience, this merge sort implementation does show better performance than the build-in quick sort (though it uses more memory).

Garnett answered 7/9, 2017 at 13:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.