118 votes

Collection triable en C# autorisant les clés dupliquées

J'écris un programme pour définir l'ordre dans lequel divers objets apparaîtront dans un rapport. La séquence est la position Y (cellule) sur la feuille de calcul Excel.

Une partie du code de démonstration est présentée ci-dessous. Ce que je veux accomplir est d'avoir une collection, qui me permettra d'ajouter plusieurs objets et je peux obtenir une collection triée basée sur la séquence

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

Je sais que la SortedList ne le permet pas et j'ai cherché une alternative. Je ne veux pas éliminer les doublons et déjà essayé List<KeyValuePair<int, object>> .

Merci.

97voto

Knasterbax Points 1383

Utilisez votre propre IComparer !

Comme indiqué dans d'autres réponses, vous devriez utiliser votre propre classe de comparaison. Pour ma part, j'utilise une classe IComparer générique, qui fonctionne avec tout ce qui implémente 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
}

Vous l'utiliserez lors de l'instanciation d'une nouvelle liste triée, d'un nouveau dictionnaire trié, etc :

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

Ici, int est la clé qui peut être dupliquée.

18voto

Dipti Mehta Points 317

Vous pouvez utiliser en toute sécurité List<> . La liste possède une méthode Sort, dont la surcharge accepte IComparer. Vous pouvez créer votre propre classe de tri en tant que . Voici un exemple :

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);
    }
}

10voto

user450 Points 11

J'utilise les éléments suivants :

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);
    }

}

Mon cas d'école :

[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);
    }

Le résultat :

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

9voto

Peter Huber Points 79

Le problème est que la conception de la structure des données ne correspond pas aux besoins : Il est nécessaire de stocker plusieurs en-têtes pour le même XPos. C'est pourquoi, SortedList<XPos, value> ne doit pas avoir une valeur de Header mais une valeur de List<Header> . Il s'agit d'une modification simple et mineure, mais elle résout tous les problèmes et évite d'en créer de nouveaux comme le font d'autres solutions proposées (voir l'explication ci-dessous) :

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

Veuillez noter que l'ajout d'une clé "amusante", comme l'ajout d'un nombre aléatoire ou le fait de prétendre que deux XPos ayant la même valeur sont différents, entraîne de nombreux autres problèmes. Par exemple, il devient difficile, voire impossible, de supprimer un en-tête particulier.

Notez également que les performances de tri sont bien meilleures si seulement quelques List<Header> doivent être triés que chaque Header . Exemple : S'il y a 100 XPos et que chacun a 100 en-têtes, 10000 Header doivent être triés par rapport à 100 List<Header> .

Bien entendu, cette solution présente également un inconvénient : s'il y a de nombreux XPos avec un seul en-tête, autant de listes doivent être créées, ce qui représente un certain surcoût.

Mise à jour 22.12.2021

J'ai enfin trouvé le temps d'écrire une véritable collection intitulée SortedBucketCollection qui se comporte comme un SortedList . Il utilise 2 clés pour chaque élément, la première étant la même que celle d'un SortedList et de nombreux éléments peuvent avoir la même valeur pour cette clé. La seconde clé est utilisée pour différencier les éléments partageant les mêmes valeurs pour la clé 1. SortedBucketCollection utilise moins d'espace de stockage que SortedList<int, List<Header>> parce qu'il utilise pour chaque "seau" une liste chaînée et non une liste d'adresses. List<> .

Code utilisant SortedBucketCollection ressemble à ceci :

en utilisant 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());
      }
    }
  }
}

Sortie du premier 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 }

Notez que les éléments ne sont pas itérés dans l'ordre dans lequel ils ont été ajoutés, mais triés par leur key1 y key2 .

Pour une description détaillée de SortedBucketCollection et le code source, voir mon article sur CodeProject SortedBucketCollection : Une liste triée efficace en termes de mémoire, acceptant plusieurs éléments avec la même clé.

6voto

knocte Points 4320

Solution la plus simple (par rapport à toutes les solutions précédentes) : utiliser SortedSet<T> , il accepte un IComparer<SortableKey> puis implémenter la méthode Compare de cette manière :

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();

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X