web-dev-qa-db-fra.com

Détecter si une chaîne de caractères a des caractères uniques: comparer ma solution à "Cracking the Coding Interview?"

Je suis en train de parcourir le livre "Cracking the Coding Interview" et j'ai rencontré des questions ici demandant des réponses, mais j'ai besoin d'aide pour comparer ma réponse à la solution. Mon algorithme fonctionne, mais j'ai du mal à comprendre la solution proposée dans le livre. Principalement parce que je ne comprends pas ce que certains opérateurs font réellement. 

La tâche est la suivante: "Implémentez un algorithme pour déterminer si une chaîne a tous les caractères uniques. Et si vous ne pouvez pas utiliser de structures de données supplémentaires?"

Ceci est ma solution:

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

Cela fonctionne, mais à quel point est-ce efficace? J'ai vu que la complexité des fonctions d'index pour String en Java est O (n * m)

Voici la solution du livre:

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

Quelques choses que je ne comprends pas très bien avec la solution. Tout d'abord, que fait l'opérateur "| ="? Pourquoi "a" est-il soustrait du caractère actuel de la chaîne pour la valeur "val"? Je sais que "<<" est un décalage gauche au niveau des bits, mais que fait (checker & (1<<val))? Je sais que c'est au niveau des bits et, mais je ne le comprends pas car je ne comprends pas la ligne où checker obtient une valeur.

Je ne suis tout simplement pas au courant de ces opérations et, malheureusement, le livre n’explique pas les solutions, probablement parce qu’il suppose que vous comprenez déjà ces opérations.

46
Seephor

Il y a deux questions distinctes ici: quelle est l'efficacité de votre solution et que fait la solution de référence? Traitons chacun indépendamment.

Tout d'abord, votre solution:

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

Votre solution consiste essentiellement en une boucle sur tous les caractères de la chaîne (disons qu'il y en a n), en vérifiant à chaque itération si le premier et le dernier index des caractères sont identiques. Les méthodes indexOf et lastIndexOf prennent chacune un temps O (n), car elles doivent analyser tous les caractères de la chaîne pour déterminer si l'un d'entre eux correspond à celui que vous recherchez. Par conséquent, puisque votre boucle exécute O(n) fois et que O(n) fonctionne par itération, son temps d'exécution est O (n2).

Cependant, votre code a quelque chose d'incertain. Essayez de l'exécuter sur la chaîne aab. Est-ce que cela fonctionne correctement sur cette entrée? A titre indicatif, dès que vous déterminez qu'il y a deux caractères ou plus dupliqués, vous avez la garantie qu'il y a des doublons et vous pouvez indiquer que tous les caractères ne sont pas uniques.

Maintenant, regardons la référence:

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

Cette solution est mignonne. L'idée de base est la suivante: imaginez que vous avez un tableau de 26 booléens, chacun permettant de savoir si un caractère particulier est déjà apparu dans la chaîne. Vous commencez avec tous faux. Vous parcourez ensuite les caractères de la chaîne, et chaque fois que vous voyez un caractère, vous regardez dans le tableau pour ce caractère. Si c'est false, c'est la première fois que vous voyez le personnage et vous pouvez définir l'emplacement à true. Si c'est true, vous avez déjà vu ce personnage et vous pouvez immédiatement signaler qu'il y a un doublon.

Notez que cette méthode n'alloue pas un tableau de booléens. Au lieu de cela, il opte pour une astuce intelligente. Comme il n'y a que 26 caractères différents possibles et qu'il y a 32 bits dans une variable int, la solution crée une variable int dans laquelle chaque bit de la variable correspond à l'un des caractères de la chaîne. Au lieu de lire et d'écrire un tableau, la solution lit et écrit les bits du nombre.

Par exemple, regardez cette ligne:

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

Que fait checker & (1 << val)? 1 << val crée une valeur int qui a tous les bits nuls sauf le bit valth. Il utilise ensuite bit à bit ET à ET cette valeur avec checker. Si le bit à la position val dans checker est déjà défini, sa valeur est différente de zéro (ce qui signifie que nous avons déjà vu le nombre) et nous pouvons renvoyer false. Sinon, la valeur est 0 et nous n'avons pas vu le nombre.

La ligne suivante est la suivante:

checker |= (1 << val);

Ceci utilise l'opérateur "bitwise OR avec assignation", ce qui équivaut à

checker = checker | (1 << val);

Cet OU est checker avec une valeur dont le bit 1 est défini uniquement à la position val, ce qui active le bit. Cela équivaut à définir le valth bit du nombre sur 1.

Cette approche est beaucoup plus rapide que la vôtre. Premièrement, puisque la fonction commence par vérifier si la longueur de la chaîne est supérieure à 26 (je suppose que le 256 est une faute de frappe), la fonction ne doit jamais tester une chaîne de longueur supérieure ou égale à 27. Par conséquent, la boucle interne s'exécute au plus 26 fois. Chaque itération O(1) fonctionne par bits, de sorte que le travail global effectué est de O(1) (O (1) fois le nombre de itérations O(1) par itération), qui est significativement plus rapide que votre implémentation.

Si vous n'avez pas vu les opérations au niveau des bits utilisées de cette manière, je vous conseillerais de rechercher des "opérateurs au niveau des bits" sur Google pour en savoir plus.

J'espère que cela t'aides!

98
templatetypedef

La solution de livre est une solution que je n'aime pas et qui, à mon avis, est dysfonctionnelle. Templatetypedef a publié une réponse complète indiquant que la solution est bonne. Je ne suis pas d'accord, car la réponse du livre suppose que la chaîne ne comporte que des caractères minuscules (ascii) et ne fait aucune validation pour le garantir.

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

En fin de compte, étant donné la réponse de templatedef également, il n’ya pas suffisamment d’informations pour déterminer si la réponse du livre est correcte.

Je m'en méfie cependant.

la réponse de templatedef sur la complexité est celle avec laquelle je suis d'accord .... ;-)

EDIT: En tant qu’exercice, j’ai converti la réponse du livre en une réponse qui fonctionnera (quoique plus lentement que la réponse du livre - BigInteger est lente) .... Cette version suit la même logique que celle du livre, mais n’a pas la même validation problèmes d'assomption (mais c'est plus lent). Il est utile de montrer la logique aussi.

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

Puisqu'une valeur char peut contenir l'une des 256 valeurs différentes au maximum, toute chaîne de plus de 256 caractères must doit contenir au moins un doublon.

Le reste du code utilise checker comme séquence de bits, chaque bit représentant un caractère. Il semble convertir chaque caractère en un entier commençant par a = 1. Il vérifie ensuite le bit correspondant dans checker. Si elle est définie, cela signifie que le caractère a déjà été vu et nous savons donc que la chaîne contient au moins un caractère en double. Si le caractère n'a pas encore été vu, le code définit le bit correspondant dans checker et continue.

Plus précisément, (1<<val) génère un entier avec un seul 1 en position val. Par exemple, (1<<3) serait binaire 1000 ou 8. L’expression checker & (1<<val) renverra zéro si le bit en position val n’est pas défini (c’est-à-dire que sa valeur est 0) dans checker et (1<<val), qui est toujours différent de zéro, si le bit est mis. L'expression checker |= (1<<val) définira ce bit dans checker.

Cependant, l’algorithme semble défectueux: il ne semble pas tenir compte des majuscules ni de la ponctuation (qui précèdent généralement les minuscules lexicographiquement). Il semblerait également qu’il faille un entier de 256 bits, ce qui n’est pas la norme.

Comme rolfl mentionne dans le commentaire ci-dessous, je préfère votre solution car elle fonctionne. Vous pouvez l’optimiser en retournant false dès que vous identifiez un caractère non unique.

3
Adam Liss

6ème édition mise à jour

    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

La solution du livre est insensible à la casse. 'A' et 'a' sont considérés comme des doublons conformément à la mise en œuvre.

Explication: Pour la chaîne d'entrée avec le caractère 'A', 'A' - 'a' est -32so '1 << val' sera évalué comme 1 << -32. décalage sur tout nombre négatif décale les bits dans le sens opposé . Ainsi 1 << -32 sera 1 >> 32. Ce qui mettra le premier bit à 1. Ceci est également le cas avec '. Ainsi, «A» et «a» sont considérés comme des caractères en double. De même pour "B" et "b", le second bit est mis à 1 et ainsi de suite.

0
allen joseph

Comme indiqué dans "Cracking the Coding Interview", il existe une solution alternative:

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

Bien entendu, pour obtenir une meilleure complexité de l'espace, veuillez vous reporter à l'exemple ci-dessus à l'aide de @templatetypedef

0
asus