Сортировка Java TreeMap по значению не работает, когда более двух значений имеют одинаковое свойство сортировки

#java #sorting #treemap

#java #сортировка #treemap

Вопрос:

Я хочу отсортировать древовидную карту Java на основе некоторого атрибута value. Если быть точным, я хочу отсортировать a TreeMap<Integer, Hashset<Integer>> на основе размера Hashset<Integer> . Для достижения этой цели я сделал следующее:

Класс компаратора:

 private static class ValueComparer implements Comparator<Integer> {
    private Map<Integer, HashSet<Integer>>  map = null;
    public ValueComparer (Map<Integer, HashSet<Integer>> map){
        super();
        this.map = map;
    }

@Override
    public int compare(Integer o1, Integer o2) {
        HashSet<Integer> h1 = map.get(o1);
        HashSet<Integer> h2 = map.get(o2);

        int compare = h2.size().compareTo(h1.size());

        if (compare == 0 amp;amp; o1!=o2){
            return -1;
        }
        else {
            return compare;
        }
    }
}
  

Пример использования:

 TreeMap<Integer, HashSet<Integer>> originalMap = new TreeMap<Integer, HashSet<Integer>>();

//load keys and values into map

ValueComparer comp = new ValueComparer(originalMap);
TreeMap<Integer, HashSet<Integer>> sortedMap = new TreeMap<Integer, HashSet<Integer>>(comp);
sortedMap.putAll(originalMap);
  

Проблема:

Это не работает, если originalMap содержит более 2 значений одинакового размера. В других случаях это работает нормально. Когда более двух значений на карте имеют одинаковый размер, третье значение в новой отсортированной карте равно null и вызывает исключение NullPointerException при попытке получить к нему доступ.

Я не могу понять, в чем проблема. Было бы неплохо, если бы кто-нибудь мог указать.

Обновление: вот пример, который работает, когда два значения имеют одинаковый размер: http://ideone.com/iFD9c В приведенном выше примере, если вы раскомментируете строки 52-54, этот код завершится ошибкой — вот в чем моя проблема.

Комментарии:

1. Нам нужно знать, что вы ожидаете, когда 2 или более значений имеют одинаковый размер (который, как я понимаю, используется в качестве ключа в новой карте). Знаете ли вы о свойствах карты, то есть о том, что ключи должны быть уникальными?

2. Да, я знаю об этом. С ключами ничего не делается — они остаются уникальными, и приведенное выше работает отлично, когда менее 3 значений имеют одинаковый размер. У меня нет никаких предпочтений в порядке сортировки для нескольких значений одинакового размера, но вместо этого их можно сортировать по ключу.

3. Боюсь, вы не знаете, что здесь происходит — термин «отсортированная карта» определен неправильно. Карта — это структура данных для хранения пар ключ / значение (где ключ уникален) и быстрого поиска значений. Предполагается, что вы ничего не знаете о деталях реализации, поэтому вы даже не можете сказать, в чем должна быть разница между картой и отсортированной картой. Что вы можете сделать с отсортированной картой, чего вы не можете сделать с несортированной?

4. Я отредактировал вопрос, заменив SortedMap на TreeMap . Решает ли это проблему сейчас? Несортированный сохраняет свои элементы в произвольном порядке, в то время как sortedmap сохраняет их в некотором определенном порядке — если я правильно понимаю.

5. Боюсь, что вы этого не делаете — карта всегда сохраняет свои элементы в определенном порядке, а именно в порядке, который позволяет быстро вставлять и искать. Если вам нужно отсортировать данные, используйте списки или массивы — карта для этого не подходит (за исключением того, что вы можете получить значения в порядке следования ключей).

Ответ №1:

Обновление: вы не можете вернуться -1 из ValueComparator только потому, что хотите избежать удаления дубликатов ключей. Проверьте контракт Comparator.compare .


Когда вы передаете a Comparator TreeMap , вы вычисляете («новое») место для размещения записи. Ни один (вычисляемый) ключ не может существовать более одного раза в TreeMap .


Если вы хотите отсортировать orginalMap по размеру значения, вы можете сделать следующее:

 public static void main(String[] args) throws Exception {

    TreeMap<Integer, HashSet<Integer>> originalMap = 
        new TreeMap<Integer, HashSet<Integer>>();

    originalMap.put(0, new HashSet<Integer>() {{ add(6); add(7); }});
    originalMap.put(1, new HashSet<Integer>() {{ add(6); }});
    originalMap.put(2, new HashSet<Integer>() {{ add(9); add(8); }});


    ArrayList<Map.Entry<Integer, HashSet<Integer>>> list = 
        new ArrayList<Map.Entry<Integer, HashSet<Integer>>>();
    list.addAll(originalMap.entrySet());

    Collections.sort(list, new Comparator<Map.Entry<Integer,HashSet<Integer>>>(){
        public int compare(Map.Entry<Integer, HashSet<Integer>> o1,
                           Map.Entry<Integer, HashSet<Integer>> o2) {

            Integer size1 = (Integer) o1.getValue().size();
            Integer size2 = (Integer) o2.getValue().size();
            return size2.compareTo(size1);
        }
    });

    System.out.println(list);
}
  

Комментарии:

1. Огромное спасибо — ваше решение отлично работает 🙂 Могу я все же спросить, почему мой метод не работает (мне интересно узнать). Если какое-то изменение в моей реализации исправит это, было бы неплохо использовать карту вместо списка для хранения отсортированного результата.

2. Извините, вы не можете, поскольку ключ в a TreeMap не может существовать более одного раза (в вашем случае ключ — это размер значения). Когда вы добавляете ему новое значение с тем же размером, оно удаляет старое значение. Немного обновлен ответ.

3. Хорошо, но почему это работает, когда у меня есть два значения одинакового размера (оба они вставлены) и не работает, когда у меня есть три значения одинакового размера?

4. Ой, возврат -1 из вашего Comparator , чтобы избежать удаления повторяющихся записей, — это не то, для Comparator.compare чего указан контракт. Вы просто не можете так это сделать! (обновленный ответ)

5. Из документации api: разработчик должен убедиться, что sgn(compare(x, y)) == -sgn(compare(y, x)) для всех x и y. Если вы всегда возвращаете -1 одинаковые размеры, значит, вы нарушили это свойство.

Ответ №2:

Ваша логика компаратора (которую я не уверен, что понимаю, почему вы возвращаете -1, если заданные размеры равны, но их ключи разные) не должна влиять на то, что сама карта возвращает при вызове get(key) .

Вы уверены, что не вставляете нулевые значения в исходную карту? Как выглядит этот код?

Комментарии:

1. Если размеры сравниваемых значений одинаковы, то переменная «compare» будет равна нулю — таким образом, новая карта будет считать, что два ключа являются дубликатами, и вставит только один из них. Я не хочу этого, я хочу сохранить ВСЕ пары ключ-значение из исходной карты, поэтому я не могу вернуть 0 из компаратора. Да, я не вставляю нули в исходную карту.

2. Вот как выглядит код: ideone.com/iFD9c (добавил это тоже к вопросу).

3. Это приведет к нестабильной сортировке, если вы вернете -1, когда A и B имеют одинаковый размер, а также -1, когда B и A имеют одинаковый размер. Я бы переосмыслил использование компаратора для выполнения подобных действий.

4. как вы предлагаете мне это сделать?

Ответ №3:

Ваш компаратор не соблюдает контракт компаратора: если compare(o1, o2) < 0 , то compare(o2, o1) должно быть > 0 . Вы должны найти детерминированный способ сравнения ваших элементов, когда оба размера одинаковы, а целые числа не идентичны. Возможно, вы могли бы использовать System.identityHashCode() из целых чисел, чтобы сравнить их в этом случае.

Тем не менее, мне действительно интересно, что вы могли бы сделать с такой картой: вы не можете создавать новые целые числа и использовать их для получения значения из карты, и вы не можете изменять наборы, которые она содержит.

Примечание: в вашем примере кода компаратора используется map и data для ссылки на одну и ту же карту.

Комментарии:

1. Я думал, что это касается случая: «когда оба размера одинаковы, а целые числа не идентичны»: if (compare == 0 amp;amp; o1!= o2){ return -1; } Да, я знаю о недостатках этого метода, и я найду лучшие альтернативы для своегопроблемы позже. Но я начал с этого и хотел бы узнать, почему это происходит. Спасибо, отредактировано, чтобы исправить ошибку (данные и карта).

2. Вы обрабатываете случай, но вы обрабатываете его, разрывая контракт. Перечитайте мой ответ еще раз: вы возвращаете -1 при сравнении i1 и i2, а также возвращаете -1 при возврате i2 и i1. Это означает, что i1 является одновременно < и > i2 .

Ответ №4:

Вы можете упорядочить TreeMap только по ключам. Невозможно создать древовидную карту, упорядоченную по значениям, потому что вы получите исключение StackOverflowException.

Подумайте об этом. Чтобы получить элемент из дерева, вам нужно выполнить сравнения, но для выполнения сравнений вам нужно получить элементы.

Вам нужно будет отсортировать его в другой коллекции или использовать Tree, вам нужно будет инкапсулировать целое число (из значения ввода) также в ключ ввода и определить компаратор, используя это целое число, взятое из ключа.

Ответ №5:

Предполагая, что вы не можете использовать компаратор, который возвращает 0 с набором, это может сработать: добавьте все элементы в originalMap.entrySet() ArrayList , а затем отсортируйте ArrayList , используя your ValueComparer , изменяя его по return 0 мере необходимости.

Затем добавьте все записи в отсортированном ArrayList в a LinkedHashMap .

Ответ №6:

У меня была аналогичная проблема, что и у исходного плаката. У меня была древовидная карта, которую я хотел отсортировать по значению. Но когда я создал компаратор, который просматривал значение, у меня возникли проблемы из-за поломки компаратора, о котором говорил JB. Я смог использовать свой пользовательский компаратор и по-прежнему соблюдать контракт. Когда значения, на которые я смотрел, были равны, я вернулся к сравнению ключей. Я не заботился о порядке, если значения были равны.

     public int compare(String a, String b) {
    if(base.get(a)[0] == base.get(b)[0]){ //need to handle when they are equal
        return a.compareTo(b);
    }else if (base.get(a)[0] < base.get(b)[0]) {
        return -1;
    } else {
        return 1;
    } // returning 0 would merge keys