Подсчитать количество разных значений в выбранном (большом) диапазоне в VBA?

Как я могу подсчитать количество различных значений (смешанных чисел и строк) в выбранном (большом) диапазоне в VBA?

Я думаю об этом следующим образом:
1. Считать данные в одномерный массив.
2. Сортировать массив (быстрая сортировка или сортировка слиянием), который нужно проверить,
3. Просто подсчитать количество различных значений, если отсортированный массив: if(a[i]<>a[i+1]) then counter=counter+1.

Это самый эффективный способ решить эту проблему?

Изменить: я хочу сделать это в Excel.


person Qbik    schedule 01.08.2012    source источник
comment
Вы можете загрузить диапазон в двумерный массив, затем прокрутить его и использовать словарь сценариев для проверки уникальности. В словаре есть ваш счет, когда вы закончите.   -  person Tim Williams    schedule 01.08.2012
comment
@TimWilliams, ты меня опередил, именно моя мысль :)   -  person i_saw_drones    schedule 01.08.2012
comment
Три ответа - хорошо, я проверю их и выберу один в пятницу. Спасибо   -  person Qbik    schedule 01.08.2012


Ответы (4)


Вот решение VBA

Вам не нужен массив, чтобы сделать это. Вы также можете использовать сборник. Пример

Sub Samples()
    Dim scol As New Collection

    With Sheets("Sheet1")
        For i = 1 To 100 '<~~ Assuming the range is from A1 to A100
            On Error Resume Next
            scol.Add .Range("A" & i).Value, Chr(34) & _
            .Range("A" & i).Value & Chr(34)
            On Error GoTo 0
        Next i
    End With

    Debug.Print scol.Count

    'For Each itm In scol
    '   Debug.Print itm
    'Next
End Sub

ПОСЛЕДУЮЩИЕ

Sub Samples()
    Dim scol As New Collection
    Dim MyAr As Variant

    With Sheets("Sheet1")
        '~~> Select your range in a column here
        MyAr = .Range("A1:A10").Value

        For i = 1 To UBound(MyAr)
            On Error Resume Next
            scol.Add MyAr(i, 1), Chr(34) & _
            MyAr(i, 1) & Chr(34)
            On Error GoTo 0
        Next i
    End With

    Debug.Print scol.Count

    'For Each itm In scol
    '   Debug.Print itm
    'Next
End Sub
person Siddharth Rout    schedule 01.08.2012
comment
+1 Приятно добавить, что вам не нужны какие-либо специальные библиотеки для использования объекта Collection, что может упростить задачу. :-) - person Gaffi; 01.08.2012
comment
+1 Хороший ответ! Перебор объектов (например, объектов Range) по сравнению с массивами по-прежнему медленнее, поэтому копирование в вариантный массив и последующее добавление в коллекцию намного быстрее (извините, я специалист по производительности Excel!) - person i_saw_drones; 02.08.2012
comment
@i_saw_drones: Да, ты прав. Зацикливание было просто примером. В реальном сценарии я бы использовал массив, как в моем примере выше. - person Siddharth Rout; 02.08.2012

Вместо шагов 2 и 3, возможно, вы могли бы использовать Scripting.Dictionary и добавлять каждое значение в словарь. Любые повторяющиеся записи вызовут ошибку времени выполнения, которую можно либо перехватить, либо проигнорировать (resume next). Наконец, вы можете просто вернуть словарь count, который даст вам количество уникальных записей.

Вот кусок кода, который я наспех собрал:

Function UniqueEntryCount(SourceRange As Range) As Long

    Dim MyDataset As Variant
    Dim dic As Scripting.Dictionary
    Set dic = New Scripting.Dictionary

    MyDataset = SourceRange

    On Error Resume Next

    Dim i As Long

    For i = 1 To UBound(MyDataset, 1)

        dic.Add MyDataset(i, 1), ""

    Next i

    On Error GoTo 0

    UniqueEntryCount = dic.Count

    Set dic = Nothing

End Function

Я знаю, что resume next можно считать «запахом кода», но альтернативой может быть использование функции exists словаря, чтобы проверить, существует ли уже указанный ключ, а затем добавить значение, если это не так. У меня просто есть ощущение, что когда я делал подобное в прошлом, было быстрее просто игнорировать любые ошибки, возникающие для дубликатов ключей, а не использовать exists YMMY. Для полноты картины вот еще один метод, использующий exists:

Function UniqueEntryCount(SourceRange As Range) As Long

    Dim MyDataset As Variant
    Dim dic As Scripting.Dictionary
    Set dic = New Scripting.Dictionary

    MyDataset = SourceRange

    Dim i As Long

    For i = 1 To UBound(MyDataset, 1)

        if not dic.Exists(MyDataset(i,1)) then dic.Add MyDataset(i, 1), ""

    Next i

    UniqueEntryCount = dic.Count

    Set dic = Nothing

End Function

Хотя приведенный выше код проще, чем предложенный вами метод, было бы целесообразно проверить его производительность в сравнении с вашим решением.

person i_saw_drones    schedule 01.08.2012

Основываясь на идее, представленной i_saw_drones, я настоятельно рекомендую Scripting.Dictionary. Однако это можно сделать и без On Error Resume Next, как показано ниже. Кроме того, его пример требует линковки библиотеки Microsoft Scripting Runtime. Мой пример продемонстрирует, как это сделать без необходимости связывания.

Кроме того, поскольку вы делаете это в Excel, вам вообще не нужно создавать массив на шаге 1. Функция ниже примет диапазон ячеек, которые будут полностью перебираться.

(i.e. UniqueCount = UniqueEntryCount(ActiveSheet.Cells) or UniqueCount = UniqueEntryCount(MySheet.Range("A1:D100"))

Function UniqueEntryCount(SourceRange As Range) As Long
    Dim MyDataset As Variant
    Dim MyRow As Variant
    Dim MyCell As Variant
    Dim dic As Object
    Dim l1 As Long, l2 As Long

    Set dic = CreateObject("Scripting.Dictionary")
    MyDataset = SourceRange

    For l1 = 1 To UBound(MyDataset)
        ' There is no function to get the UBound of the 2nd dimension 
        ' of an array (that I'm aware of), so use this division to 
        ' get this value. This does not work for >=3 dimensions!
        For l2 = 1 To SourceRange.Count / UBound(MyDataset)
            If Not dic.Exists(MyDataset(l1, l2)) Then
                dic.Add MyDataset(l1, l2), MyDataset(l1, l2)
            End If
        Next l2
    Next l1

    UniqueEntryCount = dic.Count
    Set dic = Nothing
End Function

Также может быть важно отметить, что в приведенном выше примере пустая строка "" будет считаться отдельным значением. Если вы не хотите, чтобы это было так, просто измените код на этот:

    For l1 = 1 To UBound(MyDataset)
        For l2 = 1 To SourceRange.Count / UBound(MyDataset)
            If Not dic.Exists(MyDataset(l1, l2)) And MyDataset(l1, l2) <> "" Then
                dic.Add MyDataset(l1, l2), MyDataset(l1, l2)
            End If
        Next l2
    Next l1
person Gaffi    schedule 01.08.2012
comment
С точки зрения производительности я бы не рекомендовал перебирать каждую ячейку (т. е. объект) и выполнять неявное приведение типа к варианту, поскольку перебор объектов в цикле требует больших вычислительных ресурсов. Вот почему более эффективно принуждать его к массиву и циклически проходить по массиву. Microsoft также рекомендует следующее: msdn.microsoft.com/en-us/library /office/ff726673.aspx — раздел, озаглавленный «Чтение и запись больших блоков данных за одну операцию». - person i_saw_drones; 01.08.2012
comment
@i_saw_drones Я согласен. :-) Думал просто выкинуть как вариант. Я также хотел сплагиатить вас как можно меньше. ;-) - person Gaffi; 01.08.2012
comment
@i_saw_drones Да, вы можете выполнить принуждение к 2D-массиву, что можно сделать в моей версии вашей функции (обновил мой ответ), вместо того, чтобы передавать 1D-массив/диапазон в функцию. - person Gaffi; 01.08.2012
comment
Хороший вопрос и хорошая идея :) Хороший ответ! Кстати, UBound принимает дополнительный параметр для измерения, для которого вы хотите найти верхнюю границу (msdn.microsoft.com/en-us/library/gg278658.aspx) - person i_saw_drones; 01.08.2012
comment
@i_saw_drones Я видел, как ты это делаешь, но я не знал, что это было. Позор мне за то, что я не исследовал лучше. ;-) По крайней мере, теперь я знаю. Спасибо! Затем, конечно, вы можете очень просто расширить это до массива ›=3D. - person Gaffi; 01.08.2012

Извините, это написано на C#. Вот как бы я это сделал.

// first copy the array so you don't lose any data
List<value> copiedList = new List<value>(yourArray.ToList());

//for through your list so you test every value
for (int a = 0; a < copiedList.Count; a++)
{
  // copy instances to a new list so you can count the values and do something with them
  List<value> subList = new List<value>(copiedList.FindAll(v => v == copiedList[i]);

  // do not do anything if there is only 1 value found
  if(subList.Count > 1)
                        // You would want to leave 1 'duplicate' in
    for (int i = 0; i < subList.Count - 1; i++)
        // remove every instance from the array but one
        copiedList.Remove(subList[i]);
}
int count = copiedList.Count; //this is your actual count

Не проверял, попробуйте.

Вы должны обернуть это внутри метода, чтобы не возиться с мусором. В противном случае вы потеряете копию массива только позже. (количество возвратов)

РЕДАКТИРОВАТЬ: для этого вам нужен список, используйте Array.ToList();

person AmazingDreams    schedule 01.08.2012
comment
если (subArray.count › 1) проверка не нужна, это учитывает цикл for. - person AmazingDreams; 01.08.2012
comment
Как помогает ответ на вопрос VBA на C#? ;) - person Siddharth Rout; 01.08.2012
comment
А если пользователь не знает С#, то как это поможет? - person Siddharth Rout; 01.08.2012
comment
Я согласен с @SiddharthRout. Не все языки имеют одинаковые функции, поэтому ответ C # для перевода не очень полезен. - person Nick Perkins; 02.08.2012