問題: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
パフォーマンスについて話す場合、いくつかのテストを実行するための代替手段はありません。私の経験では、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
「文字列値が配列に存在するかどうかを見つけるより効率的な方法(_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つの興味深いリンクは、 here と here です。
私は最高の代替ソリューションを探していました。単純な検索でも機能するはずです。
文字列の最初のインスタンスを見つけるには、次のコードを使用してみてください。
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つの結果のみを返すことに注意してください。
それでも、見つかった値を別の値に置き換えるためのコードが必要な場合は、最初のソリューションを使用しますが、少し変更する必要があります。