【问题标题】:Finding (NOT deleting) duplicate values(rows) in multi-dimensional array using Excel VBA使用 Excel VBA 在多维数组中查找(不删除)重复值(行)
【发布时间】:2016-05-25 15:39:56
【问题描述】:

以我过去的一个 questions
我想要完成的事情为基础:

我正在寻找并使用基于多个标准的 VBA 代码突出显示重复的 Upcharges:

  1. 产品的 XID(A 列)
  2. 升压标准 1(CT 柱)
  3. 充电标准 2(CU 列)
  4. Upcharge 类型(列 CV)和
  5. Upcharge 水平(CW 列)

如果电子表格中有多个实例/行共享/匹配所有这些条件,则意味着 Upcharge 是重复的。正如我在上面链接的上一篇文章中所见:

我的尝试:

  1. 创建了一个通用公式(见下文),该公式插入到 Helper 列中,并一直复制到电子表格中,指出哪些 Upcharges 是重复的。这种方法太耗费资源并且耗时太长(所有公式计算需要 8-10 分钟,但过滤时不会滞后)。然后我尝试了
  2. 将通用公式演变为条件格式公式,并通过 VBA 代码将其应用于 Upcharge Name 列。(过滤时花费相同的时间和滞后)
  3. 我还研究过是否可以使用 scripting.dictionary,但我不确定如何(或是否)可以使用多维数组。

现在我终于找到了我认为会更快的方法,

我希望使用的更快的方法: 将上述列转储到多维数组中,在数组中找到重复的“行”,然后突出显示相应的电子表格行。

我对更快方法的尝试: 这是我填充多维数组的方法

Sub populateArray()
    Dim arrXID() As Variant, arrUpchargeOne() As Variant, arrUpchargeTwo() As Variant, arrUpchargeType() As Variant, arrUpchargeLevel() As Variant
    Dim arrAllData() As Variant
    Dim i As Long, lrow As Long
    lrow = ActiveSheet.Cells(Rows.Count, 1).End(xlUp).Row

    arrXID = Range("A2:A" & lrow) 'amend column number
    arrUpchargeOne = Range("CT2:CT" & lrow)
    arrUpchargeTwo = Range("CU2:CU" & lrow)
    arrUpchargeType = Range("CV2:CV" & lrow)
    arrUpchargeLevel = Range("CW2:CW" & lrow)

    ReDim arrAllData(1 To UBound(arrXID, 1), 4) As Variant
        For i = 1 To UBound(arrXID, 1)
            arrAllData(i, 0) = arrXID(i, 1)
            arrAllData(i, 1) = arrUpchargeOne(i, 1)
            arrAllData(i, 2) = arrUpchargeTwo(i, 1)
            arrAllData(i, 3) = arrUpchargeType(i, 1)
            arrAllData(i, 4) = arrUpchargeLevel(i, 1)
        Next i
End Sub

我可以将列放入数组中,但我从那里卡住了。我不确定如何检查数组中重复的“行”。

我的问题:

  1. 有没有一种方法可以应用我在上一篇文章中第一次尝试的公式(见下文)并将其应用到数组中?:
  2. 或者,更好的是,有没有更快的方法可以在数组中找到重复的“行”?
  3. 那么如何在电子表格行中突出显示与数组中标记为重复的“行”相对应的 Upcharge Name (CS) 单元格?

我之前帖子中的公式供参考:

=AND(SUMPRODUCT(($A$2:$A$" & lastRow & "=$A2)*($CT$2:$CT$" & lastRow & "=$CT2)*($CU$2:$CU$" & lastRow & "=$CU2)*($CV$2:$CV$" & lastRow & "=$CV2)*($CW$2:$CW$" & lastRow & "=$CW2))>1,$CT2 <> """")"
Returns TRUE if Upcharge is a duplicate 

【问题讨论】:

  • SUMPRODUCT 函数应替换为 COUNTIFS 函数;后者通常占计算负载(和时间)的 25-35%。也可以使用完整的列引用而不会造成损害。例如=AND(COUNTIFS(A:A, A2,CT:CT, CT2,CU:CU, CU2,CV:CV, CV2,CW:CW, CW2)&gt;1, SIGN(LEN(CT2))) 大约需要 80 秒。不是真的可以接受,但在里面放了一个别针。

标签: arrays excel vba multidimensional-array conditional-formatting


【解决方案1】:

您说识别重复项;我听到Scripting.Dictionary 对象。

Public Sub lminyDupes()
    Dim d As Long, str As String, vAs As Variant, vCTCWs As Variant
    Dim dDUPEs As Object                      '<~~ Late Binding
    'Dim dDUPEs As New Scripting.Dictionary   '<~~ Early Binding

    Debug.Print Timer
    Application.ScreenUpdating = False '<~~ uncomment this once you are no longer debugging

    'Remove the next line with Early Binding¹
    Set dDUPEs = CreateObject("Scripting.Dictionary")
    dDUPEs.comparemode = vbTextCompare

    With Worksheets("Upcharge") '<~~ you know what worksheet you are supposed to be on
        With .Cells(1, 1).CurrentRegion
            With .Resize(.Rows.Count - 1, .Columns.Count).Offset(1, 0)
                .Columns(97).Interior.Pattern = xlNone  '<~~ reset column CS

                'the following is intended to mimic a CF rule using this formula
                '=AND(COUNTIFS(A:A, A2, CT:CT, CT2, CU:CU, CU2, CV:CV, CV2, CW:CW, CW2)>1, SIGN(LEN(CT2)))

                vAs = .Columns(1).Value2
                vCTCWs = Union(.Columns(98), .Columns(99), .Columns(100), .Columns(101)).Value2

                For d = LBound(vAs, 1) To UBound(vAs, 1)
                    If CBool(Len(vCTCWs(d, 1))) Then
                        'make a key of the criteria values
                        str = Join(Array(vAs(d, 1), vCTCWs(d, 1), vCTCWs(d, 2), vCTCWs(d, 3), vCTCWs(d, 4)), ChrW(8203))
                        If dDUPEs.exists(str) Then
                            'the comboned key exists in the dictionary; append the current row
                            dDUPEs.Item(str) = dDUPEs.Item(str) & Chr(44) & "CS" & d
                        Else
                            'the combined key does not exist in the dictionary; store the current row
                            dDUPEs.Add Key:=str, Item:="CS" & d
                        End If
                    End If
                Next d

                'reuse a variant var to provide row highlighting
                Erase vAs
                For Each vAs In dDUPEs.keys
                    'if there is more than a single cell address, highlight all
                    If CBool(InStr(1, dDUPEs.Item(vAs), Chr(44))) Then _
                        .Range(dDUPEs.Item(vAs)).Interior.Color = vbRed
                Next vAs
            End With
        End With

    End With

    dDUPEs.RemoveAll: Set dDUPEs = Nothing
    Erase vCTCWs

    Application.ScreenUpdating = True
    Debug.Print Timer

End Sub

这似乎比公式方法更快。


¹ 如果您计划将 Scripting.Dictionary 对象的后期绑定转换为早期绑定,则必须将 Microsoft Scripting Runtime 添加到 VBE 的工具 ► 参考中。

【讨论】:

  • 再一次,一个惊人的快速和有效的方法。我没有考虑过scripting.dictionary,因为因为我还在不断地了解它们,所以我还不知道我可以将它与.Union 结合使用。我的印象是一个值/列将被视为scripting.dictionary 中的键和值。我从你那里学到了很多东西,你对这种语言的了解和掌握让我感到惊讶。
  • 我是一个新手/正在朝着我希望成为该语言的中级用户的方向发展,但在先进技术的能力和最有效的方法方面仍有很多需要学习实施某些技术。再次感谢!
【解决方案2】:

条件格式和过滤

SUMPRODUCT vs COUNTIFS

首先,您选择的函数不适合如此多的行以及几个条件。 COUNTIFS function 可以执行许多与SUMPRODUCT function 相同的多条件操作,但通常需要 25-35% 的计算负载和时间。此外,可以在 COUNTIFS 中使用完整的列引用而不会造成损害,因为列引用在内部被截断到 Worksheet.UsedRange property 的限制。

你的标准公式可以用 COUNTIFS 写成,

=AND(COUNTIFS(A:A, A2, CT:CT, CT2, CU:CU, CU2, CV:CV, CV2, CW:CW, CW2)>1, CT2<>"")
'... or,
=COUNTIFS(A:A, A2, CT:CT, CT2, CT:CT, "<>", CU:CU, CU2, CV:CV, CV2, CW:CW, CW2)>1

将非空列 CT 条件直接带入 COUNTIFS 函数实际上稍微缩短了计算时间。

Only Calculate When You Have To

原始公式可以分解为两个主要条件。

  1. CT 列中的单元格是否非空白?
  2. 五列中的值是否与任何其他行中的相同五列相匹配?

如果条件不成立,基本的IF function 会停止处理。如果对 CT 列中的非空白单元格的测试被移动到换行 IF 中,那么只有在当前行的 CT 列中有值时才会处理 COUNTIFS(计算的大部分)。

改进后的标准公式变为,

=IF(CT2<>"", COUNTIFS(A:A, A2, CT:CT, CT2, CU:CU, CU2, CV:CV, CV2, CW:CW, CW2)>1)

此修改的好处取决于 CT 列中空白单元格的数量。如果 15,000 个单元格中只有 1% 是空白的,则几乎不会注意到改进。但是,如果 CT 列中 50% 的单元格通常是空白的,则会有很大的改进,因为您实际上将计算周期缩短了一半。

Sorting the Data to Limit the Ranges

到目前为止,最大的计算寄生虫是 COUNTIFS 在五个单独的列中查看 15,000 行数据。如果数据是按一个或多个条件列排序的,则无需查看所有 15,000 行以匹配所有五个条件列。

出于本练习的目的,假设 A 列以升序方式排序。如果您想检验此处讨论的假设,请立即对数据进行排序。

INDEX function 不仅仅返回一个值;它实际上返回一个有效的单元格地址。当以最常见的查找容量使用时,您会看到返回的值,但实际上,与仅返回单元格值的类似 VLOOKUP 操作不同,INDEX 正在返回实际单元格;例如=A1,而不是 A1 包含的 99。这种超功能可用于创建可用于其他功能的有效范围。例如A2:A9也可以写成INDEX(A:A, 2):INDEX(A:A, 9)

此功能不能直接在条件格式规则中使用。但是,它可以用在 Named Range 中,而命名范围可以用在条件格式规则中。

tl;dr

Sub lminyCFrule()

    Debug.Print Timer
    'Application.ScreenUpdating = False '<~~ uncomment this once you are no longer debugging
    On Error Resume Next    '<~~ needed for deleting objects without checking to see if they exist

    With Worksheets("Upcharge") '<~~ you know what worksheet you are supposed to be on
        If .AutoFilterMode Then .AutoFilterMode = False

        'delete any existing defined name called 'localXID' or 'local200'
        With .Parent
            .Names("localXID").Delete
            .Names("local200").Delete
        End With

        'create a new defined name called 'localXID' for CF rule method 1
        .Names.Add Name:="localXID", RefersToR1C1:= _
            "=INDEX('" & .Name & "'!C1:C104, MATCH('" & .Name & "'!RC1, '" & .Name & "'!C1, 0), 0):" & _
             "INDEX('" & .Name & "'!C1:C104, MATCH('" & .Name & "'!RC1, '" & .Name & "'!C1 ), 0)"
        'create a new defined name called 'local200' for CF rule method 2
        .Names.Add Name:="local200", RefersToR1C1:= _
            "=INDEX(Upcharge!C1:C104, MAX(2, ROW()-100), 0):INDEX(Upcharge!C1:C101, ROW()+100, 0)"

        With .Cells(1, 1).CurrentRegion
            'sort on column A in ascending order
             .Cells.Sort Key1:=.Columns(1), Order1:=xlAscending, _
                        Orientation:=xlTopToBottom, Header:=xlYes

            'create a CF rule on column CS
            With .Resize(.Rows.Count - 1, 1).Offset(1, 96)
                With .FormatConditions
                    .Delete
                    ' method 1 and method 2. Only use ONE of these!
                    ' method 1 - definitively start and end of XIDs in column A (slower, no mistakes)
                    '.Add Type:=xlExpression, Formula1:= _
                        "=IF(CT2<>"""", COUNTIFS(INDEX(localXID, 0, 1), A2, INDEX(localXID, 0, 98), CT2," & _
                                                "INDEX(localXID, 0, 99), CU2, INDEX(localXID, 0, 100), CV2," & _
                                                "INDEX(localXID, 0, 101), CW2)-1)"
                    ' method 2 - best guess at start and end of XIDs in column A (faster, guesswork at true scope)
                    .Add Type:=xlExpression, Formula1:= _
                        "=IF(CT2<>"""", COUNTIFS(INDEX(local200, 0, 1), A2, INDEX(local200, 0, 98), CT2," & _
                                                "INDEX(local200, 0, 99), CU2, INDEX(local200, 0, 100), CV2," & _
                                                "INDEX(local200, 0, 101), CW2)-1)"
                End With
                .FormatConditions(.FormatConditions.Count).Interior.ColorIndex = 3
            End With

            'Filter based on column CS is red
            .Columns(97).AutoFilter Field:=1, Criteria1:=vbRed, Operator:=xlFilterCellColor
        End With
    End With

    Application.ScreenUpdating = True
    Debug.Print Timer

End Sub

虽然不会快速尖叫,但它可以轻松完成工作。 “最佳猜测”比“确定的开始和结束”要快,但您冒着无法完全覆盖 A 列中重复项范围的风险。当然,控制范围的偏移量(例如上下 100)可能调整。

【讨论】:

  • 等等,你为什么回答两次?你的第一个答案对我来说完美。
  • 我昨晚为代码审查网站写的。它解决了使公式/条件格式化过程具有可接受的效率。我不愿意放弃这么多的努力,所以我把它贴在这里。
  • 啊,我明白了。仍然,非常感谢!你知识渊博。我会记住这一点,以备将来使用。
【解决方案3】:

为什么不删除Indirect() 并用一些稳定的Row 引用替换Countif() 函数。由于Indirect() 部分是易失性的,因此您可以直接使用一些稳定的行引用,例如$A$2:$A$50000,而不是使用Indirect(),这可能会显示出一些显着的性能变化。

或者

为您的数据使用创建表。在您的公式中使用表格参考,这将比Indirect() 参考更快。

编辑

你的实际公式

=AND(SUMPRODUCT(($A$2:$A$500=$A2)*($CU$2:$CU$500=$CU2)*($CV$2:$CV$500=$CV2)*($CW$2:$CW$500=$CW2)*($CX$2:$CX$500=$CX2))>1,$CU2 <> "")

你为什么不把它转换成Counti(S) 像下面这样的稳定​​引用?

=AND(COUNTIFS($A$2:$A$500,$A2,$CU$2:$CU$500,$CU2,$CV$2:$CV$500,$CV2,$CW$2:$CW**$500,$CW2,$CX$2:$CX$500,$CX2)>1,$CU12<>"")

【讨论】:

  • 只需选择您的完整数据并按 Ctrl+T 即可将数据转换为表格。之后进入公式并选择一个范围引用并将光标移至表的相关列,然后按 Ctrl+Spacebase(选择表的整个列),就像替换您对相关列表引用的范围引用一样。这样,如果在表格中添加任何数据,公式将自动覆盖。
  • 我需要通过 VBA 完成整个过程,所以这听起来不像一个选项。
  • 我已经编辑了原始帖子并添加了 Countif(S) 公式解决方案以提高性能。所以请尝试一下并告诉我们。
  • 为什么不将所有数据连接到辅助列并使用单个 countif() 公式以获得更好的性能?
  • 其实@Raystafarian,the consensus on meta was that formulas are code,即使你我不同意。
【解决方案4】:

考虑一个 SQL 解决方案,因为这是一个典型的 aggregate group by query,您可以在其中过滤大于 1 的计数。要执行您的路线,需要在循环中跨数组的所有元素进行许多条件逻辑。

虽然我建议您将数据简单地导入到类似 Excel 的同级 MS Access 的数据库中,但 Excel 可以使用 ADO connection 在自己的工作簿上运行 SQL 语句(不详述,但 Excel 和 Access 都使用相同的 Jet/ ACE 引擎)。一件好事是,您似乎已经准备好使用类似命名列结构的表来运行这样的查询。

以下示例在名为 Data (Data$) 的工作表中引用您的字段,并将查询输出到名为 Results 的工作表(带有标题)。根据需要更改名称。包括两个连接字符串(其中一个被注释掉)。希望它在你的最后运行!

Sub RunSQL()

    Dim conn As Object, rst As Object
    Dim i As Integer, fld As Object
    Dim strConnection As String, strSQL As String

    Set conn = CreateObject("ADODB.Connection")
    Set rst = CreateObject("ADODB.Recordset")

    ' Connection and SQL Strings
'    strConnection = "DRIVER={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" _
'                      & "DBQ=C:\Path\To\Workbook.xlsm;"
    strConnection = "Provider=Microsoft.ACE.OLEDB.12.0;" _
                       & "Data Source='C:\Path\To\Workbook.xlsm';" _
                       & "Extended Properties=""Excel 8.0;HDR=YES;"";"

    strSQL = " SELECT [Data$].[Product's XID], [Data$].[Upcharge Criteria 1]," _
                & " [Data$].[Upcharge Criteria 2], [Data$].[Upcharge Type]," _
                & " [Data$].[Upcharge Type], [Data$].[Upcharge Level]" _ 
                & " FROM [Data$]" _
                & " GROUP BY [Data$].[Product's XID], [Data$].[Upcharge Criteria 1]," _
                & " [Data$].[Upcharge Criteria 2], [Data$].[Upcharge Type]," _
                & " [Data$].[Upcharge Type], [Data$].[Upcharge Level]," _
                & " [Data$].[Product's XID]" _
                & " HAVING COUNT(*) > 1;"

    ' Open the db connection
    conn.Open strConnection
    rst.Open strSQL, conn

    ' Column headers
    i = 0
    Worksheets("Results").Range("A1").Activate
    For Each fld In rst.Fields
        ActiveCell.Offset(0, i) = fld.Name
        i = i + 1
    Next fld

    ' Data rows        
    Worksheets("Results").Range("A2").CopyFromRecordset rst

    rst.Close
    conn.Close

End Sub

【讨论】:

    【解决方案5】:

    这可能像魔术一样起作用,但不确定它是否会起作用。

    您能否创建另一个支持性(临时)列,连接所有四个条件?

    ZZ_Temp = 连接(CS;CV;CZ;等)

    我想,这样可以更快地显示/突出显示重复项。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多