Vergleichen Sie zwei Arrays und erhalten Sie die Werte, die nicht üblich sind

71

Ich wollte eine kleine Logik, um den Inhalt von zwei Arrays zu vergleichen und mit Powershell den Wert zu erhalten, der unter ihnen nicht üblich ist

Beispiel wenn

$a1=@(1,2,3,4,5)
$b1=@(1,2,3,4,5,6)

$ c, das die Ausgabe ist, sollte mir den Wert " 6" geben, der die Ausgabe des ungewöhnlichen Werts zwischen beiden Arrays ist.

Kann mir jemand dabei helfen? Vielen Dank!

Power Shell
quelle
Um der vorliegenden Aufgabe einen Namen zu geben, zumindest in Bezug auf das, was die Compare-ObjectAntworten hier implementieren: Der symmetrische Unterschied zwischen zwei Mengen wird bestimmt - aber nur, wenn die Eingabearrays wirklich Mengen sind (wie in der Frage), dh nein doppelte Elemente .
mklement0
Eine verwandte Aufgabe - die relative Komplement aka Einstelldifferenzdruck - die Elemente eines Satzes nicht auch in einem anderen? - ist das Thema dieser verwandten Frage .
mklement0

Antworten:

108
PS > $c = Compare-Object -ReferenceObject (1..5) -DifferenceObject (1..6) -PassThru
PS > $c
6
Shay Levy
quelle
2
Ein Hinweis für diejenigen, die versuchen, die Keys-Sammlungen von zwei Hashtabellen zu vergleichen: Ich nahm an, dass Keys-Sammlungen wie Arrays sind und dass ich Compare-Object verwenden könnte, um sie zu vergleichen. Es stellt sich heraus, dass Compare-Object jede Keys-Auflistung als ein einzelnes Objekt betrachtet und daher ein Ergebnis zurückgibt, das angibt, dass alle Schlüssel in Hashtabelle 1 in Hashtabelle 2 fehlen und umgekehrt. Damit es funktioniert, musste ich die Keys-Sammlungen in Arrays konvertieren. Der schnellste Weg, den ich gefunden habe, ist: $keys = @($Null) * $ht.Keys.Countein Array mit der richtigen Größe zu initialisieren und dann $ht.Keys.CopyTo($keys, 0)die Schlüssel in das Array zu kopieren.
Simon Tewsi
1
Es sieht so aus, als könnten Sie die KeyCollectionto- object[]Konvertierung durchführen, indem Sie den Wert einfach in @()like einschließen @($keys).
mdonoughe
Tolle Lösung, kleine Einschränkung: Während -PassThruauch die interessierenden Eingabeelemente durchlaufen werden, werden sie zusätzlich mit einer Note-Eigenschaft dekoriertSideIndicator , die in Szenarien wie der JSON-Serialisierung auftreten kann. Versuchen Sie es (Compare-Object 1 2 -PassThru).SideIndicator. (Compare Object ...).InputObject, wie in dieser Antwort , vermeidet dieses Problem.
mklement0
@ SimonTewsi: mdonoughe ist richtig; zur Veranschaulichung:$ht1 = @{foo=1;bar=2}; $ht2 = @{foo=1;baz=3}; Compare-Object @($ht1.Keys) @($ht2.Keys)
mklement0
69

Sammlung

$a = 1..5
$b = 4..8

$Yellow = $a | Where {$b -NotContains $_}

$Yellowenthält alle Elemente in $aaußer denen, die in enthalten sind $b:

PS C:\> $Yellow
1
2
3

$Blue = $b | Where {$a -NotContains $_}

$Blueenthält alle Elemente in $baußer denen, die in enthalten sind $a:

PS C:\> $Blue
6
7
8

$Green = $a | Where {$b -Contains $_}

Nicht in Frage, aber trotzdem; Greenenthält die Elemente, die in beiden $aund enthalten sind $b.

PS C:\> $Green
4
5

Hinweis : Whereist ein Alias ​​von Where-Object. Alias ​​kann mögliche Probleme verursachen und die Wartung von Skripten erschweren.


Nachtrag 12. Oktober 2019

Wie von @xtreampb und @ mklement0 kommentiert: Obwohl aus dem Beispiel in der Frage nicht ersichtlich, ist die Aufgabe, die die Frage impliziert (Werte "nicht gemeinsam"), die symmetrische Differenz zwischen den beiden Eingabesätzen (die Vereinigung von Gelb und Blau). .

Union

Der symmetrische Unterschied zwischen $aund $bkann wörtlich definiert werden als die Vereinigung von $Yellowund $Blue:

$NotGreen = $Yellow + $Blue

Welches ist ausgeschrieben:

$NotGreen = ($a | Where {$b -NotContains $_}) + ($b | Where {$a -NotContains $_})

Performance

Wie Sie vielleicht bemerken, gibt es in dieser Syntax einige (redundante) Schleifen: Alle Elemente in der Liste $adurchlaufen (verwenden Where) Elemente in der Liste $b(verwenden -NotConatins) und umgekehrt. Leider ist die Redundanz schwer zu vermeiden, da es schwierig ist, das Ergebnis jeder Seite vorherzusagen. Eine Hash-Tabelle ist normalerweise eine gute Lösung, um die Leistung redundanter Schleifen zu verbessern. Dazu definiere ich gerne die Frage neu: Holen Sie sich die Werte, die einmal in der Summe der Sammlungen erscheinen ( $a + $b) :

$Count = @{}
$a + $b | ForEach-Object {$Count[$_] += 1}
$Count.Keys | Where-Object {$Count[$_] -eq 1}

Wenn Sie die ForEachAnweisung anstelle des ForEach-ObjectCmdlets und die WhereMethode anstelle von verwenden, können Where-ObjectSie die Leistung um den Faktor 2,5 erhöhen:

$Count = @{}
ForEach ($Item in $a + $b) {$Count[$Item] += 1}
$Count.Keys.Where({$Count[$_] -eq 1})

LINQ

Aber Language Integrated Query (LINQ) leicht schlagen alle nativen Powershell und nativen .NET - Methoden (siehe auch High Performance Powershell mit LINQ und mklement0 Antwort für Can folgende Nested foreach - Schleife in Powershell vereinfacht werden? :

Um LINQ verwenden zu können, müssen Sie die Array-Typen explizit definieren:

[Int[]]$a = 1..5
[Int[]]$b = 4..8

Und benutze den [Linq.Enumerable]::Operator:

$Yellow   = [Int[]][Linq.Enumerable]::Except($a, $b)
$Blue     = [Int[]][Linq.Enumerable]::Except($b, $a)
$Green    = [Int[]][Linq.Enumerable]::Intersect($a, $b)
$NotGreen = [Int[]]([Linq.Enumerable]::Except($a, $b) + [Linq.Enumerable]::Except($b, $a))

Benchmark

Die Benchmark-Ergebnisse hängen stark von der Größe der Sammlungen und der Anzahl der tatsächlich freigegebenen Elemente ab. Als "Durchschnitt" gehe ich davon aus, dass die Hälfte jeder Sammlung mit der anderen geteilt wird.

Using             Time
Compare-Object    111,9712
NotContains       197,3792
ForEach-Object    82,8324
ForEach Statement 36,5721
LINQ              22,7091

Um einen guten Leistungsvergleich zu erhalten, sollten Caches gelöscht werden, indem beispielsweise eine neue PowerShell-Sitzung gestartet wird.

$a = 1..1000
$b = 500..1500

(Measure-Command {
    Compare-Object -ReferenceObject $a -DifferenceObject $b  -PassThru
}).TotalMilliseconds
(Measure-Command {
    ($a | Where {$b -NotContains $_}), ($b | Where {$a -NotContains $_})
}).TotalMilliseconds
(Measure-Command {
    $Count = @{}
    $a + $b | ForEach-Object {$Count[$_] += 1}
    $Count.Keys | Where-Object {$Count[$_] -eq 1}
}).TotalMilliseconds

(Measure-Command {
    $Count = @{}
    ForEach ($Item in $a + $b) {$Count[$Item] += 1}
    $Count.Keys.Where({$Count[$_] -eq 1})
}).TotalMilliseconds

[Int[]]$a = $a
[Int[]]$b = $b
(Measure-Command {
    [Int[]]([Linq.Enumerable]::Except($a, $b) + [Linq.Enumerable]::Except($b, $a))
}).TotalMilliseconds
Eisen
quelle
1
Was sich auch als nützlich erweisen kann, ist das, was nicht üblich ist (! Grün). Also, was ist nur in gelb oder blau (1,2,3,6,7,8)
xtreampb
@xtreampb, ich habe Ihrem Vorschlag einige Gedanken gegeben und bin zu dem Schluss gekommen, dass Sie alle Arten von hoch entwickelten eingebetteten ForEachSchleifen dafür erstellen könnten , aber am Ende ist es einfach : $NotGreen = $Yellow + $Blue, was geschrieben steht:$NotGreen = ($a | Where {$b -NotContains $_}) + ($b | Where {$a -NotContains $_})
iRon
1
Zum Kommentar von @ xtreampb hinzufügen: Die Aufgabe, die die Frage impliziert (Werte "nicht gemeinsam"), ist die symmetrische Differenz zwischen den beiden Eingabesätzen (die Vereinigung von Gelb und Blau). Das ist es, was die anderen Antworten hier implementieren, während Ihre etwas anderes implementiert: den relativen Komplement- / Mengenunterschied (entweder gelb oder blau) und den Schnittpunkt - obwohl Sie diese sehr gut veranschaulichen. Ich schlage vor, dies in der Antwort klar zu machen.
mklement0
Klarstellung: Die Compare-ObjectLösungen hier implementieren die symmetrische Differenz nur, wenn die Eingabearrays keine Duplikate aufweisen . Erwähnenswert ist auch: Die Where-Object/ -not[contains]-Lösungen sind konzeptionell einfach und prägnant, aber bei größeren Arrays kann dies zu einem Leistungsproblem führen, da für jedes Eingabeelement eine Array-Suche durchgeführt wird. LINQ bietet eine viel schnellere Lösung , obwohl diese etwas komplex ist.
mklement0
1
@ mklement0, danke für die Klarstellungen und den Hinweis auf die tatsächliche Anfrage nach einem symmetrischen Unterschied , das habe ich verpasst (teilweise, weil es nicht aus dem Beispiel in der Frage stammt). Ich habe einige Leistungstests durchgeführt und werde meine Antwort an diesem Wochenende aktualisieren.
iRon
15

Ansehen Compare-Object

Compare-Object $a1 $b1 | ForEach-Object { $_.InputObject }

Oder wenn Sie wissen möchten, wo das Objekt hingehört, schauen Sie sich SideIndicator an:

$a1=@(1,2,3,4,5,8)
$b1=@(1,2,3,4,5,6)
Compare-Object $a1 $b1
stej
quelle
8
Durch Hinzufügen der Option -PassThru wird die Ausgabe angenehmer. Vergleichsobjekt $ a1 $ b1 -PassThru
MonkeyWrench
Soweit ich sehen kann Compare-Object $a1 $b1 | ForEach-Object { $_.InputObject }und Compare-Object $a1 $b1 -PassThruscheinbar identische Ergebnisse erbringe. Natürlich ist die Option -PassThru prägnanter.
Simon Tewsi
1
@SimonTewsi: Sie sind fast gleich: Während sie -PassThruauch die interessierenden Eingabeelemente weiterleiten , werden sie zusätzlich mit einer Note-Eigenschaft dekoriertSideIndicator , die in unerwarteten Szenarien auftreten kann. Versuchen Sie es (Compare-Object 1 2 -PassThru).SideIndicator.
mklement0
3

Versuchen:

$a1=@(1,2,3,4,5)
$b1=@(1,2,3,4,5,6)
(Compare-Object $a1 $b1).InputObject

Oder Sie können verwenden:

(Compare-Object $b1 $a1).InputObject

Die Reihenfolge spielt keine Rolle.

Slogmeister Extraordinaire
quelle
3

Ihre Ergebnisse sind nur dann hilfreich, wenn die Arrays zuerst sortiert werden. Um ein Array zu sortieren, führen Sie es über Sort-Object aus.

$x = @(5,1,4,2,3)
$y = @(2,4,6,1,3,5)

Compare-Object -ReferenceObject ($x | Sort-Object) -DifferenceObject ($y | Sort-Object)
Macher
quelle
1
-SyncWindow hilft bei "wie weit man im Array nach dem Match sucht"
Garrett
3
Nein, eine Sortierung ist nicht erforderlich: Compare-Object $x $yGibt das gleiche Ergebnis wie oben zurück und zeigt, dass 6 im Referenzarray fehlt. (Ich habe dies sowohl ab der heutigen PS-Version (5.1) als auch ab der PS-Version 3 überprüft.)
Michael Sorens
1

Dies sollte helfen, verwendet einfache Hash-Tabelle.

$a1=@(1,2,3,4,5) $b1=@(1,2,3,4,5,6)


$hash= @{}

#storing elements of $a1 in hash
foreach ($i in $a1)
{$hash.Add($i, "present")}

#define blank array $c
$c = @()

#adding uncommon ones in second array to $c and removing common ones from hash
foreach($j in $b1)
{
if(!$hash.ContainsKey($j)){$c = $c+$j}
else {hash.Remove($j)}
}

#now hash is left with uncommon ones in first array, so add them to $c
foreach($k in $hash.keys)
{
$c = $c + $k
}
Adithya Surampudi
quelle
1
Es ist nicht in Ordnung, den fragwürdigen Codierungsstil wegzulassen und Hashtabellen als Ersatz für den Operator -contains zu verwenden. Das Schlimmste ist, dass diese Lösung dem Vergleichsobjekt nichts hinzufügt.
dmitry