Strings Questions Part 1
106. (LC 125) Test palindromicity (Valid Palindrome)
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.
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]
- def is_palindromic(s):
return all(s[i] == s[~i] for i in range(len(s) // 2))
s = “abcba” print(is_palindromic(s)) # True
>>> ~2
-3
>>> ~4
-5
107. (LC 8) String to Integer
8. String to Integer Medium
### 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
Easy (Compute the spreadsheet column encoding)
Spreadsheets have column names ‘A’, ‘B’…’Z’, ‘AA’, ‘AB’…’AAA’, ‘AAB’..
Implement a function that converts a spreadsheet column id to the corresponding integer, with ‘A’ corresponding to 1.
You would return 4 for ‘D’, 27 for AA, 702 for ZZ.
How would you test the function? (Test edge cases and a few random ones.)
Solution 1 [2], O(n)
import functools
def convert_col(col):
return functools.reduce(
lambda result, c: result * 26 + ord(c) - ord("A") + 1, col, 0
)
### 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
>>> 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
)
col = "Z"
col2 = "AB"
col3 = "ZZ"
print(convert_col2(col)) # 26
print(convert_col2(col2)) # 28
print(convert_col2(col3)) # 702
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
109. (LC 186) Reverse Words in a String II
(Reverse all the words in a sentence)
# 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 V
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
### 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)
# 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
112. (LC 13) Roman to Integer
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
### 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
### 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']
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).
### 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
### 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)
[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