仅使用通过索引获得单词的方法在未知大小的字典中查找单词

|| 几天前,我在某大公司接受了面试,名字不是必需的:),面试官让我找到下一个任务的解决方案: 预定义:    有未指定大小的单词字典,我们只知道字典中的所有单词都已排序(例如,按字母排序)。而且我们只有一种方法
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.5倍。巨大的字典和小的固定初始步骤(如您所拥有的(100))可能确实有问题。想想如果有5000万个单词,如果您要搜索“斑马”,您的算法将向上扩展范围的次数必须达到多少次。 这是一个主意:通过假设每个单词的第一个字母均匀分布在字母表中的各个字母来利用集合的有序性质(这永远都是不对的,但在不了解更多单词集合的情况下)这可能是您所能做的最好的事情。然后,根据您希望字典单词到末尾的距离,对范围扩展量进行加权。 因此,如果您迈出了第一步,并在该索引处查找了字典单词,并且它是\'aardvark \',那么与下一步相比,如果您将其范围扩大到\“ walrus”,则您的范围会更多。 O(log n),但对于大多数单词集合而言可能更好。     
        这是使用
Collections.binarySearch
的替代实现。如果列表中的单词之一以字符
\'\\uffff\'
(即Unicode 0xffff,不是合法的无效Unicode字符)开头,则失败。
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
,因为否则Collections中的binarySearch会在如此大的列表上使用基于迭代器的搜索,这将非常慢。但是,这现在应该相当快,因为​​即使List伪装成尽可能大,二进制搜索也只需要进行31次迭代。 这是一个经过稍微修改的版本,它记住最小的失败索引以将其声明的大小收敛到整个字典的实际大小,从而避免了连续查找中的几乎所有异常。尽管每当字典大小发生变化时,您都需要创建一个新的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开始)。 如果您要查找的单词小于当前词典单词,则将当前索引和您的“低”值之间的距离减半。 (当然,\“ low \”从0开始)。 如果您要查找的单词比刚检查的索引上的单词“大于”,则将当前索引和您的“ high”值(\“ high \”开始于2)或,如果索引和\“ high \”相同,则将索引加倍。 如果将索引加倍会使您超出范围,则将当前值和加倍值之间的距离减半。因此,如果从16到32抛出异常,请尝试24。并且,当然,请注意32大于最大值的事实。 因此,搜索顺序可能看起来像1、2、4、8、16、12、14-找到了! 它与二进制搜索的概念相同,但不是从低= 0,高= n-1开始,而是从低= 0,高= 2开始,并在需要时将高值加倍。它仍然是O(log N),尽管该常数将比使用“正常”二进制搜索的常数大一点。     
        如果您知道字典不会改变,则可能会产生O(n)的一次性成本。您可以将字典中的所有单词添加到哈希表中,然后对isWordInDictionary()的任何后续调用将为O(1)(理论上)。     
        使用getWord()API将字典的全部内容复制到更合理的数据结构中(例如,哈希表,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所说的,二进制搜索就是答案。 我看到的其他选项仅在您可以扩展词典的情况下可用。您可以使其稍好一点。例如。您可以对每个字母上的单词进行计数,并以这种方式保持它们的踪迹,这样您就只能有效地处理一部分单词。 还是像伙计们所说的那样,完全实现自己的字典结构。 我知道这不能正确回答您的问题。但是我看不到其他可能性。 顺便说一句,很高兴看到您的算法。 编辑: 在回答bshields时扩展我的评论... @Sergii Zagriichuk更好的是,我想应该记住上一个索引为null(无字)的索引。然后,您可以在每次运行时检查它是否仍然正确。如果不是,则将范围扩展到通过反转二进制搜索行为而获得的“上一个索引”,因此我们再次具有null。这样,您将始终调整搜索算法范围的大小,从而根据需要适应词典的当前状态。另外,所做的更改必须很重要才能引起范围调整,因此调整不会对算法产生任何实际的负面影响。而且字典在本质上往往是静态的,所以这应该起作用:)     
        一方面,是的,您对二进制搜索实现是正确的。但是另一方面,如果字典是静态的,并且在两次查找之间没有变化,则我们可以建议使用其他算法。这里我们有一个共同的问题-字符串排序/搜索与排序/搜索int数组相比有所不同,因此getWord(int i).compareTo(string)为O(min(length0,length1))。 假设我们有请求找到单词w0,w1,... wN,在查找过程中我们可以建立一个带有索引的树(可能有些后缀树足以完成此任务)。 在下一个查找请求期间,我们遵循以下设置a1,a2,... aM,因此要减少平均时间,我们可以先通过搜索树中的位置来减小范围。 此实现的问题是并发性和内存使用情况,因此下一步是实现使搜索树更小的策略。 PS:主要目的是检查您提出的想法和问题。     
好吧,我认为可以更好地利用字典排序的信息。 假设您要查找单词“ Zebra \”,而第一个猜测搜索结果为\“ abcg \”。 因此,我们可以在选择第二个猜测索引时使用此信息。就像我的情况一样,结果单词以a开头,而我正在寻找以z开头的东西。因此,除了进行静态跳转外,我还可以根据当前结果和所需结果进行一些计算得出的跳转。因此,以这种方式假设如果我的下一个跳转将我带到“ yvu”一词,那么我现在就很近了,因此与上一个案例相比,我将进行一个相当缓慢的小跳转。     
        这是我的解决方案..使用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的,我将搜索分为两部分。 首先,假设getWord()的参数索引是一个整数,并且假定索引必须是0到最大正整数之间的数字,请在该范围内执行二进制搜索以找到最大有效索引(无论字值)。由于是简单的二进制搜索,因此此操作为O(log N)。 一旦获得字典的大小,第二个普通的二进制搜索(再次是复杂度O(log N))将带来所需的答案。 由于O(log N)+ O(log N)为O(log N),因此该算法符合您的要求。     
        我在招聘过程中问了我同样的问题... 我的方法有些不同,考虑到我拥有的字典(网络服务),它的效率提高了约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;
    }
    

要回复问题请先登录注册