2016-06-09 76 views
3

再次這個例子是一個非常簡化版本的實際問題,涉及linq分組的自定義比較器。我做錯了什麼?寫一個linq groupby的自定義比較器由

下面的代碼產生下面 的結果(1.2,0),因爲1.1, (4.1,0),(4.1,0), (1.1,0),

但是我期待以下和1.2是<分開。 (1.2,0),(1.1,0), (4.1,0),(4.1,0),

class Program 
{ 
    static void Main(string[] args) 
    { 
     IEnumerable<Point> points = new List<Point> { 
      new Point(1.1, 0.0) 
      , new Point(4.1, 0.0) 
      , new Point(1.2, 0.0) 
      , new Point(4.1, 0.0) 
     }; 

     foreach (var group in points.GroupBy(p => p, new PointComparer())) 
     { 
      foreach (var num in group) 
       Console.Write(num.ToString() + ", "); 

      Console.WriteLine(); 
     } 

     Console.ReadLine(); 
    } 
} 

class PointComparer : IEqualityComparer<Point> 
{ 
    public bool Equals(Point a, Point b) 
    { 
     return Math.Abs(a.X - b.X) < 1.0; 
    } 

    public int GetHashCode(Point point) 
    { 
     return point.X.GetHashCode() 
      ^point.Y.GetHashCode(); 
    } 
} 

class Point 
{ 
    public double X; 
    public double Y; 

    public Point(double p1, double p2) 
    { 
     X = p1; 
     Y = p2; 
    } 

    public override string ToString() 
    { 
     return "(" + X + ", " + Y + ")"; 
    } 
} 
+1

我不認爲你可以使用group by作爲聚類點的解決方案。一個原因是GetHashcode必須返回相同項目的哈希。 –

回答

4

使用相等比較器的分組算法(我認爲所有的LINQ方法)總是首先比較哈希碼,並且只有在兩個哈希碼相等時才執行Equals。你可以看到,如果在相等比較添加跟蹤語句:

class PointComparer : IEqualityComparer<Point> 
{ 
    public bool Equals(Point a, Point b) 
    { 
     Console.WriteLine("Equals: point {0} - point {1}", a, b); 
     return Math.Abs(a.X - b.X) < 1.0; 
    } 

    public int GetHashCode(Point point) 
    { 
     Console.WriteLine("HashCode: {0}", point); 
     return point.X.GetHashCode() 
      ^point.Y.GetHashCode(); 
    } 
} 

其中在結果:

HashCode: (1.1, 0) 
HashCode: (4.1, 0) 
HashCode: (1.2, 0) 
HashCode: (4.1, 0) 
Equals: point (4.1, 0) - point (4.1, 0) 
(1.1, 0), 
(4.1, 0), (4.1, 0), 
(1.2, 0), 

只有兩個百分點Equals被執行死刑等於哈希碼。

現在,您可以通過總是返回0作爲哈希碼來欺騙比較。如果你這樣做,輸出將是:

HashCode: (1.1, 0) 
HashCode: (4.1, 0) 
Equals: point (1.1, 0) - point (4.1, 0) 
HashCode: (1.2, 0) 
Equals: point (4.1, 0) - point (1.2, 0) 
Equals: point (1.1, 0) - point (1.2, 0) 
HashCode: (4.1, 0) 
Equals: point (4.1, 0) - point (4.1, 0) 
(1.1, 0), (1.2, 0), 
(4.1, 0), (4.1, 0), 

現在每對Equals被執行,你有你的分組。

但...

什麼是「平等」?如果您添加另一個點(2.1, 0.0),您希望在一個組中使用哪些點?使用符號的模糊平等,我們有 -

1.1 ≈ 1.2 
1.2 ≈ 2.1 

1.1 !≈ 2.1 

這意味着1.12.1將永遠不會在一個組(其Equals從來沒有通過),並這取決於按點的順序是1.1還是2.1是否與1.2分組。

所以,你在這裏滑坡。通過接近來聚類點遠非微不足道。你正在進入cluster analysis的境界。

+0

這需要考慮一下。我試過你總是返回散列碼0和我的實際數據(不在示例中)的建議,它工作得很好。我將不得不分析它可能會失敗的地方。 – DustyB

+1

獲得某種規律性(可預測的結果)的一種方法是在分組之前始終按照相同的方式對分組點進行排序(例如先X,然後Y)。 –

3

不要忘記的GetHashCode的影響。有一個期望GetHashCode將始終返回任何兩個對象相同的值爲每個Equals將返回true。如果你未能達到這個期望,你會得到意想不到的結果。

具體來說,GroupBy可能使用類似哈希表的東西,以便它可以將項目組合在一起,而不必將每個項目與每個其他項目進行比較。如果GetHashCode返回的值不會最終將兩個對象放入哈希表的同一個桶中,則它會假定它們不相等,並且永遠不會嘗試對它們調用Equals

當您嘗試找出GetHashCode的正確實施方案時,您會發現存在您試圖對對象進行分組的基本問題。如果您有x值爲1.0,1.62.2的點,您會期望什麼? 1.02.2離彼此太遠而落入同一組,但是1.6已經足夠接近其他兩點,它應該與它們在同一組中。所以,你的Equals方法打破平等的Transitive屬性:

每當A = B和B = C,然後還一個= C

如果你想要做聚類分組,你將需要使用更多不同的數據結構和算法。如果你只是試圖使點的位置標準化,你可能只能說points.GroupBy(p => (int)p.X),並完全避免相等比較器。

+0

塔塔正是我所看到的,我的GetHasCode對這個問題有着重要的影響。我對它做出的任何更改都會導致我的輸出發生更改。我的GetHashCode方法應該是什麼樣子? – DustyB

+1

@DustyB:看到我更新的答案。你目前的定義是什麼使兩個項目「相等」是站不住腳的。你必須提出一個更具體的想法,試圖將項目分組,或者使用基於聚類而不是平等的不同數據結構和算法。 – StriplingWarrior

+0

我的實際問題使用3D點,我想將它們聚類在一起,使得10個單位內的點與同一個平面組內的點相連。用自定義比較器做到這一點是否可行? – DustyB