Найти слово в словаре неизвестного размера, используя только метод, чтобы получить слово по индексу

несколько дней назад у меня было интервью в какой-то крупной компании, название не требуется :), и интервьюер попросил меня найти решение следующей задачи:

стандартные: Существует словарь слов с неуказанных в размере, мы просто знаем, что все слова в словаре сортируются (например по алфавиту). Также у нас есть только один метод

String getWord(int index) throws IndexOutOfBoundsException

потребности: Необходимо разработать алгоритм поиска некоторого входного слова в словаре использовать Java. Для этого следует реализовать метод

public boolean isWordInTheDictionary(String word)

ограничения: Мы не можем изменить внутреннюю структуру словаря, у нас нет доступа к внутренней структуре, мы не знаем количества элементов в словаре.

вопросы: Я разработал модифицированный двоичный поиск, и опубликует мой вариант (работает вариант) алгоритма, но есть ли другие варианты с логарифмической сложностью? Мой вариант имеет сложность О(Фремонт, Калифорния).

мой вариант реализации:

public class Dictionary {
    private static final int BIGGEST_TOP_MASK = 0xF00000;
    private static final int LESS_TOP_MASK = 0x0F0000;
    private static final int FULL_MASK = 0xFFFFFF;
    private String[] data;
    private static final int STEP = 100; // for real test step should be Integer.MAX_VALUE
    private int shiftIndex = -1;
    private static final int LESS_MASK = 0x0000FF;
    private static final int BIG_MASK = 0x00FF00;


    public Dictionary() {
        data = getData();
    }

    String getWord(int index) throws IndexOutOfBoundsException {
        return data[index];
    }

    public String[] getData() {
        return new String[]{"a", "aaaa", "asss", "az", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "test", "u", "v", "w", "x", "y", "z"};
    }


    public boolean isWordInTheDictionary(String word) {
        boolean isFound = false;
        int constantIndex = STEP; // predefined step
        int flag = 0;
        int i = 0;
        while (true) {
            i++;
            if (flag == FULL_MASK) {
                System.out.println("Word is not found ... Steps " + i);
                break;
            }
            try {
                String data = getWord(constantIndex);
                if (null != data) {
                    int compareResult = word.compareTo(data);
                    if (compareResult > 0) {
                        if ((flag & LESS_MASK) == LESS_MASK) {
                            constantIndex = prepareIndex(false, constantIndex);
                            if (shiftIndex == 1)
                                flag |= BIGGEST_TOP_MASK;
                        } else {
                            constantIndex = constantIndex * 2;
                        }
                        flag |= BIG_MASK;

                    } else if (compareResult < 0) {
                        if ((flag & BIG_MASK) == BIG_MASK) {
                            constantIndex = prepareIndex(true, constantIndex);
                            if (shiftIndex == 1)
                                flag |= LESS_TOP_MASK;
                        } else {
                            constantIndex = constantIndex / 2;
                        }
                        flag |= LESS_MASK;
                    } else {
// YES!!! We found word.
                        isFound = true;
                        System.out.println("Steps " + i);
                        break;
                    }
                }
            } catch (IndexOutOfBoundsException e) {
                if (flag > 0) {
                    constantIndex = prepareIndex(true, constantIndex);
                    flag |= LESS_MASK;
                } else constantIndex = constantIndex / 2;
            }
        }
        return isFound;
    }

    private int prepareIndex(boolean isBiggest, int constantIndex) {
        shiftIndex = (int) Math.ceil(getIndex(shiftIndex == -1 ? constantIndex : shiftIndex));
        if (isBiggest)
            constantIndex = constantIndex - shiftIndex;
        else
            constantIndex = constantIndex + shiftIndex;
        return constantIndex;
    }

    private double getIndex(double constantIndex) {
        if (constantIndex <= 1)
            return 1;
        return constantIndex / 2;
    }
}

12 ответов


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

Как только вы нашли значение в словаре, которое больше, чем ваша цель поиска (или вне пределов), остальное выглядит как стандартный двоичный поиск. Самое трудное-как ты это делаешь? оптимально расширить диапазон, когда целевое значение больше, чем значение словаря, которое вы искали. Похоже, что вы расширяетесь в 1,5 раза. Это может быть действительно проблематично с огромным словарем и небольшим фиксированным начальным шагом как у вас (100). Подумайте, если бы было 50 миллионов слов, сколько раз ваш алгоритм должен был бы расширить диапазон вверх, если вы ищете "зебру".

вот идея: используйте упорядоченный характер коллекции для вашего преимущество, предполагая, что первая буква каждого слова равномерно распределена между буквами алфавита (это никогда не будет правдой, но, не зная больше о коллекции слов, это, вероятно, лучшее, что вы можете сделать). Затем взвесьте количество вашего расширения диапазона на то, насколько далеко от конца вы ожидаете, что слово словаря будет.

Итак, если вы сделали свой первый шаг 100 и посмотрели словарное слово в этом индексе, и это было "aardvark", вы бы расширили свой диапазон гораздо больше для следующего шага, чем если бы это был морж."Все еще O (log n), но, вероятно, намного лучше для большинства коллекций слов.


вот альтернативная реализация, которая использует Collections.binarySearch. Он терпит неудачу, если одно из слов в списке начинается с символа '\uffff' (Это Unicode 0xffff, а не юридический недопустимый символ Юникода).

public static class ListProxy extends AbstractList<String> implements RandomAccess
{
    @Override public String get( int index )
    {
        try {
            return getWord( index );
        } catch( IndexOutOfBoundsException ex ) {
            return "\uffff";
        }
    }

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

public static boolean isWordInTheDictionary( String word )
{
    return Collections.binarySearch( new ListProxy(), word ) >= 0;
}

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

вот немного измененная версия, которая запоминает наименьший неудачный индекс, чтобы свести его объявленный размер к фактическому размеру словаря En passant и, таким образом, избегает почти всех исключений в последовательных поисках. Хотя вам нужно будет создать новый экземпляр ListProxy всякий раз, когда размер словаря мог измениться.

public static class ListProxy extends AbstractList<String> implements RandomAccess
{
    private int size = Integer.MAX_VALUE;

    @Override public String get( int index )
    {
        try {
            if( index < size )
                return getWord( index );
        } catch( IndexOutOfBoundsException ex ) {
            size = index;
        }
        return "\uffff";
    }

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

private static ListProxy listProxy = new ListProxy();

public static boolean isWordInTheDictionary( String word )
{
    return Collections.binarySearch( listProxy , word ) >= 0;
}

У вас есть правильная идея, но я думаю, что ваша реализация слишком сложна. Вы хотите выполнить двоичный поиск, но вы не знаете, что такое верхняя граница. Поэтому вместо того, чтобы начинать с середины, вы начинаете с индекса 1 (предполагая, что индексы словаря начинаются с 0).

Если слово, которое вы ищете, " меньше, чем "текущее словарное слово, вдвое уменьшите расстояние между текущим индексом и вашим" низким " значением. ("низкий" начинается с 0, конечно).

Если слово вы поиск "больше" слова в индексе, который вы только что исследовали, затем либо вдвое сократить расстояние между текущим индексом и Вашим "высоким" значением ("высокий" начинается с 2), либо, если индекс и "высокий" одинаковы, удвоить индекс.

Если удвоение индекса дает исключение вне диапазона, вы вдвое уменьшаете расстояние между текущим значением и удвоенным значением. Поэтому, если переход от 16 к 32 вызывает исключение, попробуйте 24. И, конечно же, следите за тем, что 32-это больше, чем максимум.

Итак, последовательность поиска может выглядеть 1, 2, 4, 8, 16, 12, 14 - нашли!

Это та же концепция, что и двоичный поиск, но вместо того, чтобы начинать с low = 0, high = n-1, вы начинаете с low = 0, high = 2 и удваиваете высокое значение, когда вам нужно. Это все еще O (log N), хотя константа будет немного больше, чем при "нормальном" двоичном поиске.


вы можете понести разовую стоимость O (n), если знаете, что словарь не изменится. Вы можете добавить все слова в словаре в хэш-таблицу, а затем любые последующие вызовы isWordInDictionary() будут O(1) (теоретически).


используйте API getWord (), чтобы скопировать все содержимое словаря в более разумную структуру данных (например, хэш-таблицу, trie, возможно, даже дополненную фильтром Bloom). ;-)


на другом языке:

#!/usr/bin/perl

$t=0;
$cur=1;
$under=0;
$EOL=int(rand(1000000))+1;
$TARGET=int(rand(1000000))+1;
if ($TARGET>$EOL)
{
  $x=$EOL;
  $EOL=$TARGET;
  $TARGET=$x;
}
print "Looking for $TARGET with EOL $EOL\n";

sub testWord($)
{
  my($a)=@_;
  ++$t;
 return 0 if ($a eq $TARGET);
 return -2 if ($a > $EOL);
 return 1 if ($a > $TARGET);
 return -1;
}

while ($r = testWord($cur))
{
  print "Tested $cur, got $r\n";
  if ($r == 1) { $over=$cur; }
  if ($r == -1) { $under=$cur; }
  if ($r == -2) { $over = $cur; }
  if ($over)
  {
    $cur = int(($over-$under)/2)+$under;
    $cur++ if ($cur <= $under);
    $cur-- if ($cur >= $over);
  }
  else
  {
    $cur *= 2;
  }
}
print "Found $TARGET at $r in $t tests\n";

главным преимуществом этого является то, что это немного проще понять. Я думаю, что это может быть более эффективным, если ваши первые догадки ниже цели, так как я не думаю, что вы используете пространство, которое вы уже "искали", но это просто быстрый взгляд на ваш код. Поскольку он ищет числа для простоты, ему не нужно иметь дело с не поиском цели, но это простое расширение.


@Sergii Zagriichuk надеюсь, что интервью прошло хорошо. Удачи с этим.

Я думаю так же, как @alexcoco сказал, что двоичный поиск-это ответ.

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

или да, как говорят ребята, полностью реализовать свой собственный структура словаря.

Я знаю, что это не отвечает на ваш вопрос должным образом. Но я не вижу других возможностей.

кстати было бы неплохо увидеть ваш алгоритм.

EDIT: Расширение на мой комментарий под ответом bshields...

@Sergii Zagriichuk еще лучше было бы вспомнить последний индекс, где у нас был null (нет слова), Я думаю. Затем при каждом запуске вы можете проверить, все ли еще верно. Если нет, то разверните диапазон до "предыдущего индекса", полученного путем реверсирования поведения двоичного поиска, поэтому мы снова имеем null. Таким образом, вы всегда будете корректировать размер диапазона алгоритма поиска, адаптируясь к текущему состоянию словаря по мере необходимости. Кроме того, изменения должны быть значительными, чтобы вызвать корректировку диапазона, чтобы корректировка не оказала реального негативного влияния на алгоритм. Также словари, как правило, статичны по своей природе, поэтому это должно работать:)


с одной стороны, да, вы правы с реализацией двоичного поиска. Но, с другой стороны, если словарь статичен и не изменяется между поисками - мы могли бы предложить другой алгоритм. Здесь у нас есть общая проблема - сортировка/поиск строк отличается от сортировки/поиска в массиве int, поэтому getWord(int i).compareTo (строка) - O (min (length0, length1)).

Предположим, у нас есть запрос на поиск слов w0, w1,... шя, во время поиска мы могли бы построить дерево с индексов (вероятно, какое-то дерево суффиксов будет достаточно хорошо для этой задачи). Во время следующего запроса поиска у нас есть следующий набор a1, a2,... я, так, чтобы уменьшить среднее время, мы могли бы сначала уменьшить диапазон поиска позиции в дереве. Проблема с этой реализацией-параллелизм и использование памяти, поэтому следующий шаг-реализация стратегии уменьшения дерева поиска.

PS: основной целью было проверить идеи и проблемы, которые вы предлагаете.


Ну, я думаю, что словарь сортируется может быть использован в лучшую сторону. Скажем, вы ищете слово "зебра", тогда как первый поиск догадки привел к"abcg". Поэтому мы можем использовать эту информацию в chossing второй индекс догадываться . как и в моем случае, полученное слово начинается с a, тогда как я ищу что-то, начинающееся с z. Поэтому вместо статического прыжка я могу сделать какой-то расчетный прыжок, основанный на текущем результате и желаемом результате. Таким образом, предположим, если мой следующий прыжок приведет меня к слову "yvu", я сейчас очень близко, поэтому сделаю довольно медленный маленький прыжок, чем в случае с prev.


вот мое решение.. использует операции O(logn). Первая часть кода пытается найти оценку длины, а затем вторая часть использует тот факт, что словарь сортируется и выполняет двоичный поиск.

boolean isWordInTheDictionary(String word){
    if (word == null){
        return false;
    }
    // estimate the length of the dictionary array
    long len=2;
    String temp= getWord(len);

    while(true){
        len = len * 2;
        try{
          temp = getWord(len);
        }catch(IndexOutOfBoundsException e){
           // found upped bound break from loop
           break;
        }
    }

    // Do a modified binary search using the estimated length
    long beg = 0 ;
    long end = len;
    String tempWrd;
    while(true){
        System.out.println(String.format("beg: %s, end=%s, (beg+end)/2=%s ", beg,end,(beg+end)/2));
        if(end - beg <= 1){
            return false;
        }
        long idx = (beg+end)/2;
        tempWrd = getWord(idx);
        if(tempWrd == null){
            end=idx;
            continue;
        }
        if ( word.compareTo(tempWrd) > 0){
            beg = idx;
        }
        else if(word.compareTo(tempWrd) < 0){
            end= idx;
        }else{
            // found the word..
            System.out.println(String.format("getword at index: %s, =%s", idx,getWord(idx)));
            return true;
        }
    }
}

предполагая, что словарь основан на 0, я бы разложил поиск на две части.

во-первых, учитывая, что параметр index to getWord() является целым числом, и предполагая, что индекс должен быть числом от 0 до максимального положительного целого числа, выполните двоичный поиск по этому диапазону, чтобы найти максимальный допустимый индекс (независимо от значений слова). Эта операция O (log N), так как это простой двоичный поиск.

полученный размер словарь, второй обычный двоичный поиск (опять же сложности O (log N)) принесет желаемый ответ.

Так как O(log N)+O(log N) является O (log N), этот алгоритм соответствует вашему требованию.


Я нахожусь в процессе найма, который задал мне эту же проблему... Мой подход был немного другим, и, учитывая словарь (webservice), который у меня есть, он примерно на 30% эффективнее (для слов, которые я тестировал).

вот решение: https://github.com/gustavompo/wordfinder

Я не буду публиковать здесь все решение, потому что оно разделено через классы и методы, но основной алгоритм таков:

public WordFindingResult FindWord(string word)
    {
        var callsCount = 0;
        var lowerLimit = new WordFindingLimit(0, null);
        var upperLimit = new WordFindingLimit(int.MaxValue, null);
        var wordToFind = new Word(word);
        var wordIndex = _initialIndex;

        while (callsCount <= _maximumCallsCount)
        {
            if (CouldNotFindWord(lowerLimit, upperLimit))
                return new WordFindingResult(callsCount, -1, string.Empty, WordFindingResult.ErrorCodes.NOT_FOUND);

            var wordFound = RetrieveWordAt(wordIndex);
            callsCount++;

            if (wordToFind.Equals(wordFound))
                return new WordFindingResult(callsCount, wordIndex, wordFound.OriginalWordString);

            else if (IsIndexTooHigh(wordToFind, wordFound))
            {
                upperLimit = new WordFindingLimit(wordIndex, wordFound);
                wordIndex = IndexConsideringTooHighPreviousResult(lowerLimit, wordIndex);
            }
            else
            {
                lowerLimit = new WordFindingLimit(wordIndex, wordFound);
                wordIndex = IndexConsideringTooLowPreviousResult(lowerLimit, upperLimit, wordToFind);
            }

        }
        return new WordFindingResult(callsCount, -1, string.Empty, WordFindingResult.ErrorCodes.CALLS_LIMIT_EXCEEDED);
    }

    private int IndexConsideringTooHighPreviousResult(WordFindingLimit maxLowerLimit, int current)
    {
        return BinarySearch(maxLowerLimit.Index, current);
    }

    private int IndexConsideringTooLowPreviousResult(WordFindingLimit maxLowerLimit, WordFindingLimit minUpperLimit, Word target)
    {
        if (AreLowerAndUpperLimitsDefined(maxLowerLimit, minUpperLimit))
            return BinarySearch(maxLowerLimit.Index, minUpperLimit.Index);

        var scoreByIndexPosition = maxLowerLimit.Index / maxLowerLimit.Word.Score;
        var indexOfTargetBasedInScore = (int)(target.Score * scoreByIndexPosition);
        return indexOfTargetBasedInScore;
    }