The idea is simple: A random ordering will result in very small contiguous ordered blocks in either direction. Snake sort begins by identifying all those blocks, and then merges them together. Each merge pass will halve the remaining number of blocks, so it very quickly resolves to a sorted state.
I’ve been toying with sorting algorithms for fun over the last month, and I think I’ve come up with an honest-to-god new algorithm that is wholly original, and all mine. My general questions are at the end of this very long post. Skimmers can just read the bolded paragraph to see how it works, which I’ve also set to be displayed in mouseover preview.
In my toying with algorithms, I wrote a benchmark program that compares pure speed with no extra overhead, and also a second screen which does graphical head-to-head races that look pretty much exactly like this java page.
The algorithms generally perform in this order, best to worst:
Quick sort (scales well to very large arrays)
Shell sort
Merge sort (scales well to very large arrays)
Comb sort
Shaker sort (optimized by a better programmer than me)
Heap sort
Jump sort (original to a programmer on another messageboard)
Insertion sort
Selection sort
Gnome sort
Cocktail sort
Bubble sort
The algorithm I came up with is as fast or faster than any other algorithm, including quick sort, and it scales up as well as quicksort excluding memory constraints. It is in the merge sort family, and it is unstable, out-of-place, offline, and non-recursive. I call it snake sort due to its similarities to fantasy football snake drafts.
The idea is simple: A random ordering will result in very small contiguous ordered blocks in either direction. Snake sort begins by identifying all those blocks, and then merges them together. Each merge pass will halve the remaining number of blocks, so it very quickly resolves to a sorted state.
It uses quite a bit of memory: a full copy of the original array, plus an index array (to remember the block cutoffs) half the size of the original array.
Consider the 10 character string SJDFGASLKD. The first three letters, SJD, are already in descending order, so they are the first block. FG are in ascending order, so that’s the second block. AS becomes the third block, and LKD (descending order) rounds us out with the fourth and final block.
One key feature is to bounce the array contents back and forth between the original array and the mirror array instead of merging to the mirror and then copying back to the original each step as with a standard merge sort. This means that if the last step leaves the contents in the mirror, a final pass must be run to copy that back over the original.
Here’s the debug info I generated in testing for a shuffled array of 50 elements. Descending blocks are denoted by negative numbers:
----------------------
| Copy to Mirror |
----------------------
(0)=73...Block(0)=0
(1)=69
(2)=49
(3)=15...Block(1)=-3
(4)=91
(5)=1...Block(2)=-5
(6)=47
(7)=23...Block(3)=-7
(8)=53
(9)=13...Block(4)=-9
(10)=81
(11)=79
(12)=55...Block(5)=-12
(13)=57
(14)=9...Block(6)=-14
(15)=31
(16)=39...Block(7)=16
(17)=7
(18)=33
(19)=63
(20)=97...Block(8)=20
(21)=83
(22)=11...Block(9)=-22
(23)=25
(24)=59
(25)=89...Block(10)=25
(26)=35
(27)=37
(28)=67...Block(11)=28
(29)=3
(30)=21...Block(12)=30
(31)=17
(32)=75...Block(13)=32
(33)=61
(34)=51...Block(14)=-34
(35)=71
(36)=29
(37)=27
(38)=5...Block(15)=-38
(39)=85
(40)=87...Block(16)=40
(41)=41
(42)=95...Block(17)=42
(43)=77
(44)=43...Block(18)=-44
(45)=45
(46)=99...Block(19)=46
(47)=19
(48)=93...Block(20)=48
(49)=65...Block(21)=49
----------------------
| Copy to Original |
----------------------
(0)=1...Block(0)=0
(1)=15
(2)=49
(3)=69
(4)=73
(5)=91...Block(1)=5
(6)=13
(7)=23
(8)=47
(9)=53...Block(2)=9
(10)=9
(11)=55
(12)=57
(13)=79
(14)=81...Block(3)=14
(15)=7
(16)=31
(17)=33
(18)=39
(19)=63
(20)=97...Block(4)=20
(21)=11
(22)=25
(23)=59
(24)=83
(25)=89...Block(5)=25
(26)=3
(27)=21
(28)=35
(29)=37
(30)=67...Block(6)=30
(31)=17
(32)=51
(33)=61
(34)=75...Block(7)=34
(35)=5
(36)=27
(37)=29
(38)=71
(39)=85
(40)=87...Block(8)=40
(41)=41
(42)=43
(43)=77
(44)=95...Block(9)=44
(45)=19
(46)=45
(47)=93
(48)=99...Block(10)=48
(49)=65...Block(11)=49
----------------------
| Copy to Mirror |
----------------------
(0)=1...Block(0)=0
(1)=13
(2)=15
(3)=23
(4)=47
(5)=49
(6)=53
(7)=69
(8)=73
(9)=91...Block(1)=9
(10)=7
(11)=9
(12)=31
(13)=33
(14)=39
(15)=55
(16)=57
(17)=63
(18)=79
(19)=81
(20)=97...Block(2)=20
(21)=3
(22)=11
(23)=21
(24)=25
(25)=35
(26)=37
(27)=59
(28)=67
(29)=83
(30)=89...Block(3)=30
(31)=5
(32)=17
(33)=27
(34)=29
(35)=51
(36)=61
(37)=71
(38)=75
(39)=85
(40)=87...Block(4)=40
(41)=19
(42)=41
(43)=43
(44)=45
(45)=77
(46)=93
(47)=95
(48)=99...Block(5)=48
(49)=65...Block(6)=49
----------------------
| Copy to Original |
----------------------
(0)=1...Block(0)=0
(1)=7
(2)=9
(3)=13
(4)=15
(5)=23
(6)=31
(7)=33
(8)=39
(9)=47
(10)=49
(11)=53
(12)=55
(13)=57
(14)=63
(15)=69
(16)=73
(17)=79
(18)=81
(19)=91
(20)=97...Block(1)=20
(21)=3
(22)=5
(23)=11
(24)=17
(25)=21
(26)=25
(27)=27
(28)=29
(29)=35
(30)=37
(31)=51
(32)=59
(33)=61
(34)=67
(35)=71
(36)=75
(37)=83
(38)=85
(39)=87
(40)=89...Block(2)=40
(41)=19
(42)=41
(43)=43
(44)=45
(45)=65
(46)=77
(47)=93
(48)=95
(49)=99...Block(3)=49
----------------------
| Copy to Mirror |
----------------------
(0)=1...Block(0)=0
(1)=3
(2)=5
(3)=7
(4)=9
(5)=11
(6)=13
(7)=15
(8)=17
(9)=21
(10)=23
(11)=25
(12)=27
(13)=29
(14)=31
(15)=33
(16)=35
(17)=37
(18)=39
(19)=47
(20)=49
(21)=51
(22)=53
(23)=55
(24)=57
(25)=59
(26)=61
(27)=63
(28)=67
(29)=69
(30)=71
(31)=73
(32)=75
(33)=79
(34)=81
(35)=83
(36)=85
(37)=87
(38)=89
(39)=91
(40)=97...Block(1)=40
(41)=19
(42)=41
(43)=43
(44)=45
(45)=65
(46)=77
(47)=93
(48)=95
(49)=99...Block(2)=49
----------------------
| Copy to Original |
----------------------
(0)=1
(1)=3
(2)=5
(3)=7
(4)=9
(5)=11
(6)=13
(7)=15
(8)=17
(9)=19
(10)=21
(11)=23
(12)=25
(13)=27
(14)=29
(15)=31
(16)=33
(17)=35
(18)=37
(19)=39
(20)=41
(21)=43
(22)=45
(23)=47
(24)=49
(25)=51
(26)=53
(27)=55
(28)=57
(29)=59
(30)=61
(31)=63
(32)=65
(33)=67
(34)=69
(35)=71
(36)=73
(37)=75
(38)=77
(39)=79
(40)=81
(41)=83
(42)=85
(43)=87
(44)=89
(45)=91
(46)=93
(47)=95
(48)=97
(49)=99
----------------------
| Sort Complete |
----------------------
Once the initial pass identifies the original blocks, no basic comparisons are needed ever again, since by definition any elements inside a block are already in a sorted order. (Comparisons are still needed in the merging operations, of course.) Each subsequent pass merges two existing blocks together, so the new block cutoffs are already known and do not need to be identified via comparisons. What makes snake sort unique from merge sort is that snake sort bases its block boundaries on the preexisting natural state of the array, as opposed to choosing arbitrary cutoff points. (Also, snake sort supports descending blocks, which merge sort does not. And it’s not recursive.)
The most interesting feature of snake sort is that the more ordered the array is initially, the faster it runs. Each alogorithm has its own unique worst case scenario. Quick sort’s worst case appears to be a camel hump, such as:
.1
…5
…9
…7
…3
This puts the first pivot in the worst possible place, and it goes downhill from there. It would be logical to conclude that snake sort’s worst case would be alternating blocks of two:
.1
…7
…3
…9
…5
Snakesort is extremely efficient on already ordered lists; virtually equal to bubblesort. It really shines on descending lists, where it can transform the array by looping only halfway through it and swapping the ends.
Most importantly, its absolute worst case scenario is light years faster than quicksort’s worst case. (Both scenarios are, IMO, unlikely in the extreme.) A rough approximation in analyzing sorting efficiency is to assign each comparison a time value of 1, each exchange (swapping two elements with each other) a time value of 2, and each overwrite (pretty much the only kind of swap merge and snake sort do) a time value of 1. This is only a rough approximation, but it predicts real performance fairly accurately. Here are some time numbers comparing snake sort with quick sort sorting a 50-element array:
Best case (ascending):
Quicksort: 317
Snakesort: 50 (absolute minimum + 1)
Descending:
Quicksort: 368
Snakesort: 100 (absolute minimum + 1)
Quicksort’s worst case: (camel hump)
Quicksort: 884
Snakesort: 199
Snakesort’s worst case: (thatched)
Quicksort: 408
Snakesort: 510
Random shuffle:
Quckisort: (roughly) 500
Snakesort: (roughly) 560
As you can see, snake sort approaches quicksort efficiency on randomly ordered lists, but gets much faster the moment any order presents itself, unlike quicksort. Oddly, it manages to sort its theoretical worst case (thatched) faster than a generic random set. This leads me to conclude that it has no worst case scenario; only best case – where it approaches perfect efficiency – and average case, where it approaches quick sort.
So given the strong performance, I’m pretty happy with it. The only questions that remain are whether or not it already exists, and what formulas describe its efficiency. Note that just because it uses merging as its operation doesn’t mean it is a merge sort. Snake sort is as different from merge sort as shell sort is from insertion sort, or comb sort is from bubble sort. Here’s a fully function visual basic (not .NET) implementation:
Public Sub SnakeSort1(ByRef pvarArray As Variant)
Dim i As Long
Dim iMin As Long
Dim iMax As Long
Dim lngIndex() As Long
Dim lngLevel As Long
Dim lngOldLevel As Long
Dim lngNewLevel As Long
Dim varMirror As Variant
Dim lngDirection As Long
Dim blnMirror As Boolean
Dim varSwap As Variant
iMin = LBound(pvarArray)
iMax = UBound(pvarArray)
ReDim lngIndex((iMax - iMin + 3) \ 2)
lngIndex(0) = iMin
i = iMin
' Initial loop: locate cutoffs for each ordered section
Do Until i >= iMax
Select Case lngDirection
Case 1
Do Until i = iMax
If pvarArray(i) > pvarArray(i + 1) Then Exit Do
i = i + 1
Loop
Case -1
Do Until i = iMax
If pvarArray(i) < pvarArray(i + 1) Then Exit Do
i = i + 1
Loop
Case Else
Do Until i = iMax
If pvarArray(i) <> pvarArray(i + 1) Then Exit Do
i = i + 1
Loop
If i = iMax Then lngDirection = 1
End Select
If lngDirection = 0 Then
If pvarArray(i) > pvarArray(i + 1) Then
lngDirection = -1
Else
lngDirection = 1
End If
Else
lngLevel = lngLevel + 1
lngIndex(lngLevel) = i * lngDirection
lngDirection = 0
End If
i = i + 1
Loop
' Note final block
If Abs(lngIndex(lngLevel)) < iMax Then
If lngDirection = 0 Then lngDirection = 1
lngLevel = lngLevel + 1
lngIndex(lngLevel) = i * lngDirection
End If
' If the list is already sorted, exit
If lngLevel <= 1 Then
' If sorted descending, reverse before exiting
If lngIndex(lngLevel) < 0 Then
For i = 0 To (iMax - iMin) \ 2
varSwap = pvarArray(iMin + i)
pvarArray(iMin + i) = pvarArray(iMax - i)
pvarArray(iMax - i) = varSwap
Next
End If
Exit Sub
End If
' Main loop - merge section pairs together until only one section left
ReDim varMirror(iMin To iMax)
Do Until lngLevel = 1
lngOldLevel = lngLevel
For lngLevel = 1 To lngLevel - 1 Step 2
If blnMirror Then
SnakeSortMerge varMirror, lngIndex(lngLevel - 1), lngIndex(lngLevel), lngIndex(lngLevel + 1), pvarArray
Else
SnakeSortMerge pvarArray, lngIndex(lngLevel - 1), lngIndex(lngLevel), lngIndex(lngLevel + 1), varMirror
End If
lngNewLevel = lngNewLevel + 1
lngIndex(lngNewLevel) = Abs(lngIndex(lngLevel + 1))
Next
If lngOldLevel Mod 2 = 1 Then
If blnMirror Then
For i = lngIndex(lngNewLevel) + 1 To iMax
pvarArray(i) = varMirror(i)
Next
Else
For i = lngIndex(lngNewLevel) + 1 To iMax
varMirror(i) = pvarArray(i)
Next
End If
lngNewLevel = lngNewLevel + 1
lngIndex(lngNewLevel) = lngIndex(lngOldLevel)
End If
lngLevel = lngNewLevel
lngNewLevel = 0
blnMirror = Not blnMirror
Loop
' Copy back to main array if necessary
If blnMirror Then
For i = iMin To iMax
pvarArray(i) = varMirror(i)
Next
End If
End Sub
Private Sub SnakeSortMerge(pvarSource As Variant, plngLeft As Long, plngMid As Long, plngRight As Long, pvarDest As Variant)
Dim L As Long
Dim LMin As Long
Dim LMax As Long
Dim LStep As Long
Dim R As Long
Dim RMin As Long
Dim RMax As Long
Dim RStep As Long
Dim O As Long
If plngLeft <> 0 Then O = Abs(plngLeft) + 1
If plngMid > 0 Then
LMin = O
LMax = Abs(plngMid)
LStep = 1
Else
LMin = Abs(plngMid)
LMax = O
LStep = -1
End If
If plngRight > 0 Then
RMin = Abs(plngMid) + 1
RMax = Abs(plngRight)
RStep = 1
Else
RMin = Abs(plngRight)
RMax = Abs(plngMid) + 1
RStep = -1
End If
L = LMin
R = RMin
Do
If pvarSource(L) <= pvarSource(R) Then
pvarDest(O) = pvarSource(L)
If L = LMax Then
For R = R To RMax Step RStep
O = O + 1
pvarDest(O) = pvarSource(R)
Next
Exit Do
End If
L = L + LStep
Else
pvarDest(O) = pvarSource(R)
If R = RMax Then
For L = L To LMax Step LStep
O = O + 1
pvarDest(O) = pvarSource(L)
Next
Exit Do
End If
R = R + RStep
End If
O = O + 1
Loop
End Sub
So, the general questions:[ul][li]Have you seen anything like this before? Links would be great, but any cite will do.[/li][*]Can you math guys quantify this algorithm? As in, the same type of best/worst/average descriptors given for various algorithms in the table on this wikipedia page?[/ul]