簡體   English   中英

檢測字符串是否具有唯一字符:將我的解決方案與“破解編碼面試?”進行比較

[英]Detecting if a string has unique characters: comparing my solution to "Cracking the Coding Interview?"

我正在閱讀“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))是做什么的? 我知道它是按位和,但我不理解它,因為我不理解檢查器獲取值的那一行。

我只是不熟悉這些操作,不幸的是這本書沒有給出解決方案的解釋,可能是因為它假設你已經了解這些操作。

這里有兩個單獨的問題:您的解決方案的效率如何,參考解決方案在做什么? 讓我們獨立對待每個人。

首先,您的解決方案:

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進行 AND 運算。 如果checker val位置的位已經設置,那么它的計算結果為非零值(意味着我們已經看到了數字),我們可以返回 false。 否則,它的計算結果為 0,我們還沒有看到這個數字。

下一行是這樣的:

checker |= (1 << val);

這使用了“按位或賦值”運算符,相當於

checker = checker | (1 << val);

此 ORs checker的值僅在val位置設置了 1 位,從而打開該位。 它相當於將數字的第val位設置為 1。

這種方法比你的方法快得多。 首先,由於函數首先檢查字符串的長度是否大於 26(我假設 256 是一個錯字),因此函數永遠不必測試任何長度為 27 或更大的字符串。 因此,內循環最多運行 26 次。 每次迭代在按位運算中執行 O(1) 工作,因此完成的總體工作為 O(1)(每次迭代 O(1) 迭代乘以 O(1) 工作),這明顯快於您的實現。

如果您還沒有見過以這種方式使用的按位運算,我建議您在 Google 上搜索“按位運算符”以了解更多信息。

希望這可以幫助!

這本書的解決方案是我不喜歡的,我認為它是功能失調的..... 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;
}

由於char值只能包含 256 個不同值之一,因此任何長度超過 256 個字符的字符串必須至少包含一個重復項。

代碼的其余部分使用checker作為位序列,每一位代表一個字符。 它似乎將每個字符轉換為整數,從a = 1 開始。然后檢查checker的相應位。 如果設置了,則表示該字符已被看到,因此我們知道該字符串至少包含一個重復字符。 如果尚未看到該字符,則代碼在checker設置相應的位並繼續。

具體來說, (1<<val)生成一個整數,在val位置有一個1位。 例如, (1<<3)將是二進制1000或 8。如果在checker未設置位置val的位(即值為 0),則表達式checker & (1<<val)將返回零,和(1<<val) ,如果設置了該位,則它始終為非零。 表達式checker |= (1<<val)將在checker設置該位。

但是,該算法似乎有缺陷:它似乎沒有考慮大寫字符和標點符號(按字典順序通常在小寫字符之前)。 它似乎也需要一個 256 位整數,這不是標准的。

正如rolfl在下面的評論中提到的,我更喜歡你的解決方案,因為它有效。 您可以通過在識別出非唯一字符后立即返回false來優化它。

書中的解決方案不區分大小寫。 根據實現,“A”和“a”被認為是重復的。

說明:對於帶有字符 'A' 的輸入字符串,'A' - 'a' 是 -32,因此 '1 << val' 將被評估為 1 << -32。 shift 任何負數都會向相反的方向移動位。 因此 1 << -32 將是 1 >> 32。這會將第一位設置為 1。這也是 char 'a' 的情況。 因此,'A' 和 'a' 被視為重復字符。 同樣,對於 'B' 和 'b' 第二位設置為 1,依此類推。

第六版更新

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

正如“破解編碼面試”中所引用的,存在一種替代解決方案:

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上面的例子

萬一有人需要這個作為 Javascript 實現。 @whoopdedoo 和其他人的解釋已經很清楚了。 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'))

這是對本書代碼的必要更正:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM