поиск ключа по ключу в карте Java или наборе

#java #dictionary #collections

Вопрос:

В большинстве языков, включая Java, есть API, что-то вроде java.util.Map которого разработано, чтобы упростить циклическую обработку значения, учитывая ключ, который к нему привязан. Но не всегда существует удобный способ поиска ключа, учитывая ключ (я почти уверен, что Python усложняет его, C упрощает (просто попросите итератор), этот вопрос о Java, который, как я подозреваю, так же плох, как и Python). Сначала это может показаться глупым: зачем вам нужно искать ключ, который у вас уже есть? Но подумайте о чем-то подобном (в примере ниже используется Set вместо Map , но та же идея):

 TreeSet<String> dictionary = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
dictionary.add("Monday"); // populate dictionary
String word = "MONDAY"; // user input, or something
if(dictionary.contains(word)) System.out.println(word   " already in dictionary");
 

Приведенный выше фрагмент кода будет напечатан MONDAY already in dictionary . Это, конечно, неправильно, потому что «ПОНЕДЕЛЬНИК» нет в словаре; скорее, «понедельник» есть. Как мы можем сделать сообщение более точным? В этом случае мы можем создать функцию справки, которая использует тот факт, что a TreeSet есть a NavigableSet (на самом деле аналогичный трюк работает SortedSet , хотя он немного менее удобен).:

 String lookup(NavigableSet<String> set, String key) {
    assert set.contains(key) : key   " not in set";
    return set.floor(key);
}
 

Теперь мы можем исправить последнюю строку предыдущего фрагмента кода:

 if(dictionary.contains(word)) System.out.println(lookup(word)   " already in dictionary");
 

Который напечатает правильную вещь. Но теперь давайте попробуем пример с набором хэшей:

 import java.util.HashSet;
/** Maintains a set of strings; useful as a replacement for String.intern() */
class StringInterner {
    private final HashSet<String> set = new HashSet<>();
    /** use this instead of String.intern() */
    String intern(String s) {
         if(!set.contains(s)) {
             s.add(s);
             return s;
         }
         for(String str : set) // linear scan!!
             if(str.equals(s)) return str;
         throw new AssertionError("something went very wrong");
    }
}
 

Приведенный выше код прибегает к линейному сканированию, чтобы найти то, о чем он уже знает, что там есть. Обратите внимание, что это HashSet может легко дать нам то, что мы ищем, потому что оно должно уметь это делать только для реализации contains() . Но для этого нет API, поэтому мы даже не можем задать этот вопрос. (На самом деле, HashMap есть внутренний метод, называемый getNode , который в значительной степени является тем, что мы хотим, но он является внутренним.) Простой обходной путь в этом случае-использовать карту вместо набора: вместо этого set.add(s) мы могли бы использовать map.put(s,s) . Но что, если мы уже используем карту, потому что у нас уже есть данные, которые мы хотим связать с нашим ключом? Затем мы можем либо использовать две карты и тщательно синхронизировать их, либо сохранить кортеж размером 2 в качестве «значения» на нашей карте, где первый элемент в кортеже-это просто ключ карты. Оба эти решения кажутся излишне неловкими.

Есть ли лучший способ?

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

1. используйте хэш-карту… повторите набор записей.

2. Конечно, вы всегда можете выполнить линейное сканирование. Но самое приятное в хэш-картах то, что они обычно имеют быстрый поиск; жаль терять это и вместо этого использовать сканирование методом перебора.

Ответ №1:

Есть ли лучший способ?

Нет, это не так.

Для хэш-карт это не имеет значения, потому что ваш аргумент «два эквивалентных ключа» не имеет смысла для HashMap . Эти два ключа всегда должны быть equals , что с точки зрения Java означает, что они должны быть заменяемыми во всех отношениях.

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

1. Не совсем во всех отношениях. Иногда вы заботитесь об идентичности объекта (то есть иногда вас волнует, есть ли вещи == , а не просто equals() ). Например, правильная реализация String.intern() должна учитывать идентичность объекта. (Чтобы понять, почему, подумайте о цели интернирования строки: обычно вам нужна логика типа «если я видел такую строку раньше, отбросьте ее и используйте предыдущую». Это позволяет экономить память, поскольку вы избегаете хранения нескольких одинаковых строк.)

2. ДА. И в случае с практикантами, лучшее, что вы можете сделать, это Map<Foo, Foo> . Нет ничего лучше, извините, если вы надеялись на что-то большее. Причина, по которой нет ничего лучше, заключается в том, что практически никому не нужно ничего лучшего, и это даже не имеет смысла для менее стандартных Map реализаций, например, тех, которые даже не сохраняют идентичность своих ключей.

3. Вздох. Несчастный. Похоже на дыру в API, которую было бы неплохо заполнить в один прекрасный день.

4. Примечание: контракт на equals() явно указан в документах java. Нет необходимости equals() фиксировать все, что вас может волновать, и действительно, иногда есть веские причины не делать этого (я признаю, что это довольно редко, но случается). Меня, вероятно, очень волнует, есть ли у меня ArrayList или LinkedList (например, если я рассматриваю возможность двоичного поиска или нет), но в соответствии с контрактом на список любые два списка с одинаковыми элементами в одном и том же порядке должны считаться равными().

5. Обратите внимание, что вы можете создать регистр HashMap без учета/ HashSet , используя специальный тип ключа (может быть оболочкой вокруг строки) с соответствующими hashCode equals реализациями/, что приведет к правильной постановке вопроса; вы не можете запросить a Set для строки, первоначально использованной для создания ключа. Но стоит отметить, что начиная с Java 2, эталонная реализация (в настоящее время OpenJDK и все реализации, построенные поверх нее) реализуется HashSet как оболочка вокруг a HashMap . Таким образом, использование a HashMap для решения этой проблемы не изменяет эксплуатационных характеристик.