2299. Strong Password Checker II

EasyString
Leetcode Link

Problem Description

The problem presents a scenario where we need to validate a string, password, to determine if it qualifies as a strong password. For a password to be considered strong, it must meet all these criteria:

  1. The length of the password must be at least 8 characters.
  2. It must include at least one lowercase letter.
  3. It must include at least one uppercase letter.
  4. It must have at least one digit.
  5. It must contain at least one special character, which must be from the set !@#$%^&*()-+.
  6. It must not have more than one identical character in a row, meaning no two adjacent characters can be the same.

The goal is to write a function that returns true if the password meets all the above-described conditions, otherwise returns false.

Intuition

The intuition behind the solution is to go through the password character by character to check if it meets all the necessary criteria for being strong. We can do this by iterating through the string and using flags to mark if we've detected each type of required character. Here's the step-by-step process:

  1. Length Check: First, we check if the password has at least 8 characters. If it's shorter, we immediately return false.

  2. Adjacency Check: As we iterate, we check if the current character is the same as the previous one - if it is, we return false because this violates the non-adjacent character condition.

  3. Character Type Checks: For every character, we need to check if it is a lowercase letter, an uppercase letter, a digit, or a special character. We can do this using the .islower(), .isupper(), .isdigit() methods, and by verifying if the character is in the specified special characters string.

  4. Aggregation with Bitmasking: Instead of keeping four separate flags, we can use a bitmask (mask) to aggregate all the flags into a single integer. Bitwise OR operations |= are used to set the corresponding bits when we encounter lowercase letters, uppercase letters, digits, and special characters. Each character type corresponds to a different bit in the mask, so for example:

    • If we encounter a lowercase letter, we set the first bit (mask |= 1).
    • For an uppercase letter, the second bit (mask |= 2), and so on.
  5. Final Verification: After going through every character of the password, we check if the mask equals 15 (binary 1111). This means that all four bits are set, so every type of required character is included in the password at least once.

The code is efficient and compact. By using bit operations, it avoids the use of multiple boolean variables and reduces the number of conditions checked.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

Which of the following is a good use case for backtracking?

Solution Approach

The implementation of the solution involves a simple yet effective approach by scanning through each character in the password and using bitwise operations to track whether the password criteria have been met. Here’s a detailed walk-through:

  1. Initial Length Check: Immediately check if the password is less than 8 characters. If so, the function returns false.

    • if len(password) < 8: return False
  2. Setup: Initialize a variable mask to 0. This will serve as a 4-bit mask where each bit represents the presence of a different character type (lowercase, uppercase, digit, special character).

  3. Iterate Through Password Characters: By using a for loop with enumeration, we iterate over the password's characters, keeping track of each character and its index.

    • for i, c in enumerate(password):
  4. Adjacency Check: Inside the loop, we first check if the current character is the same as the previous one. If that’s the case, we immediately return false since this violates the non-adjacent identical character condition.

    • if i and c == password[i - 1]: return False
  5. Character Type Detection: Still in the loop, we check the type of the current character:

    • If it’s a lowercase letter (c.islower()), set the first bit of the mask (mask |= 1).
    • If it’s an uppercase letter (c.isupper()), set the second bit of the mask (mask |= 2).
    • If it’s a digit (c.isdigit()), set the third bit of the mask (mask |= 4).
    • If it’s a special character (checked by seeing if it is not any of the above types), set the fourth bit of the mask (mask |= 8).
  6. Final Verification: After the loop, we check if all the bits are set in the mask by comparing it to 15 (binary 1111). This means all required character types are present in the password. The function returns true if mask == 15; otherwise, it returns false.

  7. Data Structures, Algorithms & Patterns:

    • Data Structure: A single integer variable is used for tracking the presence of character types through a concept called bitmasking.
    • Algorithms: A single pass through the string is the main algorithmic component. All checks are done in this single pass, making the time complexity O(n) with n as the password length.
    • Patterns: The solution uses bitwise operations to aggregate checks into a single value, reducing the need for multiple variables.

The simplicity and efficiency of the bitwise operations are key in making the code concise and performant. The choice to use a bitmask over multiple boolean variables exemplifies a common pattern in problems where aggregating flags into a single integer helps optimize space and improve code readability.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which data structure is used in a depth first search?

Example Walkthrough

Let's consider the password string password as Aa1!Aa1!. To determine if this is a strong password according to the given criteria, our function will proceed as follows:

  1. Initial Length Check: Our password Aa1!Aa1! is 8 characters long, so it passes the length requirement.

  2. Setup: We initialize the mask to 0. This mask will help us track whether we have encountered at least one lowercase letter, uppercase letter, digit, and special character.

  3. Iterate Through Password Characters: We start looping through each character in the password.

  4. Adjacency Check:

    • For the first character 'A', there is no previous character, so we move on to type checking.
    • The second character 'a' is different from the first, so no adjacency violation occurs.
    • This process continues, and since no adjacent characters are identical, no adjacency checks fail.
  5. Character Type Detection: As we go through each character:

    • For 'A': It's uppercase, so we set the second bit of the mask (mask |= 2).
    • For 'a': It's lowercase, so we set the first bit of the mask (mask |= 1).
    • For '1': It's a digit, so we set the third bit of the mask (mask |= 4).
    • For '!': It's a special character, so we set the fourth bit of the mask (mask |= 8).
    • As we continue through each character, no additional bits are set since each type has already been encountered.
  6. Final Verification: At the end, our mask is 1111 in binary, or 15 in decimal, which means all types of required characters were present at least once.

  7. Data Structures, Algorithms & Patterns: The use of bitmasking to track character types in a single integer is an efficient data structure choice, and iterating through the password is the primary algorithm. No patterns are additional, and the bitwise operations signify a common pattern to optimize space and improve code readability.

The password Aa1!Aa1! is therefore confirmed to be a strong password by our implementation.

Solution Implementation

1class Solution:
2    def strongPasswordCheckerII(self, password: str) -> bool:
3        # Minimum password length required
4        min_password_length = 8
5
6        # Check if the password length is at least 8 characters
7        if len(password) < min_password_length:
8            return False
9          
10        # Initializing a variable to use as a bitmask to track the requirement fulfillment
11        requirement_mask = 0
12      
13        # Loop through each character in the password
14        for i, char in enumerate(password):
15            # Check if the current character is the same as the previous character
16            if i > 0 and char == password[i - 1]:
17                return False  # Consecutive characters are not allowed
18
19            # Check if the character is a lowercase letter
20            if char.islower():
21                requirement_mask |= 1  # Set the bit for lowercase letter
22            # Check if the character is an uppercase letter
23            elif char.isupper():
24                requirement_mask |= 2  # Set the bit for uppercase letter
25            # Check if the character is a digit
26            elif char.isdigit():
27                requirement_mask |= 4  # Set the bit for digit
28            # Check if the character is a special character
29            else:
30                requirement_mask |= 8  # Set the bit for special character
31      
32        # Check if all 4 requirements are met, which is when all 4 bits are set (i.e., requirement_mask == 1111 binary, which is 15 in decimal)
33        return requirement_mask == 15
34
1class Solution {
2    // Method to check if a given password is strong according to specified rules
3    public boolean strongPasswordCheckerII(String password) {
4        // Requirement: password should be at least 8 characters long
5        if (password.length() < 8) {
6            return false;
7        }
8
9        // A mask to keep track of the types of characters found
10        int characterTypesMask = 0;
11
12        // Iterate through each character in the password
13        for (int i = 0; i < password.length(); ++i) {
14            // Current character being checked
15            char currentChar = password.charAt(i);
16
17            // Requirement: password should not contain consecutive identical characters
18            if (i > 0 && currentChar == password.charAt(i - 1)) {
19                return false;
20            }
21
22            // Identifying the type of the current character and updating the mask accordingly
23            // If it is lowercase, set the first bit using OR operation with 1 (001)
24            if (Character.isLowerCase(currentChar)) {
25                characterTypesMask |= 1; // 0001
26            }
27            // If it is uppercase, set the second bit using OR operation with 2 (010)
28            else if (Character.isUpperCase(currentChar)) {
29                characterTypesMask |= 2; // 0010
30            }
31            // If it is a digit, set the third bit using OR operation with 4 (100)
32            else if (Character.isDigit(currentChar)) {
33                characterTypesMask |= 4; // 0100
34            }
35            // If it is a special character, set the fourth bit using OR operation with 8 (1000)
36            else {
37                characterTypesMask |= 8; // 1000
38            }
39        }
40
41        // Requirement: password must contain all types of characters (lowercase, uppercase, digit, special character)
42        // This is true if, after going through the entire string, the mask equals 15 (1111)
43        // which corresponds to having all four types of characters
44        return characterTypesMask == 15;
45    }
46}
47
1class Solution {
2public:
3    // Function to check if a given password meets strong password criteria.
4    bool strongPasswordCheckerII(string password) {
5        // The password must be at least 8 characters long.
6        if (password.size() < 8) {
7            return false;
8        }
9
10        // 'requirementsMet' will track the types of characters present in the password.
11        // Each bit in 'requirementsMet' corresponds to a different requirement:
12        // Bit 0 (1) represents the presence of a lowercase letter,
13        // Bit 1 (2) represents the presence of an uppercase letter,
14        // Bit 2 (4) represents the presence of a digit,
15        // Bit 3 (8) represents the presence of a special character.
16        int requirementsMet = 0;
17
18        // Iterate over the password characters.
19        for (int i = 0; i < password.size(); ++i) {
20            char currentChar = password[i];
21
22            // Check if the current character is the same as the previous one.
23            if (i > 0 && currentChar == password[i - 1]) {
24                return false; // Return false if two adjacent characters are the same.
25            }
26
27            // Check for different types of characters and update 'requirementsMet'.
28            if (currentChar >= 'a' && currentChar <= 'z') {
29                requirementsMet |= 1; // Presence of a lowercase letter.
30            } else if (currentChar >= 'A' && currentChar <= 'Z') {
31                requirementsMet |= 2; // Presence of an uppercase letter.
32            } else if (currentChar >= '0' && currentChar <= '9') {
33                requirementsMet |= 4; // Presence of a digit.
34            } else {
35                requirementsMet |= 8; // Presence of a special character.
36            }
37        }
38
39        // Check if all four types of characters are present (binary 1111 is decimal 15).
40        return requirementsMet == 15;
41    }
42};
43
1function strongPasswordCheckerII(password: string): boolean {
2    // Length check - password must be at least 8 characters
3    if (password.length < 8) {
4        return false;
5    }
6
7    // Initialize a bitmask to keep track of character types encountered
8    // bit 0 for lowercase, bit 1 for uppercase, bit 2 for digits, bit 3 for special characters
9    let charTypesMask = 0;
10
11    // Iterate over the characters of the password to validate the rules
12    for (let i = 0; i < password.length; ++i) {
13        const currentChar = password[i];
14        // Check for consecutive identical characters
15        if (i > 0 && currentChar === password[i - 1]) {
16            return false;
17        }
18
19        // Check the type of character and update the bitmask accordingly
20        if (currentChar >= 'a' && currentChar <= 'z') {
21            charTypesMask |= 1; // Set bit 0 for lowercase
22        } else if (currentChar >= 'A' && currentChar <= 'Z') {
23            charTypesMask |= 2; // Set bit 1 for uppercase
24        } else if (currentChar >= '0' && currentChar <= '9') {
25            charTypesMask |= 4; // Set bit 2 for digit
26        } else {
27            charTypesMask |= 8; // Set bit 3 for special character
28        }
29    }
30
31    // Check if all four character types are present by confirming all bits are set in the mask
32    return charTypesMask === 15; // (binary 1111)
33}
34
Not Sure What to Study? Take the 2-min Quiz:

How many ways can you arrange the three letters A, B and C?

Time and Space Complexity

The time complexity of the provided code is O(n), where n is the length of the password string. This is because the function consists of a single for loop that iterates through each character of the password string exactly once, performing a constant number of operations per character.

The space complexity of the code is O(1). The extra space used by the function is constant and does not depend on the size of the input password string. The variables used (mask and c) require a fixed amount of space and their size does not scale with the input.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

Which one best describes the time complexity of the following code?

1int factorial(int n) {
2  if (n < 0) {
3    return -1;
4  } else if (n == 0) {
5    return 1;
6  } else {
7    return n * factorial(n - 1);
8  }
9}

Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫