VBA sorting a collection by value - vba

I have the VBA collection below
I want to sort by the value such that the collection will end up with the highest double value in the highest index position (i.e. "e" with value 14 is in first index, "c" with value 10 is second, etc). How is this possible?
Public Function SortCollection(ByVal c As Collection) As Collection
Dim n As Long: n = c.Count
If n = 0 Then
Set SortCollection = New Collection
Exit Function
ReDim Index(0 To n - 1) As Long ' allocate index array
Dim i As Long, m As Long
For i = 0 To n - 1: Index(i) = i + 1: Next ' fill index array
For i = n \ 2 - 1 To 0 Step -1 ' generate ordered heap
Heapify c, Index, i, n
Next
For m = n To 2 Step -1 ' sort the index array
Exchange Index, 0, m - 1 ' move highest element to top
Heapify c, Index, 0, m - 1
Next
Dim c2 As New Collection
For i = 0 To n - 1 ' fill output collection
c2.Add c.item(Index(i))
Next
Set SortCollection = c2
End Function
Private Sub Heapify(ByVal c As Collection, Index() As Long, ByVal i1 As Long, ByVal n As Long)
' Heap order rule: a[i] >= a[2*i+1] and a[i] >= a[2*i+2]
Dim nDiv2 As Long: nDiv2 = n \ 2
Dim i As Long: i = i1
Do While i < nDiv2
Dim k As Long: k = 2 * i + 1
If k + 1 < n Then
If c.item(Index(k)) < c.item(Index(k + 1)) Then k = k + 1
End If
If c.item(Index(i)) >= c.item(Index(k)) Then Exit Do
Exchange Index, i, k
i = k
Loop
End Sub
Private Sub Exchange(Index() As Long, ByVal i As Long, ByVal j As Long)
Dim Temp As Long: Temp = Index(i)
Index(i) = Index(j)
Index(j) = Temp
End Sub

As per Domenic in comment and he hasn't answered.
"If you use a Dictionary object instead of a Collection and you can sort by value as shown here. – Domenic Aug 30 at 22:29 "
This works now.

Related

Find the indices for the minimum values in a multi dimensional array in VBA

In the code below I have an n x n x n array of values. I need to identify the indices that contain the minimum, second to minimum, third to minimum, ..., and put them into their own array to be used later on in the code. CC is currently defined as a 11 x 11 x 11 array and I need to identify the minimums. Below is the setup of my array CC that contains the values. n is defined as the length of the array h2s, which happens to be 11 in this case. h2st is the sum of the values in h2s.
h2s = [1.099, 0.988, 0.7, 0.8, 0.5, 0.432, 0.8, 1.12, 0.93, 0.77, 0.658]
h2st = 0
n = Ubound(h2s) - Lbound(h2s) + 1
For i = 1 to n
h2st = h2st + h2s(i)
Next i
For i = 1 To n
For j = i + 1 To n
For k = j + 1 To n
CC(i, j, k) = Abs(h2st - ((h2s(i) + h2s(j) + h2s(k)) * (n / 3)))
Next k
Next j
Next i
You can use this function that takes a multidimensional array and returns an array of its n minimum values, where n is a parameter. Importantly, the elements in the returned array are a data structure of Type Point, containing the coordinates and the value of each found point.
You can easily adjust it for finding the n max values, just by changing two characters in the code, as indicated in comments (the initialization and the comparison)
Option Explicit
Type Point
X As Long
Y As Long
Z As Long
value As Double
End Type
Function minVals(ar() As Double, nVals As Long) As Point()
Dim i As Long, j As Long, k As Long, m As Long, n As Long, pt As Point
'Initialize returned array with max values.
pt.value = 9999999# ' <-------- change to -9999999# for finding max
ReDim ret(1 To nVals) As Point
For i = LBound(ret) To UBound(ret): ret(i) = pt: Next
For i = LBound(ar, 1) To UBound(ar, 1)
For j = LBound(ar, 2) To UBound(ar, 2)
For k = LBound(ar, 3) To UBound(ar, 3)
' Find first element greater than this value in the return array
For m = LBound(ret) To UBound(ret)
If ar(i, j, k) < ret(m).value Then ' <------- change to > for finding max
' shift the elements on this position and insert the current value
For n = UBound(ret) To m + 1 Step -1: ret(n) = ret(n - 1): Next n
pt.X = i: pt.Y = j: pt.Z = k: pt.value = ar(i, j, k)
ret(m) = pt
Exit For
End If
Next m
Next k
Next j
Next i
minVals = ret
End Function
Sub Test()
Dim i As Long, j As Long, k As Long, pt As Point
Const n As Long = 11
ReDim CC(1 To n, 1 To n, 1 To n) As Double
For i = 1 To n
For j = 1 To n
For k = 1 To n
CC(i, j, k) = Application.RandBetween(100, 100000)
Next k
Next j
Next i
' Testing the function: get the smalles 5 values and their coordinates
Dim mins() As Point: mins = minVals(CC, 5)
' Printing the results
For i = LBound(mins) To UBound(mins)
Debug.Print mins(i).value, mins(i).X, mins(i).Y, mins(i).Z
Next
End Sub

Is there a quicksort routine without calling itself / without using recursion

The well known quicksort routine uses two recursive calls at the end. However, using the quicksort routine in Excel-VBA for large unsorted arrays (> 400 thousand elements) may lead to a memory stack overflow because of the many recursive calls.
Public Sub dQsort(List() As Double, ByVal min As Long, ByVal max As Long)
Dim med_value As Double
Dim hi As Long
Dim lo As Long
Dim i As Long
' If min >= max, the list contains 0 or 1 items so it is sorted.
If min >= max Then GoTo ErrorExit
' Pick the dividing value.
i = (max + min + 1) / 2
med_value = List(i)
' Swap it to the front.
List(i) = List(min)
lo = min
hi = max
Do
' Look down from hi for a value < med_value.
Do While List(hi) >= med_value
hi = hi - 1
If hi <= lo Then Exit Do
Loop
If hi <= lo Then
List(lo) = med_value
Exit Do
End If
' Swap the lo and hi values.
List(lo) = List(hi)
' Look up from lo for a value >= med_value.
lo = lo + 1
Do While List(lo) < med_value
lo = lo + 1
If lo >= hi Then Exit Do
Loop
If lo >= hi Then
lo = hi
List(hi) = med_value
Exit Do
End If
' Swap the lo and hi values.
List(hi) = List(lo)
Loop
' Sort the two sublists.
dQsort List(), min, lo - 1 ' Recursive call which I would like to avoid
dQsort List(), lo + 1, max ' Recursive call which I would like to avoid
End Sub
My question is: Who knows a modified quicksort routine with only a small penalty in extra time compared to the traditional quicksort routine (because of the mentioned memory stack overflow, you can only compare between the "old" and "new" routine for relative small unsorted arrays)?
Answers shown for the "Questions that may already have your answer" are not the answers for my question.
Here is a simple sort for doubles:
Public Sub aSort(ByRef InOut)
Dim i As Long, J As Long, Low As Long
Dim Hi As Long, Temp As Variant
Low = LBound(InOut)
Hi = UBound(InOut)
J = (Hi - Low + 1) \ 2
Do While J > 0
For i = Low To Hi - J
If InOut(i) > InOut(i + J) Then
Temp = InOut(i)
InOut(i) = InOut(i + J)
InOut(i + J) = Temp
End If
Next i
For i = Hi - J To Low Step -1
If InOut(i) > InOut(i + J) Then
Temp = InOut(i)
InOut(i) = InOut(i + J)
InOut(i + J) = Temp
End If
Next i
J = J \ 2
Loop
End Sub
Sub MAIN()
Dim ary(1 To 3) As Double, msg As String
Dim i As Long
ary(1) = 0.4
ary(2) = 0.1
ary(3) = 0.5
Call aSort(ary)
msg = ""
For i = 1 To 3
msg = msg & ary(i) & vbCrLf
Next i
MsgBox msg
End Sub
I don't know if it is "quick" enough.:
The mentioned merge sort has the same disadvantage as the traditional Quicksort: it also uses a recursive call (see the code for Excel’s VBA below, adapted from the named Wiki-page). The TopDownMergeSort sorts only the n-1 array value’s. Therefore, you need to insert the n-th value in the sorted array (of course at the correct place).
Sub Test_Method_MergeSort()
'Array adData with Doubles, starting from index = 1
Call TopDownMergeSort(adData)
Call InsertIntoSortedArray(adData, adData(UBound(adData)), 1, False)
End Sub
'// Array A[] has the items to sort; array B[] is a work array.
Sub TopDownMergeSort(ByRef A() As Double)
Dim B() As Double
Dim n As Long
Dim i As Long
'// duplicate array A[] into B[]
n = UBound(A)
ReDim B(n)
For i = 1 To n
B(i) = A(i)
Next i
'// sort data from B[] into A[]
TopDownSplitMerge B, 1, n, A
End Sub
'Sort the given run of array A[] using array B[] as a source.
'iBegin is inclusive; iEnd is exclusive (A[iEnd] is not in the set).
Sub TopDownSplitMerge(ByRef B() As Double, ByVal iBegin As Long, ByVal iEnd As Long, ByRef A() As Double)
Dim iMiddle As Long
Dim dTmp As Double
If (iEnd - iBegin) < 2 Then Exit Sub ' // if run size == 1
'// split the run longer than 1 item into halves
iMiddle = (iEnd + iBegin) / 2 '// iMiddle = mid point
'// recursively sort both runs from array A[] into B[]
TopDownSplitMerge A, iBegin, iMiddle, B '// sort the left run
TopDownSplitMerge A, iMiddle, iEnd, B '// sort the right run
'// merge the resulting runs from array B[] into A[]
TopDownMerge B, iBegin, iMiddle, iEnd, A
End Sub
'// Left source half is A[ iBegin:iMiddle-1].
'// Right source half is A[iMiddle:iEnd-1].
'// Result is B[ iBegin:iEnd-1].
Sub TopDownMerge(ByRef A() As Double, ByVal iBegin As Long, ByVal iMiddle As Long, ByVal iEnd As Long, ByRef B() As Double)
Dim i As Long
Dim j As Long
Dim k As Long
i = iBegin
j = iMiddle
'// While there are elements in the left or right runs...
For k = iBegin To iEnd - 1
'// If left run head exists and is <= existing right run head.
If ((i < iMiddle) And ((j >= iEnd) Or (A(i) <= A(j)))) Then
B(k) = A(i)
i = i + 1
Else
B(k) = A(j)
j = j + 1
End If
Next k
End Sub
Sub InsertIntoSortedArray(ByRef dSortedArray() As Double, ByVal dNewValue As Double, ByVal LowerBound As Long, Optional ByVal RedimNeeded As Boolean = False) ', xi As Long) As Long
Dim n As Long, ii As Long
n = UBound(dSortedArray)
If RedimNeeded Then
ReDim Preserve dSortedArray(n + 1)
Else
n = n - 1
End If
ii = n + 1
Do Until dSortedArray(ii - 1) <= dNewValue Or ii < (LowerBound + 1)
dSortedArray(ii) = dSortedArray(ii - 1)
ii = ii - 1
Loop
dSortedArray(ii) = dNewValue
End Sub
The solution I was looking for is without any recursive calls. With several additional variables for necessary administration purposes during the sorting steps I succeeded in the following Alternative quicksort “IAMWW_QSort”:
' This code belongs to one and the same Excel’s code module
Private Const msMODULE As String = "M_QSort"
Private alMin() As Long
Private alMax() As Long
Private abTopDownReady() As Boolean
Private aiTopDownIndex() As Integer ' 1 (= TopList) or 2 ( = DownList)
Private alParentIndex() As Long
Sub IAMWW_Qsort(ByRef List() As Double, ByVal Min As Long, ByVal Max As Long)
Dim med_value As Double
Dim hi As Long
Dim lo As Long
Dim i As Long
Dim l_List As Long
' If min >= max, the list contains 0 or 1 items so it is sorted.
If Min >= Max Then GoTo ExitPoint
Call Init(l_List, Min, Max)
Start:
If abTopDownReady(l_List, 1) And abTopDownReady(l_List, 2) Then
abTopDownReady(alParentIndex(l_List), aiTopDownIndex(l_List)) = True
l_List = l_List - 1
If l_List >= 0 Then
GoTo Start
Else
' Ready/list is sorted
GoTo ExitPoint
End If
End If
Min = alMin(l_List)
Max = alMax(l_List)
' -----------------------------------
' The traditional part of QuickSort
' Pick the dividing value.
i = (Max + Min + 1) / 2
med_value = List(i)
' Swap it to the front.
List(i) = List(Min)
lo = Min
hi = Max
Do
' Look down from hi for a value < med_value.
Do While List(hi) >= med_value
hi = hi - 1
If hi <= lo Then Exit Do
Loop
If hi <= lo Then
List(lo) = med_value
Exit Do
End If
' Swap the lo and hi values.
List(lo) = List(hi)
' Look up from lo for a value >= med_value.
lo = lo + 1
Do While List(lo) < med_value
lo = lo + 1
If lo >= hi Then Exit Do
Loop
If lo >= hi Then
lo = hi
List(hi) = med_value
Exit Do
End If
' Swap the lo and hi values.
List(hi) = List(lo)
Loop
' End of the traditional part of QuickSort
' -----------------------------------------
If Max > (lo + 1) Then
' top part as a new sublist
l_List = l_List + 1
Init_NewSubList l_List, l_List - 1, 1, lo + 1, Max
If (lo - 1) > Min Then
' down part as a new sublist
l_List = l_List + 1
Init_NewSubList l_List, l_List - 2, 2, Min, lo - 1
Else
' down part (=2) is sorted/ready
abTopDownReady(l_List - 1, 2) = True
End If
ElseIf (lo - 1) > Min Then
' Top part is sorted/ready...
abTopDownReady(l_List, 1) = True
' ... and down part is a new sublist.
l_List = l_List + 1
Init_NewSubList l_List, l_List - 1, 2, Min, lo - 1
Else
' Both the top (=1) and down part (=2) are sorted/ready ...
abTopDownReady(l_List, 1) = True
abTopDownReady(l_List, 2) = True
' ... and therefore, the parent (sub)list is also sorted/ready ...
abTopDownReady(alParentIndex(l_List), aiTopDownIndex(l_List)) = True
' ... and continue with the before last created new sublist.
l_List = l_List - 1
End If
If l_List >= 0 Then GoTo Start
ExitPoint:
End Sub
Private Sub Init_NewSubList(ByVal Nr As Long, ByVal Nr_Parent As Long, ByVal iTopDownIndex As Integer, ByVal Min As Long, ByVal Max As Long)
' Nr = number of new sublist
' Nr_Parent = the parent's list number of the new sublist
' iTopDownIndex = index for top (=1) or down part (=2) sublist
aiTopDownIndex(Nr) = iTopDownIndex '= 2 ' new sub list is a down part sublist
alParentIndex(Nr) = Nr_Parent 'l_List - 2
abTopDownReady(Nr, 1) = False 'The new sublist has a top part sublist, not ready yet
abTopDownReady(Nr, 2) = False 'The new sublist has a down part sublist, not ready yet
' min and max values of the new sublist
alMin(Nr) = Min
alMax(Nr) = Max 'lo - 1
End Sub
Private Sub Init(ByRef Nr As Long, ByVal Min As Long, ByVal Max As Long)
Dim lArraySize As Long
lArraySize = Max - Min + 1
ReDim alMin(lArraySize)
ReDim alMax(lArraySize)
ReDim abTopDownReady(lArraySize, 2)
ReDim aiTopDownIndex(lArraySize)
ReDim alParentIndex(lArraySize)
Nr = 0
alMin(Nr) = Min
alMax(Nr) = Max
aiTopDownIndex(0) = 2 ' Initial list is assumed to be a down part (= 2)
End Sub
The penalty in extra time because of the additional administrative code lines is very small.

Where is my error in this visual basic Bubblesort

I am currently making a highscore table - reading the times from a .csv file and sorting them from lowest to highest. The list only becomes partially sorted after the code runs.
All the data inputs correctly but when it goes to sort it sorts the data out incorrectly.
Private Sub BeginnerProcess(ByRef player() As pe_player, ByVal x As Integer)
Dim i As Integer
Dim j As Integer
Dim temp As Object
For i = x To 0 Step -1
For j = 0 To i - 1
If player(j).playerTime > player(j + 1).playerTime Then
temp = player(j)
player(j) = player(j + 1)
player(j + 1) = temp
End If
Next
Next
Dim k As Integer
For k = 1 To x
player(k).position = k
Next
End Sub
Here's the output
Leaderboard
Adapting the classic bubble-sort to your case, I think i should be something like the code below:
For i = 0 To x - 1
For j = i + 1 To x
If player(i).playerTime > player(j).playerTime Then
temp = player(i)
player(i) = player(j)
player(j) = temp
End If
Next
Next

vba array element removal

j = LBound(arrayTime)
Do Until j = UBound(arrayTime)
j = j + 1
b = b + 1
cnc = b + r
MsgBox cnc
If cnc > 7 Then
b = 0
r = 0
cnc = b + r
End If
numMins = Sheet5.Cells(cnc + 3, 2) - arrayTime(j)
If numMins < 0 Then
g = g + 1
ReArrangeArray arrayTime, j
'ReDim Preserve arrayTime(numrows - 1 + g)
'arrayTime(numrows - 1 + g) = arrayTime(j)
'MsgBox (arrayTime(numrows - 1 + g))
Else
Sheet5.Cells(cnc + 3, 2) = numMins
End If
Loop
If the if statement is true I want to be able to put the array value at the end of the array and remove that value from its current spot. As the code is, it just adds it to the end and increases the size of the array from 12 to 13. How can I get the array to remain size 12 and still place the value at the end of the array and then remove it from its original position? I do not want to touch the array values in front. Just want to take that value and move it to the end.
For instance
array(1,2,3,4,5)
If statement
j on third loop.
array(j)=3
end array should be
array(1,2,4,5,3)
You could use a helper Sub like this one:
Sub ReArrangeArray(inputArray as Variant, indexToSwap as long)
Dim I As Long
Dim tempVal As Variant
If indexToSwap >= LBound(inputArray) And indexToSwap < UBound(inputArray) Then
tempVal = inputArray(indexToSwap)
For I = indexToSwap To UBound(inputArray) - 1
inputArray(i) = inputArray(i + 1)
Next I
InputArray(UBound(inputArray)) = tempVal
End If
End Sub
To be called by your main Sub as follows:
ReArrangeArray arrayTime, j

Stirling numbers second kind in vba

i created a code in VBA to calculate the amount of combinations for stirling numbers of second kind. But in following example only half of the values are correct.
The result should be 1,7,6,1 if n is equal to 4. (Wikipedia stirling numbers
I get 1,7,6.5,4.16
Sub stirlingerzahlen()
Dim n As Integer
Dim sum As Double
Dim subsum As Double
Dim k As Long
Dim j As Long
n = 4
For k = 1 To n Step 1
For j = 0 To k Step 1
subsum = 1 / Application.WorksheetFunction.Fact(k) * (-1) ^ (k - j) * Application.WorksheetFunction.Fact(k) / Application.WorksheetFunction.Fact(j) * j ^ n
sum = sum + subsum
Next
Sheets("Tabelle2").Cells(k, 1) = sum
sum = 0
Next
End Sub
Can someone find the mistake?
There is another version of the formula which seems to be easier to implement:
http://home.mathematik.uni-freiburg.de/junker/ss10/DAS-SS10.pdf
(Page 13)
And here the updated code:
Sub stirlingerzahlen()
Dim n As Integer
Dim sum As Double
Dim subsum As Double
Dim k As Long
Dim j As Long
n = 4
For k = 1 To n Step 1
For j = 0 To k
subsum = (((-1) ^ (k - j)) * ((j ^ n) / (Application.WorksheetFunction.Fact(j) * Application.WorksheetFunction.Fact(k - j))))
sum = sum + subsum
Next
Sheets("Tabelle2").Cells(k, 1) = sum
sum = 0
Next
End Sub