可排序的 C# 集合,允许使用重复的键。

119

我正在编写一个程序,用于设置报告中各种对象出现的顺序。

这个顺序是在Excel电子表格上的Y位置(单元格)。

下面是代码的演示部分。

我想要做到的是拥有一个集合,使我能够添加多个对象,并且我可以得到一个基于顺序排序的集合。

SortedList list = new SortedList();

Header h = new Header();
h.XPos = 1;
h.name = "Header_1";
list.Add(h.XPos, h);

h = new Header();
h.XPos = 1;
h.name = "Header_2";
list.Add(h.XPos, h);
我知道SortedList不允许这样做,而且我一直在寻找替代方案。我不想消除重复,已经尝试过List<KeyValuePair<int, object>>了。 谢谢。

1
集合在获得初始成员列表后,是否需要支持插入/删除操作? - Ani
2
当您尝试使用“List”时,有什么问题没有解决? - diceguyd30
我不仅仅想要排序并获取对象,而是想获取整个排序后的列表。因此在下面的示例中,两个Header对象都应该存在,并且按顺序排列。如果我添加另一个XPos为2的Header对象,则应该在列表中有3个对象,其中2个对象的XPos为1,第三个对象为XPos为2。 - Mayur Kotlikar
只是一点提示:当我遇到这种情况时,我发现通用的List与不太知名的BinarySearch行为(针对未找到的项)结合使用效果非常好。 - J Trana
16个回答

99

使用自己的IComparer!

如其他答案所述,您应该使用自己的比较器类。为此,我使用了一个通用的IComparer类,它可以与任何实现了IComparable接口的对象一起使用:

/// <summary>
/// Comparer for comparing two keys, handling equality as beeing greater
/// Use this Comparer e.g. with SortedLists or SortedDictionaries, that don't allow duplicate keys
/// </summary>
/// <typeparam name="TKey"></typeparam>
public class DuplicateKeyComparer<TKey>
                :
             IComparer<TKey> where TKey : IComparable
{
    #region IComparer<TKey> Members

    public int Compare(TKey x, TKey y)
    {
        int result = x.CompareTo(y);

        if (result == 0)
            return 1; // Handle equality as being greater. Note: this will break Remove(key) or
        else          // IndexOfKey(key) since the comparer never returns 0 to signal key equality
            return result;
    }

    #endregion
}

当实例化一个新的SortedList、SortedDictionary等时,您将使用它:

SortedList<int, MyValueClass> slist = new SortedList<int, MyValueClass>(new DuplicateKeyComparer<int>());

这里的int是可以重复的关键字。


51
但是你无法从中移除任何关键部件。 - Shashwat
12
没错,Shachwat!你不能使用Remove(key)或IndexOfKey(key),因为比较器从未返回0来表示键相等。但如果你有它们的索引,你可以使用RemoveAt(index)来删除项目。 - Knasterbax
1
我也遇到了同样的问题,我使用了 SortedDictionary。它允许删除操作。 - Shashwat
13
请注意,这样做会破坏比较器的自反性。它可能(并且将)破坏BCL中的某些内容。 - ghord
2
这应该返回-1以保持顺序。 - M.kazem Akhgary
显示剩余8条评论

18

你可以安全地使用List<>。List有一个Sort方法,其中一个重载接受IComparer。你可以创建自己的排序器类。这里是一个例子:

private List<Curve> Curves;
this.Curves.Sort(new CurveSorter());

public class CurveSorter : IComparer<Curve>
{
    public int Compare(Curve c1, Curve c2)
    {
        return c2.CreationTime.CompareTo(c1.CreationTime);
    }
}

1
我不仅仅想要排序并获取对象,而是想要获取整个已排序列表。因此,在下面的示例中,两个Header对象都应该存在,并且按顺序一个在另一个下面。如果我添加另一个XPos = 2的Header对象,则应该在列表中有3个对象,其中2个对象的XPos = 1,第三个对象的XPos = 2。 - Mayur Kotlikar
1
好的,你的意思是说,每当一个元素被插入到列表中时,它都应该按照排序顺序插入到正确的位置。如果我理解有误,请纠正我。让我看一下,很快就会回复你。 - Dipti Mehta
请注意,List<T>.Sort使用多种排序算法,具体取决于集合大小,并非所有算法都是稳定排序。因此,与等效对象进行比较的添加到集合中的对象可能不会按照它们添加的顺序出现。 - silent tone
我选择了这个选项,这样我就可以停止使用reduce函数创建大量的KeyValuePairs了。 - Chris Marisic

11

我使用以下内容:

public class TupleList<T1, T2> : List<Tuple<T1, T2>> where T1 : IComparable
{
    public void Add(T1 item, T2 item2)
    {
        Add(new Tuple<T1, T2>(item, item2));
    }

    public new void Sort()
    {
        Comparison<Tuple<T1, T2>> c = (a, b) => a.Item1.CompareTo(b.Item1);
        base.Sort(c);
    }

}

我的测试案例:

[TestMethod()]
    public void SortTest()
    {
        TupleList<int, string> list = new TupleList<int, string>();
        list.Add(1, "cat");
        list.Add(1, "car");
        list.Add(2, "dog");
        list.Add(2, "door");
        list.Add(3, "elephant");
        list.Add(1, "coconut");
        list.Add(1, "cab");
        list.Sort();
        foreach(Tuple<int, string> tuple in list)
        {
            Console.WriteLine(string.Format("{0}:{1}", tuple.Item1,tuple.Item2));
        }
        int expected_first = 1;
        int expected_last = 3;
        int first = list.First().Item1;  //requires using System.Linq
        int last = list.Last().Item1;    //requires using System.Linq
        Assert.AreEqual(expected_first, first);
        Assert.AreEqual(expected_last, last);
    }

输出:

1:cab
1:coconut
1:car
1:cat
2:door
2:dog
3:elephant

2
元组在 .NET 的所有版本中都不可用,但可以用 KeyValuePair<K,V> 替代。 - Reuben

9
问题在于数据结构设计与要求不匹配:需要为相同的XPos存储多个标头。因此,SortedList<XPos, value>不应该具有Header值,而应该具有List<Header>值。这是一个简单且小的改变,但它可以解决所有问题并避免创建新的问题(请参见下面的说明):
using System;
using System.Collections.Generic;

namespace TrySortedList {
  class Program {

    class Header {
      public int XPos;
      public string Name;
    }

    static void Main(string[] args) {
      SortedList<int, List<Header>> sortedHeaders = new SortedList<int,List<Header>>();
      add(sortedHeaders, 1, "Header_1");
      add(sortedHeaders, 1, "Header_2");
      add(sortedHeaders, 2, "Header_3");
      foreach (var headersKvp in sortedHeaders) {
        foreach (Header header in headersKvp.Value) {
          Console.WriteLine(header.XPos + ": " + header.Name);
        }
      }
    }

    private static void add(SortedList<int, List<Header>> sortedHeaders, int xPos, string name) {
      List<Header> headers;
      if (!sortedHeaders.TryGetValue(xPos, out headers)){
        headers = new List<Header>();
        sortedHeaders[xPos] = headers;
      }
      headers.Add(new Header { XPos = xPos, Name = name });
    }
  }
}

Output:
1: Header_1
1: Header_2
2: Header_3

请注意,添加“有趣”的键,例如添加随机数或假装具有相同值的2个XPos不同,会导致许多其他问题。例如,删除特定标题变得困难甚至不可能。
还要注意,仅需要对少量List
进行排序比每个Header都要好得多。例如:如果有100个XPos,每个XPos都有100个标题,则需要对10000个Header进行排序,而不是100个List

当然,这种解决方案也有一个缺点:如果有许多只有1个Header的XPos,则需要创建许多列表,这是一些开销。
更新22.12.2021
我终于找到时间编写了一个名为SortedBucketCollection的适当集合,它的行为类似于SortedList。它为每个项目使用2个键,第一个与SortedList键相同,许多项目可以具有相同值的该键。第二个键用于区分共享key1值的项目。SortedBucketCollection使用的存储空间比SortedList>少,因为它为每个“bucket”使用链接列表而不是List<>。
使用SortedBucketCollection的代码如下: using System;
namespace SortedBucketCollectionDemo {

  public record FinanceTransaction
  (int No, DateTime Date, string Description, decimal Amount);

  class Program {
    static void Main(string[] args) {
      //Constructing a SortedBucketCollection
      var transactions = 
        new SortedBucketCollection<DateTime, int, FinanceTransaction>
                                  (ft=>ft.Date, ft=>ft.No);
      var date1 = DateTime.Now.Date;

      //Adding an item to SortedBucketCollection
      transactions.Add(new FinanceTransaction(3, date1, "1.1", 1m));
      transactions.Add(new FinanceTransaction(1, date1, "1.2", 2m));
      transactions.Add(new FinanceTransaction(0, date1, "1.3", 3m));
      var date2 = date1.AddDays(-1);
      transactions.Add(new FinanceTransaction(1, date2, "2.1", 4m));
      transactions.Add(new FinanceTransaction(2, date2, "2.2", 5m));

      //Looping over all items in a SortedBucketCollection
      Console.WriteLine("foreach over all transactions");
      foreach (var transaction in transactions) {
        Console.WriteLine(transaction.ToString());
      }

      //Accessing one particular transaction
      var transaction12 = transactions[date1, 1];

      //Removing  a transaction
      transactions.Remove(transaction12!);

      //Accessing all items of one day
      Console.WriteLine();
      Console.WriteLine("foreach over transactions of one day");
      Console.WriteLine(date1);
      foreach (var transaction in transactions[date1]) {
        Console.WriteLine(transaction.ToString());
      }
    }
  }
}

第一个foreach循环的输出:

FinanceTransaction { No = 1, Date = 07.11.2021 00:00:00, Description = 2.1, Amount = 4 }
FinanceTransaction { No = 2, Date = 07.11.2021 00:00:00, Description = 2.2, Amount = 5 }
FinanceTransaction { No = 0, Date = 08.11.2021 00:00:00, Description = 1.3, Amount = 3 }
FinanceTransaction { No = 1, Date = 08.11.2021 00:00:00, Description = 1.2, Amount = 2 }
FinanceTransaction { No = 3, Date = 08.11.2021 00:00:00, Description = 1.1, Amount = 1 }

请注意,项目不是按照它们添加的顺序进行迭代,而是按照它们的key1key2进行排序。
有关SortedBucketCollection的详细描述和源代码,请参阅我在CodeProject上的文章SortedBucketCollection: A memory efficient SortedList accepting multiple items with the same key

这是最直接的解决方案。还要检查SortedDictionary,它在某些情况下类似,速度更快。 - Hogan
这是一个非常好的解决方案。可以很容易地将该功能封装到一些自定义集合对象中,这将非常有用。思路不错,谢谢彼得分享! - Adam P
@AdamP:感谢您的建议,我刚刚按照您的建议编写了一个名为SortedBucketCollection的集合,它的作用类似于SortedList<int,List<Header>>,但使用的内存更少。请查看我的回答中的更新内容。 - Peter Huber

7

相对于以上所有解决方案,最简单的方法是使用SortedSet<T>,它接受一个IComparer<SortableKey>类,然后按照以下方式实现Compare方法:

public int Compare(SomeClass x, SomeClass y)
{
    var compared = x.SomeSortableKeyTypeField.CompareTo(y.SomeSortableKeyTypeField);
    if (compared != 0)
        return compared;

    // to allow duplicates
    var hashCodeCompare = x.GetHashCode().CompareTo(y.GetHashCode());
    if (hashCodeCompare != 0)
        return hashCodeCompare;

    if (Object.ReferenceEquals(x, y))
        return 0;

    // for weird duplicate hashcode cases, throw as below or implement your last chance comparer
    throw new ComparisonFailureException();

}

4
我使用了SortedSet<T>,其中T具有自己的递增int ID,在每次实例化时递增以确保即使其他字段相同,每个T都是唯一的。 - Skystrider
3
使用 GetHashCode 进行比较是危险的,可能会导致意外的假重复。虽然大部分时间它都能正常工作,但我绝不会在任何重要的事情上使用它。 - Hogan

4
非常感谢您的帮助。在搜索更多内容时,我找到了这个解决方案。(在Stackoverflow.com的其他问题中可用)
首先,我创建了一个类,用于封装我的类(标题、页脚等)的对象。
public class MyPosition
{
    public int Position { get; set; }
    public object MyObjects{ get; set; }
}

所以这个类应该保持对象,并且每个对象的PosX作为int Position。

List<MyPosition> Sequence= new List<MyPosition>();
Sequence.Add(new MyPosition() { Position = 1, Headerobject });
Sequence.Add(new MyPosition() { Position = 2, Headerobject1 });
Sequence.Add(new MyPosition() { Position = 1, Footer });

League.Sort((PosA, PosB) => PosA.Position.CompareTo(PosB.Position));

我最终得到的是已排序的“Sequence”列表。

3

谢谢。我的问题是对象不仅仅是一个类型(不只是Header),它们可以是不同的类型(比如Footer、Sidebar等),但每个对象都会有XPos属性。 - Mayur Kotlikar
此外,我相信Lookup上没有公共构造函数。有什么好的解决方法吗? - Jeff B
1
@JeffBridgman,你将不得不依赖于Linq。 你可以对任何IEnumerable<T>进行ToLookup操作。 - nawfal
8
允许重复的键,但不会保持任何排序! - Roman Starkov

2

这个集合类将维护重复项并按插入排序顺序进行排序。诀窍在于在插入时使用唯一值标记项目以保持稳定的排序顺序。然后我们将所有内容封装在ICollection接口中。

public class SuperSortedSet<TValue> : ICollection<TValue>
{
    private readonly SortedSet<Indexed<TValue>> _Container;
    private int _Index = 0;
    private IComparer<TValue> _Comparer;

    public SuperSortedSet(IComparer<TValue> comparer)
    {
        _Comparer = comparer;
        var c2 = new System.Linq.Comparer<Indexed<TValue>>((p0, p1) =>
        {
            var r = _Comparer.Compare(p0.Value, p1.Value);
            if (r == 0)
            {
                if (p0.Index == -1
                    || p1.Index == -1)
                    return 0;

                return p0.Index.CompareTo(p1.Index);

            }
            else return r;
        });
        _Container = new SortedSet<Indexed<TValue>>(c2);
    } 

    public IEnumerator<TValue> GetEnumerator() { return _Container.Select(p => p.Value).GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    public void Add(TValue item) { _Container.Add(Indexed.Create(_Index++, item)); }

    public void Clear() { _Container.Clear();}

    public bool Contains(TValue item) { return _Container.Contains(Indexed.Create(-1,item)); }

    public void CopyTo(TValue[] array, int arrayIndex)
    {
        foreach (var value in this)
        {
            if (arrayIndex >= array.Length)
            {
                throw new ArgumentException("Not enough space in array");
            }
            array[arrayIndex] = value;
            arrayIndex++;
        }
    }

    public bool Remove(TValue item) { return _Container.Remove(Indexed.Create(-1, item)); }

    public int Count {
        get { return _Container.Count; }
    }
    public bool IsReadOnly {
        get { return false; }
    }
}

一个测试类
[Fact]
public void ShouldWorkWithSuperSortedSet()
{
    // Sort points according to X
    var set = new SuperSortedSet<Point2D>
        (new System.Linq.Comparer<Point2D>((p0, p1) => p0.X.CompareTo(p1.X)));

    set.Add(new Point2D(9,10));
    set.Add(new Point2D(1,25));
    set.Add(new Point2D(11,-10));
    set.Add(new Point2D(2,99));
    set.Add(new Point2D(5,55));
    set.Add(new Point2D(5,23));
    set.Add(new Point2D(11,11));
    set.Add(new Point2D(21,12));
    set.Add(new Point2D(-1,76));
    set.Add(new Point2D(16,21));

    var xs = set.Select(p=>p.X).ToList();
    xs.Should().BeInAscendingOrder();
    xs.Count.Should()
       .Be(10);
    xs.ShouldBeEquivalentTo(new[]{-1,1,2,5,5,9,11,11,16,21});

    set.Remove(new Point2D(5,55));
    xs = set.Select(p=>p.X).ToList();
    xs.Count.Should()
       .Be(9);
    xs.ShouldBeEquivalentTo(new[]{-1,1,2,5,9,11,11,16,21});

    set.Remove(new Point2D(5,23));
    xs = set.Select(p=>p.X).ToList();
    xs.Count.Should()
       .Be(8);
    xs.ShouldBeEquivalentTo(new[]{-1,1,2,9,11,11,16,21});

    set.Contains(new Point2D(11, 11))
       .Should()
       .BeTrue();

    set.Contains(new Point2D(-1, 76))
        .Should().BeTrue();

    // Note that the custom compartor function ignores the Y value
    set.Contains(new Point2D(-1, 66))
        .Should().BeTrue();

    set.Contains(new Point2D(27, 66))
        .Should().BeFalse();

}

标签结构体
public struct Indexed<T>
{
    public int Index { get; private set; }
    public T Value { get; private set; }
    public Indexed(int index, T value) : this()
    {
        Index = index;
        Value = value;
    }

    public override string ToString()
    {
        return "(Indexed: " + Index + ", " + Value.ToString () + " )";
    }
}

public class Indexed
{
    public static Indexed<T> Create<T>(int indexed, T value)
    {
        return new Indexed<T>(indexed, value);
    }
}

Lambda比较器助手
public class Comparer<T> : IComparer<T>
{
    private readonly Func<T, T, int> _comparer;

    public Comparer(Func<T, T, int> comparer)
    {
        if (comparer == null)
            throw new ArgumentNullException("comparer");
        _comparer = comparer;
    }

    public int Compare(T x, T y)
    {
        return _comparer(x, y);
    }
}

2
您可以使用SortedList,将您的值用作TKey,将int(计数)用作TValue。
以下是一个示例:对单词字母进行排序的函数。
    private string sortLetters(string word)
    {
        var input = new System.Collections.Generic.SortedList<char, int>();

        foreach (var c in word.ToCharArray())
        {
            if (input.ContainsKey(c))
                input[c]++;
            else
                input.Add(c, 1);
        }

        var output = new StringBuilder();

        foreach (var kvp in input)
        {
            output.Append(kvp.Key, kvp.Value);
        }

        string s;

        return output.ToString();

    }

1

Linq.Lookup很酷,但如果你的目标只是在允许重复的情况下循环遍历“键”,那么你可以使用这个结构:

List<KeyValuePair<String, String>> FieldPatterns = new List<KeyValuePair<string, string>>() {
   new KeyValuePair<String,String>("Address","CommonString"),
   new KeyValuePair<String,String>("Username","UsernamePattern"),
   new KeyValuePair<String,String>("Username","CommonString"),
};

那么你可以写:

foreach (KeyValuePair<String,String> item in FieldPatterns)
{
   //use item.Key and item.Value
}

HTH


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接