【问题标题】:Detecting if a string has unique characters: comparing my solution to "Cracking the Coding Interview?"检测字符串是否具有唯一字符:将我的解决方案与“Cracking the Coding Interview?”进行比较
【发布时间】:2013-10-21 00:11:01
【问题描述】:

我正在阅读“Cracking the Coding Interview”一书,在这里我遇到了一些要求答案的问题,但我需要帮助来比较我的答案和解决方案。我的算法有效,但我很难理解书中的解决方案。主要是我不明白一些运营商到底在做什么。

任务是:“实现一个算法来确定一个字符串是否具有所有唯一字符。如果你不能使用额外的数据结构怎么办?”

这是我的解决方案:

public static boolean checkForUnique(String str){
    boolean containsUnique = false;

    for(char c : str.toCharArray()){
        if(str.indexOf(c) == str.lastIndexOf(c)){
            containsUnique = true;
        } else {
            containsUnique = false;
        }
    }

    return containsUnique;
}

它有效,但它的效率如何?我看到Java中String的索引函数的复杂度是O(n*m)

这是书中的解决方案:

public static boolean isUniqueChars(String str) {
    if (str.length() > 256) {
        return false;
    }
    int checker = 0;
    for (int i = 0; i < str.length(); i++) {
        int val = str.charAt(i) - 'a';
        if ((checker & (1 << val)) > 0) return false;
        checker |= (1 << val);
    }
    return true;
}

我对解决方案不太了解的几件事。首先,“|=”运算符是做什么的?为什么要从字符串中的当前字符中减去“a”以获得“val”的值?我知道“(checker & (1<<val)) 是做什么的?我知道它是按位的,但我不理解它,因为我不理解 checker 获取值的行。

我只是不熟悉这些操作,不幸的是这本书没有给出解决方案的解释,可能是因为它假设你已经理解了这些操作。

【问题讨论】:

  • 我知道 Google 会过滤掉特殊字符,但您可以使用 symbolhound.com 来执行相当于 Google 搜索 |= 或 &amp; 等特殊字符的操作。
  • @levininja 哇比你厉害多了。无法在谷歌上搜索它让我发疯。我希望我能想到谷歌搜索“如何使用特殊字符进行搜索”大声笑
  • 道德:不要相信你读到的一切。答案不一定是对的,甚至是好的,仅仅因为它出现在一本书中。 :-)
  • 请在发布答案之前仔细阅读问题!这个问题提出了一些非常具体的问题。 这里不是让您放弃自己尝试实施此算法的地方。此类答案将被删除。如果您正在寻找有关如何改进自己的工作代码的建议,请访问Code Review。

标签: java string algorithm big-o time-complexity


【解决方案1】:

这里有两个独立的问题:您的解决方案的效率如何,参考解决方案在做什么?让我们分别对待。

首先,您的解决方案:

public static boolean checkForUnique(String str){
    boolean containsUnique = false;

    for(char c : str.toCharArray()){
        if(str.indexOf(c) == str.lastIndexOf(c)){
            containsUnique = true;
        } else {
            containsUnique = false;
        }
    }

    return containsUnique;
}

您的解决方案基本上包括对字符串中所有字符的循环(假设有 n 个),在每次迭代时检查字符的第一个和最后一个索引是否相同。 indexOf 和 lastIndexOf 方法每个都需要 O(n) 时间,因为它们必须扫描字符串的所有字符以确定是否有任何字符与您要查找的字符匹配。因此,由于您的循环运行 O(n) 次并且每次迭代执行 O(n) 次工作,因此它的运行时间为 O(n2)。

但是,您的代码有些问题。尝试在字符串aab 上运行它。它在此输入上是否正常工作?作为提示,一旦您确定有两个或多个重复字符,就可以保证存在重复,并且您可以返回并非所有字符都是唯一的。

现在,让我们看一下参考:

public static boolean isUniqueChars(String str) {
    if (str.length() > 256) { // NOTE: Are you sure this isn't 26?
        return false;
    }
    int checker = 0;
    for (int i = 0; i < str.length(); i++) {
        int val = str.charAt(i) - 'a';
        if ((checker & (1 << val)) > 0) return false;
        checker |= (1 << val);
    }
    return true;
}

这个解决方案很可爱。基本思想如下:假设您有一个由 26 个布尔值组成的数组,每个布尔值都跟踪特定字符是否已经出现在字符串中。你一开始都是假的。然后,您遍历字符串的字符,每次看到一个字符时,您都会查看该字符的数组槽。如果是false,这是你第一次看到这个角色,你可以将槽设置为true。如果是true,你已经看到了这个角色,你可以立即报告有重复。

请注意,此方法不分配布尔数组。相反,它选择了一个聪明的把戏。由于可能只有 26 个不同的字符,并且 int 中有 32 位,因此该解决方案创建了一个 int 变量,其中变量的每一位对应于字符串中的一个字符。该解决方案不是读取和写入数组,而是读取和写入数字的位。

例如,看这一行:

if ((checker & (1 << val)) > 0) return false;

checker &amp; (1 &lt;&lt; val) 是做什么的?好吧,1 &lt;&lt; val 创建了一个int 值,除了valth 位之外,所有位都为零。然后,它使用按位与将该值与checker 进行与运算。如果checker 中val 位置的位已设置,则计算结果为非零值(意味着我们已经看到了该数字)并且我们可以返回false。否则,它的计算结果为 0,我们还没有看到这个数字。

下一行是这样的:

checker |= (1 << val);

这使用了“按位或赋值”运算符,相当于

checker = checker | (1 << val);

这会将 checker 与仅在位置 val 设置 1 位的值进行或运算,从而打开该位。相当于将数字的valth位设置为1。

这种方法比你的要快得多。首先,由于该函数首先检查字符串的长度是否大于 26(我假设 256 是错字),因此该函数永远不必测试任何长度为 27 或更大的字符串。因此,内循环最多运行 26 次。每次迭代在按位运算中完成 O(1) 工作,因此完成的整体工作是 O(1)(O(1) 次迭代乘以 O(1) 每次迭代的工作量),这明显快于你的实现。

如果您还没有看到以这种方式使用按位运算,我建议您在 Google 上搜索“按位运算符”以了解更多信息。

希望这会有所帮助!

【讨论】:

  • @Seephor- 我不会担心的。该解决方案有点笨拙且难以阅读,除非您 100% 确定输入完全由小写字母组成,并且该功能对时间非常关键,因此需要进行积极优化,否则您不会想编写它解决方案。
  • 'int checker' 有点老套,但实际上它可以替换为性能几乎相同的布尔数组甚至哈希映射。
  • @PhamTrung 是的,这很棘手,但是学习的好例子。这个问题有一个约束,比如说,不使用额外的空间,那么你就不能真正使用哈希映射。
  • 这里没有解决,但我认为减去“a”是为了在适当的范围内创建整数 [0,25]?
  • @templatetypedef 谢谢你的详细解释:)
【解决方案2】:

这本书的解决方案是我不喜欢的,我认为它功能失调..... templatetypedef 发布了一个全面的答案,表明该解决方案是一个好的解决方案。我不同意,因为本书的答案假设字符串只有小写字符(ascii)并且没有进行验证来确保这一点。

public static boolean isUniqueChars(String str) {
    // short circuit - supposed to imply that
    // there are no more than 256 different characters.
    // this is broken, because in Java, char's are Unicode,
    // and 2-byte values so there are 32768 values
    // (or so - technically not all 32768 are valid chars)
    if (str.length() > 256) {
        return false;
    }
    // checker is used as a bitmap to indicate which characters
    // have been seen already
    int checker = 0;
    for (int i = 0; i < str.length(); i++) {
        // set val to be the difference between the char at i and 'a'
        // unicode 'a' is 97
        // if you have an upper-case letter e.g. 'A' you will get a
        // negative 'val' which is illegal
        int val = str.charAt(i) - 'a';
        // if this lowercase letter has been seen before, then
        // the corresponding bit in checker will have been set and
        // we can exit immediately.
        if ((checker & (1 << val)) > 0) return false;
        // set the bit to indicate we have now seen the letter.
        checker |= (1 << val);
    }
    // none of the characters has been seen more than once.
    return true;
}

考虑到 templatedef 的答案,底线是实际上没有足够的信息来确定这本书的答案是否正确。

我不相信它。

templatedef 关于复杂性的答案是我同意的答案.... ;-)

编辑:作为练习,我将本书的答案转换为可行的答案(尽管比本书的答案慢 - BigInteger 很慢)....这个版本与本书的逻辑相同,但没有相同的验证和假设问题(但速度较慢)。展示逻辑也很有用。

public static boolean isUniqueChars(String str) {
    if (str.length() > 32768) {
        return false;
    }
    BigInteger checker = new BigInteger(0);
    for (int i = 0; i < str.length(); i++) {
        int val = str.charAt(i);
        if (checker.testBit(val)) return false;
        checker = checker.setBit(val);
    }
    // none of the characters has been seen more than once.
    return true;
}

【讨论】:

  • 这个问题是在没有更多上下文的情况下提出的,我在书中假设它使这些限制更加明确。
  • 我猜你是对的(当你出现时我的回答是我的一部分)。但是,这本书的解决方案是一个很好的例子,说明了不该做什么。
  • @templatetypedef 这本书没有提到其他限制。这就是我提供的一句话。
  • 2^16 = 65536,不是 32768
  • @Seephor 你应该再看看这本书。 Take book 说:“我们将假设,在下面的代码中,字符串只是小写的 'a' 到 'z'。这将允许我们只使用一个 int。”
【解决方案3】:

由于char 值只能包含 256 个不同值之一,任何长度超过 256 个字符的字符串必须至少包含一个重复值。

代码的其余部分使用checker 作为位序列,每个位代表一个字符。它似乎将每个字符转换为一个整数,从a = 1 开始。然后它检查checker 中的相应位。如果已设置,则表示该字符已被看到,因此我们知道该字符串包含至少一个重复字符。如果尚未看到该字符,则代码设置checker 中的相应位并继续。

具体来说,(1&lt;&lt;val) 生成一个整数,其中在位置val 中有一个1 位。例如,(1&lt;&lt;3) 将是二进制 1000,或 8。如果 val 位置的位未在 checker 中设置(即值为 0),则表达式 checker &amp; (1&lt;&lt;val) 将返回零,并且 @ 987654335@,如果该位被设置,它总是非零。表达式checker |= (1&lt;&lt;val) 将在checker 中设置该位。

但是,该算法似乎存在缺陷:它似乎没有考虑大写字符和标点符号(按字典顺序,它们通常排在小写字母之前)。它似乎还需要一个 256 位整数,这不是标准的。

正如rolfl 在下面的评论中提到的那样,我更喜欢您的解决方案,因为它有效。一旦识别出非唯一字符,您可以通过返回 false 来优化它。

【讨论】:

  • 我要补充一点:checker 是一个 32 位变量,高位是符号位,所以该值也可能是负数(&gt; 0 测试被破坏)。就我而言,OP 有更好的解决方案仅仅是因为它可以工作,即使它是 O(n^2)
  • @rolfl- 我认为 256 是一个错误。只有 26 个可能出现的字符,我不认为该解决方案旨在处理超出该范围的字符。
  • 作为算法减去字母a,我认为它只检查小写字母,而不检查数字、大写、特殊字符...
  • 同意,如 templatetypedef 所述。但如果是这样的话,它应该验证它的输入。
  • 作为第二个注释,Java 中的一个 char 是 2 个字节,并且有 32768 个可能的值,而不是 256
【解决方案4】:

书中的解决方案不区分大小写。根据实现,“A”和“a”被认为是重复的。

说明: 对于带有 char 'A' 的输入字符串,'A' - 'a' 为 -32 所以 '1 > 32。这会将第一位设置为 1。char 'a' 也是如此。因此,“A”和“a”被视为重复字符。同样对于 'B' 和 'b' 第二位设置为 1,依此类推。

【讨论】:

    【解决方案5】:

    第 6 版更新

        public static void main(String[] args) {
            System.out.println(isUniqueChars("abcdmc")); // false
            System.out.println(isUniqueChars("abcdm")); // true
            System.out.println(isUniqueChars("abcdm\u0061")); // false because \u0061 is unicode a
        }
    
    
        public static boolean isUniqueChars(String str) {
            /*
             You should first ask your interviewer if the string is an ASCII string or a Unicode string.
             Asking this question will show an eye for detail and a solid foundation in computer science.
             We'll assume for simplicity the character set is ASCII.
             If this assumption is not valid, we would need to increase the storage size.
             */
            // at 6th edition of the book, there is no pre condition on string's length
            /*
             We can reduce our space usage by a factor of eight by using a bit vector.
             We will assume, in the below code, that the string only uses the lowercase letters a through z.
             This will allow us to use just a single int.
              */
            // printing header to provide nice csv format log, you may uncomment
    //        System.out.println("char,val,valBinaryString,leftShift,leftShiftBinaryString,checker");
            int checker = 0;
            for (int i = 0; i < str.length(); i++) {
                /*
                    Dec Binary Character
                    97  01100001    a
                    98  01100010    b
                    99  01100011    c
                    100 01100100    d
                    101 01100101    e
                    102 01100110    f
                    103 01100111    g
                    104 01101000    h
                    105 01101001    i
                    106 01101010    j
                    107 01101011    k
                    108 01101100    l
                    109 01101101    m
                    110 01101110    n
                    111 01101111    o
                    112 01110000    p
                    113 01110001    q
                    114 01110010    r
                    115 01110011    s
                    116 01110100    t
                    117 01110101    u
                    118 01110110    v
                    119 01110111    w
                    120 01111000    x
                    121 01111001    y
                    122 01111010    z
                 */
                // a = 97 as you can see in ASCII table above
                // set val to be the difference between the char at i and 'a'
                // b = 1, d = 3.. z = 25
                char c = str.charAt(i);
                int val = c - 'a';
                // means "shift 1 val numbers places to the left"
                // for example; if str.charAt(i) is "m", which is the 13th letter, 109 (g in ASCII) minus 97 equals 12
                // it returns 1 and 12 zeros = 1000000000000 (which is also the number 4096)
                int leftShift = 1 << val;
                /*
                    An integer is represented as a sequence of bits in memory.
                    For interaction with humans, the computer has to display it as decimal digits, but all the calculations
                    are carried out as binary.
                    123 in decimal is stored as 1111011 in memory.
    
                    The & operator is a bitwise "And".
                    The result is the bits that are turned on in both numbers.
    
                    1001 & 1100 = 1000, since only the first bit is turned on in both.
    
                    It will be nicer to look like this
    
                    1001 &
                    1100
                    =
                    1000
    
                    Note that ones only appear in a place when both arguments have a one in that place.
    
                 */
                int bitWiseAND = checker & leftShift;
                String leftShiftBinaryString = Integer.toBinaryString(leftShift);
                String checkerBinaryString = leftPad(Integer.toBinaryString(checker), leftShiftBinaryString.length());
                String leftShiftBinaryStringWithPad = leftPad(leftShiftBinaryString, checkerBinaryString.length());
    //            System.out.printf("%s &\n%s\n=\n%s\n\n", checkerBinaryString, leftShiftBinaryStringWithPad, Integer.toBinaryString(bitWiseAND));
                /*
                in our example with string "abcdmc"
                0 &
                1
                =
                0
    
                01 &
                10
                =
                0
    
                011 &
                100
                =
                0
    
                0111 &
                1000
                =
                0
    
                0000000001111 &
                1000000000000
                =
                0
    
                1000000001111 &
                0000000000100
                =
                100
                 */
    //            System.out.println(c + "," + val + "," + Integer.toBinaryString(val) + "," + leftShift + "," + Integer.toBinaryString(leftShift) + "," + checker);
                /*
                char val valBinaryString leftShift leftShiftBinaryString checker
                a   0       0               1       1                       0
                b   1       1               2       10                      1
                c   2       10              4       100                     3
                d   3       11              8       1000                    7
                m   12      1100            4096    1000000000000           15
                c   2       10              4       100                     4111
                 */
                if (bitWiseAND > 0) {
                    return false;
                }
                // setting 1 on on the left shift
                /*
                0000000001111 |
                1000000000000
                =
                1000000001111
                 */
                checker = checker | leftShift;
            }
            return true;
            /*
            If we can't use additional data structures, we can do the following:
            1. Compare every character of the string to every other character of the string.
                This will take 0( n 2 ) time and 0(1) space
            2. If we are allowed to modify the input string, we could sort the string in O(n log(n)) time and then linearly
                check the string for neighboring characters that are identical.
                Careful, though: many sorting algorithms take up extra space.
    
            These solutions are not as optimal in some respects, but might be better depending on the constraints of the problem.
             */
        }
    
        private static String leftPad(String s, int i) {
            StringBuilder sb = new StringBuilder(s);
            int charsToGo = i - sb.length();
            while (charsToGo > 0) {
                sb.insert(0, '0');
                charsToGo--;
            }
            return sb.toString();
        }
    

    【讨论】:

      【解决方案6】:

      正如“Cracking the Coding Interview”中所引用的,存在一种替代解决方案:

      boolean isUniqueChars(String str) {
        if(str.length() > 128) return false;
      
        boolean[] char_set = new boolean[128];
        for(int i = 0; i < str.length(); i++) {
          int val = str.charAt(i);
      
          if(char_set[val]) {
            return false;
          }
          char_set[val] = true;
        }
        return true;
      }
      

      当然,要达到更好的空间复杂度,请参考上面的例子@templatetypedef

      【讨论】:

        【解决方案7】:

        这是对本书代码的必要更正:

        public static boolean checkForUnique(String str){
            boolean containsUnique = false;
        
            for(char c : str.toCharArray()){
                if(str.indexOf(c) == str.lastIndexOf(c)){
                    containsUnique = true;
                } else {
                    return false;
                }
            }
        
            return containsUnique;
        }
        

        【讨论】:

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