【问题标题】:Find word in dictionary of unknown size using only a method to get a word by index仅使用按索引获取单词的方法在未知大小的字典中查找单词
【发布时间】:2011-09-03 15:22:44
【问题描述】:

前几天我在某大公司面试,名字不要求:),面试官让我想办法解决下一个任务:

预定义: 有一个未指定大小的单词字典,我们只知道字典中的所有单词都是排序的(例如按字母表)。我们也只有一种方法

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

【问题讨论】:

  • 二分搜索可能是最好的方法之一
  • 嗯?您说您的变体具有复杂度 O(logN),这通常是在排序列表中的搜索应该是什么,并且所需的解决方案具有“对数复杂度”(意思是,我假设 O(logN)。你在找什么?
  • 向字典作者抱怨他们糟糕的 API 并让他们更改它。 O(1)
  • @Seth 我正在寻找另一种算法,因为面试官告诉我我的变体很好,但还有另一个具有相同复杂性的算法。
  • @alexcoco 谢谢 :) 我知道修改后的二进制搜索是个好主意,我已经实现了它,但我正在寻找另一种解决方案。

标签: java algorithm binary-search


【解决方案1】:

听起来他们真正希望您考虑的部分是如何处理您不知道字典大小的事实。我认为他们认为你可以给他们一个二分搜索。所以真正的问题是如何在搜索过程中控制搜索范围。

一旦您在字典中找到大于您的搜索目标(或超出范围)的值,其余的看起来就像标准的二分搜索。困难的部分是当目标值大于您查找的字典值时,如何以最佳方式扩展范围。看起来您正在扩大 1.5 倍。对于一个庞大的字典和一个小的固定初始步骤(如您所拥有的(100)),这可能真的有问题。想想如果有 5000 万个单词,如果您要搜索“斑马”,您的算法必须向上扩展范围多少次。

这是一个想法:通过假设每个单词的第一个字母均匀分布在字母表中的字母中来利用集合的有序性质(这永远不会是真的,但在不了解单词集合的情况下)这可能是你能做的最好的)。然后根据您期望字典单词距离结尾的距离来衡量您的范围扩展量。

因此,如果您在初始步骤 100 中查找该索引处的字典单词,并且它是“土豚”,那么与如果它是“海象”相比,您下一步的范围会扩大很多。仍然是 O(log n),但对于大多数单词集合来说可能要好得多。

【讨论】:

  • 我已经向我的面试官描述了同样的想法,谢谢,我认为它会起作用,但我认为很难在 20 分钟内实施。以上算法我在 20-25 分钟内实现。
  • +1 这确实是一个非常有趣的想法。 @Sergii Zagriichuk 我想知道你真的可以假设字典的状态,即它的大小不会显着改变吗?如果是这样,那么您可以升级您的算法,使其在第一次运行时找到初始步骤。即使对它使用二进制搜索,例如记住你第一次得到其他东西时,然后 null(no word) 作为答案。然后,您可以将其存储在文件中或将其保存在内存中,这样在第一次调用之后的每个连续调用都会得到优化。
  • @Sergii Zagriichuk 在我上面的评论之后,我在回答中扩展了我的想法,假设我们总是在处理未知大小的字典。请检查并让我知道您对这种方法有何看法。您认为它是否可行/可用。
  • 这个想法是Interpolation_search,是的,它仍然是 O(log n),也许更好的 O(log(log n)),这个链接是由 Seth Robertson 提供的,谢谢他 :) ,但是这个算法的实现是相当困难的。
【解决方案2】:

这是使用Collections.binarySearch 的替代实现。如果列表中的单词之一以字符 '\uffff' 开头(即 Unicode 0xffff 而不是合法的 not a valid unicode character),则会失败。

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

【讨论】:

  • 这是一个有趣的解决方案,但我已经对其进行了测试,但它不起作用。 :(顺便说一下,这是二分查找,我正在寻找另一种解决方案,因为二分查找我已经实现了。
  • 你能告诉我什么不工作吗?对于这个问题,我认为没有比二分搜索更好的方法了,我只是试图提供一种更快、更容易实现并且可能更快的方法。
  • @x4u +1 提供了一个非常简洁的解决方案。我正在考虑实际重用默认的二进制搜索,但我不知道它会如此好和容易。 :) 如果我能的话,我什至会给你 +2 这个 :) 感谢它被存储以备将来使用。顺便说一句,您不扩展 ArrayList(已经实现了 RandomAccess)的论点是什么?
  • 是的,你的代码的第一个变体没有工作,这个变体是正确的,是的,这是一个很好的解决方案,但是,我认为,公司不能接受这样的解决方案,因为强制知道如何实现二分查找或其他算法。
  • @Sergii Zagriichuk:二进制搜索是一个有趣且重要的小算法,每个开发人员都应该知道和理解,并且应该在他的一生中实现过几次,但要快速获得它可能会非常棘手和健壮。 Jon Bentley 在他的书amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880 中详细讨论了它,这本书物有所值。因此,当您不需要任何特别的东西时,我始终建议您依赖经过测试的现有实现,即使 Collections 中的 on 不如它可能的那么好,但您会知道它有效。
【解决方案3】:

您的想法是正确的,但我认为您的实现过于复杂。你想做一个二分搜索,但你不知道上限是多少。所以不是从中间开始,而是从索引 1 开始(假设字典索引从 0 开始)。

如果您要查找的单词“小于”当前字典单词,请将当前索引与“低”值之间的距离减半。 (当然,“低”从 0 开始)。

如果您要查找的单词“大于”您刚刚检查的索引处的单词,则将当前索引与您的“高”值之间的距离减半(“高”从 2 开始)或者,如果 index 和 "high" 相同,则将 index 加倍。

如果将索引加倍导致超出范围异常,则将当前值与加倍值之间的距离减半。因此,如果从 16 变为 32 会引发异常,请尝试 2​​4。当然,请注意 32 大于最大值这一事实。

因此,搜索序列可能看起来像 1、2、4、8、16、12、14 - 找到了!

这与二分查找的概念相同,但不是从低 = 0、高 = n-1 开始,而是从低 = 0、高 = 2 开始,并在需要时将高值加倍。它仍然是 O(log N),尽管这个常数会比“正常”二分查找大一点。

【讨论】:

  • 您的方法适用于非常小的列表,但比仅假设 Integer.MAX_VALUE 作为上限更糟糕的情况,因为它需要逐步上升然后再次下降,最多可能是 61 次迭代。以 Integer.MAX_VALUE 作为上限,无论实际列表大小如何,您都需要平均 30 次迭代,并且实现变得更加容易。
  • @x4u:我确实说过它会慢一些。最坏的情况下它会慢两倍。实际上它不应该那么糟糕,因为这种方法通常会抛出更少的异常,因为单词词典通常没有数亿个条目。因此,虽然正常的二分搜索需要较少的探针,但其中许多探针会抛出异常,这是非常昂贵的。
  • 我同意您关于异常的负面影响的观点,尽管如果可以假定列表的大小是静态的(问题中没有说明),您可以跟踪之前运行的最低失败索引到迅速减小声明列表的大小,从而避免大多数异常。
  • 请看我的实现,这个实现和你描述的一样,只是不是从1开始,而是从100开始。
【解决方案4】:

如果您知道字典不会更改,则可能会产生 O(n) 的一次性成本。您可以将字典中的所有单词添加到哈希表中,然后对 isWordInDictionary() 的任何后续调用都将是 O(1)(理论上)。

【讨论】:

  • :) 我无法改变字典的内部状态,这是限制性写的,请看一下。
  • 对,但是如果你知道字典不会改变,那么你可以像我说的那样做,而不需要触及它的内部状态。
  • 是的,但我不知道,我只知道我写的信息。
  • 嗯,很公平。那样的话我就糊涂了。
  • 虽然现在我想起来,我们至少可以假设一些事情。似乎至少我们知道字典的内容不会在您调用 getWord 的同时被修改;否则,很可能会发生不好的事情。我不确定是否可以提出字典不会可变的论点,但这似乎有些合理。
【解决方案5】:

使用 getWord() API 将字典的全部内容复制到更合理的数据结构中(例如哈希表、trie,甚至可能通过 Bloom 过滤器进行扩充)。 ;-)

【讨论】:

  • 然后呢?复制所有单词的复杂度为 O(n),查找单词的复杂度为 +,这不是真正的答案和解决方案!
  • 这也不是一个真正的问题,不是吗?如果可以为大量后续查询节省时间,则 O(N) 的前期副本很好。但我们不知道真正的要求和限制是什么,因为这是一个面试问题。
  • 是的,但这是来自真实采访的真实问题,是的,我同意你的观点,在实际开发中,我们必须基于更好的结构重新开发字典,并使用这种结构来查找单词,是的,你是对的,但我问的是面试问题的解决方案,而不仅仅是“现实世界”问题,我已经在主题中描述了它,这个问题来自面试:)干杯:)。
【解决方案6】:

使用不同的语言:

#!/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";

这个的主要好处是它更容易理解。我认为如果您的第一次猜测低于目标可能会更有效,因为我认为您没有利用您已经“搜索”的空间,但这只是快速浏览您的代码。因为它是为了简单而寻找数字,所以它不必处理找不到目标,但这是一个简单的扩展。

【讨论】:

  • 感谢您的实施,但它与我仅使用 perl 实施的算法相同:)
  • @Sergii Zagriichuk:嗯,正如我所说,我认为它更有效,但是它是二进制搜索。
  • @Sergii Zagriichuk:唯一的另一个想法是使用插值搜索,假设随机分布(我们知道英语不是这样)来计算除中间以外的概率位置点。 en.wikipedia.org/wiki/Interpolation_search 但这增加了相当多的复杂性,并且需要根据您正在搜索的语言进行调整,甚至添加更多内容。
【解决方案7】:

@Sergii Zagriichuk 希望采访顺利。祝你好运。

我认为正如@alexcoco 所说,二进制搜索就是答案。

我看到的其他选项只有在您可以扩展字典时才可用。你可以让它稍微好一点。例如。您可以计算每个字母上的单词,并以这种方式跟踪它们,您实际上只需要处理一部分单词。

或者是的,正如人们所说的,完全实现自己的字典结构。

我知道这不能正确回答您的问题。但我看不到其他可能性。

顺便说一句,很高兴看到您的算法。

编辑: 扩展 my comment under answer of bshields...

@Sergii Zagriichuk 我认为最好记住最后一个索引为空(无字)的索引。然后在每次运行时,您都可以检查它是否仍然正确。如果不是,则将范围扩展为通过反转二进制搜索行为获得的“先前索引”,因此我们再次为 null。这样,您将始终调整搜索算法范围的大小,从而根据需要适应字典的当前状态。此外,更改必须非常重要才能导致您的范围调整,因此调整不会对算法产生任何真正的负面影响。字典在本质上往往是静态的,所以这应该可以工作:)

【讨论】:

  • 我已经在第一篇文章中发布了我的实现,请看一下......谢谢你的回答...... :)
  • 是的,我知道找到的字典大小的解决方案,感谢您的解释:)
【解决方案8】:

一方面是的,您对二分搜索实现是正确的。但另一方面,如果字典是静态的并且在查找之间没有改变 - 我们可以建议不同的算法。这里我们有一个共同的问题 - 字符串排序/搜索与排序/搜索 int 数组相比是不同的,所以 getWord(int i).compareTo(string) 是 O(min(length0, length1))。

假设我们请求查找单词 w0, w1, ... wN,在查找过程中,我们可以建立一个带有索引的树(可能一些后缀树足以胜任这项任务)。 在下一次查找请求中,我们有以下集合 a1, a2, ... aM,因此为了减少平均时间,我们可以首先通过在树中搜索位置来减少范围。 这个实现的问题是并发和内存使用,所以下一步是实现使搜索树更小的策略。

PS:主要目的是检查您提出的想法和问题。

【讨论】:

  • 你的建议基于静态(不可更改)字典,但我没有这个要求,我无法实现一些搜索树(但我已经向面试官提供了这个解决方案)
【解决方案9】:

我认为可以更好地利用字典排序的信息。 假设您正在寻找单词“Zebra”,而第一次猜测搜索结果为“abcg”。 所以我们可以使用这个信息来选择第二个猜测索引。就像在我的情况下,结果词以 a 开头,而我正在寻找以 z 开头的东西。因此,我可以根据当前结果和所需结果进行一些计算跳跃,而不是进行静态跳转。所以以这种方式假设如果我的下一次跳跃将我带到“yvu”这个词,我现在非常接近,所以我会做一个比上一个例子更慢的小跳跃。

【讨论】:

  • 你已经描述了插值搜索(但不是所有细节,只是一些事情),但如果你的所有单词都从 Z compexity O 开始,这个搜索会更糟(n),但我的变体在所有情况下都是 O(log n)。
  • 嗯......我只是举了一个例子......如果一切都从字母'z'开始,那么字典开头的数据元素之间会有一些增量然后一次最后。所以我的意思是建立这种人工智能逻辑......是的,它必须是持久性系统,因为最后的结果将有助于决定当前的跳跃评估。
  • 所以,为此我应该构建一些字母位置或 BST 矩阵,但我无法更改字典的内部状态,它是在预定义的部分中编写的,如果字典是静态的,是的,我们可以构建一些结构不只是像你写的那样,它可以是一些哈希表或 BST 或 RBT 并使用这种结构来查找单词,但字典不是静态的!
【解决方案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;
        }
    }
}

【讨论】:

  • 您的解决方案根本不起作用!而且复杂度不等于 O(log N),它更高。
  • 为什么你认为这行不通?我已经用字典测试了这段代码。复杂度为 2 * O(logn),即 O(logn)
【解决方案11】:

假设字典是从 0 开始的,我会将搜索分解为两部分。

首先,假设 getWord() 参数的索引是一个整数,并且假设索引必须是介于 0 和最大正整数之间的数字,请在该范围内执行二进制搜索以找到最大有效值索引(与单词值无关)。这个操作是 O(log N),因为是一个简单的二分查找。

一旦获得了字典的大小,第二次普通的二分查找(复杂度也是 O(log N))就会得到想要的答案。

由于 O(log N)+O(log N) 是 O(log N),所以这个算法符合你的要求。

【讨论】:

    【解决方案12】:

    我正在招聘过程中问我同样的问题... 我的方法有点不同,考虑到我拥有的字典(网络服务),它的效率大约提高了 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;
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-07
      相关资源
      最近更新 更多