【问题标题】:Speed up sorting array and binary search加快排序数组和二分查找
【发布时间】:2016-05-24 01:48:18
【问题描述】:

我正在尝试比较两个工作表,它们都有我想要比较的“EMAIL”列。一列包含已发送的电子邮件,另一列包含人们实际点击的电子邮件。

我已将两列定义为数组,EmailList 和 ClickthroughsList,并且对于 EmailList 数组中的每个位置,我都有一个 If 循环循环通过 ClickthroughList 直到找到匹配项:

            For i = 1 To lRow
                EmailList(i) = ThisWorkbook.Sheets(2).Cells(i, col).Value
                Sheets(7).Cells(i, 1).Value = EmailList(i)

                If i = 1 Then
                Sheets(7).Cells(i, 2).Value = "Sent"
                Sheets(7).Cells(i, 5).Value = "Unique Clickthroughs"

                Else
                Sheets(7).Cells(i, 2).Value = 1

                    For bi = 1 To bRow
                    If EmailList(i) = ClickthroughsList(bi) Then
                    Sheets(7).Cells(i, 5).Value = 1
                    End If
                    Next bi

                End If

            Next i

如果找到匹配项,我希望它在 Sheets(7) 上显示“1”,如上所述。这意味着该特定电子邮件已被点击。

当数组为 700k+ 行或以上时,对于两个数组,此代码需要几个小时才能运行。

有人建议我可以先进行排序,然后再进行二进制搜索。但是,我仍然需要数组在 EmailList 中的位置,以便我可以在它旁边放一个“1”(并且该行将包含更多特定于该电子邮件的信息)。

想到的是定义一个新的排序数组,同时保留旧数组,并且当我在新排序数组中找到匹配项时,将其匹配回旧数组以知道位置?

            Option Explicit
            Private wsSent As Worksheet
            Private aCell As Range, Rng As Range
            Private col As Long, lRow As Long
            Private colName As String
            Private i As Long
            Private EmailList() As String

            Private wsClickthroughs As Worksheet
            Private bCell As Range, bRng As Range
            Private bcol As Long, bRow As Long
            Private bcolName As String
            Private bi As Long
            Private ClickthroughsList() As String

            Sub EmailArrayClickthroughs()

            Application.ScreenUpdating = False

            GetClickthroughsArray
                '~~> Change this to the relevant sheet
                Set wsSent = ThisWorkbook.Sheets(2)

                With wsSent
                    Set aCell = .Range("A1:DZ1").Find(What:="EMAIL", LookIn:=xlValues, LookAt:=xlWhole, _
                                MatchCase:=False, SearchFormat:=False)

                    '~~> If Found
                    If Not aCell Is Nothing Then
                        col = aCell.Column
                        colName = Split(.Cells(, col).Address, "$")(1)
                        lRow = .Range(colName & .Rows.Count).End(xlUp).Row
                    '~~> This is your range
                        Set Rng = .Range(colName & "2:" & colName & lRow)

                    Else
                        MsgBox "EMAIL (Clickthroughs) Not Found"
                    End If
                End With

            ReDim EmailList(lRow)

            For i = 1 To lRow
                EmailList(i) = ThisWorkbook.Sheets(2).Cells(i, col).Value
                Sheets(7).Cells(i, 1).Value = EmailList(i)

                If i = 1 Then
                Sheets(7).Cells(i, 2).Value = "Sent"
                Sheets(7).Cells(i, 5).Value = "Unique Clickthroughs"

                Else
                Sheets(7).Cells(i, 2).Value = 1

                    For bi = 1 To bRow
                    If EmailList(i) = ClickthroughsList(bi) Then
                    Sheets(7).Cells(i, 5).Value = 1
                    End If
                    Next bi

                End If

            Next i

            Debug.Print Rng.Address

            Application.ScreenUpdating = True

            End Sub


            Sub GetClickthroughsArray()

                '~~> Change this to the relevant sheet
                Set wsClickthroughs = ThisWorkbook.Sheets(5)

                With wsClickthroughs
                    Set bCell = .Range("A1:DZ1").Find(What:="EMAIL", LookIn:=xlValues, LookAt:=xlWhole, _
                                MatchCase:=False, SearchFormat:=False)

                    '~~> If Found
                    If Not bCell Is Nothing Then
                        bcol = bCell.Column
                        bcolName = Split(.Cells(, bcol).Address, "$")(1)
                        bRow = .Range(bcolName & .Rows.Count).End(xlUp).Row
             '~~> This is your range
                        Set bRng = .Range(bcolName & "2:" & bcolName & bRow)
                    Else
                        MsgBox "EMAIL (opens) Not Found"
                    End If
                End With

            Debug.Print bRng.Address

            ReDim ClickthroughsList(bRow)

            For bi = 1 To bRow
                ClickthroughsList(bi) = ThisWorkbook.Sheets(5).Cells(bi, bcol).Value
            Next bi

            End Sub

【问题讨论】:

    标签: arrays excel vba sorting binary


    【解决方案1】:

    如果我了解您要执行的操作,则无需使用 VBA。

    只需使用 MATCH 工作表功能。在要查看值是否在“ClickThrough”表中的列中,放入

     =IF(ISNA(MATCH(colEmail, YourSheet!colEmail1:colEmail1000, 0)), "0", "1")
    

    YourSheet 必须是带有点击电子邮件的工作表名称。 colEmail 需要是包含您要查找的“电子邮件”的任何列,并且 1000 是记录数。您可以改为只输入“A:A”来获取整个列。

    最后它看起来像这样:

    = IF(IF(ISNA(MATCH(A2, mySheet!$A$2:$A$1000, 0)), "0", "1")
    

    然后将其复制到整个列。

    【讨论】:

    • 您好 OpiesDad,感谢您的回复!你理解我需要做什么是正确的,事实上我正在按照你的建议做。这是因为我觉得这种方法有点乏味(首先等待时间非常长,如果我将它复制到太多行,即使我的 excel 中的计算也会变得不稳定,即错误)。这就是为什么我想写一个脚本,这样我至少可以专注于其他事情。
    • Per Holmes 的回答,循环不会帮助你。 MATCH 功能非常快。使用他建议的方法可能会加快速度,但是大约 250k 行的匹配功能大约需要 20 秒。 750k 行可能需要大约一分钟。如果您需要处理这么多数据并且希望它比这更快,您将需要使用另一个平台,例如 Access 或 SQL Server。比赛花了你多长时间?
    • 排序会使查找速度更快,但排序本身需要时间......
    • 嗨 OpiesDad,谢谢。匹配实际上需要 45 分钟才能匹配 700k 数组
    • 对不起,我忘了说这是在 64 位机器上。顺便说一句,谢谢你告诉我关于循环的事情,绝对是从这次经历中学到的。
    【解决方案2】:

    好的,这是一个相当简单的修复。您可以在 VBA 中为性能做的最糟糕的事情是循环单元格。要解决此问题,请创建一个变量范围并将列表分配给它。即使有 700K 行,在单个命令中将两个列表分配给一个变量也应该花费不到一秒的时间。然后在比较它们时,您会喜欢在不到 30 秒的时间内进行比较。如果您需要根据比较的结果写回行,请创建一个大小相同的变量,然后将您想要写入的任何内容写入该变量。然后在一切完成后,通过循环或范围粘贴将该变量转储到电子表格中。我猜您可以轻松地将其缩短到 2 分钟以下。 Make it faster?

    【讨论】:

    • 您好福尔摩斯,感谢您的回复。你是绝对正确的,我不应该为这个问题做循环。我在下面对 Tim 的代码做了一个变体,现在不到两分钟。再次感谢您的帮助。
    【解决方案3】:

    这是匹配两个列表的一种方法:

    Sub TestMatch()
    
        Dim rng1 As Range, rng2 As Range, f
    
        Set rng1 = Range("C3:C22")
        Set rng2 = Range("F3:F19")
    
        f = "=IFERROR(MATCH(" & rng1.Address(False, False) & _
          "," & rng2.Address(True, True) & ",0),0)>0"
    
        Debug.Print f
    
        rng1.Offset(0, 1).Value = ActiveSheet.Evaluate(f)
    
    End Sub
    

    ...而且这个更快(

    Sub TestMatch2()
    
        Dim d, arr1, arr2, r
        Set d = CreateObject("scripting.dictionary")
    
        'get the data in arrays
        arr1 = Range("A1:A700000").Value
        arr2 = Range("E1:E7000").Value
    
        'put the smaller list in the dictionary
        For r = 1 To UBound(arr2, 1)
            If Not d.exists(arr2(r, 1)) Then d.Add arr2(r, 1), 1
        Next r
    
        'check the larger list against the dictionary,
        '   modifiying the array as we go
        For r = 1 To UBound(arr1, 1)
            arr1(r, 1) = IIf(d.exists(arr1(r, 1)), 1, "")
        Next r
        'populate the result column
        Range("A1:A700000").Offset(0, 1).Value = arr1
    
    End Sub
    

    【讨论】:

    • 嗨蒂姆,非常感谢!我对您的 Sub TestMatch 做了一个变体,在两个范围内,每个范围都有 700k+ 行,现在可以运行不到 2 分钟。再次感谢您,这真的让我很开心。
    猜你喜欢
    • 1970-01-01
    • 2021-12-20
    • 1970-01-01
    • 2013-04-23
    • 1970-01-01
    • 1970-01-01
    • 2011-12-25
    • 2013-04-01
    • 1970-01-01
    相关资源
    最近更新 更多