web-dev-qa-db-de.com

Feststellen, ob eine Zeichenfolge eindeutige Zeichen enthält: Vergleichen meiner Lösung mit "Cracking the Coding Interview?"

Ich arbeite durch das Buch "Cracking the Coding Interview" und bin hier auf Fragen gestoßen, die nach Antworten fragen, aber ich brauche Hilfe, um meine Antwort auf die Lösung zu vergleichen. Mein Algorithmus funktioniert, aber ich habe Schwierigkeiten, die Lösung im Buch zu verstehen. Hauptsächlich, weil ich nicht verstehe, was einige der Betreiber wirklich tun. 

Die Aufgabe lautet: "Implementieren Sie einen Algorithmus, um festzustellen, ob eine Zeichenfolge alle eindeutigen Zeichen enthält. Was ist, wenn Sie keine zusätzlichen Datenstrukturen verwenden können?"

Das ist meine Lösung:

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

Es funktioniert, aber wie effizient ist das? Ich habe gesehen, dass die Komplexität der Indexfunktionen für String in Java O (n * m) ist.

Hier ist die Lösung aus dem Buch:

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

Ein paar Dinge, die ich mit der Lösung nicht ganz verstehe. Was macht der Operator "| =" zuerst? Warum wird 'a' für den Wert von "val" vom aktuellen Zeichen in der Zeichenfolge abgezogen? Ich weiß, "<<" ist eine bitweise Verschiebung nach links, aber was macht (checker & (1<<val))? Ich weiß, dass es bitweise ist, aber ich verstehe es nicht, da ich die Zeile nicht verstehe, wo checker einen Wert bekommt.

Ich kenne diese Vorgänge einfach nicht, und leider gibt das Buch keine Erklärung für die Lösungen, wahrscheinlich, weil Sie davon ausgehen, dass Sie diese Vorgänge bereits verstehen.

46
Seephor

Hier gibt es zwei getrennte Fragen: Was ist die Effizienz Ihrer Lösung und was macht die Referenzlösung? Lassen Sie uns jeden unabhängig behandeln.

Zuerst deine Lösung:

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

Ihre Lösung besteht im Wesentlichen aus einer Schleife über alle Zeichen in der Zeichenfolge (angenommen, es gibt n davon), wobei bei jeder Iteration geprüft wird, ob der erste und der letzte Index der Zeichen gleich sind. Die indexOf- und lastIndexOf-Methoden benötigen jeweils eine Zeitdauer O (n), da sie alle Zeichen der Zeichenfolge durchsuchen müssen, um zu ermitteln, ob sie mit der gesuchten Methode übereinstimmen. Da Ihre Schleife O(n) -Zeiten ausführt und O(n) pro Iteration ausgeführt wird, ist ihre Laufzeit daher O (n. N)2).

Ihr Code hat jedoch etwas Unsicheres. Versuchen Sie es mit der Zeichenfolge aab auszuführen. Funktioniert es korrekt bei dieser Eingabe? Wenn Sie feststellen, dass zwei oder mehr Zeichen vorhanden sind, stellen Sie sicher, dass es Duplikate gibt, und Sie können zurückgeben, dass nicht alle Zeichen eindeutig sind.

Nun sehen wir uns die Referenz an:

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

Diese Lösung ist süß. Die Grundidee ist folgende: Stellen Sie sich vor, Sie haben ein Array von 26 Booleans, von denen jedes verfolgt, ob bereits ein bestimmtes Zeichen in der Zeichenfolge vorkommt. Sie fangen mit allen falsch an. Sie durchlaufen dann die Zeichen der Zeichenfolge und jedes Mal, wenn Sie ein Zeichen sehen, schauen Sie in den Array-Steckplatz für dieses Zeichen. Wenn es false ist, haben Sie den Charakter zum ersten Mal gesehen und Sie können den Platz auf true setzen. Wenn es true ist, haben Sie diesen Charakter bereits gesehen und können sofort melden, dass es ein Duplikat gibt.

Beachten Sie, dass bei dieser Methode kein Array von Booleans zugewiesen wird. Stattdessen entscheidet sie sich für einen cleveren Trick. Da nur 26 verschiedene Zeichen möglich sind und in einer int 32 Bits vorhanden sind, erstellt die Lösung eine Variable int, in der jedes Bit der Variablen einem der Zeichen in der Zeichenfolge entspricht. Anstatt ein Array zu lesen und zu schreiben, liest und schreibt die Lösung die Bits der Zahl.

Betrachten Sie zum Beispiel diese Zeile:

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

Was macht checker & (1 << val)? Nun, 1 << val erstellt einen int-Wert, bei dem alle Bits bis auf das valth-Bit null sind. Dann verwendet es bitweise UND zu UND diesen Wert mit checker. Wenn das Bit an Position val in checker bereits gesetzt ist, wird dies zu einem Wert ungleich Null ausgewertet (was bedeutet, dass wir die Zahl bereits gesehen haben) und wir können false zurückgeben. Andernfalls wird der Wert 0, und die Nummer wurde nicht angezeigt.

Die nächste Zeile lautet:

checker |= (1 << val);

Hierbei wird der Operator "bitweises OR mit Zuweisung" verwendet, der äquivalent ist

checker = checker | (1 << val);

Diese ORs checker mit einem Wert, für den nur an Position val ein 1-Bit festgelegt ist, wodurch das Bit aktiviert wird. Dies ist gleichbedeutend mit dem Setzen des valth-Bits der Zahl auf 1.

Dieser Ansatz ist viel schneller als bei Ihnen. Da die Funktion zunächst mit der Prüfung beginnt, ob die Zeichenfolge länger als 26 ist (ich gehe davon aus, dass die Zahl 256 ein Tippfehler ist), muss die Funktion niemals eine Zeichenfolge der Länge 27 oder größer testen. Daher läuft die innere Schleife höchstens 26 Mal. Jede Iteration arbeitet O(1) in bitweisen Operationen, so dass die gesamte geleistete Arbeit O(1) (O (1) Iterationszeiten O(1) Arbeit pro Iteration) ist. Das ist deutlich schneller als Ihre Implementierung.

Wenn Sie bitweise Operationen auf diese Weise nicht gesehen haben, würde ich empfehlen, nach "bitweisen Operatoren" bei Google zu suchen, um mehr zu erfahren.

Hoffe das hilft!

98
templatetypedef

Die Buchlösung ist eine, die ich nicht mag und ich glaube, dass sie nicht funktionsfähig ist ..... templatetypedef hat eine umfassende Antwort gepostet, die darauf hinweist, dass die Lösung eine gute ist. Ich stimme dem nicht zu, da die Antwort des Buches davon ausgeht, dass die Zeichenfolge nur Kleinbuchstaben enthält (ascii), und dies nicht überprüft wird.

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

Unter dem Strich ist auch die Antwort von templatedef, dass nicht genug Informationen vorhanden sind, um zu bestimmen, ob die Antwort des Buches richtig ist.

Ich misstraue aber trotzdem.

templatedefs Antwort auf die Komplexität stimme ich allerdings zu ... ;-)

BEARBEITEN: Als Übung habe ich die Antwort des Buches in eine konvertiert, die funktionieren wird (wenn auch langsamer als die Antwort des Buches - BigInteger ist langsam) ... Diese Version folgt der gleichen Logik wie das Buch, hat aber nicht die gleiche Gültigkeit und Vermutungsprobleme (aber es ist langsamer). Es ist nützlich, auch die Logik zu zeigen.

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

Da ein char-Wert einen von nur 256 verschiedenen Werten enthalten kann, enthält jeder String mit mehr als 256 Zeichen muss mindestens ein Duplikat.

Der Rest des Codes verwendet checker als Folge von Bits, wobei jedes Bit ein Zeichen darstellt. Es scheint, jedes Zeichen in eine Ganzzahl umzuwandeln, beginnend mit a = 1. Es überprüft dann das entsprechende Bit in checker. Wenn es gesetzt ist, bedeutet dies, dass ein Zeichen bereits gesehen wurde, und wir wissen daher, dass die Zeichenfolge mindestens ein doppeltes Zeichen enthält. Wenn das Zeichen noch nicht gesehen wurde, setzt der Code das entsprechende Bit in checker und wird fortgesetzt.

Insbesondere generiert (1<<val) eine Ganzzahl mit einem einzelnen 1-Bit an Position val. Beispiel: (1<<3) wäre binär 1000 oder 8. Der Ausdruck checker & (1<<val) gibt Null zurück, wenn das Bit in Position val nicht gesetzt ist (dh, hat den Wert 0) in checker und (1<<val), das immer ungleich Null ist Bit ist gesetzt. Der Ausdruck checker |= (1<<val) setzt dieses Bit in checker.

Der Algorithmus scheint jedoch fehlerhaft zu sein: er scheint die Großbuchstaben und die Interpunktion (die im Allgemeinen lexikographisch vor den Kleinbuchstaben stehen) nicht zu berücksichtigen. Es scheint auch eine 256-Bit-Ganzzahl zu erfordern, was kein Standard ist.

Da rolfl in dem Kommentar unten erwähnt, bevorzuge ich Ihre Lösung, weil sie funktioniert. Sie können es optimieren, indem Sie false zurückgeben, sobald Sie ein nicht eindeutiges Zeichen identifiziert haben.

3
Adam Liss

6. Ausgabe Update

    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
Iddo

Die Lösung aus dem Buch unterscheidet nicht zwischen Groß- und Kleinschreibung. 'A' und 'a' werden gemäß der Implementierung als Duplikat betrachtet.

Erläuterung: Für die Eingabezeichenfolge mit dem Zeichen 'A', 'A' - 'a' ist -32so '1 << val' wird als 1 << -32 bewertet. shift bei einer negativen Zahl verschiebt die Bits in die entgegengesetzte Richtung . 1 << -32 ist 1 >> 32. Dadurch wird das erste Bit auf 1 gesetzt. Dies ist auch bei char 'a der Fall '. Somit werden 'A' und 'a' als doppelte Zeichen betrachtet. Ähnlich wird für 'B' und 'b' das zweite Bit auf 1 gesetzt und so weiter.

0
allen joseph

Wie aus "Cracking the Coding Interview" hervorgeht, gibt es eine alternative Lösung:

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

Um eine bessere Raumkomplexität zu erreichen, beachten Sie bitte das obige Beispiel unter @templatetypedef

0
asus