web-dev-qa-db-fra.com

En Java, comment trier une liste en fonction d’une autre?

J'ai vu plusieurs autres questions similaires à celle-ci, mais je n'ai pas vraiment trouvé quoi que ce soit qui résolve mon problème. 

Mon cas d'utilisation est le suivant: l'utilisateur a initialement une liste d'éléments (listA). Ils réorganisent les éléments et souhaitent conserver cet ordre (listB). Cependant, en raison de restrictions, je ne parviens pas à conserver l'ordre sur le backend. Je dois donc trier ListA après l'avoir récupéré.

Donc, fondamentalement, j'ai 2 ArrayLists (listA et listB). L'une avec l'ordre spécifique dans lequel les listes doivent figurer (listeB) et l'autre contient la liste des éléments (listeA). Je veux trier listA basé sur listB.

38
Debacle
Collections.sort(listB, new Comparator<Item>() {
    public int compare(Item left, Item right) {
        return Integer.compare(listA.indexOf(left), listA.indexOf(right));
    }
});

Ceci est cependant assez peu efficace et vous devriez probablement créer un Map<Item, Integer> à partir de listA pour rechercher plus rapidement les positions des éléments.

Guava dispose d'un comparateur prêt à l'emploi pour le faire: Ordering.explicit()

56
JB Nizet

Java 8:

Collections.sort(listToSort, 
    Comparator.comparing(item -> listWithOrder.indexOf(item)));

ou mieux:

listToSort.sort(Comparator.comparingInt(listWithOrder::indexOf));
29
Thermech

Supposons que vous avez une liste listB qui définit l'ordre dans lequel vous souhaitez trier listA. Ceci est juste un exemple, mais il montre un ordre défini par une liste, et non l'ordre naturel du type de données:

List<String> listB = Arrays.asList("Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday");

Maintenant, disons que listA doit être trié selon cet ordre. C'est un List<Item>, et Item a une méthode public String getWeekday().

Créez un Map<String, Integer> qui mappe les valeurs de tout ce qui est dans listB à quelque chose qui peut être facilement trié, tel que l'index, c'est-à-dire "Sunday" => 0, ..., "Saturday" => 6. Cela fournira une recherche rapide et facile.

Map<String, Integer> weekdayOrder = new HashMap<String, Integer>();
for (int i = 0; i < listB.size(); i++)
{
    String weekday = listB.get(i);
    weekdayOrder.put(weekday, i);
}

Ensuite, vous pouvez créer votre Comparator<Item> personnalisé qui utilise la Map pour créer une commande:

public class ItemWeekdayComparator implements Comparator<Item>
{
    private Map<String, Integer> sortOrder;

    public ItemWeekdayComparator(Map<String, Integer> sortOrder)
    {
        this.sortOrder = sortOrder;
    }

    @Override
    public int compare(Item i1, Item i2)
    {
        Integer weekdayPos1 = sortOrder.get(i1.getWeekday());
        if (weekdayPos1 == null)
        {
            throw new IllegalArgumentException("Bad weekday encountered: " +
               i1.getWeekday());
        }
        Integer weekdayPos2 = sortOrder.get(i2.getWeekday());
        if (weekdayPos2 == null)
        {
            throw new IllegalArgumentException("Bad weekday encountered: " +
               i2.getWeekday());
        }
        return weekdayPos1.compareTo(weekdayPos2);
    }
}

Ensuite, vous pouvez trier listA en utilisant votre Comparator personnalisé.

Collections.sort(listA, new ItemWeekdayComparator(weekdayOrder));
10
rgettman

Amélioration de la vitesse de réponse de JB Nizet (d'après la suggestion qu'il a lui-même faite). Avec cette méthode:

  • Trier 100 fois la liste de 1000 articles améliore la vitesse 10 fois avec mes tests unitaires

  • Le tri d'une liste de 10000 éléments 100 fois améliore la vitesse 140 fois (265 ms pour l'ensemble du lot au lieu de 37 secondes) sur les tests unitaires

Cette méthode fonctionnera également lorsque les deux listes ne sont pas identiques:

/**
 * Sorts list objectsToOrder based on the order of orderedObjects.
 * 
 * Make sure these objects have good equals() and hashCode() methods or
 * that they reference the same objects.
 */
public static void sortList(List<?> objectsToOrder, List<?> orderedObjects) {

    HashMap<Object, Integer> indexMap = new HashMap<>();
    int index = 0;
    for (Object object : orderedObjects) {
        indexMap.put(object, index);
        index++;
    }

    Collections.sort(objectsToOrder, new Comparator<Object>() {

        public int compare(Object left, Object right) {

            Integer leftIndex = indexMap.get(left);
            Integer rightIndex = indexMap.get(right);
            if (leftIndex == null) {
                return -1;
            }
            if (rightIndex == null) {
                return 1;
            }

            return Integer.compare(leftIndex, rightIndex);
        }
    });
}
9
Frank

Voici une solution qui augmente la complexité temporelle de 2n, mais accomplit ce que vous voulez. Il importe également que la liste R que vous souhaitez trier contienne des éléments comparables, tant que l'autre liste L que vous utilisez pour les trier est uniformément comparable.

public HeavyPair<L extends Comparable<L>, R> implements Comparable<HeavyPair<L, ?>> {
    public final L left;
    public final R right;

    public HeavyPair(L left, R right) {
        this.left = left;
        this.right = right;
    }

    public compareTo(HeavyPair<L, ?> o) {
        return this.left.compareTo(o.left);
    }

    public static <L extends Comparable<L>, R> List<R> sort(List<L> weights, List<R> toSort) {
        assert(weights.size() == toSort.size());
        List<R> output = new ArrayList<>(toSort.size());
        List<HeavyPair<L, R>> workHorse = new ArrayList<>(toSort.size());
        for(int i = 0; i < toSort.size(); i++) {
            workHorse.add(new HeavyPair(weights.get(i), toSort.get(i)))
        }
        Collections.sort(workHorse);
        for(int i = 0; i < workHorse.size(); i++) {
            output.add(workHorse.get(i).right);
        }
        return output;
    }
}

Excusez toutes les pratiques terribles que j'ai utilisées lors de l'écriture de ce code, cependant. J'étais pressé.

Il suffit d'appeler HeavyPair.sort(listB, listA);

Edit: Correction de cette ligne return this.left.compareTo(o.left);. Maintenant cela fonctionne réellement.

5
Axoren

Voici un exemple montrant comment trier une liste, puis apporter les modifications dans une autre liste en fonction des modifications apportées à la première liste de tableaux. Cette astuce n'échouera jamais et assure la correspondance entre les éléments de la liste. La taille des deux listes doit être la même pour utiliser cette astuce. 

    ArrayList<String> listA = new ArrayList<String>();
    ArrayList<String> listB = new ArrayList<String>();
    int j = 0;
    // list of returns of the compare method which will be used to manipulate
    // the another comparator according to the sorting of previous listA
    ArrayList<Integer> sortingMethodReturns = new ArrayList<Integer>();

    public void addItemstoLists() {
        listA.add("Value of Z");
        listA.add("Value of C");
        listA.add("Value of F");
        listA.add("Value of A");
        listA.add("Value of Y");

        listB.add("this is the value of Z");
        listB.add("this is the value off C");
        listB.add("this is the value off F");
        listB.add("this is the value off A");
        listB.add("this is the value off Y");

        Collections.sort(listA, new Comparator<String>() {

            @Override
            public int compare(String lhs, String rhs) {
                // TODO Auto-generated method stub
                int returning = lhs.compareTo(rhs);
                sortingMethodReturns.add(returning);
                return returning;
            }

        });
        // now sort the list B according to the changes made with the order of
        // items in listA
        Collections.sort(listB, new Comparator<String>() {

            @Override
            public int compare(String lhs, String rhs) {
                // TODO Auto-generated method stub

                // comparator method will sort the second list also according to
                // the changes made with list a
                int returning = sortingMethodReturns.get(j);
                j++;
                return returning;
            }

        });

    }
2
Gopal Singh Sirvi

essayez ceci pour Java 8:

listB.sort((left, right) -> Integer.compare(list.indexOf(left), list.indexOf(right)));

ou

listB.sort(Comparator.comparingInt(item -> list.indexOf(item)));
1
Arif Acar

Une autre solution qui peut fonctionner en fonction de votre paramètre n'est pas de stocker des instances dans listB mais plutôt des index de listA. Cela pourrait être fait en encapsulant listA dans une liste triée personnalisée comme ceci:

public static class SortedDependingList<E> extends AbstractList<E> implements List<E>{
    private final List<E> dependingList;
    private final List<Integer> indices;

    public SortedDependingList(List<E> dependingList) {
        super();
        this.dependingList = dependingList;
        indices = new ArrayList<>();
    }

    @Override
    public boolean add(E e) {
        int index = dependingList.indexOf(e);
        if (index != -1) {
            return addSorted(index);
        }
        return false;
    }

    /**
     * Adds to this list the element of the depending list at the given
     * original index.
     * @param index The index of the element to add.
     * 
     */
    public boolean addByIndex(int index){
        if (index < 0 || index >= this.dependingList.size()) {
            throw new IllegalArgumentException();
        }
        return addSorted(index);
    }

    /**
     * Returns true if this list contains the element at the
     * index of the depending list.
     */
    public boolean containsIndex(int index){
        int i = Collections.binarySearch(indices, index);
        return i >= 0;
    }

    private boolean addSorted(int index){
        int insertIndex = Collections.binarySearch(indices, index);
        if (insertIndex < 0){
            insertIndex = -insertIndex-1;
            this.indices.add(insertIndex, index);
            return true;
        }
        return false;
    }

    @Override
    public E get(int index) {
        return dependingList.get(indices.get(index));
    }

    @Override
    public int size() {
        return indices.size();
    }
}

Ensuite, vous pouvez utiliser cette liste personnalisée comme suit:

public static void main(String[] args) {
    class SomeClass{
        int index;
        public SomeClass(int index) {
            super();
            this.index = index;
        }
        @Override
        public String toString() {
            return ""+index;
        }
    }

    List<SomeClass> listA = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        listA.add(new SomeClass(i));
    }
    SortedDependingList<SomeClass> listB = new SortedDependingList<>(listA);
    Random Rand = new Random();

    // add elements by index:
    for (int i = 0; i < 5; i++) {
        int index = Rand.nextInt(listA.size());
        listB.addByIndex(index);
    }

    System.out.println(listB);

    // add elements by identity:
    for (int i = 0; i < 5; i++) {
        int index = Rand.nextInt(listA.size());
        SomeClass o = listA.get(index);
        listB.add(o);
    }
    System.out.println(listB);      
}

Bien entendu, cette liste personnalisée ne sera valide que tant que les éléments de la liste d'origine ne seront pas modifiés. Si des modifications sont possibles, vous devez d'une manière ou d'une autre écouter les modifications apportées à la liste d'origine et mettre à jour les index dans la liste personnalisée.

Notez également que la liste SortedDependingList ne permet pas pour le moment d'ajouter un élément de listA - à cet égard, elle fonctionne en fait comme un ensemble d'éléments de listA car c'est généralement ce que vous souhaitez dans un tel contexte.

La méthode recommandée pour ajouter quelque chose à SortedDependingList consiste à connaître déjà l'index d'un élément et à l'ajouter en appelant trieList.addByIndex (index);

1
Balder

Une façon de procéder consiste à parcourir la liste en boucle et à ajouter les éléments à une liste temporaire si cette liste les contient:

List<?> tempList = new ArrayList<?>();
for(Object o : listB) {
    if(listA.contains(o)) {
        tempList.add(o);
    }
}
listA.removeAll(listB);
tempList.addAll(listA);
return tempList;
1
Runemoro

Essaye ça. Le code ci-dessous est d'usage général pour un scénario où listA est une liste de Objects puisque vous n'avez pas indiqué de type particulier.

Object[] orderedArray = new Object[listA.size()];

for(int index = 0; index < listB.size(); index ++){
    int position = listB.get(index); //this may have to be cast as an int
    orderedArray[position] = listA.get(index);
}
//if you receive UnsupportedOperationException when running listA.clear()
//you should replace the line with listA = new List<Object>() 
//using your actual implementation of the List interface
listA.clear(); 
listA.addAll(orderedArray);
1
M7Jacks

Pas tout à fait clair ce que vous voulez, mais si telle est la situation: A: [c, b, a] B: [2,1,0]

Et vous voulez les charger tous les deux et ensuite produire: C: [a, b, c]

Alors peut-être ça?

List c = new ArrayList(b.size());
for(int i=0;i<b.size();i++) {
  c.set(b.get(i),a.get(i));
}

cela nécessite une copie supplémentaire, mais je pense que la mise en place est beaucoup moins efficace, et toutes sortes de choses pas claires:

for(int i=0;i<b.size();i++){
    int from = b.get(i);
    if(from == i) continue;
    T tmp = a.get(i);
    a.set(i,a.get(from));
    a.set(from,tmp);
    b.set(b.lastIndexOf(i),from); 
}

Notez que je n'ai pas testé non plus, peut-être un signe s'est-il retourné.

1
Brad Tofel

Si les références d'objet doivent être identiques, vous pouvez initialiser listA new.

listA = new ArrayList(listB)
0
Khinsu

Je viens de rencontrer le même problème. 
J'ai une liste de clés ordonnées et j'ai besoin de classer les objets dans une liste en fonction de l'ordre des clés. 
Mes listes sont suffisamment longues pour rendre les solutions de complexité temporelle de N ^ 2 inutilisables. 
Ma solution:

<K, T> List<T> sortByOrder(List<K> orderedKeys, List<T> objectsToOrder, Function<T, K> keyExtractor) {
    AtomicInteger ind = new AtomicInteger(0);
    Map<K, Integer> keyToIndex = orderedKeys.stream().collect(Collectors.toMap(k -> k, k -> ind.getAndIncrement(), (oldK, newK) -> oldK));
    SortedMap<Integer, T> indexToObj = new TreeMap<>();
    objectsToOrder.forEach(obj -> indexToObj.put(keyToIndex.get(keyExtractor.apply(obj)), obj));
    return new ArrayList<>(indexToObj.values());
}

La complexité temporelle est O (N * Log (N)). 
La solution suppose que tous les objets de la liste à trier ont des clés distinctes. Sinon, remplacez simplement SortedMap<Integer, T> indexToObj par SortedMap<Integer, List<T>> indexToObjList.

0
Alexander

Comme Tim Herold l'a écrit, si les références d'objet doivent être identiques, vous pouvez simplement copier listB vers listA, soit:

listA = new ArrayList(listB);

Ou ceci si vous ne voulez pas changer la liste à laquelle se réfère la listeA:

listA.clear();
listA.addAll(listB);

Si les références ne sont pas identiques mais qu'il existe une relation d'équivalence entre les objets de listA et listB, vous pouvez trier listA à l'aide d'une variable Comparator personnalisée qui trouve l'objet dans listB et utilise son index dans listB comme clé de tri. L'implémentation naïve que la recherche par force brute listB ne serait pas la meilleure en termes de performances, mais serait suffisante sur le plan fonctionnel.

0
Jason C

Pour éviter une recherche très inefficace, vous devez indexer les éléments dans listB, puis trier listA en fonction.

Map<Item, Integer> index = IntStream.range(0, listB.size()).boxed()
  .collect(Collectors.toMap(listB::get, x -> x));

listA.sort((e1, e2) -> Integer.compare(index.get(c1), index.get(c2));
0
Duarte Meneses

OMI, vous devez persister autre chose. Peut-être pas la liste complète B, mais quelque chose . Peut-être que les index des éléments modifiés par l'utilisateur.

0
ArturoTena