Strings Questions Part 1 ========================= 106. (LC 125) Test palindromicity (Valid Palindrome) ------------------------------------------------------- `125. Valid Palindrome `_ Easy **Logic** Use two indices to traverse the string forward and backwards (skipping nonalphanumeric), which gives us the effect of reversing without using extra space. (Just reversing the given string and comparing it with the original requires extra space.) :: ### Solution 1 (doocs), O(n). Doesn't use extra space. class Solution: def isPalindrome(self, s: str) -> bool: # i moves forward, j backwards i, j = 0, len(s) - 1 while i < j: if not s[i].isalnum(): #** i += 1 elif not s[j].isalnum(): #** j -= 1 elif s[i].lower() != s[j].lower(): #compare only when both are alnum return False else: i, j = i + 1, j - 1 return True #** If values are not alphanumeric, we proceed to the next index. | ### Note that this fails when s='.,' | (Expect True, the below code will return False, | it will end up comparing . != , which it should not compare at all) :: def palindromic(s): if len(s) < 2: return True lp = 0 rp = len(s) - 1 while lp < len(s) and rp >= 0: while s[lp].isalnum() == False and lp < len(s) - 1: lp += 1 while s[rp].isalnum() == False and rp > 0: rp -= 1 if s[lp].lower() != s[rp].lower(): return False rp -= 1 lp += 1 return True ### Solution 2 (neetcode) Uses extra space. class Solution: def isPalindrome(self, s: str) -> bool: new = '' for a in s: if a.isalpha() or a.isdigit(): new += a.lower() return (new == new[::-1]) ### My V import string def is_palindrom(s): s2 = "" for c in s: if c not in string.punctuation and c not in string.whitespace: s2 += c.lower() return s2 == s2[::-1] | **Is a string a palindrome** | Reads forwards and backwards the same. :: def is_palindromic(s): return all(s[i] == s[~i] for i in range(len(s) // 2)) s = "abcba" print(is_palindromic(s)) # True | # Explained | ~ bitwise not operator. | Recall that in Python, when NOT is used in expressions, it does what it should - | flips bits. But when used not in expressions, ~number = -(number + 1). E.g.: >>> ~2 -3 >>> ~4 -5 | It is handy for indexing the opposite side of a string/or array. | s[~i] = s[-(i+1)] | We use s[i] to traverse a string forwards, s[~i] to traverse backwards. | //2 | Handles both odd and even length strings. 107. (LC 8) String to Integer ---------------------------------- `8. String to Integer `_ Medium | *Interconvert strings and integers* | E.g. given "123" return 123. | You should handle negative integers as well. | Do not use Python int(). :: ### Solution (EPI book) def int_to_str(x): is_negative = False if x < 0: x, is_negative = -x, True s = [] while True: s.append(chr(ord("0") + x % 10)) #e.g.x=346, x%10=6 x //= 10 #346//10=34 if x == 0: break return ("-" if is_negative else "") + "".join(reversed(s)) Append to s dif=gits from the end of x. x=346, s='6', s='64', s='643'. Reverse as the last step. :: def string_to_int(s): return functools.reduce( lambda running_sum, c: running_sum * 10 + string.digits.index(c), s[s[0] == "-" :], 0, ) * (-1 if s[0] == "-" else 1) **int(), str()** Do consider: >>> n=12 >>> str(n) '12' >>> s='12' >>> int(n) 12 **functools.reduce(function, iterable[, initializer])** Apply function of two arguments cumulatively to the items of iterable, from left to right, so as to reduce the iterable to a single value. For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5). The left argument, x, is the accumulated value and the right argument, y, is the update value from the iterable. If the optional initializer is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty. :: ### Solution to Leetcode, My V (LC accepted T80, S50 %) class Solution: def myAtoi(self, s: str) -> int: num = 0 reading = False sign_set = False positive = True for c in s: if not reading: if c == " " and not sign_set: continue elif c == "+" and not sign_set: sign_set = True elif c == "-" and not sign_set: positive = False sign_set = True elif not c.isdigit(): break elif c.isdigit(): reading = True num += int(c) else: if not c.isdigit(): break else: num *= 10 num += int(c) if not positive: num = min(num, 2**31) num *= -1 else: num = min(num, 2**31 - 1) return num 108. (LC 171) Excel Sheet Column Number ------------------------------------------- `171. Excel Sheet Column Number `_ Easy **Keys:** :: # Calculation: # char1 * 26 + char2 # * 26 + char3 # *26 + char4 # ... **Solution** :: ### My V2 (when you do know how ord() works) (LC accepted 97,91%) class Solution: def titleToNumber(self, s: str) -> int: res=0 for char in s: res = (res * 26) + ((ord(char) - ord('A')) +1) return res | s='FXDH' (6,24,19,8) | res = (((6*26 + 24) * 26 + 24) * 26 + 19) * 26 + 8 = 122182 | res=0 | 0*26 + 6, res=6 | 6 * 26 + 24 | etc. | **Logic:** | We should convert a string representing a base-26 number to the corresponding integer. | We take that A corresponds to 1, not 0. | So we use the 'string to integer' conversion. **Solution 1** [:ref:`2 `], O(n) :: import functools def convert_col(col): return functools.reduce( lambda result, c: result * 26 + ord(c) - ord("A") + 1, col, 0 ) | Note: | -ord(char1) - ord(first char) = char value a=1, b=2 etc. | -because a=1, not a=0, we add +1 >>> ord('a')-ord('a') 0 -ord('a'), ord('A') are different values! :: ### My V1 simple (LC accepted: 13, 95%) #(when you don't know how ord works) import string class Solution: def titleToNumber(self, s: str) -> int: letters = string.ascii_uppercase d={} cnt=1 for c in letters: d[c]=cnt cnt+=1 res=0 for char in s: res = res * 26 + d[char] return res ### Solution import functools, string def convert_col2(col): return functools.reduce( lambda result, c: result * 26 + string.ascii_uppercase.index(c) + 1, col, 0 ) .. code-block:: cpp //C++ //LC accepted 100, 6% class Solution { public: int titleToNumber(string columnTitle) { int res{0}; for (char a : columnTitle){ res = res * 26 + (int(a) - int('A') + 1); } return res; } }; 108.2 (LC 168) Excel Sheet Column Title ------------------------------------------ `LC 168. `_ Easy :: ### My rewrite (LC accepted) class Solution: def convertToTitle(self, n: int) -> str: ans='' while n >0: res=(n-1) % 26 #**off by 1, because A=1 res = chr(ord('A') + res) ans += res n = (n-1)//26 #**off by 1, because A=1 return ans[::-1] ### Solution 1 (neetcode) class Solution: def convertToTitle(self, columnNumber: int) -> str: # Time: O(logn) - Log base 26 of n res = "" while columnNumber > 0: remainder = (columnNumber - 1) % 26 res += chr(ord('A') + remainder) columnNumber = (columnNumber - 1) // 26 return res[::-1] # reverse output | E.g. n=701 (maps to 'ZY') | (701-1)%26=24 | ord('A')+24=89 | chr(89)='Y' | n= (n-1)//26 (n-1 takes care of the fact that A=1 in task description) | Repeat | -Time complexity | In each loop we divide by 26. So (log base 26 of n). | The base of the logarithm is not relevant in big O, so O(log N). | Space O(1). 109. (LC 186) Reverse Words in a String II -------------------------------------------- (Reverse all the words in a sentence) | Task 1: | For example, "Alice likes Bob" transforms to "Bob likes Alice". | We do not need to keep the original string. | LC task: | Solve the problem in-place, i.e. without allocating extra space. | Input: s = ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] | Output: ["b","l","u","e"," ","i","s"," ","s","k","y"," ","t","h","e"] | Difference: | In task 1 we are given a string encoded as bytearray. | Logic: | Reverse all characters, then reverse again but individual words. | "ram is costly" -> "yltsoc si mar" -> "costly is ram" **Python3** My V2 :: def f(s): s = s[::-1] start = 0 for i in range(len(s)+1): if i == len(s) or s[i].isalpha() == False: s2 = s[start:i] s2 = s2[::-1] s[start:i] = s2 start = i+1 return s s = ["t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"] print(f(s)) #['b', 'l', 'u', 'e', ' ', 'i', 's', ' ', 's', 'k', 'y', ' ', 't', 'h', 'e'] **C++** My V .. code-block:: cpp #include #include #include #include #include using namespace std; void reverse_words(vector& s){ reverse(s.begin(), s.end()); auto start = s.begin(); for(auto it = s.begin(); it <= s.end(); ++it){ if(it == s.end() || *it == " "){ // if(it == s.end() || !isalpha((*it)[0])){ //OR (because isalpha takes char) reverse(start, it); start = ++it; } } } int main(){ vector s = {"t","h","e"," ","s","k","y"," ","i","s"," ","b","l","u","e"}; reverse_words(s); for (auto e : s){ cout << e; } cout << endl; //blue is sky the return 0; } | **Solution** [:ref:`2 `] | O(n) time, O(1) space :: # s is bytearray def reverse_sentence(s): # Reverse the whole string s.reverse() def reverse_word(s, start, end): while start < end: s[start], s[end] = s[end], s[start] start, end = start + 1, end - 1 start = 0 while True: end = s.find(b" ", start) if end < 0: break reverse_word(s, start, end - 1) start = end + 1 # reverse the last word reverse_word(s, start, len(s) - 1) return s s = "ram is costly" s_b = bytearray(s, "UTF-8") print(reverse_sentence(s_b)) # bytearray(b'costly is ram') ### My V1 def f(s): s = s[::-1] p1 = p2 = 0 while p2 < len(s): if s[p2] == " ": if p1 == 0: s[p1:p2] = s[p2 - 1 : None : -1] #**otherwise doesn't work when p1=0 else: s[p1:p2] = s[p2 - 1 : p1 - 1 : -1] p1 = p2 + 1 elif p2 == len(s) - 1: #reversing last word when p2 is not ' ' s[p1 : p2 + 1] = s[p2 : p1 - 1 : -1] p2 += 1 return s s = ["t", "h", "e", " ", "s", "k", "y", " ", "i", "s", " ", "b", "l", "u", "e"] print(f(s)) #['b', 'l', 'u', 'e', ' ', 'i', 's', ' ', 's', 'k', 'y', ' ', 't', 'h', 'e'] 110. (LC 17) Letter Combinations of a Phone Number --------------------------------------------------- `LC 17. Letter Combinations of a Phone Number `_ Medium :: # Solution 1 def letterCombinations0(digits: str) -> List[str]: if not digits: return [] d = ["abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"] ans = [""] for i in digits: s = d[int(i) - 2] ans = [a + b for a in ans for b in s] return ans digits = "23" print(letterCombinations0(digits)) # ['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf'] # After 1st pass ans = ['a', 'b', 'c'] # During 2nd pass ans = ['a'+'g', 'a'+'h', 'a'+'i', 'b'+'g', 'b'+'h' etc. ] # Solution 2 Cartesian product ### My V (LC accepted 90,40%) class Solution: def letterCombinations(self, digits: str) -> List[str]: if len(digits) == 0: return [] d = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'} letters = [d[s] for s in digits] res = itertools.product(*letters) return [''.join(item) for item in res] #* #* Have to join because product() returns res = [['a', 'd'], ['a', 'e'] .. ] --> so we join to have the required format ["ad","ae"..] 111. (LC 38) Count and Say (Look-and-say) ------------------------------------------- `38. Count and Say (Look-and-say) `_ Medium | # More elaboration on the task. | The sequence starts with 1. | "How many + Item" - 1 -> one 1 -> 11 | 11 -> two 1 -> 21 NOTE, we do not append to previous, we build from scratch each time. | 21 -> one 2, one 1 -> 1211 NOTE, we count from MSB | The first 6 items in such a sequence. | [1, 11, 21, 1211, 111221, 312211] | You are given integer n, return nth item in sequence. | Note, return the result as a string. :: ### My V def f(n): if n == 1: return 1 ans = ["1"] s = "" for _ in range(n - 1): cnt = 1 c = ans[-1] for j in range(len(c)): if j == len(c) - 1 or c[j + 1] != c[j]: s += str(cnt) s += c[j] cnt = 1 else: cnt += 1 ans.append(s) s = "" return ans[-1] print(f(3)) print(f(4)) print(f(6)) #21 #1211 #312211 # Use Stdlib import itertools as it def look_and_say_pythonic(n): s = "1" for _ in range(n - 1): s = "".join(str(len(list(group))) + key for key, group in it.groupby(s)) return s print(look_and_say_pythonic(4)) # 1211 | ``itertools.groupby(iterable, key=None)`` | How it works. | import itertools as it | L = "224555" | # Output groups | print([list(g) for k, g in it.groupby(L)]) | [['2', '2'], ['4'], ['5', '5', '5']] | # Omitting k (for k, g ..) it gets weird | print([list(g) for g in it.groupby(L)]) | [['2', ], ['4',.. | # Output (group, group item) | print([(list(g), k) for k, g in it.groupby(L)]) | [(['2', '2'], '2'), (['4'], '4'), (['5', '5', '5'], '5')] | - what is g | The actual group. ['2', '2'] | - what is k | Grouper function. When None it is the value of item (result of identity function). '2' :: # Solution 1 (book attr) def look_and_say1(n): def next_number(s): result, i = [], 0 while i < len(s): count = 1 while i + 1 < len(s) and s[i] == s[i + 1]: i += 1 count += 1 result.append(str(count) + s[i]) i += 1 return "".join(result) s = "1" for _ in range(1, n): s = next_number(s) return s print(look_and_say1(4)) # 1211 # Solution 2 (doocs attribution) def countAndSay2(n: int) -> str: s = "1" for _ in range(n - 1): i = 0 t = [] while i < len(s): j = i while j < len(s) and s[j] == s[i]: j += 1 t.append(str(j - i)) t.append(str(s[i])) i = j s = "".join(t) return s print(countAndSay2(4)) # 1211 | Time O(n2**n) | 2**n - because each successive number can have twice as many digits as the previous. | n - n iteration 112. (LC 13) Roman to Integer ------------------------------------ `LC 13. Roman to Integer `_ Easy | **Solution 1** [:ref:`2 `] - Invalid roman numbers Note, we do not account for invalid romans (like IC and would return 99 for it, while I can only come before V and X), because the task guarantees that we are given only valid roman numbers. - key points We iterate from right to left. If current value is smaller than the value to the right, then we subtract (right-left), otherwise add. :: import functools def roman_to_integer(s): T = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} return functools.reduce( lambda val, i: val + (-T[s[i]] if T[s[i]] < T[s[i + 1]] else T[s[i]]), reversed(range(len(s) - 1)), T[s[-1]], ) print(roman_to_integer("IX")) # 9 print(roman_to_integer("XI")) # 11 | **Explained** | - Reminder | functools.reduce(function, iterable[, initializer]) | So | T[s[-1]], | as last arg to reduce() is the initializer, | E.g. 'IX', initializer is 'X'. | reversed(range(len(s) - 1)), | We iterate from right to left, start at i=-2 | E.g. 'IX', we start with 'I' | if T[s[i]] < T[s[i + 1]] | if 'I' < 'X', yes it is, then | val + (-T[s[i]]), i.e.('X' - 'I') (because we iterate from right to left). :: ### Solution 1 but no magic (Iterate left->right, no jumps) class Solution: def romanToInt(self, s: str) -> int: roman = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} res = 0 for i in range(len(s)): if i + 1 < len(s) and roman[s[i]] < roman[s[i + 1]]: res -= roman[s[i]] else: res += roman[s[i]] return res ### Solution 1, still no magic, my V (LC accepted) (iterate right->left, 2 step jumps if met e.g. IV) def f(s): d = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} ans = 0 i = len(s) - 1 while i >= 0: if i > 0 and (d[s[i]] > d[s[i - 1]]): ans += d[s[i]] - d[s[i - 1]] i -= 2 else: ans += d[s[i]] i -= 1 return ans #### My brute force (LC accepted, with middle stats) def f(s): d = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} ans = 0 i = 0 def add_from_dict(): nonlocal d, i, ans ans += d[s[i]] i += 1 while i < len(s): if s[i] == "I": if i != len(s) - 1 and s[i + 1] == "V": ans += 4 i += 2 elif i != len(s) - 1 and s[i + 1] == "X": ans += 9 i += 2 else: add_from_dict() elif s[i] == "X": if i != len(s) - 1 and s[i + 1] == "L": ans += 40 i += 2 elif i != len(s) - 1 and s[i + 1] == "C": ans += 90 i += 2 else: add_from_dict() elif s[i] == "C": if i != len(s) - 1 and s[i + 1] == "D": ans += 400 i += 2 elif i != len(s) - 1 and s[i + 1] == "M": ans += 900 i += 2 else: add_from_dict() else: add_from_dict() return ans 113. (LC 93) Restore IP Addresses ------------------------------------ `LC 93. Restore IP Addresses `_ Medium (Or compute all valid IP addresses.) The simpler version of a backtrack process is 22. Generate Parentheses (in stacks, here N 177.) => When you see: create all possible combinations: think Backtracking :: ### Solution 2 (neetcode, LC accepted 95, 18%) # The order of s stays the same, but we place the "." in different places class Solution: def restoreIpAddresses(self, s: str) -> List[str]: res = [] if len(s) > 12: #no valid response possible return res def backtrack(i, dots, curIP): if dots == 4 and i == len(s): #we're beyond limits res.append(curIP[:-1]) return if dots > 4: return for j in range(i, min(i + 3, len(s))): if int(s[i:j+1]) <= 255 and (i == j or s[i] != "0"): backtrack(j + 1, dots + 1, curIP + s[i:j+1] + ".") backtrack(0,0, "") return res **Explained** | if len(s) > 12: | s='123454245653356' | Our given initial string is too long, not possible to construct a single valid IP | from such a string. | def backtrack(i, dots, curIP): | =>Give to backtrack func variables that will change during the backtracking. | i - index we are at in the given string | dots - we keep count of total num of dots inserted so far. | curIP - current IP we are constructing. | if dots == 4 and i == len(s): | res.append(curIP[:-1]) | The "good" base case. When we are done with constructing the current IP and add it to result. | When the i index in the current loop would be out of bounds, and we've recorded the 4th dot: '0.1.2.3.' | We also slice off the last dot: | 0.1.2.3. | if dots > 4: | return | We got 4 dots but index didn't yet reach the end of the string. | E.g. 1.2.3.4.5667 | So no valid IP from this combination of dots. | for j in range(i, min(i + 3, len(s))): | The max of our looping is the size of 1 part, which is index+3 (or len of initial string). | if int(s[i:j+1]) <= 255 and (i == j or s[i] != "0"): | If slice results in a num <=255 AND (that num is EITHER a single digit OR the first digit of several digits is not zero). | i == j -> if len of char is just 1. We are slicing, so when i==j, len of slice is 1. | s[i] != "0" -> that is the first char is not zero. | **Solution 2** [:ref:`2 `] | Tips: | -Brute force with nested loops. | -Separate func that checks part validity. :: ### Solution 1 (LC accepted 80, 90%) class Solution: def restoreIpAddresses(self, s: str) -> List[str]: def is_valid_part(s): # '00', '000', '01', etc. are not valid, but '0' is valid. return len(s) == 1 or (s[0] != "0" and int(s) <= 255) result, parts = [], [None] * 4 for i in range(1, min(4, len(s))): parts[0] = s[:i] if is_valid_part(parts[0]): for j in range(1, min(len(s) - i, 4)): parts[1] = s[i : i + j] if is_valid_part(parts[1]): for k in range(1, min(len(s) - i - j, 4)): parts[2], parts[3] = s[i + j : i + j + k], s[i + j + k :] if is_valid_part(parts[2]) and is_valid_part(parts[3]): result.append(".".join(parts)) return result s = "25525511135" s2 = "101023" print(get_valid_ip_address(s)) # ['255.255.11.135', '255.255.111.35'] print(get_valid_ip_address(s2)) # ['1.0.10.23', '1.0.102.3', '10.1.0.23', '10.10.2.3', '101.0.2.3'] | Time complexity (don't quite get it). | The total number of IP addresses is a constant (2**32), implying an O(1) time complexity | for the above algorithm. | Explained: | s[0] != '0' | Meaning first digit of a part is not 0. E.g. 01.. with leading 0 is not valid. | (While 0 alone is valid.) | in range(1, min(4, len(s)) | till 4 (exclusive 4, i.e. really 3 digits) or len(s) whichever we reach first. | parts[0] = s[:i] | .. | parts[1] = s[i : i + j] | E.g. s='255..' i=1 | parts[0] = s[:1] (=[2]) | parts[1] = s[1:1+1] (=[5]) 114. Write a string sinusoidally ------------------------------------- **Task** Given string s like "Hello_World". Its sinusoid (snakestring) is:: # e _ l #H l o W r d # l o ! Written in 1 dimension it is: "e_lHloWrdlo!". Given string, output its sinusoid (snakestring). | **Logic:** | Look for pattern. | I.e. write down concrete examples. | Any sinusoid has 3 rows. | The above concrete example shows this pattern: | Row 1: s[1],s[5],s[9],... ->step=4 starting with 1 | Row 2: s[0],s[2],s[4],.. ->step=2 starting with 0 | Row 3: s[3],s[7],s[11],.. ->step=4 starting with 3 :: ### Solution V1 (full book attr) # Three iterations through s def snake_string(s): result = [] # outputs the first row, step=4, starting with 1 for i in range(1, len(s), 4): result.append(s[i]) for i in range(0, len(s), 2): result.append(s[i]) for i in range(3, len(s), 4): result.append(s[i]) return "".join(result) s = "Hello_World" print(snake_string(s)) # e_lHloWrdlo ### Solution V2 (full book attr) # Using Python slicing. def snake_string_pythonic(s): return s[1::4] + s[::2] + s[3::4] Time: 3 iterations, O(n) each, so overall O(n). n is len of string. :: ### My V def sin_string(s): s1, s2, s3 = "", "", "" for i in range(0, len(s), 2): s2 += s[i] for j in range(1, len(s), 4): s1 += s[j] for k in range(3, len(s), 4): s3 += s[k] return s1 + s2 + s3 s = "Hello_World" print(sin_string(s)) #e_lHloWrdlo 115. Implement run-length encoding ------------------------------------- | **Task** | Run-length encoding (RLE) compression. | Encoding. | Encodes string "aaaabcccaa" into "4a1b3c2a". | Decoding of "3e4f2e" refurns "eeeffffee". | Implement both encoding and decoding functions. :: ### Solution (O(n)) def decoding(s): cnt, res = 0, [] for c in s: if c.isdigit(): cnt = int(c) else: res.append(c * cnt) cnt = 0 return "".join(res) [:ref:`2 `] :: def encoding(s): result, count = [], 1 for i in range(1, len(s) + 1): if i == len(s) or s[i] != s[i - 1]: #if 1st part of or is True, he is not going to eval the 2nd, so no out of bounds error # Found new character so write the count of previous character result.append(str(count) + s[i - 1]) count = 1 else: # s[i] == s[i - 1]. count += 1 return "".join(result) s = "ffyyu" print(encoding(s)) # 2f2y1u ### My V def encode2(s): index = 0 s += str(0) # to make 'aaabb..c0' extra char to compare with the previous char ans = "" for i in range(1, len(s)): if s[i] != s[index]: cnt = i - index ans += str(cnt) ans += s[index] index = i return ans s = "aaaabcccaa" print(encode2(s)) # 4a1b3c2a from string import ascii_lowercase def decode1(s): ans = "" for i in range(len(s) - 1): if s[i] not in ascii_lowercase: ans += s[i + 1] * int(s[i]) return ans s = "2a5f1t" print(decode1(s)) # aaffffft