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

krcsximq  于 2022-12-25  发布在  Java
关注(0)|答案(8)|浏览(129)

我正在读《破解编程面试》这本书,我遇到了一些需要答案的问题,但我需要帮助来比较我的答案和解决方案。我的算法工作正常,但我很难理解书中的解决方案。主要是因为我不明白一些运算符真正在做什么。
任务是:"实现一个算法来确定字符串是否都是唯一的字符。如果不能使用其他数据结构该怎么办?"
这是我的解决方案:

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获取值的行。
我只是不熟悉这些运算不幸的是书中没有给出解决方案的解释,可能是因为它假设你已经理解了这些运算。

mmvthczy

mmvthczy1#

这里有两个独立的问题:您的解决方案的效率如何,参考解决方案的效果如何?让我们分别讨论这两个问题。
首先,您的解决方案:

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(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开始,然后遍历字符串中的字符,每次你看到一个字符,你就在数组槽中查找这个字符,如果它是false,这是你第一次看到这个角色,你可以把插槽设置为true。如果是true,你已经看到这个角色了,你可以立即报告有重复。
注意,这个方法没有分配一个布尔数组,而是选择了一个聪明的技巧。由于int中只有26个不同的字符,而int中有32位,因此该解决方案创建了一个int变量,其中变量的每一位对应于字符串中的一个字符。该解决方案读取和写入该数的位。
例如,看下面这行:

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

checker & (1 << val)是做什么的?1 << val创建一个int值,除了val位之外,所有位都为零。然后它使用按位AND将这个值与checker进行AND运算。如果checker中位置val的位已经置位,那么它的值为非零值(意味着我们已经看到了这个数字),我们可以返回false,否则,它的值为0,我们还没有看到这个数字。
下一行是这样的:

checker |= (1 << val);

它使用“带赋值的位OR”运算符,该运算符等效于

checker = checker | (1 << val);

此函数将checker与仅在位置val处设置了1位的值进行OR运算,从而打开该位。这相当于将数字的第val位设置为1。
这种方法比你的方法要快得多。首先,由于函数一开始就检查字符串的长度是否大于26(我假设256是一个拼写错误),所以这个函数从来不需要测试任何长度为27或更长的字符串,因此,内部循环最多运行26次。(1)按位操作,所以完成的总工作量是O(1)(O(1)次迭代乘以每次迭代O(1)次工作量),这比您的实现要快得多。
如果您还没有见过以这种方式使用的位运算,我建议您在Google上搜索“位运算符”以了解更多信息。

t30tvxxf

t30tvxxf2#

我不喜欢书中的解决方案,而且我认为它是功能失调的...... templatypedef已经发布了一个全面的答案,表明这个解决方案是好的,我不同意,因为书中的答案假设字符串只有小写字符(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;
}
ylamdve6

ylamdve63#

由于char值只能保存256个不同值中的一个,因此任何长度超过256个字符的字符串 * 必须 * 至少包含一个重复值。
代码的其余部分使用checker作为位序列,每个位代表一个字符。它似乎将每个字符转换为整数,从a = 1开始。然后检查checker中的相应位。如果设置了该位,则意味着该字符已经被看到。因此我们知道字符串至少包含一个重复字符,如果还没有看到这个字符,代码就会设置checker中的相应位并继续。
具体而言,(1<<val)生成位置val处具有单个1位的整数。例如,(1<<3)将为二进制1000或8。如果未设置位置val中的位,则表达式checker & (1<<val)将返回零(即值为0)和(1<<val)(如果设置了该位,则始终为非零)。表达式checker |= (1<<val)将设置checker中的该位。
然而,该算法似乎存在缺陷:它似乎没有考虑到大写字符和标点符号(按字典顺序,它们通常在小写字符之前),而且似乎需要一个256位整数,这不是标准的。
正如rolfl在下面的评论中提到的,我更喜欢您的解决方案,因为它很有效,您可以通过在识别出非唯一字符后立即返回false来优化它。

v9tzhpje

v9tzhpje4#

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

k97glaaz5#

书中的解决方案不区分大小写。根据实现,"a"和"a"被视为重复。
说明:对于具有字符"A"的输入字符串,"A"-"a"为-32,因此"1〈〈val"的计算结果为1〈〈-32。对任何负数进行移位都将以相反方向移位位。因此,1〈〈-32将为1〉〉32。这将把第一位设置为1。字符"a"也是这种情况。因此,"A"和"a"被认为是重复字符。类似地,'B'和'b'的第二位被设置为1,依此类推。

qxsslcnc

qxsslcnc6#

如“破解编码访谈”所述,存在替代解决方案:

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

当然,要实现更好的空间复杂度,请参考上面的示例**@ templatypedef**

hk8txs48

hk8txs487#

如果有人需要它作为Javascript实现。@ whoopdoo和其他人已经清楚地说明了这一点。https://stackoverflow.com/a/46105690/8090964

function isUniqueChars(str) {
    let ASCIICodeOfa = 'a'.charCodeAt(0);
    let checker = 0;
    for (let i = 0; i < str.length; i++) {
        let currentChar = str.charCodeAt(i);
        let val = currentChar - ASCIICodeOfa;
        if ((checker & (1 << val)) > 0) {
            return false;
        }
        checker |= (1 << val);
    }
    return true;
}

console.log(isUniqueChars('abcdef'))
console.log(isUniqueChars('abcdefb'))
vjhs03f7

vjhs03f78#

以下是对该书代码的必要更正:

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

相关问题