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

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

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

String getWord(int index) throws IndexOutOfBoundsException

Требуется: Нужно разработать алгоритм для поиска некоторого слова в словаре с помощью java. Для этого мы должны реализовать метод

public boolean isWordInTheDictionary(String word)

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

Вот альтернативная реализация, использующая 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;
}

Обновление: я изменил его так, чтобы он реализовал RandomAccess, так как binarySearch in Collections в противном случае использовал бы поиск по итератору в таком большом списке, который был бы чрезвычайно медленным. Однако теперь это должно быть прилично быстрым, поскольку для бинарного поиска потребуется только 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;
}

Ответ 3

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Я думаю, что @alexcoco сказал, что Binary Search - это ответ.

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

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

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

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

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

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

Ответ 7

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

#!/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)[email protected]_;
  ++$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";

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

Вот мое решение.. использует операции 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;
        }
    }
}

Ответ 11

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

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

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

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

Ответ 12

Я нахожусь в процессе найма, который задал мне эту же проблему... Мой подход был несколько иным, и, учитывая словарь (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;
    }