在VBA中减去范围(Excel)

我正在努力做什么

我试图编写一个函数来减去Excel范围 。 它应该有两个输入参数:范围A和范围B.它应该返回范围对象,该范围对象是范围A的一部分,不属于范围B的一部分(如在设置减法中)

我试过的

我在网上看到了一些使用临时工作表来做这件事的例子(很快,但可能会引入一些受保护的工作簿等问题)以及其他一些例子,它们逐个单元格通过第一个范围检查与第二个一个(非常缓慢)。

经过一番思考,我已经提出了这个代码{1} ,它工作得更快,但仍然很慢。 从表示整个工作表的范围中减去1到5分钟取决于第二范围的复杂程度。

当我查看那些试图找到更快速的代码时,我发现应用分而治之的范例的可能性,我做了{2} 。 但是这让我的代码变慢了。 我不是一个CS家伙,所以我可能做错了什么,或者这个算法根本不是分治策略应该使用的那个,我不知道。

我也曾尝试使用大部分递归来重写它,但是这花了很长时间才完成,或者(更经常)抛出堆栈空间错误。 我没有保存代码。

我已经能够做的唯一(略微)成功的改进是添加一个翻转开关{3} ,首先通过行,然后(在下一次调用中)通过列,而不是在同一次调用中通过两次,但效果没有我所希望的那么好。 现在我看到,即使我们不通过第一次调用中的所有行,在第二次调用中,我们仍然会循环执行与第一次调用相同数量的行,但只有这些行稍微短一点:)

我很感谢任何帮助改进或重写此功能,谢谢!

该解决方案基于Dick Kusleika接受的答案

迪克库斯莱卡,非常感谢你提供你的答案! 我想我会用它做一些修改:

  • 摆脱了全局变量(mrBuild)
  • 修正了“一些重叠”的情况以排除“不重叠”的情况
  • 增加了更复杂的条件来选择是分割上下还是从左到右的范围
  • 通过这些修改,代码在大多数常见情况下运行速度非常快。 正如已经指出的那样,它仍然会缓慢,我同意这种棋盘式的巨大范围是不可避免的。

    我认为这个代码仍然有改进的空间,我会更新这个帖子,以防我修改它。

    改进可能性:

  • 选择如何分割范围的启发式(按列或按行)
  • {0}解决方案代码

    Public Function SubtractRanges(rFirst As Range, rSecond As Range) As Range
    '
    ' Returns a range of cells that are part of rFirst, but not part of rSecond
    ' (as in set subtraction)
    '
    ' This function handles big input ranges really well!
    '
    ' The reason for having a separate recursive function is
    ' handling multi-area rFirst range
    '
        Dim rInter As Range
        Dim rReturn As Range
        Dim rArea As Range
    
        Set rInter = Intersect(rFirst, rSecond)
        Set mrBuild = Nothing
    
        If rInter Is Nothing Then 'no overlap
            Set rReturn = rFirst
        ElseIf rInter.Address = rFirst.Address Then 'total overlap
            Set rReturn = Nothing
        Else 'partial overlap
            For Each rArea In rFirst.Areas
                Set mrBuild = BuildRange(rArea, rInter) 'recursive
            Next rArea
            Set rReturn = mrBuild
        End If
    
        Set SubtractRanges = rReturn
    End Function
    
    
    Private Function BuildRange(rArea As Range, rInter As Range, _
    Optional mrBuild As Range = Nothing) As Range
    '
    ' Recursive function for SubtractRanges()
    '
    ' Subtracts rInter from rArea and adds the result to mrBuild
    '
        Dim rLeft As Range, rRight As Range
        Dim rTop As Range, rBottom As Range
        Dim rInterSub As Range
        Dim GoByColumns As Boolean
    
        Set rInterSub = Intersect(rArea, rInter)
        If rInterSub Is Nothing Then 'no overlap
            If mrBuild Is Nothing Then
                Set mrBuild = rArea
            Else
                Set mrBuild = Union(mrBuild, rArea)
            End If
        ElseIf Not rInterSub.Address = rArea.Address Then 'some overlap
            If Not rArea.Cells.CountLarge = 1 Then 'just in case there is only one cell for some impossible reason
    
                ' Decide whether to go by columns or by rows
                ' (helps when subtracting whole rows/columns)
                If Not rInterSub.Columns.Count = rArea.Columns.Count And _
                ((Not rInterSub.Cells.CountLarge = 1 And _
                (rInterSub.Rows.Count > rInterSub.Columns.Count _
                And rArea.Columns.Count > 1) Or (rInterSub.Rows.Count = 1 _
                And Not rArea.Columns.Count = 1)) Or _
                (rInterSub.Cells.CountLarge = 1 _
                And rArea.Columns.Count > rArea.Rows.Count)) Then
                        GoByColumns = True
                Else
                        GoByColumns = False
                End If
    
                If Not GoByColumns Then
                    Set rTop = rArea.Resize(rArea.Rows.Count  2) 'split the range top to bottom
                    Set rBottom = rArea.Resize(rArea.Rows.Count - rTop.Rows.Count).Offset(rTop.Rows.Count)
                    Set mrBuild = BuildRange(rTop, rInterSub, mrBuild) 'rerun it
                    Set mrBuild = BuildRange(rBottom, rInterSub, mrBuild)
                Else
                    Set rLeft = rArea.Resize(, rArea.Columns.Count  2) 'split the range left to right
                    Set rRight = rArea.Resize(, rArea.Columns.Count - rLeft.Columns.Count).Offset(, rLeft.Columns.Count)
                    Set mrBuild = BuildRange(rLeft, rInterSub, mrBuild) 'rerun it
                    Set mrBuild = BuildRange(rRight, rInterSub, mrBuild)
                End If
            End If
        End If
    
        Set BuildRange = mrBuild
    End Function
    

    其他代码在问题中提到

    {1}初始代码(逐行,逐列)

    Function SubtractRanges(RangeA, RangeB) As Range
    '
    ' Returns a range of cells that are part of RangeA, but not part of RangeB
    '
    ' This function handles big RangeA pretty well (took less than a minute
    ' on my computer with RangeA = ActiveSheet.Cells)
    '
        Dim CommonArea As Range
        Dim Result As Range
    
        Set CommonArea = Intersect(RangeA, RangeB)
        If CommonArea Is Nothing Then
            Set Result = RangeA
        ElseIf CommonArea.Address = RangeA.Address Then
            Set Result = Nothing
        Else
            'a routine to deal with A LOT of cells in RangeA
            'go column by column, then row by row
            Dim GoodCells As Range
            Dim UnworkedCells As Range
    
            For Each Area In RangeA.Areas
                For Each Row In Area.Rows
                    Set RowCommonArea = Intersect(Row, CommonArea)
                    If Not RowCommonArea Is Nothing Then
                        If Not RowCommonArea.Address = Row.Address Then
                            Set UnworkedCells = AddRanges(UnworkedCells, Row)
                        End If
                    Else
                        Set GoodCells = AddRanges(GoodCells, Row)
                    End If
                Next Row
    
                For Each Column In Area.Columns
                    Set ColumnCommonArea = Intersect(Column, CommonArea)
                    If Not ColumnCommonArea Is Nothing Then
                        If Not ColumnCommonArea.Address = Column.Address Then
                            Set UnworkedCells = AddRanges(UnworkedCells, Column)
                        End If
                    Else
                        Set GoodCells = AddRanges(GoodCells, Column)
                    End If
                Next Column
            Next Area
    
            If Not UnworkedCells Is Nothing Then
                For Each Area In UnworkedCells
                    Set GoodCells = AddRanges(GoodCells, SubtractRanges(Area, CommonArea))
                Next Area
            End If
    
            Set Result = GoodCells
        End If
    
        Set SubtractRanges = Result
    End Function
    

    {2}分而治之

    Function SubtractRanges(RangeA, RangeB) As Range
    '
    ' Returns a range of cells that are part of RangeA, but not part of RangeB
    '
        Dim CommonArea As Range
        Dim Result As Range
    
        Set CommonArea = Intersect(RangeA, RangeB)
        If CommonArea Is Nothing Then
            Set Result = RangeA
        ElseIf CommonArea.Address = RangeA.Address Then
            Set Result = Nothing
        Else
            'a routine to deal with A LOT of cells in RangeA
            'go column by column, then row by row
            Dim GoodCells As Range
            Dim UnworkedCells As Range
    
            For Each Area In RangeA.Areas
    
                RowsNumber = Area.Rows.Count
                If RowsNumber > 1 Then
                    Set RowsLeft = Range(Area.Rows(1), Area.Rows(RowsNumber / 2))
                    Set RowsRight = Range(Area.Rows(RowsNumber / 2 + 1), Area.Rows(RowsNumber))
                Else
                    Set RowsLeft = Area
                    Set RowsRight = CommonArea.Cells(1, 1) 'the next best thing to Nothing - will end its cycle rather fast and won't throw an error with For Each statement
                End If
                For Each Row In Array(RowsLeft, RowsRight)
                    Set RowCommonArea = Intersect(Row, CommonArea)
                    If Not RowCommonArea Is Nothing Then
                        If Not RowCommonArea.Address = Row.Address Then
                            Set UnworkedCells = AddRanges(UnworkedCells, Row)
                        End If
                    Else
                        Set GoodCells = AddRanges(GoodCells, Row)
                    End If
                Next Row
    
                ColumnsNumber = Area.Columns.Count
                If ColumnsNumber > 1 Then
                    Set ColumnsLeft = Range(Area.Columns(1), Area.Columns(ColumnsNumber / 2))
                    Set ColumnsRight = Range(Area.Columns(ColumnsNumber / 2 + 1), Area.Columns(ColumnsNumber))
                Else
                    Set ColumnsLeft = Area
                    Set ColumnsRight = CommonArea.Cells(1, 1)
                End If
                For Each Column In Array(ColumnsLeft, ColumnsRight)
                    Set ColumnCommonArea = Intersect(Column, CommonArea)
                    If Not ColumnCommonArea Is Nothing Then
                        If Not ColumnCommonArea.Address = Column.Address Then
                            Set UnworkedCells = AddRanges(UnworkedCells, Column)
                        End If
                    Else
                        Set GoodCells = AddRanges(GoodCells, Column)
                    End If
                Next Column
            Next Area
    
            If Not UnworkedCells Is Nothing Then
                For Each Area In UnworkedCells
                    Set GoodCells = AddRanges(GoodCells, SubtractRanges(Area, CommonArea))
                Next Area
            End If
    
            Set Result = GoodCells
        End If
    
        Set SubtractRanges = Result
    End Function
    

    {3}初始代码+翻转开关(逐行或逐列逐行)

    Function SubtractRanges(RangeA, RangeB, Optional Flip As Boolean = False) As Range
    '
    ' Returns a range of cells that are part of RangeA, but not part of RangeB
    '
    ' This function handles big RangeA pretty well (took less than a minute
    ' on my computer with RangeA = ActiveSheet.Cells)
    '
        Dim CommonArea As Range
        Dim Result As Range
    
        Set CommonArea = Intersect(RangeA, RangeB)
        If CommonArea Is Nothing Then
            Set Result = RangeA
        ElseIf CommonArea.Address = RangeA.Address Then
            Set Result = Nothing
        Else
            'a routine to deal with A LOT of cells in RangeA
            'go column by column, then row by row
            Dim GoodCells As Range
            Dim UnworkedCells As Range
    
            For Each Area In RangeA.Areas
                If Flip Then
                    For Each Row In Area.Rows
                        Set RowCommonArea = Intersect(Row, CommonArea)
                        If Not RowCommonArea Is Nothing Then
                            If Not RowCommonArea.Address = Row.Address Then
                                Set UnworkedCells = AddRanges(UnworkedCells, Row)
                            End If
                        Else
                            Set GoodCells = AddRanges(GoodCells, Row)
                        End If
                    Next Row
                Else
                    For Each Column In Area.Columns
                        Set ColumnCommonArea = Intersect(Column, CommonArea)
                        If Not ColumnCommonArea Is Nothing Then
                            If Not ColumnCommonArea.Address = Column.Address Then
                                Set UnworkedCells = AddRanges(UnworkedCells, Column)
                            End If
                        Else
                            Set GoodCells = AddRanges(GoodCells, Column)
                        End If
                    Next Column
                End If
            Next Area
    
            If Not UnworkedCells Is Nothing Then
                For Each Area In UnworkedCells
                    Set GoodCells = AddRanges(GoodCells, SubtractRanges(Area, CommonArea, Not Flip))
                Next Area
            End If
    
            Set Result = GoodCells
        End If
    
        Set SubtractRanges = Result
    End Function
    

    这里和那里提到的一个小帮手功能:

    Function AddRanges(RangeA, RangeB)
    '
    ' The same as Union built-in but handles empty ranges fine.
    '
        If Not RangeA Is Nothing And Not RangeB Is Nothing Then
            Set AddRanges = Union(RangeA, RangeB)
        ElseIf RangeA Is Nothing And RangeB Is Nothing Then
            Set AddRanges = Nothing
        Else
            If RangeA Is Nothing Then
                Set AddRanges = RangeB
            Else
                Set AddRanges = RangeA
            End If
        End If
    End Function
    

    你的分而治之看起来似乎是一条好路。 你需要引入一些递归,并且应该相当快

    Private mrBuild As Range
    
    Public Function SubtractRanges(rFirst As Range, rSecond As Range) As Range
    
        Dim rInter As Range
        Dim rReturn As Range
        Dim rArea As Range
    
        Set rInter = Intersect(rFirst, rSecond)
        Set mrBuild = Nothing
    
        If rInter Is Nothing Then 'No overlap
            Set rReturn = rFirst
        ElseIf rInter.Address = rFirst.Address Then 'total overlap
            Set rReturn = Nothing
        Else 'partial overlap
            For Each rArea In rFirst.Areas
                BuildRange rArea, rInter
            Next rArea
            Set rReturn = mrBuild
        End If
    
        Set SubtractRanges = rReturn
    
    End Function
    
    Sub BuildRange(rArea As Range, rInter As Range)
    
        Dim rLeft As Range, rRight As Range
        Dim rTop As Range, rBottom As Range
    
        If Intersect(rArea, rInter) Is Nothing Then 'no overlap
            If mrBuild Is Nothing Then
                Set mrBuild = rArea
            Else
                Set mrBuild = Union(mrBuild, rArea)
            End If
        Else 'some overlap
            If rArea.Columns.Count = 1 Then 'we've exhausted columns, so split on rows
                If rArea.Rows.Count > 1 Then 'if one cell left, don't do anything
                    Set rTop = rArea.Resize(rArea.Rows.Count  2) 'split the range top to bottom
                    Set rBottom = rArea.Resize(rArea.Rows.Count - rTop.Rows.Count).Offset(rTop.Rows.Count)
                    BuildRange rTop, rInter 'rerun it
                    BuildRange rBottom, rInter
                End If
            Else
                Set rLeft = rArea.Resize(, rArea.Columns.Count  2) 'split the range left to right
                Set rRight = rArea.Resize(, rArea.Columns.Count - rLeft.Columns.Count).Offset(, rLeft.Columns.Count)
                BuildRange rLeft, rInter 'rerun it
                BuildRange rRight, rInter
            End If
        End If
    
    End Sub
    

    这些并不是特别大的范围,但它们都跑得很快

    ?subtractranges(rangE("A1"),range("a10")).Address
    $A$1
    ?subtractranges(range("a1"),range("a1")) is nothing
    True
    ?subtractranges(range("$B$3,$B$6,$C$8:$W$39"),range("a1:C10")).Address
    $C$11:$C$39,$D$8:$W$39
    ?subtractranges(range("a1:C10"),range("$B$3,$B$6,$C$8:$W$39")).Address
    $A$1:$A$10,$B$1:$B$2,$B$4:$B$5,$B$7:$B$10,$C$1:$C$7
    

    我的解决方案更短,但我不知道它是否是最佳解决方案:

    Sub RangeSubtraction()
    
        Dim firstRange As Range
        Dim secondRange As Range
        Dim rIntersect As Range
        Dim rOutput As Range
        Dim x As Range
    
        Set firstRange = Range("A1:B10")
        Set secondRange = Range("A5:B10")
    
        Set rIntersect = Intersect(firstRange, secondRange)
    
        For Each x In firstRange
            If Intersect(rIntersect, x) Is Nothing Then
                If rOutput Is Nothing Then 'ugly 'if-else' but needed, can't use Union(Nothing, Range("A1")) etc.
                    Set rOutput = x
                Else
                    Set rOutput = Application.Union(rOutput, x)
                End If
            End If
        Next x
    
        Msgbox rOutput.Address
    
    End Sub
    

    虽然是迭代的而不是递归的,但这是我的解决方案。 该函数返回rangeA减去rangeB

    public Function SubtractRange(rangeA Range, rangeB as Range) as Range
    'rangeA is a range to subtract from
    'rangeB is the range we want to subtract
    
     Dim existingRange As Range
      Dim resultRange As Range
      Set existingRange = rangeA
      Set resultRange = Nothing
      Dim c As Range
      For Each c In existingRange
      If Intersect(c, rangeB) Is Nothing Then
        If resultRange Is Nothing Then
          Set resultRange = c
        Else
          Set resultRange = Union(c, resultRange)
        End If
      End If
      Next c
      Set SubtractRange = resultRange
    End Sub
    
    链接地址: http://www.djcxy.com/p/77313.html

    上一篇: Subtracting ranges in VBA (Excel)

    下一篇: Indices of all matches of a regex