比较两个数组并获得不常见的值


71

我想要一个小的逻辑来比较两个数组的内容并使用Powershell获得其中不常见的值

例如

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

输出的$ c应该给我值“ 6”,这是两个数组之间不常见值的输出。

有人可以帮我吗!谢谢!


要给手头的任务起个名字,至少要考虑到Compare-Object这里实现的答案:确定两个集合之间的对称差异-但前提是输入数组是真正的集合(如问题中所述),即没有重复元素
mklement0

一个相关的任务-相对补(又称集合差异) -一组中的哪些元素不在另一组中?-是这个相关问题的主题。
mklement0

Answers:


108
PS > $c = Compare-Object -ReferenceObject (1..5) -DifferenceObject (1..6) -PassThru
PS > $c
6

2
给那些试图比较两个哈希表的Keys集合的人的注释:我假设Keys集合就像数组,并且可以使用Compare-Object比较它们。事实证明,Compare-Object将每个Keys集合视为一个对象,因此返回一个结果,指示哈希表1中的所有键均从哈希表2中丢失,反之亦然。为了使其正常工作,我不得不将Keys集合转换为数组。我发现最快的方法是:$keys = @($Null) * $ht.Keys.Count初始化一个正确大小的数组,然后$ht.Keys.CopyTo($keys, 0)将Keys复制到该数组。
西蒙·图西

1
它看起来像你能做的KeyCollectionobject[]的只是包装的价值转换@()一样@($keys)
mdonoughe

很棒的解决方案,小警告:虽然-PassThru还传递了感兴趣的输入元素,但它还用note属性装饰了它们这些SideIndicator属性可能会在JSON序列化等场景中出现。尝试(Compare-Object 1 2 -PassThru).SideIndicator(Compare Object ...).InputObject就像此答案一样,避免了该问题。
mklement0

@SimonTewsi:mdonoughe是正确的;说明:$ht1 = @{foo=1;bar=2}; $ht2 = @{foo=1;baz=3}; Compare-Object @($ht1.Keys) @($ht2.Keys)
mklement0

69

采集

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

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

$Yellow包含中的所有项目,$a但以下项目除外$b

PS C:\> $Yellow
1
2
3

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

$Blue包含中的所有项目,$b但以下项目除外$a

PS C:\> $Blue
6
7
8

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

没问题,但是无论如何;Green包含在双方的项目$a$b

PS C:\> $Green
4
5

注意Where是的别名Where-Object。别名会引入可能的问题并使脚本难以维护。


附录2019年10月12日

正如@xtreampb和@ mklement0所评论的:尽管未从问题示例中显示,但该问题所隐含的任务(值为“ not common”)是两个输入集(黄色和蓝色的并集)之间的对称差异。

联盟

$a和之间的对称差$b可以从字面上定义为$Yellow和的并集$Blue

$NotGreen = $Yellow + $Blue

写出:

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

性能

您可能会注意到,此语法中有很多循环(冗余):列表中的所有项目通过列表中的项目$a(使用Where)遍历$b(使用-NotConatins),反之亦然。不幸的是,由于难以预测每一侧的结果,因此很难避免冗余。哈希表通常是提高冗余循环性能的良好解决方案。为此,我想重新定义问题:获取在集合的总和($a + $b)中出现一次的值

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

通过使用ForEach语句而不是ForEach-Objectcmdlet和Where方法而不是cmdlet,Where-Object可以将性能提高2.5倍:

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

LINQ

但是语言集成查询(LINQ)可以轻松击败任何本机PowerShell和本机.Net方法(另请参见具有LINQ的高性能PowerShell和mklement0的答案,可以在PowerShell中简化以下嵌套的foreach循环吗?

要使用LINQ,您需要显式定义数组类型:

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

并使用[Linq.Enumerable]::运算符:

$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))

基准测试

基准测试结果在很大程度上取决于集合的大小以及实际共享的项目数(作为“平均值”),我假设每个集合的一半彼此共享。

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

为了获得良好的性能比较,应通过例如启动新的PowerShell会话来清除缓存。

$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

1
可能有用的是不常见的(绿色)。所以只有黄色或蓝色(1,2,3,6,7,8)
xtreampb

@xtreampb,我给了您一些建议,并得出结论,您可以ForEach为此创建各种复杂的嵌入式循环,但最后它很简单:$NotGreen = $Yellow + $Blue,其写为:$NotGreen = ($a | Where {$b -NotContains $_}) + ($b | Where {$a -NotContains $_})
iRon

1
要添加到@ xtreampb的评论:任务的问题暗示(值“不常见”)是对称差的两个输入集之间(该工会的黄色和蓝色)。这就是这里其他答案的实现方式,而您的答案实现了一些不同的东西:相对补数/集合差(黄色或蓝色)和交集 -尽管您很好地说明了这些。我建议在答案中阐明这一点。
mklement0

澄清:Compare-Object如果输入数组没有重复项,则此处的解决方案仅实现对称差。还值得一提的是Where-Object/-not[contains]解决方案在概念上简单明了,但是对于较大的数组,由于对每个输入元素都执行数组查找,因此可能会带来性能问题-LINQ提供了更快的解决方案,尽管有些复杂。
mklement0

1
@ mklement0,感谢您的澄清并指出对称性差异的实际要求,但我错过了(部分原因是该问题并非来自示例中的问题)。我已经进行了一些性能测试,并将在本周末更新我的答案。
iRon

15

看着 Compare-Object

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

或者,如果您想知道对象所属的位置,请查看SideIndicator:

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

8
添加-PassThru选项使其输出更好。比较对象$ a1 $ b1 -PassThru
MonkeyWrench

从我所看到的,Compare-Object $a1 $b1 | ForEach-Object { $_.InputObject }Compare-Object $a1 $b1 -PassThru似乎产生相同的输出。当然,-PassThru选项更加简洁。
西蒙·图西

1
@SimonTewsi:它们几乎是相同的:虽然-PassThru还传递了感兴趣的输入元素,但它还用note属性装饰了它们这些SideIndicator属性可能会在意外情况下浮出水面。尝试(Compare-Object 1 2 -PassThru).SideIndicator
mklement0

3

尝试:

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

或者,您可以使用:

(Compare-Object $b1 $a1).InputObject

顺序无关紧要。


3

除非先对数组进行排序,否则您的结果将无济于事。要对数组排序,请通过Sort-Object运行它。

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

Compare-Object -ReferenceObject ($x | Sort-Object) -DifferenceObject ($y | Sort-Object)

1
-SyncWindow帮助“在阵列中查找匹配项的距离”
Garrett

3
否,排序是必需的:Compare-Object $x $y将返回相同的结果如上述,显示出6从参考阵列丢失。(我从今天的PS版本(5.1)到PS版本3都对此进行了检查。)
Michael Sorens,

1

这应该有所帮助,使用简单的哈希表。

$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
}

1
不使用可疑的编码样式,使用哈希表代替-contains运算符是不行的。最糟糕的是,此解决方案未向“比较对象”添加任何内容。
德米特里(Dmitry)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.