web-dev-qa-db-ja.com

文字列に一意の文字が含まれているかどうかを検出する:ソリューションと「コーディングインタビューのクラック」を比較します。

私は本「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;
}

私はソリューションで完全に理解していないいくつかのこと。まず、「| =」演算子は何をしますか? 「val」の値の文字列の現在の文字から「a」が減算されるのはなぜですか? 「<<」はビット単位の左シフトですが、(checker & (1<<val)) 行う?私はそれがビット単位であることを知っていますが、チェッカーが値を取得する行を理解していないため、私はそれを理解していません。

私はこれらの操作に精通していないだけで、残念ながら本書では解決策について説明していません。おそらく、これらの操作を既に理解していることを前提としています。

46
Seephor

ここには2つの質問があります。ソリューションの効率と、参照ソリューションは何をしているのでしょうか。それぞれを独立して扱いましょう。

まず、あなたのソリューション:

_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で実行してみてください。この入力で正しく動作しますか?ヒントとして、2つ以上の重複する文字があると判断するとすぐに、重複があることが保証され、すべての文字が一意ではないことを返すことができます。

それでは、リファレンスを見てみましょう。

_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変数を作成します。変数の各ビットは文字列の文字の1つに対応します。ソリューションは、配列を読み書きする代わりに、数値のビットを読み書きします。

たとえば、次の行を見てください。

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

checker & (1 << val)は何をしますか?まあ、_1 << val_は、intthビットを除くすべてのビットがゼロのval値を作成します。次に、ビット単位のANDを使用して、この値とcheckerをANDします。 valcheckerの位置のビットが既に設定されている場合、これはゼロ以外の値に評価され(既に数値を見ていることを意味します)、falseを返すことができます。そうでない場合、評価は0になり、数値は表示されません。

次の行は次のとおりです。

_checker |= (1 << val);
_

これは、「ビットごとのOR割り当てあり」演算子を使用します。これは、

_checker = checker | (1 << val);
_

これは、checkerの位置にのみ1ビットが設定された値とvalを論理和し、ビットをオンにします。これは、数値のvalthビットを1に設定することと同じです。

このアプローチは、あなたよりもはるかに高速です。まず、関数は文字列の長さが26より大きいかどうかをチェックすることから開始するため(256は誤字だと仮定しています)、関数は長さ27以上の文字列をテストする必要はありません。したがって、内部ループは最大26回実行されます。各反復はO(1)ビット単位の操作で動作するため、全体の作業はO(1)(O(1)反復回数O(1)反復ごとの作業)、これは大幅に実装よりも高速です。

この方法でビット単位演算を使用したことがない場合は、Googleで「ビット単位演算子」を検索して詳細を確認することをお勧めします。

お役に立てれば!

102
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;
}
14
rolfl

char値には256個の異なる値のいずれかしか保持できないため、256文字mustより長い文字列には少なくとも1つの重複が含まれます。

コードの残りの部分では、ビットのシーケンスとしてcheckerを使用します。各ビットは1文字を表します。 a = 1で始まる各文字を整数に変換するようです。次に、checkerの対応するビットをチェックします。設定されている場合は、その文字がすでに表示されていることを意味するため、文字列に少なくとも1つの重複文字が含まれていることがわかります。文字がまだ表示されていない場合、コードはcheckerの対応するビットを設定して続行します。

具体的には、_(1<<val)_は、位置valに単一の_1_ビットを持つ整数を生成します。たとえば、_(1<<3)_はバイナリ_1000_、または8です。式checker & (1<<val)は、位置valのビットが設定されていない場合(つまり、 checkerに値0)があり、そのビットが設定されている場合は常にゼロ以外の_(1<<val)_です。式checker |= (1<<val)は、checkerのそのビットを設定します。

ただし、アルゴリズムには欠陥があるようです。大文字と句読点(通常、辞書的には小文字よりも前にある)を考慮していないようです。また、256ビット整数が必要なようですが、これは標準ではありません。

rolfl が下のコメントで言及しているように、私はあなたの解決策がうまくいくので好みです。一意でない文字を識別するとすぐにfalseを返すことで最適化できます。

3
Adam Liss

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

本からの解決策は大文字と小文字を区別しません。 「A」と「a」は実装ごとに重複していると見なされます。

説明:文字「A」の入力ストリングの場合、「A」-「a」は-32であるため、「1 << val」は1 << -32として評価されます。負の数をシフトすると、ビットが反対方向にシフトします。したがって、1 << -32は1 >> 32になります。これにより、最初のビットが1に設定されます。これは、char 'a'の場合も同様です。したがって、「A」と「a」は重複文字と見なされます。同様に、「B」および「b」の2番目のビットは1に設定されます。

0
allen joseph

「コーディングインタビューのクラック」から参照されているように、代替ソリューションが存在します。

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

0
asus