709. To Lower Case
Problem Description
You are given a string s
that may contain both uppercase and lowercase letters. Your task is to convert all uppercase letters in the string to their corresponding lowercase letters while keeping all other characters unchanged.
For example:
- If
s = "Hello"
, the output should be"hello"
- If
s = "here"
, the output should be"here"
(already lowercase) - If
s = "LOVELY"
, the output should be"lovely"
The solution uses a bit manipulation technique. It iterates through each character in the string and checks if it's uppercase using c.isupper()
. If the character is uppercase, it converts it to lowercase by performing a bitwise OR operation: chr(ord(c) | 32)
. This works because in ASCII encoding, the difference between an uppercase letter and its lowercase counterpart is exactly 32 (or 0x20
in hexadecimal). By setting the 6th bit (value 32) using the OR operation, we effectively convert uppercase to lowercase. If the character is already lowercase or not a letter, it remains unchanged.
The expression ord(c) | 32
takes the ASCII value of the character and performs a bitwise OR with 32, which sets the 6th bit to 1, converting uppercase letters to lowercase. The result is then converted back to a character using chr()
. All processed characters are collected in a list comprehension and joined together to form the final string.
Intuition
When we need to convert uppercase letters to lowercase, the most straightforward approach would be to use built-in string methods like lower()
. However, to understand the underlying mechanism, we can look at how characters are represented in ASCII.
In ASCII encoding, uppercase letters ('A' to 'Z') have values from 65 to 90, while lowercase letters ('a' to 'z') have values from 97 to 122. If we examine the difference between any uppercase letter and its lowercase counterpart, we find it's always 32. For instance, 'A' is 65 and 'a' is 97, with a difference of 32.
Looking at this difference in binary reveals an interesting pattern:
- 'A' (65) in binary:
01000001
- 'a' (97) in binary:
01100001
The only difference is the 6th bit (counting from the right, starting at 0). For uppercase letters, this bit is 0, and for lowercase letters, it's 1. The value of the 6th bit position is exactly 32 (2^5
).
This observation leads us to a clever bit manipulation trick: to convert any uppercase letter to lowercase, we simply need to set its 6th bit to 1. We can achieve this using the bitwise OR operation with 32. The operation ord(c) | 32
will:
- Set the 6th bit to 1 if it's currently 0 (converting uppercase to lowercase)
- Keep the 6th bit as 1 if it's already 1 (lowercase remains lowercase)
This approach elegantly handles the conversion without needing conditional logic for the actual transformation, though we still check c.isupper()
to avoid modifying non-alphabetic characters that might have unexpected results when OR'd with 32.
Solution Approach
The solution implements a single-line approach using list comprehension combined with bit manipulation:
return "".join([chr(ord(c) | 32) if c.isupper() else c for c in s])
Let's break down the implementation step by step:
-
Iterate through each character: We use a list comprehension to process each character
c
in the strings
. -
Check if character is uppercase: For each character, we use
c.isupper()
to determine if it's an uppercase letter. This check ensures we only modify uppercase letters and leave everything else (lowercase letters, digits, special characters, spaces) unchanged. -
Apply bit manipulation for uppercase letters:
- If the character is uppercase, we convert it using
chr(ord(c) | 32)
ord(c)
gets the ASCII value of the character| 32
performs a bitwise OR operation with 32 (binary:100000
), which sets the 6th bit to 1chr()
converts the resulting ASCII value back to a character
- If the character is uppercase, we convert it using
-
Keep other characters unchanged: If the character is not uppercase (already lowercase, digit, or special character), we keep it as is.
-
Join the results: The list comprehension produces a list of processed characters, which we join together using
"".join()
to create the final string.
Example walkthrough with s = "Hello World!"
:
- 'H' (72) → uppercase →
72 | 32 = 104
→ 'h' - 'e' (101) → not uppercase → remains 'e'
- 'l' (108) → not uppercase → remains 'l'
- 'l' (108) → not uppercase → remains 'l'
- 'o' (111) → not uppercase → remains 'o'
- ' ' (32) → not uppercase → remains ' '
- 'W' (87) → uppercase →
87 | 32 = 119
→ 'w' - And so on...
The time complexity is O(n)
where n
is the length of the string, as we process each character once. The space complexity is also O(n)
for storing the result string.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "HeLLo"
:
Step 1: Process 'H' (index 0)
- Check:
'H'.isupper()
→ True (it's uppercase) - Convert:
ord('H')
= 72 - Binary of 72:
01001000
- Apply
72 | 32
:01001000 (72) 00100000 (32) -------- 01101000 (104) = 'h'
- Result: 'H' → 'h'
Step 2: Process 'e' (index 1)
- Check:
'e'.isupper()
→ False (already lowercase) - Keep unchanged: 'e'
Step 3: Process 'L' (index 2)
- Check:
'L'.isupper()
→ True (it's uppercase) - Convert:
ord('L')
= 76 - Apply
76 | 32
:01001100 (76) 00100000 (32) -------- 01101100 (108) = 'l'
- Result: 'L' → 'l'
Step 4: Process 'L' (index 3)
- Check:
'L'.isupper()
→ True (it's uppercase) - Convert:
ord('L')
= 76 - Apply
76 | 32
= 108 = 'l' - Result: 'L' → 'l'
Step 5: Process 'o' (index 4)
- Check:
'o'.isupper()
→ False (already lowercase) - Keep unchanged: 'o'
Final Step: Join all characters
- List of processed characters:
['h', 'e', 'l', 'l', 'o']
- Join with
"".join()
:"hello"
Output: "hello"
The key insight is that the bitwise OR with 32 always sets the 6th bit to 1, which in ASCII encoding guarantees the character becomes lowercase if it was uppercase, while already lowercase letters remain unchanged.
Solution Implementation
1class Solution:
2 def toLowerCase(self, s: str) -> str:
3 """
4 Convert all uppercase letters in a string to lowercase.
5
6 Args:
7 s: Input string to be converted
8
9 Returns:
10 String with all uppercase letters converted to lowercase
11 """
12 # Build the result by iterating through each character
13 result = []
14
15 for char in s:
16 if char.isupper():
17 # Convert uppercase to lowercase using bitwise OR with 32
18 # ASCII difference between uppercase and lowercase is 32
19 # Setting the 6th bit (32 = 0b100000) converts upper to lower
20 lowercase_char = chr(ord(char) | 32)
21 result.append(lowercase_char)
22 else:
23 # Keep non-uppercase characters unchanged
24 result.append(char)
25
26 # Join all characters to form the final string
27 return "".join(result)
28
1class Solution {
2 public String toLowerCase(String s) {
3 // Convert string to character array for in-place modification
4 char[] characters = s.toCharArray();
5
6 // Iterate through each character in the array
7 for (int i = 0; i < characters.length; i++) {
8 // Check if current character is an uppercase letter (A-Z)
9 if (characters[i] >= 'A' && characters[i] <= 'Z') {
10 // Convert uppercase to lowercase using bitwise OR with 32
11 // This works because uppercase and lowercase letters differ by 32 in ASCII
12 // 'A' = 65 (01000001), 'a' = 97 (01100001)
13 // OR with 32 (00100000) sets the 6th bit, converting to lowercase
14 characters[i] |= 32;
15 }
16 }
17
18 // Convert character array back to string and return
19 return String.valueOf(characters);
20 }
21}
22
1class Solution {
2public:
3 /**
4 * Converts all uppercase letters in a string to lowercase.
5 *
6 * @param s The input string to be converted
7 * @return The string with all uppercase letters converted to lowercase
8 */
9 string toLowerCase(string s) {
10 // Iterate through each character in the string by reference
11 for (char& c : s) {
12 // Check if the character is an uppercase letter (A-Z)
13 if (c >= 'A' && c <= 'Z') {
14 // Convert to lowercase by setting the 6th bit (adding 32)
15 // In ASCII, uppercase and lowercase letters differ by 32
16 // 'A' = 65 (01000001), 'a' = 97 (01100001)
17 // OR operation with 32 (00100000) sets the 6th bit
18 c |= 32;
19 }
20 }
21
22 return s;
23 }
24};
25
1/**
2 * Converts a string to lowercase
3 * @param s - The input string to be converted to lowercase
4 * @returns The lowercase version of the input string
5 */
6function toLowerCase(s: string): string {
7 // Use the built-in toLowerCase method to convert the string
8 return s.toLowerCase();
9}
10```
11
12However, if you want to implement this without using the built-in method (which might be the intent for a LeetCode problem), here's an alternative implementation:
13
14```typescript
15/**
16 * Converts a string to lowercase by manually converting uppercase letters
17 * @param s - The input string to be converted to lowercase
18 * @returns The lowercase version of the input string
19 */
20function toLowerCase(s: string): string {
21 // Initialize result string
22 let result: string = '';
23
24 // Iterate through each character in the string
25 for (let i = 0; i < s.length; i++) {
26 const charCode: number = s.charCodeAt(i);
27
28 // Check if the character is an uppercase letter (A-Z: 65-90)
29 if (charCode >= 65 && charCode <= 90) {
30 // Convert to lowercase by adding 32 to ASCII value
31 result += String.fromCharCode(charCode + 32);
32 } else {
33 // Keep the character as is if not uppercase
34 result += s[i];
35 }
36 }
37
38 return result;
39}
40
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input string s
. The algorithm iterates through each character in the string exactly once. For each character, it performs constant time operations: ord()
, isupper()
, bitwise OR operation, and chr()
. The join()
operation also takes O(n)
time to concatenate all characters.
Space Complexity: O(n)
where n
is the length of the input string. The list comprehension creates a new list containing n
characters before joining them into the result string. Additionally, the final string created by join()
also requires O(n)
space. Therefore, the total auxiliary space used is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Assuming All Characters are Letters
A common mistake is applying the bitwise OR operation to all characters without checking if they're uppercase letters first. This can corrupt non-letter characters.
Incorrect approach:
# WRONG: This corrupts non-letter characters
return "".join([chr(ord(c) | 32) for c in s])
For example, if the input contains digits or special characters:
- '1' (ASCII 49) →
49 | 32 = 49
(happens to stay '1', but by luck) - '@' (ASCII 64) →
64 | 32 = 96
→ '`' (corrupted!) - '[' (ASCII 91) →
91 | 32 = 123
→ '{' (corrupted!)
Solution: Always check c.isupper()
before applying the bit manipulation.
2. Incorrect Bit Manipulation for Non-ASCII Characters
The bitwise OR with 32 only works correctly for ASCII uppercase letters (A-Z). For Unicode characters or extended ASCII, this approach may fail.
Problem example:
s = "CAFÉ" # É is a non-ASCII character # The bit manipulation won't correctly convert É to é
Solution: Use Python's built-in lower()
method for better Unicode support:
return s.lower() # Handles all Unicode characters correctly
3. Using XOR Instead of OR
Some might mistakenly use XOR (^
) thinking it "toggles" the case, but this creates issues.
Incorrect approach:
# WRONG: XOR doesn't guarantee lowercase conversion
chr(ord(c) ^ 32) # This toggles between upper and lower
This would convert:
- 'A' → 'a' (correct)
- 'a' → 'A' (incorrect - converts lowercase to uppercase!)
Solution: Use OR (|
) to set the bit, not XOR to toggle it.
4. Not Handling Empty Strings or None
The code might fail if the input is None
or has unexpected types.
Solution: Add input validation:
if not s: return s # Return empty string or None as-is
5. Memory Inefficiency with Large Strings
Creating intermediate lists for very large strings can be memory-intensive.
Alternative approach for better memory usage:
# Using generator expression instead of list comprehension
return "".join(chr(ord(c) | 32) if c.isupper() else c for c in s)
This avoids creating an intermediate list, processing characters on-the-fly.
Which of the following is a good use case for backtracking?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!