检测字符串是否具有唯一字符:将我的解决方案与“破解编码面试”进行比较?

时间:2013-10-21 00:11:01

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

我正在阅读“破解编码面试”一书,我在这里遇到了一些问题,但我需要帮助比较我对解决方案的回答。我的算法有效,但我很难理解书中的解决方案。主要是因为我不明白一些运营商在做什么。

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

这是我的解决方案:

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”?我知道“&lt;&lt;”是一个按位左移,但(checker & (1<<val))做了什么?我知道它是按位的,但我不理解它,因为我不理解检查器获取值的行。

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

7 个答案:

答案 0 :(得分:100)

这里有两个不同的问题:您的解决方案的效率是多少,参考解决方案的作用是什么?让我们独立对待每个人。

首先,您的解决方案:

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个字符串),检查每次迭代字符的第一个和最后一个索引是否相同。 indexOflastIndexOf方法每个都花费时间O(n),因为它们必须扫描字符串的所有字符以确定它们中的任何一个是否与您要查找的字符匹配。因此,由于你的循环运行O(n)次并且每次迭代都运行O(n),因此它的运行时为O(n 2 )。

但是,你的代码有些不确定。尝试在字符串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 & (1 << val)做什么?好吧,1 << val创建一个int值,除了val位之外,所有位都为零。然后,它使用按位AND与此值一起使用checker。如果已设置val中位置checker的位,则此计算结果为非零值(意味着我们已经看到了该数字),并且我们可以返回false。否则,它的计算结果为0,我们还没有看到数字。

下一行是:

checker |= (1 << val);

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

checker = checker | (1 << val);

这个OR checker的值仅在位置val设置了1位,这会使该位置1。这相当于将数字的val位设置为1。

这种方法比你的快得多。首先,由于函数通过检查字符串的长度是否大于26来开始(我假设256是一个错字),该函数永远不必测试长度为27或更大的任何字符串。因此,内环最多运行26次。每次迭代都按位操作进行O(1)工作,因此完成的总工作是O(1)(O(1)次迭代乘以每次迭代的O(1)工作),其显着显着你的实施。

如果您没有看到以这种方式使用的按位操作,我建议您在Google上搜索“按位运算符”以了解详情。

希望这有帮助!

答案 1 :(得分:14)

书籍解决方案是我不喜欢的,我相信功能失调..... 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;
}

答案 2 :(得分:3)

由于char值只能包含256个不同值中的一个,因此任何长度超过256个字符的字符串必须至少包含一个副本。

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

具体而言,(1<<val)生成一个位置为1的{​​{1}}位的整数。例如,val将是二进制(1<<3)或8.如果位置1000中的位未设置(即,值为0),则表达式checker & (1<<val)将返回零)valchecker,如果设置该位,则始终为非零。表达式(1<<val)将在checker |= (1<<val)中设置该位。

然而,该算法似乎有缺陷:它似乎没有考虑大写字符和标点符号(通常在词典编排的小写字母之前)。它似乎也需要256位整数,这不是标准的。

正如rolfl在下面的评论中提到的,我更喜欢你的解决方案,因为它有效。只要您确定了一个非唯一字符,就可以通过返回checker来优化它。

答案 3 :(得分:0)

本书的解决方案不区分大小写。 &#39; A&#39;和&#39; a&#39;根据实施被视为重复。

说明: 对于带有字符串的输入字符串&#39; A&#39;,&#39; A&#39; - &#39; a&#39;是-32 所以&#39; 1&lt;&lt; VAL&#39;将被评估为1&lt;&lt; -32。 任何负数的移位都会使位向相反方向移位。 因此1&lt;&lt;&lt; -32将是1>&gt; 32.将第一位设置为1.这也是char&#39; a&#39;的情况。因此&#39; A&#39;和&#39; a&#39;被视为重复字符。同样地,对于&#39; B&#39;和&#39; b&#39;第二位设置为1,依此类推。

答案 4 :(得分:0)

第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();
    }

答案 5 :(得分:0)

如“破解编码面试”所述,存在另一种解决方案:

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

参阅上面的示例

答案 6 :(得分:-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 {
            return false;
        }
    }

    return containsUnique;
}