Click here to Skip to main content
15,887,027 members
Articles / Programming Languages / Visual Basic

Fastest In-Place Stable Sort

Rate me:
Please Sign up or sign in to vote.
4.84/5 (14 votes)
30 Jan 2021CPOL10 min read 97.1K   1.5K   34  
A stable version of quicksort
In this article, I share my collection of experiments using stable in-place sorts. The attached VB project generates test data, runs all of the sorts mentioned and reports on the speed of each.
Attribute VB_Name = "modStableBinaryQuickSortCB"
Option Explicit



Const QUICKSORTDEPTH2 = 32

Global SMALLSEGMENTSIZECB As Long
Global smallBufferCB() As DataElement

'StableBinaryQuickSortCB
'-----------------------
'
'Written by Craig Brown - 21/1/13.
'
'This is a stable version of quicksort.  It differs from traditional quicksort in that
'instead of swapping elements that need to be pivotted, runs of elements are rotated around
'to maintain stability.
'
'The differences are found in the pivot function used.
'


Function StableBinaryQuickSortCB(ByRef theData() As DataElement, ByVal lngFirstElement As Long, ByVal lngLastElement As Long)

    'These two arrays can be dynamically allocated to be log2(number of elements).
    Dim lngStackStarts(QUICKSORTDEPTH2) As Long
    Dim lngStackEnds(QUICKSORTDEPTH2) As Long
    Dim lngStackSize As Long
    
    Dim lngPivotValue(3) As Long
    Dim lngPivotPoint(3) As Long
    Dim lngI As Long
    Dim lngJ As Long
    
    Dim lngNewStart1 As Long
    Dim lngNewEnd1 As Long
    Dim lngNewStart2 As Long
    Dim lngNewEnd2 As Long
    
    Dim blFoundPivot As Boolean
    
    Dim lngTemp As Long
    
    lngStackSize = 0
    lngStackStarts(lngStackSize) = lngFirstElement
    lngStackEnds(lngStackSize) = lngLastElement
    lngStackSize = lngStackSize + 1
    
    Rnd -1
    Randomize 1
    
    While lngStackSize > 0
    
        lngStackSize = lngStackSize - 1
        
        If lngStackEnds(lngStackSize) - lngStackStarts(lngStackSize) < 6 Then
            InsertSort lngStackStarts(lngStackSize), lngStackEnds(lngStackSize) + 1, theData
        Else
    
            lngPivotPoint(0) = Int(Rnd() * (lngStackEnds(lngStackSize) - lngStackStarts(lngStackSize) + 1)) + lngStackStarts(lngStackSize)
            lngPivotPoint(1) = Int(Rnd() * (lngStackEnds(lngStackSize) - lngStackStarts(lngStackSize) + 1)) + lngStackStarts(lngStackSize)
            lngPivotPoint(2) = Int(Rnd() * (lngStackEnds(lngStackSize) - lngStackStarts(lngStackSize) + 1)) + lngStackStarts(lngStackSize)
            lngPivotValue(0) = theData(lngPivotPoint(0)).theKey
            lngPivotValue(1) = theData(lngPivotPoint(1)).theKey
            lngPivotValue(2) = theData(lngPivotPoint(2)).theKey
            OrderLongs lngPivotValue(0), lngPivotValue(1)
            OrderLongs lngPivotValue(1), lngPivotValue(2)
            OrderLongs lngPivotValue(0), lngPivotValue(1)
            
            blFoundPivot = False
            If lngPivotValue(1) = lngPivotValue(2) Then
                'Top two pivot values are the same, maybe use the smaller one
                If lngPivotValue(0) = lngPivotValue(1) Then
                    'All three pivot values are the same
                    'Find another value
                    lngI = lngStackStarts(lngStackSize)
                    While lngI <= lngStackEnds(lngStackSize) And Not blFoundPivot
                        If theData(lngI).theKey <> lngPivotValue(1) Then
                            blFoundPivot = True
                            If theData(lngI).theKey < lngPivotValue(1) Then
                                lngPivotValue(1) = theData(lngI).theKey
                            Else
                                lngPivotValue(2) = theData(lngI).theKey
                            End If
                        Else
                            lngI = lngI + 1
                        End If
                    Wend
                    
                Else
                    'Top two pivot values are the same, use the smaller one
                    lngPivotValue(1) = lngPivotValue(0)
                    blFoundPivot = True
                End If
            Else
                'Use the middle pivot
                lngPivotValue(1) = lngPivotValue(1)
                blFoundPivot = True
            End If
            
            'If a pivot cannot be found then all of the data must have the same key
            If blFoundPivot Then
            
                'Swap the data around
                lngPivotPoint(1) = StableBinaryPivotCB(theData, lngStackStarts(lngStackSize), lngStackEnds(lngStackSize), lngPivotValue(1))
                
                'Put the two parts on the stack
                
                'Put the larget part on first so that the stack doesn't overflow
                lngNewStart1 = lngStackStarts(lngStackSize)
                lngNewEnd1 = lngStackStarts(lngStackSize) + lngPivotPoint(1) - 1
                lngNewStart2 = lngStackStarts(lngStackSize) + lngPivotPoint(1)
                lngNewEnd2 = lngStackEnds(lngStackSize)
                
                If lngNewEnd1 - lngNewStart1 > lngNewEnd2 - lngNewStart2 Then
                    'Part 1 is bigger put it on first
                    lngStackStarts(lngStackSize) = lngNewStart1
                    lngStackEnds(lngStackSize) = lngNewEnd1
                    lngStackStarts(lngStackSize + 1) = lngNewStart2
                    lngStackEnds(lngStackSize + 1) = lngNewEnd2
                Else
                    'Part 2 is bigger put it on first
                    lngStackStarts(lngStackSize) = lngNewStart2
                    lngStackEnds(lngStackSize) = lngNewEnd2
                    lngStackStarts(lngStackSize + 1) = lngNewStart1
                    lngStackEnds(lngStackSize + 1) = lngNewEnd1
                End If
                lngStackSize = lngStackSize + 2
                
            End If
            
        End If
    Wend

End Function

Function StableBinaryPivotCB(ByRef theData() As DataElement, ByVal lngFirstElement As Long, ByVal lngLastElement As Long, ByVal lngPivotValue As Long) As Long

    'The complicated selection of the pivot value
    'has already guaranteed that there will be some values above the pivot
    'and some at or below the pivot


    'This function pivots the block of data in a binary way from the bottom up
    '
    'Eg, if the data starts:
    '
    '     385828702948983 with a pivot value of 5 it will merge the runs together like:
    '      >>><    ><
    '     328788702498983 by swapping every second run, and then do it again
    '       >>>>><<<
    '     320248788798983 then again
    '          >>>>>>>>>
    '     320243878879898 to get the finished pivot
    '

    Dim lngFirstOverValue As Long
    Dim lngLastUnderValue As Long
    
    Dim lngStartIndex As Long
    Dim lngOverEnd1 As Long
    Dim lngUnderEnd1 As Long
    Dim lngNoElementsToProcess As Long
    
    Dim lngOverEnd2 As Long
    Dim lngUnderEnd2 As Long
    
    Dim i As Long
    Dim j As Long
    
    Dim lngSwapValue As Long
    
    Dim blKeepLooping As Long
    
    If (lngLastElement - lngFirstElement + 1) <= SMALLSEGMENTSIZECB Then
        'For small segments use a small buffer for performance
        lngFirstOverValue = StableBinaryPivotSmallCBForward(theData, lngFirstElement, lngLastElement, lngPivotValue) + lngFirstElement
    Else
    
        'Skip things that can be skipped
        lngFirstOverValue = lngFirstElement
        While theData(lngFirstOverValue).theKey <= lngPivotValue
            lngFirstOverValue = lngFirstOverValue + 1
        Wend
        lngLastUnderValue = lngLastElement
        While theData(lngLastUnderValue).theKey > lngPivotValue
            lngLastUnderValue = lngLastUnderValue - 1
        Wend
        
        'Run through the data using the small buffer as much as we can
        If lngFirstOverValue < lngLastUnderValue And SMALLSEGMENTSIZECB > 4 Then
            lngStartIndex = lngFirstOverValue
            lngNoElementsToProcess = lngLastUnderValue - lngStartIndex + 1
            If lngNoElementsToProcess > SMALLSEGMENTSIZECB Then
                lngNoElementsToProcess = SMALLSEGMENTSIZECB
            End If
            lngFirstOverValue = StableBinaryPivotSmallCBForward(theData, lngStartIndex, lngStartIndex + lngNoElementsToProcess - 1, lngPivotValue) + lngFirstOverValue
            lngStartIndex = lngStartIndex + lngNoElementsToProcess
            While lngStartIndex < lngLastUnderValue
                lngNoElementsToProcess = lngLastUnderValue - lngStartIndex + 1
                If lngNoElementsToProcess > SMALLSEGMENTSIZECB Then
                    lngNoElementsToProcess = SMALLSEGMENTSIZECB
                End If
                StableBinaryPivotSmallCBForward theData, lngStartIndex, lngStartIndex + lngNoElementsToProcess - 1, lngPivotValue
                lngStartIndex = lngStartIndex + lngNoElementsToProcess
                
                'Do it again, but do it backwards, this will group runs of overs and unders together
                If lngStartIndex < lngLastUnderValue Then
                    lngNoElementsToProcess = lngLastUnderValue - lngStartIndex + 1
                    If lngNoElementsToProcess > SMALLSEGMENTSIZECB Then
                        lngNoElementsToProcess = SMALLSEGMENTSIZECB
                    End If
                    StableBinaryPivotSmallCBBackward theData, lngStartIndex, lngStartIndex + lngNoElementsToProcess - 1, lngPivotValue
                    lngStartIndex = lngStartIndex + lngNoElementsToProcess
                End If
            Wend
            
            'Revise this if needed
            While theData(lngLastUnderValue).theKey > lngPivotValue
                lngLastUnderValue = lngLastUnderValue - 1
            Wend
        End If
            
        While lngFirstOverValue < lngLastUnderValue
        
            'Scan through the data
            lngStartIndex = lngFirstOverValue
            While lngStartIndex < lngLastUnderValue
            
                'Rotate every second run of overs with the following set of unders
                'Find the run of overs
                lngOverEnd1 = lngStartIndex
                Do
                    If lngOverEnd1 + 1 <= lngLastElement Then
                        If theData(lngOverEnd1 + 1).theKey > lngPivotValue Then
                            lngOverEnd1 = lngOverEnd1 + 1
                            blKeepLooping = True
                        Else
                            blKeepLooping = False
                        End If
                    Else
                        blKeepLooping = False
                    End If
                Loop Until Not blKeepLooping
                'Find the run of unders
                lngUnderEnd1 = lngOverEnd1 + 1
                'Icky visual basic if condition because vb evaluates all parts of the if every time
                Do
                    If lngUnderEnd1 + 1 <= lngLastElement Then
                        If theData(lngUnderEnd1 + 1).theKey <= lngPivotValue Then
                            blKeepLooping = True
                            lngUnderEnd1 = lngUnderEnd1 + 1
                        Else
                            blKeepLooping = False
                        End If
                    Else
                        blKeepLooping = False
                    End If
                Loop Until Not blKeepLooping
                
                'Rotate these two blocks around
                i = lngStartIndex
                j = lngOverEnd1
                While i < j
                    lngSwapValue = theData(i).theKey
                    theData(i).theKey = theData(j).theKey
                    theData(j).theKey = lngSwapValue
                
                    lngSwapValue = theData(i).originalOrder
                    theData(i).originalOrder = theData(j).originalOrder
                    theData(j).originalOrder = lngSwapValue
                    
                    i = i + 1
                    j = j - 1
                Wend
                
                i = lngOverEnd1 + 1
                j = lngUnderEnd1
                While i < j
                    lngSwapValue = theData(i).theKey
                    theData(i).theKey = theData(j).theKey
                    theData(j).theKey = lngSwapValue
                
                    lngSwapValue = theData(i).originalOrder
                    theData(i).originalOrder = theData(j).originalOrder
                    theData(j).originalOrder = lngSwapValue
                    
                    i = i + 1
                    j = j - 1
                Wend
                
                i = lngStartIndex
                j = lngUnderEnd1
                While i < j
                    lngSwapValue = theData(i).theKey
                    theData(i).theKey = theData(j).theKey
                    theData(j).theKey = lngSwapValue
                
                    lngSwapValue = theData(i).originalOrder
                    theData(i).originalOrder = theData(j).originalOrder
                    theData(j).originalOrder = lngSwapValue
                    
                    i = i + 1
                    j = j - 1
                Wend
                
                'Adjust the range of items needing swapping
                If lngStartIndex = lngFirstOverValue Then
                    'If this was the first run of overs than the first run of overs has moved back
                    lngFirstOverValue = lngFirstOverValue + (lngUnderEnd1 - lngOverEnd1)
                End If
                If lngUnderEnd1 = lngLastUnderValue Then
                    'If this was the last run of unders then it has moved forward
                    lngLastUnderValue = lngLastUnderValue - (lngOverEnd1 - lngStartIndex + 1)
                End If
                
                If lngUnderEnd1 < lngLastUnderValue Then
                    'Another run of each, dont swap these yet
                    'this causes the runs to bunch up in a binary way
                    
                    'Find the run of overs
                    lngOverEnd2 = lngUnderEnd1 + 1
                    'Icky visual basic if condition because vb evaluates all parts of the if every time
                    Do
                        If lngOverEnd2 + 1 <= lngLastElement Then
                            If theData(lngOverEnd2 + 1).theKey > lngPivotValue Then
                                blKeepLooping = True
                                lngOverEnd2 = lngOverEnd2 + 1
                            Else
                                blKeepLooping = False
                            End If
                        Else
                            blKeepLooping = False
                        End If
                    Loop Until Not blKeepLooping
                    
                    'Find run of unders
                    lngUnderEnd2 = lngOverEnd2 + 1
                    'Icky visual basic if condition because vb evaluates all parts of the if every time
                    Do
                        If lngUnderEnd2 + 1 <= lngLastElement Then
                            If theData(lngUnderEnd2 + 1).theKey <= lngPivotValue Then
                                blKeepLooping = True
                                lngUnderEnd2 = lngUnderEnd2 + 1
                            Else
                                blKeepLooping = False
                            End If
                        Else
                            blKeepLooping = False
                        End If
                    Loop Until Not blKeepLooping
                                    
                    'Point to the next start of overs
                    lngStartIndex = lngUnderEnd2 + 1
                Else
                    'Point to the next start of overs
                    lngStartIndex = lngUnderEnd1 + 1
                End If
            Wend
        
        Wend
    End If

    StableBinaryPivotCB = lngFirstOverValue - lngFirstElement

End Function



Function StableBinaryPivotSmallCBForward(ByRef theData() As DataElement, ByVal lngFirstElement As Long, ByVal lngLastElement As Long, ByVal lngPivotValue As Long) As Long

    'A function to pivot a small segment of data using a buffer.

    Dim lngI As Long
    Dim lngNoUnders As Long
    Dim lngNoOvers As Long
    
    lngNoUnders = 0
    lngNoOvers = 0
    
    'Loop through the data and compact the under values to the front of the original array
    'and store the over values in the buffer
    For lngI = lngFirstElement To lngLastElement
        If theData(lngI).theKey <= lngPivotValue Then
            theData(lngFirstElement + lngNoUnders).theKey = theData(lngI).theKey
            theData(lngFirstElement + lngNoUnders).originalOrder = theData(lngI).originalOrder
            lngNoUnders = lngNoUnders + 1
        Else
            smallBufferCB(lngNoOvers).theKey = theData(lngI).theKey
            smallBufferCB(lngNoOvers).originalOrder = theData(lngI).originalOrder
            lngNoOvers = lngNoOvers + 1
        End If
    Next
    
    'Now put the over values back into the original array after the unders
    lngI = 0
    While lngI < lngNoOvers
        theData(lngFirstElement + lngNoUnders + lngI).theKey = smallBufferCB(lngI).theKey
        theData(lngFirstElement + lngNoUnders + lngI).originalOrder = smallBufferCB(lngI).originalOrder
        lngI = lngI + 1
    Wend

    StableBinaryPivotSmallCBForward = lngNoUnders

End Function

Function StableBinaryPivotSmallCBBackward(ByRef theData() As DataElement, ByVal lngFirstElement As Long, ByVal lngLastElement As Long, ByVal lngPivotValue As Long) As Long

    'A function to pivot a small segment of data using a buffer.
    'The over values will be placed first

    Dim lngI As Long
    Dim lngNoUnders As Long
    Dim lngNoOvers As Long
    
    lngNoUnders = 0
    lngNoOvers = 0
    
    'Loop through the data and compact the under values to the front of the original array
    'and store the over values in the buffer
    For lngI = lngFirstElement To lngLastElement
        If theData(lngI).theKey <= lngPivotValue Then
            smallBufferCB(lngNoUnders).theKey = theData(lngI).theKey
            smallBufferCB(lngNoUnders).originalOrder = theData(lngI).originalOrder
            lngNoUnders = lngNoUnders + 1
        Else
            theData(lngFirstElement + lngNoOvers).theKey = theData(lngI).theKey
            theData(lngFirstElement + lngNoOvers).originalOrder = theData(lngI).originalOrder
            lngNoOvers = lngNoOvers + 1
        End If
    Next
    
    'Now put the over values back into the original array after the unders
    lngI = 0
    While lngI < lngNoUnders
        theData(lngFirstElement + lngNoOvers + lngI).theKey = smallBufferCB(lngI).theKey
        theData(lngFirstElement + lngNoOvers + lngI).originalOrder = smallBufferCB(lngI).originalOrder
        lngI = lngI + 1
    Wend

    StableBinaryPivotSmallCBBackward = lngNoUnders

End Function



By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions