web-dev-qa-db-ja.com

文字列配列の一致する値

問題:1d配列に正確に一致する値があるかどうかを見つけるより効率的な方法を探しています-基本的にブール値true/false

明白な何かを見落としていますか?または、コレクションオブジェクトまたは辞書を使用する必要があるときに配列を使用することで、間違ったデータ構造を使用していますか? ?後者では、.Containsまたは.Existsメソッド

Excelでは、次のようなベクトル配列の値を確認できます。

If Not IsError(Application.Match(strSearch, varToSearch, False)) Then
' Do stuff
End If

これは、このコンテキストでfirst一致する値のみを検出するMatch関数の制限の対象となる、完全一致インデックスを返します。これは一般的に使用される方法であり、私も長い間使用しています。

これはExcelには十分ですが、他のアプリケーションはどうですか?

他のアプリケーションでも、基本的に同じことができますが、Excelオブジェクトライブラリへの参照を有効にする必要があります。

   If Not IsError(Excel.Application.match(...))

しかし、それはばかげているように見え、アクセス許可/トラストセンターなどのために分散ファイルで管理するのは困難です。

Filter() 関数を使用しようとしました:

 If Not Ubound(Filter(varToSearch, strSearch)) = -1 Then
    'do stuff
 End If

しかし、このアプローチの問題は、Filterが完全一致の配列ではなく、部分一致の配列を返すことです。 (部分文字列/部分一致を返すことが有用である理由はわかりません。)

他の選択肢は、配列内の各値を文字通り反復することです(これも非常に一般的に使用されていると思います)-これは、ExcelのMatch関数を呼び出すよりも不必要に面倒です。

For each v in vArray
   If v = strSearch Then
    ' do stuff
   End If
Next
17
David Zemens

パフォーマンスについて話す場合、いくつかのテストを実行するための代替手段はありません。私の経験では、Application.Match()はループを使用する関数を呼び出すよりも最大で10倍遅くなります。

Sub Tester()

    Dim i As Long, b, t
    Dim arr(1 To 100) As String

    For i = 1 To 100
        arr(i) = "Value_" & i
    Next i

    t = Timer
    For i = 1 To 100000
        b = Contains(arr, "Value_50")
    Next i
    Debug.Print "Contains", Timer - t

    t = Timer
    For i = 1 To 100000
        b = Application.Match(arr, "Value_50", False)
    Next i
    Debug.Print "Match", Timer - t

End Sub


Function Contains(arr, v) As Boolean
Dim rv As Boolean, lb As Long, ub As Long, i As Long
    lb = LBound(arr)
    ub = UBound(arr)
    For i = lb To ub
        If arr(i) = v Then
            rv = True
            Exit For
        End If
    Next i
    Contains = rv
End Function

出力:

Contains       0.8710938 
Match          4.210938 
29
Tim Williams

「文字列値が配列に存在するかどうかを見つけるより効率的な方法(_Application.Match_と比較):」

あなたが使用しているもの、つまり_Application.Match_ほど効率的な方法はないと思います。

配列を使用すると、要素のインデックスがわかっていれば、どの要素でも効率的にアクセスできます。要素の値で何かをしたい場合(要素が存在するかどうかを確認する場合でも)、最悪の場合は配列のすべての要素をスキャンする必要があります。したがって、最悪の場合はn要素の比較が必要です。ここで、nは配列のサイズです。そのため、要素が存在するかどうかを確認するために必要な最大時間は、入力のサイズに比例します。つまり、O(n)です。これは、従来の配列を使用するすべての言語に適用されます。

より効率的にできる唯一のケースは、配列が特別な構造を持つ場合です。たとえば、配列の要素が並べ替えられている場合(アルファベット順など)、すべての配列をスキャンする必要はありません。中央の要素と比較してから、配列の左または右の部分と比較します(--- バイナリ検索) 。しかし、特別な構造を想定しなければ、希望はありません。

_Dictionary/Collection_は、あなたが指し示すように、その要素(O(1))への一定のキーアクセスを提供します。おそらく十分に文書化されていないのは、辞書要素(キーとアイテム)へのインデックスアクセスを持つこともできるということです。要素がDictionaryは保持されます。主な欠点は、要素ごとに2つのオブジェクトが保存されるため、より多くのメモリを使用することです。

まとめると、If Not IsError(Excel.Application.match(...))は愚かに見えますが、それでも(少なくとも理論的には)より効率的な方法です。許可の問題については、私の知識は非常に限られています。ホストアプリケーションに応じて、常にいくつかのFind- type関数があります(_C++_にはfindと_find_if_があります)。

それがお役に立てば幸いです!

編集

投稿の修正版とティムの回答を読んだ後、いくつかの考えを追加したいと思います。上記のテキストは、さまざまなデータ構造の理論的な時間の複雑さに焦点を当てており、実装の問題を無視しています。質問の精神はむしろ、「特定のデータ構造(配列)を与えられた」、実際に存在をチェックする最も効率的な方法だったと思います。

このため、ティムの答えは目を見張るものです。

従来のルール「VBAがあなたのためにそれを行うことができた場合、自分で再び書いてはいけない」は必ずしも真実ではありません。ループや比較などの単純な操作は、VBA関数を「同意」するよりも高速です。 2つの興味深いリンクは、 herehere です。

1
Ioannis

私は最高の代替ソリューションを探していました。単純な検索でも機能するはずです。

文字列の最初のインスタンスを見つけるには、次のコードを使用してみてください。

Sub find_strings_1()

Dim ArrayCh() As Variant
Dim rng As Range
Dim i As Integer

 ArrayCh = Array("a", "b", "c")

With ActiveSheet.Cells
    For i = LBound(ArrayCh) To UBound(ArrayCh)
        Set rng = .Find(What:=ArrayCh(i), _
        LookAt:=xlPart, _
        SearchOrder:=xlByColumns, _
        MatchCase:=False)

        Debug.Print rng.Address

    Next i
End With

End Sub

すべてのインスタンスを検索する場合は、以下を試してください。

Sub find_strings_2()

Dim ArrayCh() As Variant
Dim c As Range
Dim firstAddress As String
Dim i As Integer

 ArrayCh = Array("a", "b", "c") 'strings to lookup

With ActiveSheet.Cells
    For i = LBound(ArrayCh) To UBound(ArrayCh)
        Set c = .Find(What:=ArrayCh(i), LookAt:=xlPart, LookIn:=xlValues)

        If Not c Is Nothing Then
            firstAddress = c.Address 'used later to verify if looping over the same address
            Do
                '_____
                'your code, where you do something with "c"
                'which is a range variable,
                'so you can for example get it's address:
                Debug.Print ArrayCh(i) & " " & c.Address 'example
                '_____
                Set c = .FindNext(c)

            Loop While Not c Is Nothing And c.Address <> firstAddress
        End If
    Next i
End With

End Sub

1つのセル内に検索された文字列のインスタンスが複数ある場合、FindNextの特定により1つの結果のみを返すことに注意してください。

それでも、見つかった値を別の値に置き換えるためのコードが必要な場合は、最初のソリューションを使用しますが、少し変更する必要があります。

1
GrzMat