Brute-Force to Optimized Python Solutions for All LeetCode Blind-75 Problems

Blind-75 is a curated list of 75 LeetCode problems compiled by Yangshun Tay. The list can be accessed at this link.

Initially, I solved each problem using a brute-force approach, which often resulted in a Time Limit Exceeded (TLE) error. Then, I solved each problem with an optimized method that was accepted on LeetCode. For every problem on the list, I have provided both the naive brute force solution and an optimized solution. Additionally, I have included their respective time and space complexities. These solutions are also available on my GitHub repository titled "leetrank".

Two Sum

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

1Input: nums = [2,7,11,15], target = 9
2Output: [0,1]
3Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

Brute Force

1class Solution:
2    def twoSum(self, nums: List[int], target: int) -> List[int]:
3        n = len(nums)
4        for i in range(n):
5            for j in range(i+1, n):
6                if nums[i] + nums[j] == target:
7                    return [i, j]

Time complexity: O(n^2); n is the length of the input array. Space complexity: O(1); as we are only using a few extra variables and not using any extra data structures to store intermediate results.

Optimized

 1class Solution:
 2    def twoSum(self, nums: List[int], target: int) -> List[int]:
 3
 4        num_map = {}   # put numbers as key and index as values
 5        # Enumerate through the list 'nums'. 'enumerate' gives us an
 6        # index (idx) and the item at that index (num)
 7		# For example, if nums = [2, 7, 11, 15],
 8        # after this loop, num_map = {2: 0, 7: 1, 11: 2, 15: 3}
 9        for idx, num in enumerate(nums):
10            # key: value = number, index
11            num_map[num] = idx
12
13        for idx, num in enumerate(nums):
14            # Calculate the complement of the current number
15            # (i.e., the difference between 'target' and this number).
16            com = target - num
17            # If the complement exists in the dictionary 'num_map'
18            # and it's not the current number...
19            if com in num_map and idx != num_map[com]:
20                return [idx, num_map[com]]

Time Complexity: O(n); The code includes a single loop that iterates over the entirety of the input array 'arr' (where 'n' is the length of 'arr'). The operations within this loop - dictionary lookups and updates - each have a constant time complexity of O(1). Therefore, the overall time complexity remains linear, i.e., O(n).

Space Complexity: O(n); The code uses a dictionary to store the indices of all elements in the input array 'arr'. Since the size of this dictionary directly scales with the size of the input array 'arr', the space complexity is O(n).

Best Time to Buy and Sell Stock

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

1Input: prices = [7,1,5,3,6,4]
2Output: 5
3Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
4Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.

Brute Force

 1def maxProfit(prices: List[int]) -> int:
 2    max_profit = 0
 3    n = len(prices)
 4
 5    for i in range(n):
 6        for j in range(i+1, n):
 7            profit = prices[j] - prices[i]
 8            if profit > max_profit:
 9                max_profit = profit
10
11    return max_profit

Time Complexity: O(n^2); The code runs two loops that go through all the items in the 'prices' array. Because the loops are inside each other, every item is checked with every other item, which takes more time if the array gets bigger.

Space Complexity: O(1); The code only uses a few extra things like 'i', 'j', 'profit', and 'max_profit' to keep track of the best way to sell stock. Because we don't use any extra space that grows with the size of the 'prices' array, the space used stays the same no matter how big the array is.

Optimized

 1class Solution:
 2    def maxProfit(self, prices):
 3        max_profit = 0
 4        min_price = prices[0]
 5
 6        for price in prices:
 7            # iterate through the array and for each day,
 8            # calculate the potential profit by subtracting the current min_price from the
 9            # current price (price - min_price)
10			# If the potential profit is greater than the current max_profit,
11            # we update max_profit to the new value.
12            max_profit = max(max_profit, price - min_price)
13            # If the current price is less than min_price, we update min_price to the new value.
14            min_price = min(min_price, price)
15        return max_profit

This approach has a time complexity of O(n) and a space complexity of O(1).

Contains Duplicate

Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.

1Input: nums = [1,2,3,1]
2Output: true

Brute Force

1def containsDuplicate(nums):
2    n = len(nums)
3    # compare each element with every other element that comes after it in the list.
4    for i in range(n):
5        for j in range(i+1, n):
6            if nums[i] == nums[j]:
7                return True
8    return False

Time complexity is O(n^2).

Space complexity is O(1).

Optimized

 1class Solution:
 2    def containsDuplicate(self, nums: List[int]) -> bool:
 3        # Create a set to store the numbers we have seen.
 4        seen = set()
 5
 6        # Iterate over the numbers in the input list.
 7        for num in nums:
 8            # If we have seen the current number before, return True.
 9            if num in seen:
10                return True
11
12            # Add the current number to the set of seen numbers.
13            seen.add(num)
14
15        # If we reach here, it means we haven't seen any duplicates, so return False.
16        return False

The time complexity of this code is linear in the length of the nums list. This is because we need to iterate over the list once and perform a constant-time set membership check on each element. The space complexity is also linear in the length of the nums list, because we need to store each element of the list in the seen set. So, O(n).

Without using extra space:

1def containsDuplicate(nums):
2    nums.sort()  # Sort the array to bring duplicates together
3    for i in range(1, len(nums)):
4        if nums[i] == nums[i-1]:
5            return True
6    return False

It has O(1) space complexity, but the time complexity becomes O(n log n) due to the sorting operation, where 'n' is the length of the input array.

Product of Array Except Self

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]. You must write an algorithm that runs in O(n) time and without using the division operation.

1Input: nums = [1,2,3,4]
2Output: [24,12,8,6]

Brute Force

 1def product_except_self(nums):
 2    n = len(nums)
 3    answer = [0] * n
 4    for i in range(n):
 5        product = 1
 6        for j in range(n):
 7            # multiply all elements except the current element
 8            if i != j:
 9                product *= nums[j]
10        answer[i] = product
11    return answer

Optimized

 1class Solution:
 2    def productExceptSelf(self, nums: List[int]) -> List[int]:
 3        n = len(nums)
 4        left, right, ans = [1] * n, [1] * n, [1] * n
 5
 6        # fill left array
 7        for i in range(1, n):
 8            left[i] = nums[i-1] * left[i-1]
 9
10        # fill the right array
11        for i in reversed(range(n-1)):
12            right[i] = nums[i+1] * right[i+1]
13
14        # fill the ans array
15        for i in range(n):
16            ans[i] = left[i] * right[i]
17
18        return ans

Time Complexity: O(n); The provided code runs three loops sequentially that each iterate over the entire input array. Since these loops don't nest and run independently in linear time with respect to the size of the input array, the overall time complexity remains linear, i.e., O(n).

Space Complexity: O(n); The code creates three separate arrays - the left array, the right array, and the result array. Each of these arrays is of the same size as the input array. Therefore, the space used is directly proportional to the size of the input array, leading to a space complexity of O(n).

Maximum Subarray

Given an integer array nums, find the subarray which has the largest sum and return its sum.

1Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
2Output: 6
3Explanation: [4,-1,2,1] has the largest sum = 6.

Brute Force

1def max_subarray_sum(nums):
2    n = len(nums)
3    max_sum = float('-inf')
4    for i in range(n):
5        for j in range(i, n):
6            subarray_sum = sum(nums[i:j+1])
7            max_sum = max(max_sum, subarray_sum)
8    return max_sum

Time Complexity: O(n^3); The code has two loops that work inside each other, which would typically result in O(n^2) complexity. However, inside the inner loop, the sum(nums[i:j+1]) function essentially creates a third loop because it sums over the sublist of 'nums'. So for each pair of 'i' and 'j', it's summing over the sublist, and this causes the time complexity to be cubic, i.e., O(n^3).

Space Complexity: O(1); This code only uses a few extra things like 'i', 'j', 'subarray_sum', and 'max_sum' to keep track of the highest total it has found so far. It does not make any new lists or dictionaries that grow with the size of the 'nums' list, so the space used stays the same no matter how big the list is.

Optimized

 1class Solution:
 2    def maxSubArray(self, nums: List[int]) -> int:
 3        # Initialize the maximum sum with the first number in the input array.
 4        max_sum = current_sum = nums[0]
 5
 6        # Loop through the numbers in the input array starting from the second number.
 7        for num in nums[1:]:
 8            # Update the current sum by taking the maximum of the current number
 9            # and the current number plus the current sum.
10            current_sum = max(num, num + current_sum)
11            # Update the maximum sum with the maximum of the current sum and the maximum sum.
12            max_sum = max(max_sum, current_sum)
13
14        # Return the maximum sum.
15        return max_sum

Time Complexity: O(n); The code looks at every number in the 'nums' list one by one, only once.

Space Complexity: O(1); The code just uses a few extra things like 'num', 'current_sum', and 'max_sum' to remember the highest total it has found. It doesn't make any new lists or dictionaries that get bigger with the size of the 'nums' list, so the space used stays the same no matter how big the list is.

Maximum Product Subarray

Given an integer array nums, find a subarray that has the largest product, and return the product.

1Input: nums = [2,3,-2,4]
2Output: 6
3Explanation: [2,3] has the largest product 6.

Brute Force

 1def max_subarray_product(nums):
 2    n = len(nums)
 3    max_product = float('-inf')
 4    for i in range(n):
 5        for j in range(i, n):
 6            subarray_product = 1
 7            for k in range(i, j+1):
 8                subarray_product *= nums[k]
 9            max_product = max(max_product, subarray_product)
10    return max_product

This solution has a time complexity of O(n^3) since we are considering every possible subarray and calculating its product using a nested loop.

Optimized

 1class Solution:
 2    def maxProduct(self, nums: List[int]) -> int:
 3        max_product = min_product = nums[0]
 4
 5        # initialize the maximum product found so far to the first element
 6        max_so_far = nums[0]
 7
 8        # iterate through the array, starting from the second element
 9        for i in range(1, len(nums)):
10            # Find the maximum and minimum product of the current element and the current product
11            max_product, min_product = (
12                max(nums[i], max_product * nums[i], min_product * nums[i]),
13                min(nums[i], max_product * nums[i], min_product * nums[i]),
14            )
15
16            # update the maximum product found so far
17            max_so_far = max(max_so_far, max_product)
18
19        return max_so_far

Time complexity: O(n), n is the number of elements in the input array, because the for loop iterates over all elements in the input array. Space complexity: O(1), because it only uses a constant amount of memory to store a few variables, regardless of the size of the input.

Find Minimum in Rotated Sorted Array

Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:

  • [4,5,6,7,0,1,2] if it was rotated 4 times.
  • [0,1,2,4,5,6,7] if it was rotated 7 times.

Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in the array [a[n-1], a[0], a[1], a[2], ..., a[n-2]].

Given the sorted rotated array nums of unique elements, return the minimum element of this array.

You must write an algorithm that runs in O(log n) time.

1Input: nums = [3,4,5,1,2]
2Output: 1
3Explanation: The original array was [1,2,3,4,5] rotated 3 times.

Brute Force

1def findMin(nums):
2    # Since the array is already sorted in ascending order before rotation,
3    # the first element we encounter which is smaller than the previous element will be the minimum element.
4    for i in range(1, len(nums)):
5        if nums[i] < nums[i-1]:
6            return nums[i]
7    return nums[0]

This is O(n) time complexity solution.

Optimized

 1class Solution:
 2    def findMin(self, nums: List[int]) -> int:
 3        # Set the left and right indices for the binary search
 4        left, right = 0, len(nums) - 1
 5
 6        # Perform binary search to find the minimum value
 7        while left < right:
 8            # Calculate the midpoint of the current search range
 9            mid = (left + right) // 2
10
11            # If the midpoint value is greater than the rightmost value,
12            # the minimum value must be in the right half of the array
13            if nums[mid] > nums[right]:
14                left = mid + 1
15            else:
16                # If the midpoint value is not greater than the rightmost value,
17                # the minimum value must be in the left half of the array
18                right = mid
19
20        # Return the minimum value, which will be at the left index
21        return nums[left]

The time complexity of the above code is O(log n), where n is the length of the array. This is because the binary search reduces the search range by half in each iteration, so the number of iterations required is determined by the number of times n can be divided by 2 before reaching 1. The space complexity is O(1), since the code only uses a constant number of variables regardless of the size of the input array.

Search in Rotated Sorted Array

There is an integer array nums sorted in ascending order (with distinct values). Given the array nums after the possible rotation and an integer target, return the index of target if it is in nums, or -1 if it is not in nums.

You must write an algorithm with O(log n) runtime complexity.

1Input: nums = [4,5,6,7,0,1,2], target = 0
2Output: 4

Brute Force

1class Solution:
2    def search(self, nums: List[int], target: int) -> int:
3        for i in range(len(nums)):
4            if nums[i] == target:
5                return i
6        return -1

Optimized

 1class Solution:
 2    def search(self, nums: List[int], target: int) -> int:
 3
 4        # Initialize left and right pointers
 5        left = 0
 6        right = len(nums) - 1
 7
 8        # Keep looping until left pointer is less than or equal to right pointer
 9        while left <= right:
10            # Calculate middle index
11            mid = (left + right) // 2
12
13            # If target is at middle index, return middle index
14            if target == nums[mid]:
15                return mid
16
17            # If left side is sorted
18            if nums[left] <= nums[mid]:
19                # If target is in left side, update right pointer to middle index - 1
20                if nums[left] <= target <= nums[mid]:
21                    right = mid - 1
22                # Else, update left pointer to middle index + 1
23                else:
24                    left = mid + 1
25            # Else, right side is sorted
26            else:
27                # If target is in right side, update left pointer to middle index + 1
28                if nums[mid] <= target <= nums[right]:
29                    left = mid + 1
30                # Else, update right pointer to middle index - 1
31                else:
32                    right = mid - 1
33
34        # Target not found, return -1
35        return -1

The time complexity of the above code is O(log n) because in each iteration, the search space is halved. This is because the code uses binary search to find the target element in the given list.

The space complexity of the above code is O(1), because the code does not use any additional memory space proportional to the size of the input. The only variables used are the left and right pointers, which remain constant regardless of the size of the input list.

3 Sum

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.

Notice that the solution set must not contain duplicate triplets.

1Input: nums = [-1,0,1,2,-1,-4]
2Output: [[-1,-1,2],[-1,0,1]]
3Explanation:
4nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
5nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
6nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
7The distinct triplets are [-1,0,1] and [-1,-1,2].
8Notice that the order of the output and the order of the triplets does not matter.

Brute Force

 1class Solution:
 2    def threeSum(self, nums: List[int]) -> List[List[int]]:
 3        n = len(nums)
 4        result = []
 5        seen = set()
 6
 7        for i in range(n):
 8            for j in range(i+1, n):
 9                for k in range(j+1, n):
10
11                    if nums[i] + nums[j] + nums[k] == 0:
12                        triplet = tuple(sorted([nums[i], nums[j], nums[k]]))
13
14                        if triplet not in seen:
15                            seen.add(triplet)
16                            result.append([nums[i], nums[j], nums[k]])
17        return result

The time complexity of the above solution is O(n^3), where n is the length of the input array nums. This is because we are using three nested loops to generate all possible triplets of elements from the input array, and checking the sum of each triplet.

The space complexity of the solution is O(1), because we are not using any additional data structures that depend on the input size. The only additional memory we use is for the result list to store the valid triplets, but the size of the list is bounded by the number of valid triplets, which is at most n^3 for an input array of length n. So the space used by the result list is also O(n^3).

Optimized

 1class Solution:
 2    def threeSum(self, nums: List[int]) -> List[List[int]]:
 3        # Sort the input list
 4        nums.sort()
 5
 6        n = len(nums)
 7
 8        # Initialize empty list to store triplets
 9        triplets = []
10
11        # If the length of the input list is less than 3, return empty list
12        if n < 3:
13            return []
14
15        # Loop through all elements in the list except the last two
16        for i in range(n - 2):
17            # Initialize left pointer to the element after current element
18            left = i + 1
19            # Initialize right pointer to the last element in the list
20            right = n - 1
21
22            # Keep looping until left pointer is less than right pointer
23            while left < right:
24                # Calculate the sum of the current element, left element, and right element
25                sums3 = nums[i] + nums[left] + nums[right]
26                # If the sum is 0, append the current triplet to the triplets list and update the left and right pointers
27                if sums3 == 0:
28                    triplets.append((nums[i], nums[left], nums[right]))
29                    left += 1
30                    right -= 1
31                # If the sum is less than 0, update the left pointer
32                elif sums3 < 0:
33                    left += 1
34                # Else, update the right pointer
35                else:
36                    right -= 1
37
38        # Convert the triplets list to a set to remove duplicates, and then convert it back to a list
39        triplets = list(set(triplets))
40        # Return the triplets list
41        return triplets

The time complexity of the above code is O(n^2) and the space complexity is O(n).

Container With Most Water

You are given an integer array height of length n. There are n vertical lines drawn such that the two endpoints of the ith line are (i, 0) and (i, height[i]).

Find two lines that together with the x-axis form a container, such that the container contains the most water. Return the maximum amount of water a container can store.

Notice that you may not slant the container.

1Input: height = [1,8,6,2,5,4,8,3,7]
2Output: 49
3Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

Brute Force

One naive solution to this problem is to use a brute force approach where we consider all possible pairs of lines and calculate the area of water contained by each pair of lines. Then, we return the maximum area.

1def maxArea(height):
2    n = len(height)
3    max_area = 0
4    for i in range(n):
5        for j in range(i+1, n):
6            area = min(height[i], height[j]) * (j - i)
7            max_area = max(max_area, area)
8    return max_area

It has a time complexity of O(n^2) as we are considering all possible pairs of lines.

Optimized

 1class Solution:
 2    def maxArea(self, height: List[int]) -> int:
 3        # Initialize left and right pointers at the start and end of the list
 4        left = 0
 5        right = len(height) - 1
 6
 7        # Initialize the maximum area to 0
 8        max_area  = 0
 9
10        # Loop through the list while the left pointer is less than the right pointer
11        while left < right:
12            # Calculate the width of the rectangle by subtracting the left pointer from the right pointer
13            w = right - left
14
15            # Calculate the height of the rectangle by taking the minimum of the values at the left and right pointers
16            h = min(height[left], height[right])
17
18            # Calculate the area of the rectangle by multiplying the height and width
19            area = h * w
20
21            # Update the maximum area by comparing the current area to the previous maximum
22            max_area = max(max_area, area)
23
24            # If the value at the left pointer is less than the value at the right pointer, move the left pointer right
25            # Otherwise, move the right pointer left
26            if height[left] < height[right]:
27                left += 1
28            else:
29                right -= 1
30
31        # Return the maximum area
32        return max_area

The time complexity of this code is O(n), where n is the length of the height list. This is because the left and right pointers are iterated over the list once, and in each iteration the pointers move either left or right by one position.

The space complexity of this code is O(1), because the space used by the code is constant and does not depend on the size of the input. The only variables that are created are a few integers, which have a constant size regardless of the input size.

Sum of Two Integers

Given two integers a and b, return the sum of the two integers without using the operators + and -

1Input: a = 1, b = 2
2Output: 3
1class Solution:
2    def getSum(self, a: int, b: int) -> int:
3        while b != 0:
4            carry = a & b
5            a = a ^ b
6            b = carry << 1
7        return a

The time complexity of the code is O(n), where n is the number of bits needed to represent the numbers a and b in binary. This is because the time complexity of the code is directly proportional to the number of bits needed to represent a and b.

The space complexity of the code is O(n), where n is the maximum depth of the recursion stack. This is because at each recursive call, a new frame is added to the stack, and the maximum depth of the stack is equal to the number of bits needed to represent the numbers a and b in binary.

Number of 1 Bits

Write a function that takes an unsigned integer and returns the number of '1' bits it has (also known as the Hamming weight).

1Input: n = 00000000000000000000000000001011
2Output: 3
3Explanation: The input binary string 00000000000000000000000000001011 has a total of three '1' bits.
 1class Solution:
 2    def hammingWeight(self, n: int) -> int:
 3        # Initialize a count variable to keep track of the number of bits in n that are set to 1
 4        count = 0
 5
 6        # Loop until n is 0
 7        while n != 0:
 8            # Check if the least significant bit of n is set to 1
 9            # If it is, increment count by 1
10            count += n & 1
11
12            # Shift the bits in n to the right by 1
13            # This effectively divides n by 2 and discards any remainder
14            n >>= 1
15
16        # Return the final value of count as the result
17        return count
18
19        # another approach using built-in bin
20        # return bin(n).count('1')

The time complexity of the code is linear in the number of bits in the binary representation of n. This is because the loop continues until n is equal to 0, and the number of bits in the binary representation of n determines how many times the loop will run. Therefore, the time complexity of the code is O(b), where b is the number of bits in the binary representation of n.

The space complexity of the code is constant. This is because the only variable that is used to store any data is the count variable, which does not depend on the input n and always takes up the same amount of space. Therefore, the space complexity of the code is O(1).

Counting Bits

Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] *is the number of* 1*'s in the binary representation of* i.

1Input: n = 5
2Output: [0,1,1,2,1,2]
3Explanation:
40 --> 0
51 --> 1
62 --> 10
73 --> 11
84 --> 100
95 --> 101
 1class Solution:
 2    def countBits(self, n: int) -> List[int]:
 3        # create an array with n+1 elements
 4        # to store the number of bits of each number from 0 to n
 5        ans = [0] * (n + 1)
 6        # loop through each number from 1 to n
 7        for i in range(1, n + 1):
 8            # find the number of bits of i by dividing i by 2
 9            # and storing the result in the index i in the array
10            # this is done by right-shifting the binary representation of i by 1 bit
11            # which is equivalent to dividing i by 2
12            ans[i] = ans[i >> 1]
13            # add the remainder of the division by 2 to the number of bits of i
14            # this is done by performing a bitwise AND operation between i and 1
15            # the result is either 0 or 1 depending on the least significant bit of i
16            # if the least significant bit of i is 0, the result is 0
17            # if the least significant bit of i is 1, the result is 1
18            ans[i] += (i & 1)
19        return ans
20
21    	# return [bin(i).count('1') for i in range(n + 1)]

The time complexity of the solution is also O(n) because the loop runs n times, and the operations inside the loop have a constant time complexity. Therefore, the overall time complexity of the solution is O(n).

The space complexity of the solution is O(n) because the size of the array ans is directly proportional to the input n.

Missing Number

Given an array nums containing n distinct numbers in the range [0, n], return the only number in the range that is missing from the array.

1Input: nums = [3,0,1]
2Output: 2
3Explanation: n = 3 since there are 3 numbers, so all numbers are in the range [0,3]. 2 is the missing number in the range since it does not appear in nums.

Brute Force

1class Solution:
2    def missingNumber(self, nums: List[int]) -> int:
3        n = len(nums)
4        for i in range(n+1):
5            if i not in nums:
6                return i

The time complexity of the above solution is O(n^2), where n is the length of the input array nums. This is because we are looping through a range of numbers from 0 to n+1, and for each number, we are checking whether it is present in the input array nums using the not in operator, which takes O(n) time in the worst case.

The space complexity of the above solution is O(1), because we are not using any additional data structures to store information about the input array.

Optimized

The sum of the first n natural numbers can be calculated using the formula n * (n+1) / 2. Therefore, if we subtract the sum of the input array nums from the sum of the first n natural numbers, we can get the missing number.

1def find_missing_number(nums):
2    n = len(nums)
3    expected_sum = n * (n+1) // 2
4    actual_sum = sum(nums)
5    return expected_sum - actual_sum

The time complexity of this optimized solution is O(n), which is much faster than the brute force solution. The space complexity is O(1), which is the same as the brute force solution.

Reverse Bits

Reverse bits of a given 32 bits unsigned integer.

1Input: n = 00000010100101000001111010011100
2Output:    964176192 (00111001011110000010100101000000)
3Explanation: The input binary string 00000010100101000001111010011100 represents the unsigned integer 43261596, so return 964176192 which its binary representation is 00111001011110000010100101000000.
 1class Solution:
 2    def reverseBits(self, n: int) -> int:
 3        # Initialize the result to 0
 4        ans = 0
 5
 6        # Loop through the 32 bits in the integer
 7        for i in range(32):
 8            # Add the last bit of n to the result and shift it left by 1
 9            # This puts the next bit in the last bit position
10            ans = (ans << 1) + (n & 1)
11
12            # Shift n right by 1 to get the next bit
13            n >>= 1
14
15        # Return the reversed bits
16        return ans

The time complexity of the above code is O(1), because the number of operations performed is constant and does not depend on the input size. This is because the code always performs the same number of iterations (32) and the number of operations per iteration is also constant.

The space complexity of the above code is also O(1), because the amount of memory used does not depend on the input size. This is because the only variable that is allocated memory is ans, which has a fixed size of 32 bits regardless of the input size.

Climbing Stairs

You are climbing a staircase. It takes n steps to reach the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

1Input: n = 3
2Output: 3
3Explanation: There are three ways to climb to the top.
41. 1 step + 1 step + 1 step
52. 1 step + 2 steps
63. 2 steps + 1 step
 1from functools import lru_cache
 2
 3class Solution:
 4    # Decorate the climbStairs method with lru_cache
 5    # to enable memoization
 6    @lru_cache
 7    def climbStairs(self, n: int) -> int:
 8        # If n is less than 3, return n
 9        # This covers the cases where n is 0, 1, or 2
10        if n < 3:
11            return n
12
13        # Otherwise, return the sum of the number of ways to climb
14        # n - 1 stairs and n - 2 stairs
15        else:
16            return self.climbStairs(n-1) + self.climbStairs(n-2)

The time complexity of the above code is O(n), because the number of operations performed depends on the input size. This is because the climbStairs method is called recursively, and the number of recursive calls grows linearly with the input size.

The space complexity of the above code is also O(n), because the amount of memory used depends on the input size. This is because the lru_cache decorator stores the results of each climbStairs call in a cache, and the size of the cache grows linearly with the input size.

Coin Change

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

You may assume that you have an infinite number of each kind of coin.

1Input: coins = [1,2,5], amount = 11
2Output: 3
3Explanation: 11 = 5 + 5 + 1

Solution 1

 1# functools.lru_cache leverages dynamic programming concepts by using memoization
 2from functools import lru_cache
 3
 4class Solution:
 5    def coinChange(self, coins: List[int], amount: int) -> int:
 6        @lru_cache(maxsize=None)
 7        def find_min_coins(amount):
 8            # Base case: if amount is 0, no coins are needed, so return 0
 9            if amount == 0:
10                return 0
11
12            # Base case: if amount is negative, return infinity
13        	# This signifies that the current coin cannot be used to sum up to the given amount
14            if amount < 0:
15                return float("inf")
16
17            # This acts as a placeholder for the minimum number of coins needed to make up the amount
18            min_coin = float("inf")
19
20            # Iterate over each coin in the list
21        	# The goal is to try to subtract each coin value from the total amount
22        	# and recursively solve the problem for the remaining amount
23            for coin in coins:
24                # For each coin, subtract its value from the total amount, and recursively call
25            	# find_min_coins for the remaining amount, then add 1 to represent the coin just used
26                result = find_min_coins(amount - coin) + 1
27                # Update min_coins to be the smaller of the current min_coins and the new result
28            	# This ensures that min_coins always holds the smallest number of coins found so far
29                min_coin = min(min_coin, result)
30            return min_coin
31
32        result = find_min_coins(amount)
33        if result == float("inf"):
34            return -1
35        else:
36            return result

Time and space complexity: O(amount * n), where n is the number of different coin denominations and amount is the target amount. For each coin, we are performing a subproblem for 'amount' times. Hence, the total time complexity will be O(amount * n).

Solution 2

 1# memoization without lru_cache
 2class Solution:
 3    def coinChange(self, coins: List[int], amount: int) -> int:
 4        # memo dictionary to store the minimum number of coins for each amount.
 5        memo = {0: 0}
 6
 7        def find_min_coins(amount):
 8            # same memoization implementation without lru_cache
 9            if amount in memo:
10                return memo[amount]
11
12            # if amount is negative, there's no solution
13            if amount < 0:
14                return float("inf")
15
16            min_coin = float("inf")
17
18            for coin in coins:
19                result = find_min_coins(amount - coin) + 1
20                min_coin = min(min_coin, result)
21
22            memo[amount] = min_coin
23            return min_coin
24
25        result = find_min_coins(amount)
26        if result == float("inf"):
27            return -1
28        else:
29            return result

Time and space complexity: O(amount * n), where n is the number of different coin denominations and amount is the target amount. For each coin, we are performing a subproblem for 'amount' times. Hence, the total time complexity will be O(amount * n).

Longest Increasing Subsequence

Given an integer array nums, return *the length of the longest strictly increasing*.

1Input: nums = [10,9,2,5,3,7,101,18]
2Output: 4
3Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Brute Force

 1# generating all possible subsequences and checking which ones are increasing
 2from functools import combinations
 3
 4class Solution:
 5    def lengthOfLIS(self, nums: List[int]) -> int:
 6        # helper function to check if the subsequence is strictly increasing
 7        def is_increasing(seq):
 8            for i in range(len(seq) - 1):
 9                if seq[i] >= seq[i + 1]:
10                    return False
11            return True
12
13        longest = 0
14		# Go through all possible subsequences
15        for i in range(1, len(nums) + 1):
16            for subseq in combinations(nums, i):
17                # if the subsequence is increasing and its length is greater than the current longest
18                if is_increasing(subseq) and len(subseq) > longest:
19                    longest = len(subseq)
20
21        return longest

Time complexity: O(n * 2^n) itertools.combinations(): 2^n is_increasing(): is called, which in the worst-case scenario, iterates over every element of the subsequence. O(n)

Optimized: DP(Tabulation)

 1def lengthOfLIS(nums):
 2    # Initialize a tabulation table of length n with all 1s
 3    n = len(nums)
 4    # Initialize a list 'table' of size n, and fill it with 1s. This list will store
 5    # the length of the longest increasing subsequence (LIS) that ends at each element.
 6    # We start with 1 because a single element is itself a valid increasing subsequence.
 7    table = [1] * n
 8
 9    # Iterate over all elements in nums and compute the length of the
10    # longest increasing subsequence ending at each element
11    for i in range(1, n):
12        for j in range(i):
13            if nums[j] < nums[i]:
14                table[i] = max(table[i], table[j] + 1)
15
16    # Return the maximum length of increasing subsequence
17    return max(table)

Time complexity: O(n^2) and space complexity: O(n)

Optimized: DP(Memoization)

 1from functools import lru_cache
 2
 3class Solution:
 4    def lengthOfLIS(self, nums: List[int]) -> int:
 5        @lru_cache(maxsize=None)
 6        def LIS_ending_at(i):
 7            # base case: The LIS ending at index 0 has length of 1
 8            if i == 0:
 9                return 1
10
11            # The LIS ending at i has atleast nums[i] itself
12            # so the length is atleast 1
13            longest = 1
14
15            for j in range(i):
16                if nums[j] < nums[i]:
17                    res = LIS_ending_at(j) + 1
18                    longest = max(longest, res)
19            return longest
20
21        # corner case
22        if not nums:
23            return 0
24
25        # find the length of the LIS ending at each position and return the max
26        return max(LIS_ending_at(i) for i in range(len(nums)))

Longest Common Subsequence

Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.

1Input: text1 = "abcde", text2 = "ace"
2Output: 3
3Explanation: The longest common subsequence is "ace" and its length is 3.
 1class Solution:
 2    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
 3        # Define the helper function with lru_cache
 4        @lru_cache(maxsize=None)
 5        def lcs_helper(i, j):
 6            # If we have reached the end of one of the input strings, return 0
 7            if i == len(text1) or j == len(text2):
 8                return 0
 9            # If the characters at the current positions in the two input strings are the same,
10            # add 1 to the result and move on to the next characters in both strings
11            if text1[i] == text2[j]:
12                return 1 + lcs_helper(i + 1, j + 1)
13            # If the characters do not match, recursively call the function in two ways:
14   			# once skipping one character in the first string,
15            # and once skipping one character in the second string
16            return max(lcs_helper(i + 1, j), lcs_helper(i, j + 1))
17
18        # Return the result of calling the helper function with the starting indices (0, 0)
19        return lcs_helper(0, 0)

The time and space complexity of the code depend on the size of the input strings text1 and text2. Because the function uses memoization with lru_cache, the time complexity is effectively O(n * m), where n and m are the lengths of text1 and text2, respectively. The space complexity is also O(n * m) because the lru_cache stores the results of the function calls in a dictionary, which takes up space proportional to the number of function calls made.

Word Break Problem

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.

Note that the same word in the dictionary may be reused multiple times in the segmentation.

1Input: s = "leetcode", wordDict = ["leet","code"]
2Output: true
3Explanation: Return true because "leetcode" can be segmented as "leet code".
 1from functools import lru_cache
 2
 3class Solution:
 4    def wordBreak(self, s, wordDict):
 5        # convert the word dictionary from a list to a set for faster lookup (O(1) complexity)
 6        wordDict = set(wordDict)
 7
 8        # helper function, 'wb', to perform the recursive word break check
 9        # with lru_cache
10        @lru_cache(maxsize=None)
11        def checkSegmentation(start):
12            # Base case: if the 'start' index has reached the end of the string,
13            # it means the string 's' can be segmented into words from the dictionary,
14            # thus return True
15            if start == len(s):
16                return True
17
18            # iterate over all possible 'end' indices of substrings
19            # of 's' starting from 'start'
20            for end in range(start+1, len(s) + 1):
21                # If the substring from 'start' to 'end' is in the dictionary,
22                # and if the remainder of the string after 'end' can also be
23                # segmented into words in the dictionary
24                # (this is checked recursively), return True
25                if s[start:end] in wordDict and checkSegmentation(end):
26                    return True
27
28            # If no valid segmentation is found after checking
29            # all possible substrings from 'start', return False
30            return False
31
32        return checkSegmentation(0)

Time complexity: O(n^2), where n is the length of the string. We're still looking at every possible substring, which is an O(n^2) operation, but due to the lru_cache decorator, previously calculated results are stored and reused, preventing redundant computation.

Combination Sum IV

Given an array of distinct integers nums and a target integer target, return the number of possible combinations that add up to target.

 1Input: nums = [1,2,3], target = 4
 2Output: 7
 3Explanation:
 4The possible combination ways are:
 5(1, 1, 1, 1)
 6(1, 1, 2)
 7(1, 2, 1)
 8(1, 3)
 9(2, 1, 1)
10(2, 2)
11(3, 1)
12Note that different sequences are counted as different combinations.
 1from functools import lru_cache
 2
 3class Solution:
 4    def combinationSum4(self, nums: List[int], target: int) -> int:
 5        @lru_cache(maxsize=None)
 6        def dp(tar):
 7            if tar == 0:
 8                return 1
 9            elif tar < 0:
10                return 0
11            else:
12                result = 0
13                for num in nums:
14                    result += dp(tar - num)
15                return result
16        return dp(target)

Time complexity: O(target * n), where 'target' is the target sum we're aiming for and 'n' is the size of the input list 'nums'. This is because for each possible sum up to the target.

Space complexity: O(target) because of the recursion stack in the depth-first search. In the worst case, the recursion goes as deep as the value of 'target', hence 'target' stack frames are used. The LRU cache also uses O(target) space to store the result for each possible sum up to the target. So, the total space complexity is O(target).

House Robber

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return *the maximum amount of money you can rob tonight without alerting the police*.

1Input: nums = [1,2,3,1]
2Output: 4
3Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
4Total amount you can rob = 1 + 3 = 4.
 1from functools import lru_cache
 2
 3class Solution:
 4    def rob(self, nums):
 5        @lru_cache(maxsize=None)  # Cache the results of the recursive calls
 6        def robFrom(i):
 7            if i >= len(nums):
 8                return 0
 9            # there are 2 possible options
10            # option 1:
11            # total amount of money the robber can get if he choose to rob the current house (i).
12            # In this scenario, he gain the money in the current house (nums[i]),
13            # but they he to skip the next house due to the security systems
14            # so we add the maximum possible loot from the house i+2
15            # option 2:
16            # total amount of money the robber can get if they choose to skip the current house (i).
17            # In this scenario, he doesn't get the money in the current house
18            return max(robFrom(i+2) + nums[i], robFrom(i+1))
19
20        return robFrom(0)

Time complexity: each subproblem is solved only once, and the result is stored for later use. Therefore, the time complexity is reduced to linear, specifically O(n), where n is the size of the input list 'nums'.

Space complexity: The space complexity is also O(n), where n is the size of the input list 'nums'. This is due to the additional space used by the cache to store the results of the subproblems.

House Robber II

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return *the maximum amount of money you can rob tonight without alerting the police*.

1Input: nums = [2,3,2]
2Output: 3
3Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
 1class Solution:
 2    # This method finds the maximum amount of money that can be robbed from the houses in the given list of numbers
 3    # It returns the maximum amount of money that can be robbed
 4    def rob(self, nums: List[int]) -> int:
 5        # If there is only one house, the maximum amount of money that can be robbed is the value of that house
 6        if len(nums) == 1:
 7            return nums[0]
 8
 9        # Otherwise, find the maximum amount of money that can be robbed if the first or last house is not robbed
10        # Return the maximum of these two values
11        return max(self.rob1(nums[:-1]), self.rob1(nums[1:]))
12
13
14    # This method finds the maximum amount of money that can be robbed from the houses in the given list of numbers
15    # It returns the maximum amount of money that can be robbed
16    def rob1(self, nums: List[int]) -> int:
17        # Initialize variables to store the maximum amount of money that can be robbed at the current and previous houses
18        rob1, rob2 = 0, 0
19
20        # For each house, find the maximum amount of money that can be robbed at the current house
21        for num in nums:
22            # The maximum amount of money that can be robbed at the current house is the maximum of the sum of the money
23            # from the previous house and the value of the current house, and the maximum amount of money that can be
24            # robbed at the previous house
25            rob1, rob2 = rob2, max(rob1 + num, rob2)
26
27        # Return the maximum amount of money that can be robbed at the last house
28        return rob2

The time complexity of the above code is O(n), where n is the number of houses in the given list of numbers. This is because the rob1() method iterates through the entire list of houses, and the rob() method calls the rob1() method twice, each time with a list of houses that has a length of n-1.

The space complexity of the above code is O(1), because the rob1() method only uses a constant amount of additional memory to store the variables rob1 and rob2.

Decode Ways

Given a string s containing only digits, return the number of ways to decode it.

A message containing letters from A-Z can be encoded into numbers using the following mapping:

1'A' -> "1"
2'B' -> "2"
3...
4'Z' -> "26"

To decode an encoded message, all the digits must be grouped then mapped back into letters using the reverse of the mapping above (there may be multiple ways). For example, "11106" can be mapped into:

  • "AAJF" with the grouping (1 1 10 6)
  • "KJF" with the grouping (11 10 6)

Note that the grouping (1 11 06) is invalid because "06" cannot be mapped into 'F' since "6" is different from "06".

1Input: s = "12"
2Output: 2
3Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).
 1class Solution:
 2    def numDecodings(self, s: str) -> int:
 3        # If the string is empty or None, there are no possible decodings
 4        if len(s) == 0 or s is None:
 5            return 0
 6
 7        # Define a recursive function that uses memoization to improve performance
 8        @lru_cache(maxsize=None)
 9        def dfs(st):
10            # If the string is empty, there is only one possible decoding
11            if len(st) == 0:
12                return 1
13            # If the string starts with a zero, there are no possible decodings
14            if st[0] == "0":
15                return 0
16            # If the string has only one character, there is only one possible decoding
17            if len(st) == 1:
18                return 1
19            # If the first two characters of the string can be decoded together (i.e. are less than or equal to 26),
20            # then we can consider both decodings: one that decodes the first two characters together, and one that
21            # decodes the first character by itself. Otherwise, we can only consider the single decoding that decodes
22            # the first character by itself.
23            if int(st[:2]) <= 26:
24                return dfs(st[1:]) + dfs(st[2:])
25            else:
26                return dfs(st[1:])
27
28        # Call the recursive function with the original string and return the result
29        return dfs(s)

The time complexity of the above code is O(2^n), where n is the length of the input string. This is because each recursive call branches into two additional recursive calls, and each branch is considered independently. This means that for a string of length n, there will be 2^n total recursive calls.

The space complexity of the above code is O(n), where n is the length of the input string. This is because the dfs function uses memoization to store the results of previous recursive calls, and the memoization table will store at most one result for each possible input string. Since the length of the input string determines the number of possible input strings, the space complexity is O(n).

Jump Game

You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.

Return true if you can reach the last index, or false otherwise.

1Input: nums = [2,3,1,1,4]
2Output: true
3Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.
 1class Solution:
 2    # greedy approach
 3    def canJump(self, nums: List[int]) -> bool:
 4        goal = len(nums) - 1  # last index
 5
 6        # for i in reversed(range(goal)):
 7        for idx in range(goal, -1, -1):
 8            if idx + nums[idx] >= goal:
 9                # we have added the current index to the jump length,
10                # because at any given index, futhest we can reach is the current index + jump length
11                goal = idx
12
13        # return True if goal == 0 else False
14        return goal == 0

The time complexity of the code is O(n), where n is the length of the nums list, because the for loop iterates over all elements in the list.

The space complexity of the code is O(1), because the number of variables used does not depend on the size of the input and remains constant. The variables used are goal (1 variable), idx (1 variable), and nums (1 variable, which references the input list and is not counted as a separate variable in the space complexity calculation).

Course Schedule

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

  • For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1.

Return true if you can finish all courses. Otherwise, return false.

1Input: numCourses = 2, prerequisites = [[1,0]]
2Output: true
3Explanation: There are a total of 2 courses to take.
4To take course 1 you should have finished course 0. So it is possible.
 1class Solution:
 2    # video explanation: https://youtu.be/EgI5nU9etnU
 3    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
 4        # define a adjacency list to represent the graph and store the prerequisites
 5        # put empty list for each node initially
 6        # keys: course number
 7        # values: list of prerequisites
 8        adj_map = {i: [] for i in range(numCourses)}
 9        # {0: [], 1: []}
10
11        # add the prerequisites to the adjacency list
12        # c: course, p: prerequisites
13        for c, p in prerequisites:
14            # add course as key and pre_req as value
15            adj_map[c].append(p)
16
17        # adj_map = {0: [1], 1: [0]}
18
19        # track the visited nodes to check if there is a cycle
20        visited = set()
21
22        # apply dfs to determine if there is a cycle
23        # v: course, adj_map: adjacency list
24        # stack, because we are implementing dfs
25        def hasCycle(v, stack):
26            # if the node is already visited, return true
27            if v in visited:
28                if v in stack:
29                    return True
30                return False
31
32            # mark the node as visited
33            visited.add(v)
34            # add the node to the stack
35            stack.append(v)
36
37            # check if there is a cycle in the graph
38            # check for all values in the adjacency list of the node
39            for pre_req in adj_map[v]:
40                if hasCycle(pre_req, stack):
41                    return True
42
43            # remove the node from the stack
44            stack.pop()
45            return False
46
47        # check if there is a cycle in the graph
48        for v in range(numCourses):
49            if hasCycle(v, []):
50                # if hasCycle returns true, there is a cycle,
51                # so we cannot finish all courses
52                return False
53
54        return True

The time complexity of the code is O(V+E), where V is the number of courses (vertices) and E is the number of prerequisites (edges). This is because the hasCycle function is called once for each course, and for each course, it visits all of its prerequisites, at most once.

The space complexity of the code is O(V), because at most, the call stack will contain all the courses.

Number of Islands

Given an m x n 2D binary grid grid which represents a map of '1's (land) and '0's (water), return the number of islands.

An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

1Input: grid = [
2  ["1","1","1","1","0"],
3  ["1","1","0","1","0"],
4  ["1","1","0","0","0"],
5  ["0","0","0","0","0"]
6]
7Output: 1
 1class Solution:
 2    # video explanation: https://youtu.be/ZixJexAaOAk?t=474
 3    def numIslands(self, grid: List[List[str]]) -> int:
 4
 5        # number of rows
 6        rows = len(grid)
 7        # number of cols
 8        cols = len(grid[0])
 9
10        count = 0
11
12        for i in range(rows):
13            for j in range(cols):
14                if grid[i][j] == "1":
15                    self.dfs(i, j, rows, cols, grid)
16                    count += 1
17        return count
18
19    def dfs(self, i, j, rows, cols, grid):
20        # This is checking if the current index is out of bounds or if the current index is not a 1.
21        if i >= rows or i < 0 or j >= cols or j < 0 or grid[i][j] == "0":
22            return 0
23
24        # Use # that modifies the input to ensure that the count isn't incremented where we could accidentally
25        # traverse the same '1' cell multiple times and get into an infinite loop within an island
26        # it's basically a implicit way of marking the visited square/nodes instead of putting the
27        # visited nodes in an visited array
28        grid[i][j] = "0"
29
30        # top
31        self.dfs(i, j + 1, rows, cols, grid)
32        # bottom
33        self.dfs(i, j - 1, rows, cols, grid)
34        # left
35        self.dfs(i - 1, j, rows, cols, grid)
36        # right
37        self.dfs(i + 1, j, rows, cols, grid)

The time complexity of the code is O(R x C), where R is the number of rows and C is the number of columns in the grid. This is because the dfs function is called once for each cell in the grid, and for each cell, it visits all of its neighbors, at most once.

The space complexity of the code is O(R x C), because at most, the call stack will contain all the cells in the grid.

Insert Interval

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.

Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).

Return intervals after the insertion.

1Input: intervals = [[1,3],[6,9]], newInterval = [2,5]
2Output: [[1,5],[6,9]]
 1class Solution:
 2    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
 3        # Add the new interval to the list of intervals
 4        intervals.append(newInterval)
 5        # Sort the intervals by their start time
 6        intervals.sort(key=lambda x: x[0])
 7
 8        # Initialize the output list with the first interval in the sorted list
 9        output = [intervals[0]]
10        # Iterate over the remaining intervals in the sorted list
11        for i in range(1, len(intervals)):
12            # If the current interval overlaps with the last interval in the output list
13            # Update the last interval in the output list to include the current interval
14            if intervals[i][0] <= output[-1][1]:
15                output[-1][1] = max(output[-1][1], intervals[i][1])
16            # If the current interval doesn't overlap with the last interval in the output list
17            # Add the current interval to the output list
18            else:
19                output.append(intervals[i])
20
21        # Return the output list
22        return output

The time complexity of the code is O(N log N), where N is the number of intervals. This is because sorting the intervals takes O(N log N) time and the remaining operations take O(N) time.

The space complexity of the code is O(N), because at most, the output list will contain all the intervals.

Merge Intervals

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

1Input: intervals = [[1,3],[2,6],[8,10],[15,18]]
2Output: [[1,6],[8,10],[15,18]]
3Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].
 1class Solution:
 2
 3    # This function takes a list of intervals as input and returns a list of
 4    # intervals with overlapping intervals merged.
 5    # The intervals are sorted by their starting coordinate before merging.
 6    def merge(self, intervals: List[List[int]]) -> List[List[int]]):
 7
 8        # sort the intervals by their starting coordinate
 9        intervals.sort(key=lambda x: x[0])
10
11        # initialize the output with the first interval in the sorted list
12        output = [intervals[0]]
13
14        # iterate through the rest of the intervals in the sorted list
15        for i in range(1, len(intervals)):
16
17            # if the current interval's start coordinate is less than or equal to the
18            # end coordinate of the last interval in the output list
19            # then the two intervals overlap and need to be merged
20            if intervals[i][0] <= output[-1][1]:
21
22                # merge the current interval with the last interval in the output list
23                # by updating the end coordinate of the last interval
24                # with the maximum of its current value and the end coordinate of the current interval
25                output[-1][1] = max(output[-1][1], intervals[i][1])
26
27            # if the current interval's start coordinate is greater than the end coordinate
28            # of the last interval in the output list
29            # then the two intervals do not overlap and the current interval can be added to the output list as is
30            else:
31                output.append(intervals[i])
32
33        # return the list of merged intervals
34        return output

The time complexity of the code is O(n _ log(n)), where n is the number of intervals in the input list. This is because the intervals are sorted by their starting coordinate using the sort() method, which has a time complexity of O(n _ log(n)).

The space complexity of the code is O(n), where n is the number of intervals in the input list. This is because the output list is constructed by iterating through the input list and adding intervals to it, which requires storing a total of n intervals in the output list.

Reverse Linked List

Given the head of a singly linked list, reverse the list, and return the reversed list.

1Input: head = [1,2,3,4,5]
2Output: [5,4,3,2,1]
 1class Solution:
 2    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
 3        """
 4        - The linked list is reversed in place, so no additional space is used for the reversed list.
 5        - The function works by iterating through the linked list and reversing the links between the nodes.
 6        - The original head of the list becomes the tail of the reversed list, and the original tail becomes the head.
 7        """
 8
 9        # Initialize the previous node as None and the current node as the head of the list
10        previous, current = None, head
11
12        # Iterate through the linked list until we reach the end
13        while current:
14            # Reverse the link by pointing the current node's next reference to the previous node
15            # Then, update the previous node to the current node and the current node to the next node in the list
16            current.next, previous, current = previous, current, current.next
17
18        # Return the previous node, which is now the head of the reversed list
19        return previous

The time complexity of the above code is O(n), where n is the number of nodes in the linked list. This is because the function iterates through the entire linked list once to reverse the links between the nodes.

The space complexity of the above code is O(1), as the function reverses the linked list in place and does not use any additional space. It only uses a few variables (previous, current) to store references to nodes in the linked list.

Linked List Cycle

Given head, the head of a linked list, determine if the linked list has a cycle in it.

There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail's next pointer is connected to. Note that pos is not passed as a parameter.

Return true if there is a cycle in the linked list. Otherwise, return false.

img

1Input: head = [3,2,0,-4], pos = 1
2Output: true
3Explanation: There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed).
 1class Solution:
 2    def hasCycle(self, head: Optional[ListNode]) -> bool:
 3        """
 4        - A linked list has a cycle if any node in the list appears more than once.
 5        - This function uses the "Floyd's cycle-finding algorithm", also known as the "tortoise and hare algorithm".
 6        - It uses two pointers, "slow" and "fast", that move through the linked list at different speeds.
 7        - If there is a cycle, the fast pointer will eventually catch up to the slow pointer.
 8        """
 9
10        # Initialize both pointers to the head of the linked list
11        slow = fast = head
12
13        # Iterate through the linked list until the fast pointer reaches the end
14        while fast and fast.next:
15            # Move the slow pointer one node at a time
16            slow = slow.next
17            # Move the fast pointer two nodes at a time
18            fast = fast.next.next
19
20            # If the slow and fast pointers are pointing to the same node, there is a cycle in the linked list
21            if slow == fast:
22                return True
23
24        # If the fast pointer reached the end of the linked list, there is no cycle
25        return False

The time complexity of the above code is O(n), where n is the number of nodes in the linked list. In the worst case, the fast pointer will iterate through the entire linked list and the slow pointer will iterate through half of the linked list.

The space complexity of the above code is O(1), as the function only uses a few variables (slow, fast) to store references to nodes in the linked list and does not use any additional space.

Merge Two Sorted Lists

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists in a one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

1Input: list1 = [1,2,4], list2 = [1,3,4]
2Output: [1,1,2,3,4,4]
 1class Solution:
 2    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
 3        """
 4        - This function uses recursion to repeatedly merge the two linked lists, starting with the smallest nodes.
 5        - If either of the input lists is empty, the function returns the other list.
 6        - Otherwise, it compares the values of the two list heads and appends the smaller one to the merged list.
 7        """
 8
 9        # If either of the lists is empty, return the other list
10        if not list1 or not list2:
11            return list1 or list2
12
13        # Compare the values of the two list heads
14        # If the value of list1 is smaller, set the next node of list1 to the result of merging the next
15        # nodes of list1 and list2
16        # Otherwise, set the next node of list2 to the result of merging list1 and the next nodes of list2
17        if list1.val < list2.val:
18            list1.next = self.mergeTwoLists(list1.next, list2)
19            return list1
20        else:
21            list2.next = self.mergeTwoLists(list1, list2.next)
22            return list2

The time complexity of the above code is O(n), where n is the total number of nodes in the two linked lists. This is because the function performs a constant amount of work for each node in the lists.

The space complexity of the above code is O(n), as the function uses recursion and the call stack may grow up to the size of the larger of the two input linked lists. However, since the function is merging the linked lists in place and not creating a new list, the space complexity could also be considered O(1).

Spiral Matrix

Given an m x n matrix, return all elements of the matrix in spiral order.

img

1Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
2Output: [1,2,3,6,9,8,7,4,5]
 1class Solution:
 2    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
 3        """
 4        - The function iteratively removes the first row of the matrix and
 5        - then rotates the remaining matrix 90 degrees clockwise.
 6        - This process is repeated until the matrix is empty.
 7        """
 8
 9        result = []
10
11        # While the matrix is not empty
12        while matrix:
13
14            # Add the elements of the first row of the matrix to the result list
15            result.extend(matrix.pop(0))
16
17            # If the matrix is empty, break the loop
18            if not matrix:
19                break
20
21            # Rotate the matrix 90 degrees clockwise
22            # This is done by transposing the matrix and reversing each row
23            matrix = [*zip(*matrix)][::-1]
24
25        # Return the result list
26        return result

The time complexity of the above code is O(n), where n is the total number of elements in the matrix. This is because the function iterates through each element of the matrix once.

The space complexity of the above code is O(n), as the function creates a new list to store the elements of the matrix in spiral order. The size of the list will be the same as the number of elements in the matrix.

Rotate Image

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).

You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

img

1Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
2Output: [[7,4,1],[8,5,2],[9,6,3]]
 1class Solution:
 2    def rotate(self, matrix: List[List[int]]) -> None:
 3        """
 4        - The function first reverses the matrix horizontally and then transposes it.
 5        - Reversing the matrix horizontally is equivalent to rotating it 270 degrees clockwise.
 6        - Transposing the matrix swaps the rows and columns, which is equivalent to rotating it 90 degrees clockwise.
 7        """
 8
 9        # Reverse the matrix horizontally
10        matrix.reverse()
11
12        # Transpose the matrix
13        # This is done by swapping the elements at (i, j) and (j, i) for all i and j
14        for i in range(len(matrix)):
15            for j in range(i):
16                matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

Bonus : rotate the image by 90 degrees (anti-clockwise)

1def rotate(matrix):
2    for i in range(len(matrix) - 1, -1, -1):
3        for j in range(i):
4            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
5    matrix.reverse()

The time complexity of the above code is O(n^2), where n is the number of rows and columns in the matrix. This is because the function iterates through each element of the matrix once to reverse it horizontally and again to transpose it.

The space complexity of the above code is O(1), as the function modifies the matrix in place and does not use any additional space.

Given an m x n grid of characters board and a string word, return true if word exists in the grid.

The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.

img

1Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
2Output: true
 1class Solution:
 2    def exist(self, board: List[List[str]], word: str) -> bool:
 3        """
 4        - The function uses depth-first search to explore the possible paths through the matrix.
 5        - It uses a set, "path", to keep track of the letters that have already been visited.
 6        - If the current letter in the matrix matches the current letter in the word,
 7          the function continues the search in the four adjacent cells.
 8        - If the search reaches the end of the word, the function returns True.
 9        - If the search reaches a cell that is out of bounds, already visited,
10          or has a different letter, the function returns False.
11        """
12
13        # Get the number of rows and columns in the matrix
14        rows = len(board)
15        cols = len(board[0])
16
17        # Initialize a set to keep track of the visited cells
18        path = set()
19
20        # Define the recursive function for depth-first search
21        def dfs(r, c, w):
22            # If the search has reached the end of the word, return True
23            if w == len(word):
24                return True
25
26            # If the current cell is out of bounds, already visited, or has a different letter, return False
27            if (
28                r < 0
29                or r >= rows
30                or c < 0
31                or c >= cols
32                or (r, c) in path
33                or word[w] != board[r][c]
34            ):
35                return False
36
37            # Mark the current cell as visited
38            path.add((r, c))
39
40            res = (
41                dfs(r + 1, c, w+1)
42                or dfs(r - 1, c, w+1)
43                or dfs(r, c + 1, w+1)
44                or dfs(r, c - 1, w+1)
45            )
46
47            path.remove((r, c))
48            return res
49
50        for r in range(rows):
51            for c in range(cols):
52                if dfs(r, c, 0):
53                    return True
54        return False

The time complexity of the above code is O(nm * 4^k), where n and m are the number of rows and columns in the matrix, respectively, and where k is the length of the word being searched for. This is because at each step of the search, the algorithm has four possible options to choose from: it can move to the cell above, below, to the left, or to the right. If the search continues until it reaches the end of the word, the function will have made 4^k recursive calls.

The space complexity of the above code is O(k), as the function uses a set to store the visited cells, and the size of the set will be at most k.

Word Search II

Given an m x n board of characters and a list of strings words, return all words on the board.

Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

img

1Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
2Output: ["eat","oath"]
 1class Solution:
 2    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
 3        # Get the number of rows and columns in the board
 4        rows = len(board)
 5        cols = len(board[0])
 6
 7        # Initialize a set to store visited cells during the search
 8        path = set()
 9
10        # Initialize a set to store the words found in the board
11        result = set()
12
13        def dfs(r, c, w, word):
14            # If we have reached the end of the word, add it to the result set
15            if w == len(word):
16                result.add(word)
17                return
18
19            # If the current cell is out of bounds, already visited, or the
20            # character at the cell does not match the current character in the word,
21            # return without searching further
22            if (
23                r < 0
24                or r >= rows
25                or c < 0
26                or c >= cols
27                or (r, c) in path
28                or word[w] != board[r][c]
29            ):
30                return
31
32            # Add the current cell to the visited set
33            path.add((r, c))
34
35            # Search in all four directions from the current cell
36            dfs(r + 1, c, w + 1, word)
37            dfs(r - 1, c, w + 1, word)
38            dfs(r, c + 1, w + 1, word)
39            dfs(r, c - 1, w + 1, word)
40
41            # Remove the current cell from the visited set
42            path.remove((r, c))
43
44        # Iterate through each cell in the board
45        for r in range(rows):
46            for c in range(cols):
47                # For each word in the list of words, start a depth-first search from
48                # the current cell to find the word in the board
49                for word in words:
50                    dfs(r, c, 0, word)
51
52        # Return the list of words found in the board
53        return list(result)

The time complexity of the above code is O(mn * 4^l), where m and n are the number of rows and columns in the board, and l is the length of the longest word in the list of words.

This is because the outer loop runs in O(mn) time, and the inner loop runs in O(l) time. The dfs function is called once for each cell in the board and each character in the word, so it runs in O(4^l) time in the worst case, when the word is not found and the function needs to search in all four directions from each cell.

The space complexity of the code is O(mn + l), as the path set used to store visited cells during the search takes O(mn) space, and the word parameter of the dfs function takes O(l) space.

Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

1Input: s = "abcabcbb"
2Output: 3
3Explanation: The answer is "abc", with the length of 3.
 1class Solution:
 2    def lengthOfLongestSubstring(self, s: str) -> int:
 3        """
 4        - The function uses a sliding window approach to keep track of the current substring.
 5        - It maintains a set, "char_set", of the characters in the current substring.
 6        - It uses two pointers, "left" and "right", to define the boundaries of the window.
 7        - If the character at the right pointer is already in the set, the function removes
 8          the character at the left pointer and moves it to the right.
 9        - The function updates the maximum length of the substring every time the right pointer moves.
10        """
11
12        # initialize an empty set to keep track of the characters in the current substring
13        char_set = set()
14
15        # initialize the left and right pointers
16        left = 0
17        max_len = 0
18
19        # Iterate through the string with the right pointer
20        for right in range(len(s)):
21
22            # While the character at the right pointer is in the set
23            while s[right] in char_set:
24
25                # Remove the character at the left pointer from the set
26                char_set.remove(s[left])
27
28                # Move the left pointer to the right
29                left += 1
30
31            # Add the character at the right pointer to the set
32            char_set.add(s[right])
33
34            # Update the maximum length of the substring
35            max_len = max(max_len, len(char_set))
36
37        # Return the maximum length of the substring
38        return max_len

The time complexity of the above code is O(n), where n is the length of the input string. This is because the function uses a sliding window approach, which involves iterating through the string once with the right pointer and updating the left pointer and the set of characters in the current substring.

The space complexity of the above code is O(k), where k is the size of the set of characters in the current substring. This is because the function uses a set to store the characters in the current substring, and the size of the set will be at most k.

Valid Anagram

Given two strings s and t, return true if t is an anagram of s, and false otherwise.

An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

1Input: s = "anagram", t = "nagaram"
2Output: true
 1class Solution:
 2    def isAnagram(self, s: str, t: str) -> bool:
 3        """
 4        - An anagram is a word or phrase formed by rearranging the letters of a different word or phrase.
 5        - The function first checks if the two strings have the same length. If not, they cannot be anagrams.
 6        - It then sorts both strings and compares the characters at each position.
 7        - If any of the characters do not match, the function returns False.
 8        - Otherwise, it returns True.
 9        """
10
11        # Get the length of the two strings
12        x1 = len(s)
13        x2 = len(t)
14
15        # If the lengths are different, the strings cannot be anagrams
16        if x1 != x2:
17            return False
18
19        # Sort the two strings
20        s = sorted(s)
21        t = sorted(t)
22
23        # Compare the characters at each position
24        for i in range(0,x1):
25            # If any of the characters do not match, return False
26            if s[i] != t[i]:
27                return False
28
29        # If all characters match, return True
30        return True

The time complexity of the above code is O(nlogn), where n is the length of the input strings. This is because the function sorts the two strings using the built-in sorted function, which has a time complexity of O(nlogn).

The space complexity of the above code is O(n), where n is the length of the input strings. This is because the function creates two new sorted strings, which have a combined size of 2n.

Solving the same problem using O(n) time:

1from collections import Counter
2
3class Solution:
4    def isAnagram(self, s: str, t: str) -> bool:
5        return len(s) == len(t) and Counter(s) == Counter(t)

The time complexity of the above code is O(n), where n is the length of the input strings. This is because the function uses the Counter function from the collections module, which has a time complexity of O(n) for constructing the counter objects and a time complexity of O(1) for comparing them.

The space complexity of the above code is O(n), where n is the length of the input strings. This is because the function creates two Counter objects, which have a combined size of 2n.

Group Anagrams

Given an array of strings strs, group the anagrams together. You can return the answer in any order.

An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

1Input: strs = ["eat","tea","tan","ate","nat","bat"]
2Output: [["bat"],["nat","tan"],["ate","eat","tea"]]
 1class Solution:
 2    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
 3        """
 4        - The function creates a dictionary to store the groups of anagrams.
 5        - It iterates through the input strings and sorts each string.
 6        - If the sorted string is not in the dictionary, it creates a new group with the original string.
 7        - If the sorted string is in the dictionary, it appends the original string to the corresponding group.
 8        - Finally, it returns the values of the dictionary as a list of lists.
 9        """
10
11        # Create a dictionary to store the groups of anagrams
12        anagrams = {}
13
14        # Iterate through the input strings
15        for s in strs:
16            # Sort the string
17            sorted_s = "".join(sorted(s))
18
19            # If the sorted string is not in the dictionary, create a new group with the original string
20            if sorted_s not in anagrams:
21                anagrams[sorted_s] = [s]
22            # If the sorted string is in the dictionary, append the original string to the corresponding group
23            else:
24                anagrams[sorted_s] = [s]
25
26        return list(anagrams.values())

The time complexity of the above code is O(nmlogn), where n is the number of strings in the input list and m is the average length of the strings. This is because the function sorts each string, which has a time complexity of O(mlogn).

The space complexity of the above code is O(nm), where n is the number of strings in the input list and m is the average length of the strings. This is because the function stores each original string in the dictionary, which has a combined size of nm.

Valid Parentheses

Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.

An input string is valid if:

  1. Open brackets must be closed by the same type of brackets.
  2. Open brackets must be closed in the correct order.
  3. Every close bracket has a corresponding open bracket of the same type.
1Input: s = "()[]{}"
2Output: true

Solution 1

 1class Solution:
 2    def isValid(self, s: str) -> bool:
 3        # use stack data structure to keep track of the opening parentheses
 4        stack = []
 5
 6        # iterate over each char in the string
 7        for c in s:
 8            # if the char in opening parentheses
 9            if c in ["(", "{", "["]:
10                # push it onto the stack
11                stack.append(c)
12            # if the char is a closing parentheses
13            else:
14                # we check if the stack is empty. If it is empty,
15                # there is no corresponding opening parenthesis, so we return False
16                if not stack:
17                    return False
18                # if the stack is not empty
19                # compare the current closing parenthesis with the top element of the stack.
20                # If they form a valid pair, we pop the opening parenthesis from the stack.
21                if c == ")" and stack[-1] == "(":
22                    stack.pop()
23
24                elif c == "}" and stack[-1] == "{":
25                    stack.pop()
26
27                elif c == "]" and stack[-1] == "[":
28                    stack.pop()
29                else:
30                    # If the character is not a valid closing parenthesis or does not match the
31                    # top of the stack, we return False.
32                    return False
33
34		# after iterating through all the characters, we check if the stack is empty.
35        # If it is, it means all the opening parentheses have been closed, and the string is valid
36        return len(stack) == 0

The time complexity of the above code is O(n), where n is the length of the input string. This is because the function iterates through the characters in the string once.

The space complexity of the above code is O(n), where n is the length of the input string. This is because the function stores the opening parentheses, brackets, and curly braces in a stack, which has a size of n at most.

Solution 2

1# iteratively removing valid pairs of parentheses from the string until no more valid
2# pairs can be found. If the resulting string is empty, then the input string is valid.
3class Solution:
4    def isValid(self, s: str) -> bool:
5        while "()" in s or "[]" in s or "{}" in s:
6            s = s.replace("()", "").replace("[]", "").replace("{}", "")
7        return s == ""

Valid Palindrome

A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.

Given a string s, return true if it is a palindrome, or false otherwise.

1Input: s = "A man, a plan, a canal: Panama"
2Output: true
3Explanation: "amanaplanacanalpanama" is a palindrome.
1class Solution:
2    def isPalindrome(self, s: str) -> bool:
3        clean_s = ""
4
5        for c in s:
6            if c.isalnum():
7                clean_s += c.lower()
8
9        return clean_s == clean_s[::-1]

The time complexity of the above code is O(n), where n is the length of the input string s. This is because the code iterates through the string once to convert it to lowercase and once to filter out non-alphanumeric characters.

The space complexity of the above code is also O(n), as a new list is created that has the same length as the input string. This is because the list comprehension [c for c in s if c.isalnum()] creates a new list that contains all alphanumeric characters from the input string.

Longest Palindromic Substring

Given a string s, return the longest palindromic substring in s.

1Input: s = "babad"
2Output: "bab"
3Explanation: "aba" is also a valid answer.
 1class Solution:
 2    def longestPalindrome(self, s: str) -> str:
 3        # Initialize an empty string to store the longest palindrome found so far
 4        longest_palindrome = ""
 5
 6        # Iterate through each character in the input string
 7        for i in range(len(s)):
 8            # Check if the substring centered at this character is a palindrome
 9            # with an odd length (e.g. "aba")
10            odd_palindrome = self.isPalindrome(s, i, i)
11
12            # Check if the substring centered at this character is a palindrome
13            # with an even length (e.g. "abba")
14            even_palindrome = self.isPalindrome(s, i, i+1)
15
16            # Update the longest palindrome if either of these substrings is longer
17            # than the current longest palindrome
18            if len(odd_palindrome) > len(longest_palindrome):
19                longest_palindrome = odd_palindrome
20            if len(even_palindrome) > len(longest_palindrome):
21                longest_palindrome = even_palindrome
22
23        # Return the longest palindrome found
24        return longest_palindrome
25
26    def isPalindrome(self, s, start, end):
27        # While the start and end indices are within the bounds of the string
28        # and the characters at these indices are equal, move the indices inward
29        while start >= 0 and end < len(s) and s[start] == s[end]:
30            start -= 1
31            end += 1
32
33        # Return the palindrome substring
34        # Note that start and end are now at the indices immediately
35        # before and after the palindrome, so we need to add 1 to start
36        # and subtract 1 from end to get the actual palindrome substring
37        return s[start+1 : end]

The time complexity of the longestPalindrome() method is O(n^2), where n is the length of the input string s. This is because the method iterates through the string once to check for odd-length palindromes and once to check for even-length palindromes, and for each iteration it calls the isPalindrome() method, which has a time complexity of O(n).

The space complexity of the longestPalindrome() method is O(1), as it only stores a few constant-sized variables (e.g. longest_palindrome, odd_palindrome, and even_palindrome).

The time complexity of the isPalindrome() method is O(n), as it iterates through the string once while the indices start and end are within the bounds of the string and the characters at these indices are equal.

The space complexity of the isPalindrome() method is also O(1), as it only stores a few constant-sized variables (e.g. start and end).

Palindromic Substrings

Given a string s, return the number of palindromic substrings in it. A string is a palindrome when it reads the same backward as forward.

A substring is a contiguous sequence of characters within the string.

Example 1:

1Input: s = "abc"
2Output: 3
3Explanation: Three palindromic strings: "a", "b", "c".

Example 2:

1Input: s = "aaa"
2Output: 6
3Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".

Brute Force

 1class Solution:
 2    def countSubstrings(self, s: str) -> int:
 3        count = 0
 4        n = len(s)
 5
 6        # create all possible substrings
 7        for i in range(n):
 8            for j in range(i, n):
 9                # get the substring
10                substring = s[i: j+1]
11                # check if the substring is palindrome
12                # if it's palindrome, then increase the count by 1
13                if self.is_palindrome(substring):
14                    count += 1
15        return count
16
17
18	# helper function to check if a string is palindrome or not
19    def is_palindrome(self, s):
20        return s == s[::-1]

Time complexity: O(n^3), where n is the length of the input string. This is because we iterate over all possible substrings using two nested loops and for each substring, we check if it is a palindrome using the isPalindrome function which takes O(n) time.

Space Complexity: O(1) because we only use a constant amount of extra space to store the count and temporary substrings.

Optimized Solution

 1class Solution:
 2    def countSubstrings(self, s: str) -> int:
 3        # Initialize a counter to keep track of the number of palindromes found
 4        palindrome_count = 0
 5
 6        # Iterate through each character in the input string
 7        for i in range(len(s)):
 8            # Check for palindromes with odd length (e.g. "aba")
 9            palindrome_count += self.expandFromCenter(s, i, i)
10            # Check for palindromes with even length (e.g. "abba")
11            palindrome_count += self.expandFromCenter(s, i, i+1)
12
13        # Return the total number of palindromic substrings
14        return palindrome_count
15
16    def expandFromCenter(self, s, left, right):
17        # Initialize a counter to keep track of the number of palindromes
18        # found while expanding from the center
19        count = 0
20
21        # While the indices are within the bounds of the string
22        # and the characters at these indices are equal,
23        # increment the counter and move the indices outward
24        while left >= 0 and right < len(s) and s[left] == s[right]:
25            count += 1
26            left -= 1
27            right += 1
28
29        # Return the number of palindromes found
30        return count

This solution has a time complexity of O(n^2), as it iterates through the string once to check every possible palindrome substring, and each palindrome check requires another iteration through the string. Its space complexity is O(1), as it does not allocate any additional space beyond a few variables.

Unique Paths

There is a robot on an m x n grid. The robot is initially located at the top-left corner (i.e., grid[0][0]). The robot tries to move to the bottom-right corner (i.e., grid[m - 1][n - 1]). The robot can only move either down or right at any point in time.

Given the two integers m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.

The test cases are generated so that the answer will be less than or equal to 2 * 109.

Example 1:

img

1Input: m = 3, n = 7
2Output: 28

Example 2:

1Input: m = 3, n = 2
2Output: 3
3Explanation: From the top-left corner, there are a total of 3 ways to reach the bottom-right corner:
41. Right -> Down -> Down
52. Down -> Down -> Right
63. Down -> Right -> Down

Solution 1

 1from functools import lru_cache
 2# recursion to explore all possible paths from the top-left corner to the bottom-right corner
 3class Solution:
 4    def uniquePaths(self, m: int, n: int) -> int:
 5        @lru_cache(maxsize=None)
 6        # current row i and column j
 7        def backtrack(i, j):
 8            # Base case: reached the bottom-right corner
 9            if i == m - 1 and j == n - 1:
10                return 1
11
12            # Base case: out of bounds, return 0
13            if i >= m or j >= n:
14                return 0
15
16            # Recursive case: move down and right
17            # down: i + 1, j; increase row index
18            # right: i, j + 1; increase column index
19            return backtrack(i + 1, j) + backtrack(i, j + 1)
20
21        return backtrack(0, 0)

Time complexity: O(m _ n) because, with the LRU cache, each unique input to the backtrack function is computed only once. The number of unique inputs to the function is equal to the number of cells in the grid, which is m _ n.

Space complexity: O(m _ n) because of the LRU cache. The cache stores the computed results, and since there are m _ n unique inputs, the cache size will be proportional to that.

Longest Consecutive Sequence

Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.

You must write an algorithm that runs in O(n) time.

Example 1:

1Input: nums = [100,4,200,1,3,2]
2Output: 4
3Explanation: The longest consecutive elements sequence is [1, 2, 3, 4]. Therefore its length is 4.

Example 2:

1Input: nums = [0,3,7,2,5,8,4,6,0,1]
2Output: 9
 1class Solution:
 2    def longestConsecutive(self, nums: List[int]) -> int:
 3        # Create a set to store the elements in the array
 4        num_set = set(nums)
 5        longest_seq = 0
 6
 7        # Iterate through the array
 8        for num in nums:
 9            # Check if this element is the start of a consecutive sequence
10            if num - 1 not in num_set:
11                # Find the length of the consecutive sequence starting from this element
12                cur_seq = 1
13                while num + cur_seq in num_set:
14                    cur_seq += 1
15                # Update the longest consecutive sequence length if necessary
16                longest_seq = max(longest_seq, cur_seq)
17
18        return longest_seq

This algorithm has a time complexity of O(n), since we only iterate through the array once and all other operations (adding to a set and checking if an element is in a set) have a time complexity of O(1).

Maximum Depth of Binary Tree

Given the root of a binary tree, return its maximum depth.

A binary tree's maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

Example 1:

img

1Input: root = [3,9,20,null,null,15,7]
2Output: 3

Example 2:

1Input: root = [1,null,2]
2Output: 2
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8class Solution:
 9    def maxDepth(self, root: Optional[TreeNode]) -> int:
10        # Base case: if the root is None, return 0
11        if not root:
12            return 0
13
14        # Recursively find the maximum depth of the left and right subtrees
15        left_depth = self.maxDepth(root.left)
16        right_depth = self.maxDepth(root.right)
17
18        # Return the maximum depth of the left and right subtrees, plus 1 for the root node
19        return max(left_depth, right_depth) + 1

The time complexity of the above code is O(n), where n is the number of nodes in the binary tree. This is because the function is called once for each node in the tree, and the work done in each call is constant (O(1)).

The space complexity is also O(n), since the maximum size of the call stack will be n when the tree is a degenerate tree (i.e., a tree that is a linked list). This is because at each level of the tree, we need to store a function call on the call stack. In the worst case, the tree is a linked list, so we will have n function calls on the call stack.

Same Tree

Given the roots of two binary trees p and q, write a function to check if they are the same or not.

Two binary trees are considered the same if they are structurally identical, and the nodes have the same value.

Example 1:

img

1Input: p = [1,2,3], q = [1,2,3]
2Output: true

Example 2:

img

1Input: p = [1,2], q = [1,null,2]
2Output: false
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8class Solution:
 9    def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
10        # If both nodes are None, return True
11        if not p and not q:
12            return True
13
14        # If one of the nodes is None but the other is not, return False
15        if not p or not q:
16            return False
17
18        # If the values of the nodes are not equal, return False
19        if p.val != q.val:
20            return False
21
22        # Recursively check if the left and right subtrees are the same
23        return (
24            self.isSameTree(p.left, q.left) and
25            self.isSameTree(p.right, q.right)
26        )

The time complexity of the above code is O(n), where n is the number of nodes in the binary tree. This is because the function is called once for each node in the tree, and the work done in each call is constant (O(1)).

The space complexity is also O(n), since the maximum size of the call stack will be n when the tree is a degenerate tree (i.e., a tree that is a linked list). This is because at each level of the tree, we need to store a function call on the call stack. In the worst case, the tree is a linked list, so we will have n function calls on the call stack.

Invert Binary Tree

Given the root of a binary tree, invert the tree, and return its root.

img

1Input: root = [4,2,7,1,3,6,9]
2Output: [4,7,2,9,6,3,1]
 1# Definition for a binary tree node.
 2# class TreeNode:
 3#     def __init__(self, val=0, left=None, right=None):
 4#         self.val = val
 5#         self.left = left
 6#         self.right = right
 7
 8
 9class Solution:
10    def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
11
12        if root:
13            root.left, root.right = self.invertTree(root.right), self.invertTree(root.left)
14
15        return root

The time complexity of the above code is O(n), where n is the number of nodes in the binary tree. This is because the invertTree function is called once for each node in the tree, and the work done in each call is constant (O(1)).

The space complexity is also O(n), since the maximum size of the call stack will be n when the tree is a degenerate tree (i.e., a tree that is a linked list). This is because at each level of the tree, we need to store a function call on the call stack. In the worst case, the tree is a linked list, so we will have n function calls on the call stack.

Binary Tree Level Order Traversal

Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level).

img

1Input: root = [3,9,20,null,null,15,7]
2Output: [[3],[9,20],[15,7]]
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8# This code performs a breadth-first traversal of a binary tree and returns a list of lists,
 9# where each inner list represents the values at a particular level of the tree.
10
11
12class Solution:
13    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
14        # Initialize an empty queue and two empty lists to store the result and the current level
15        queue = []
16        result = []
17        level = []
18
19        # If the root node is not None, add it to the queue
20        if root:
21            queue.append(root)
22
23        # While the queue is not empty
24        while queue:
25            # For each node in the queue (i.e., for each node in the current level)
26            for _ in range(len(queue)):
27                # Remove the first node from the queue and add its value to the current level
28                node = queue.pop(0)
29                level.append(node.val)
30
31                # If the node has a left child, add it to the queue
32                if node.left:
33                    queue.append(node.left)
34
35                # If the node has a right child, add it to the queue
36                if node.right:
37                    queue.append(node.right)
38
39            # Add the current level to the result
40            result.append(level)
41
42            # Reset the current level
43            level = []
44
45        # Return the result
46        return result

The time complexity of this code is O(n), where n is the number of nodes in the binary tree. This is because the function is called once for each node in the tree, and the work done in each call is constant (O(1)).

The space complexity is also O(n), since the maximum size of the call stack will be n when the tree is a degenerate tree (i.e., a tree that is a linked list). This is because at each level of the tree, we need to store a function call on the call stack.

Binary Tree Maximum Path Sum

Given the root of a binary tree, return the maximum path sum of any non-empty path.

Example 1:

img

1Input: root = [1,2,3]
2Output: 6
3Explanation: The optimal path is 2 -> 1 -> 3 with a path sum of 2 + 1 + 3 = 6.

Example 2:

img

1Input: root = [-10,9,20,null,null,15,7]
2Output: 42
3Explanation: The optimal path is 15 -> 20 -> 7 with a path sum of 15 + 20 + 7 = 42.
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8
 9class Solution:
10    def maxPathSum(self, root: TreeNode) -> int:
11        # Initialize the maximum path sum to the minimum possible value
12        max_sum = float('-inf')
13
14        def helper(node):
15            nonlocal max_sum
16
17            # If the node is None, return 0
18            if not node:
19                return 0
20
21            # Recursively find the maximum path sum of the left and right subtrees
22            left_sum = helper(node.left)
23            right_sum = helper(node.right)
24
25            # Update the maximum path sum with the current node
26            # and the maximum path sum of the left and right subtrees
27            max_sum = max(max_sum, node.val + left_sum + right_sum)
28
29            # Return the maximum path sum that includes the current node
30            # and either the left or right subtree
31            return max(node.val + left_sum, node.val + right_sum, 0)
32
33        # Find the maximum path sum using the helper function
34        helper(root)
35
36        # Return the maximum path sum
37        return max_sum
38
39
40if __name__ == '__main__':
41    # Test the solution
42    root = TreeNode(1)
43    root.left = TreeNode(2)
44    root.right = TreeNode(3)
45    print(Solution().maxPathSum(root))  # Output: 6

Time & Space complexity:

$$ O(n) $$

Subtree of Another Tree

Given the roots of two binary trees root and subRoot, return true if there is a subtree of root with the same structure and node values of subRoot and false otherwise.

A subtree of a binary tree tree is a tree that consists of a node in tree and all of this node's descendants. The tree tree could also be considered as a subtree of itself.

Example 1:

img

1Input: root = [3,4,5,1,2], subRoot = [4,1,2]
2Output: true

Example 2:

img

1Input: root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
2Output: false
 1# Definition for a binary tree node.
 2class TreeNode:
 3     def __init__(self, val=0, left=None, right=None):
 4         self.val = val
 5         self.left = left
 6         self.right = right
 7
 8
 9class Solution:
10    def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
11        # if the subRoot is empty, then it is a subtree of root
12        # return True
13        if subRoot is None:
14            return True
15
16        if root is None:
17            return False
18
19        # if the root and subRoot are the same tree, then return True
20        if self.isSameTree(root, subRoot):
21            return True
22
23        # if the root and subRoot are not the same tree, then check the left and right subtree
24        return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot)
25
26
27    def isSameTree(self, p, q):
28        if p is None and q is None:
29            return True
30        if p is None or q is None:
31            return False
32        if p.val != q.val:
33            return False
34        # when same tree, return True, else return False
35        return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)

The time complexity of the isSubtree function is O(n^2) in the worst case, where n is the number of nodes in the root tree. This is because in the worst case, the function will need to traverse the entire root tree and for each node, it will also need to traverse the entire subRoot tree to check if they are the same tree.

The space complexity of the isSubtree function is O(n) in the worst case, where n is the number of nodes in the root tree. This is because in the worst case, the function will need to store all the nodes in the root tree in the call stack during the recursive calls.

The time complexity of the isSameTree function is O(n), where n is the number of nodes in the p tree. This is because in the worst case, the function will need to traverse the entire p tree to check if it is the same as the q tree.

The space complexity of the isSameTree function is O(n) in the worst case, where n is the number of nodes in the p tree. This is because in the worst case, the function will need to store all the nodes in the p tree in the call stack during the recursive calls.

Construct Binary Tree from Preorder and Inorder Traversal

Given two integer arrays preorder and inorder where preorder is the preorder traversal of a binary tree and inorder is the inorder traversal of the same tree, construct and return the binary tree.

img

1Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
2Output: [3,9,20,null,null,15,7]
 1# Definition for a binary tree node.
 2 class TreeNode:
 3     def __init__(self, val=0, left=None, right=None):
 4         self.val = val
 5         self.left = left
 6         self.right = right
 7
 8
 9class Solution:
10    def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
11        # if either preorder or inorder is empty, return None
12        if not preorder or not inorder:
13            return None
14
15        # get the root node from the preorder list and find its index in the inorder list
16        root_val = preorder.pop(0)
17        root_index = inorder.index(root_val)
18
19        # create the root node using the value from the inorder list
20        root = TreeNode(inorder[root_index])
21
22        # recursively build the left and right subtrees using the elements before and after the root node in the inorder list
23        root.left = self.buildTree(preorder, inorder[0: root_index])
24        root.right = self.buildTree(preorder, inorder[root_index + 1: ])
25
26        return root

The time complexity of the buildTree function is O(n^2) in the worst case, where n is the number of nodes in the binary tree. This is because in the worst case, the function will need to traverse the entire preorder and inorder lists for each recursive call.

The space complexity of the buildTree function is O(n) in the worst case, where n is the number of nodes in the binary tree. This is because in the worst case, the function will need to store all the nodes in the binary tree in the call stack during the recursive calls.

Validate Binary Search Tree

Given the root of a binary tree, determine if it is a valid binary search tree (BST).

A valid BST is defined as follows:

  • The left subtree of a node contains only nodes with keys less than the node's key.
  • The right subtree of a node contains only nodes with keys greater than the node's key.
  • Both the left and right subtrees must also be binary search trees.

Example 1:

img

1Input: root = [2,1,3]
2Output: true

Example 2:

img

1Input: root = [5,1,4,null,null,3,6]
2Output: false
3Explanation: The root node's value is 5 but its right child's value is 4.
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8class Solution:
 9    def isValidBST(self, root: Optional[TreeNode]) -> bool:
10        """
11        A BST is a binary tree in which the values of the left and right
12        subtrees of every node are strictly less than and greater than,
13        respectively, the value of the node.
14
15        """
16        # get the inorder traversal of the tree
17        output = self.inorder(root)
18
19        # check if the values in the inorder traversal are in ascending order
20        for i in range(1, len(output)):
21            if output[i] <= output[i-1]:
22                return False
23
24        return True
25
26
27    def inorder(self, root):
28        """
29        Get the inorder traversal of a binary tree.
30        """
31        # if the root is None, return an empty list
32        if not root:
33            return []
34
35        # get the inorder traversal of the left subtree, append the root value,
36        # and then append the inorder traversal of the right subtree
37        return self.inorder(root.left) + [root.val] + self.inorder(root.right)

The time complexity of the isValidBST function is O(n), where n is the number of nodes in the binary tree. This is because the function needs to traverse the entire tree to get the inorder traversal and then check if the values in the traversal are in ascending order.

The space complexity of the isValidBST function is O(n) in the worst case, where n is the number of nodes in the binary tree. This is because in the worst case, the function will need to store all the values in the inorder traversal in a list.

The time complexity of the inorder function is O(n), where n is the number of nodes in the binary tree. This is because the function needs to traverse the entire tree to get the inorder traversal.

The space complexity of the inorder function is O(n) in the worst case, where n is the number of nodes in the binary tree. This is because in the worst case, the function will need to store all the nodes in the binary tree in the call stack during the recursive calls.

Kth Smallest Element in a BST

Given the root of a binary search tree, and an integer k, return the kth smallest value (1-indexed) of all the values of the nodes in the tree.

Example 1:

img

1Input: root = [3,1,4,null,2], k = 1
2Output: 1

Example 2:

img

1Input: root = [5,3,6,2,4,null,null,1], k = 3
2Output: 3
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, val=0, left=None, right=None):
 4        self.val = val
 5        self.left = left
 6        self.right = right
 7
 8class Solution:
 9    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
10        # get the inorder traversal of the BST
11        output = self.inorder(root)
12
13        # sort the list in ascending order
14        output.sort()
15
16        # return the kth element from the sorted list
17        return output[k-1] # -1 because it's 1 indexed
18
19
20    def inorder(self, root):
21        """
22        Get the inorder traversal of a binary tree.
23        """
24        # if the root is None, return an empty list
25        if not root:
26            return []
27
28        # get the inorder traversal of the left subtree, append the root value,
29        # and then append the inorder traversal of the right subtree
30        return self.inorder(root.left) + [root.val] + self.inorder(root.right)

The time complexity of the kthSmallest function is O(n log n), where n is the number of nodes in the BST. This is because the function needs to traverse the entire BST to get the inorder traversal and then sort the list in ascending order.

The space complexity of the kthSmallest function is O(n) in the worst case, where n is the number of nodes in the BST. This is because in the worst case, the function will need to store all the values in the inorder traversal in a list.

The time complexity of the inorder function is O(n), where n is the number of nodes in the binary tree. This is because the function needs to traverse the entire tree to get the inorder traversal.

The space complexity of the inorder function is O(n) in the worst case, where n is the number of nodes in the binary tree. This is because in the worst case, the function will need to store all the nodes in the binary tree in the call stack during the recursive calls.

Lowest Common Ancestor of a Binary Search Tree

Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST.

According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Example 1:

img

1Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
2Output: 6
3Explanation: The LCA of nodes 2 and 8 is 6.

Example 2:

img

1Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
2Output: 2
3Explanation: The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition.
 1# Definition for a binary tree node.
 2class TreeNode:
 3    def __init__(self, x):
 4        self.val = x
 5        self.left = None
 6        self.right = None
 7
 8class Solution:
 9    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
10        # if p and q are both smaller than root, they are in the left subtree
11        if p.val < root.val and q.val < root.val:
12            return self.lowestCommonAncestor(root.left, p, q)
13
14        # if p and q are both greater than root, they are in the right subtree
15        if p.val > root.val and q.val > root.val:
16            return self.lowestCommonAncestor(root.right, p, q)
17
18        # otherwise, they are on different sides (or equal) and the root is LCA
19        return root

Implement Trie (Prefix Tree)

A trie (pronounced as "try") or prefix tree is a tree data structure used to efficiently store and retrieve keys in a dataset of strings. There are various applications of this data structure, such as autocomplete and spellchecker.

Implement the Trie class:

  • Trie() Initializes the trie object.
  • void insert(String word) Inserts the string word into the trie.
  • boolean search(String word) Returns true if the string word is in the trie (i.e., was inserted before), and false otherwise.
  • boolean startsWith(String prefix) Returns true if there is a previously inserted string word that has the prefix prefix, and false otherwise.
 1Input
 2["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
 3[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
 4Output
 5[null, null, true, false, true, null, true]
 6
 7Explanation
 8Trie trie = new Trie();
 9trie.insert("apple");
10trie.search("apple");   // return True
11trie.search("app");     // return False
12trie.startsWith("app"); // return True
13trie.insert("app");
14trie.search("app");     // return True
 1class Trie:
 2    def __init__(self):
 3        # Initialize an empty dictionary as the root of the Trie
 4        self.root = {}
 5
 6    def insert(self, word: str) -> None:
 7        # Start at the root node
 8        node = self.root
 9
10        # Iterate through each character in the word
11        for c in word:
12            # If the character is not already a key in the current node,
13            # add it as a key and set its value to an empty dictionary
14            node = node.setdefault(c, {})
15
16        # Add the special character '#' to mark the end of the word
17        node['#'] = '#'
18
19    def search(self, word: str) -> bool:
20        # Start at the root node
21        node = self.root
22
23        # Iterate through each character in the word
24        for c in word:
25            # If the character is not a key in the current node,
26            # return False as the word is not present in the Trie
27            if c not in node:
28                return False
29            # Otherwise, move to the next node in the Trie
30            node = node[c]
31
32        # Return True if the special character '#' is present in the current node,
33        # indicating that the word is present in the Trie
34        return '#' in node
35
36
37    def startsWith(self, prefix: str) -> bool:
38        # Start at the root node
39        node = self.root
40
41        # Iterate through each character in the prefix
42        for c in prefix:
43            # If the character is not a key in the current node,
44            # return False as the prefix is not present in the Trie
45            if c not in node:
46                return False
47            # Otherwise, move to the next node in the Trie
48            node = node[c]
49
50        # Return True as the prefix is present in the Trie
51        return True

Bonus Problems

3Sum

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.

Notice that the solution set must not contain duplicate triplets.

1Input: nums = [-1,0,1,2,-1,-4]
2Output: [[-1,-1,2],[-1,0,1]]
3Explanation:
4nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
5nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
6nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
7The distinct triplets are [-1,0,1] and [-1,-1,2].
8Notice that the order of the output and the order of the triplets does not matter.

Brute Force

 1# generate all possible triplets from the input array, and check if their sum is zero
 2class Solution:
 3    def threeSum(self, nums: List[int]) -> List[List[int]]:
 4        # use set to avoid duplicates
 5        res = set()
 6
 7        n = len(nums)
 8        # generate all possible triplets
 9        for i in range(n):
10            for j in range(i+1, n):
11                for k in range(j+1, n):
12                    # check if their sum equals to zero
13                    if nums[i] + nums[j] + nums[k] == 0:
14                        # here we will use tuple, because we can not add lists in the set
15                        # lists are mutable, they cannot be added to a set since the elements
16                        # of a set need to be immutable and tuple are immutable.
17                        # sort the triplet to ensure that the same triplet will not be added
18                        # more than once, regardless of order
19                        res.add(tuple(sorted([nums[i], nums[j], nums[k]])))
20        # iterate over each element of the set and convert each element to a list
21        # as the output expects lists of list
22        return [list(x) for x in res]

Time Complexity: O(n^3), where n is the number of elements in the input list. Space Complexity: O(n), where n is the number of unique triplets that sum up to zero.

Optimized Solution

 1# optimized solution for this problem involves sorting the list
 2# and then using a two-pointer technique
 3class Solution:
 4    def threeSum(self, nums: List[int]) -> List[List[int]]:
 5        # initialize result as a set to avoid duplicates
 6        res = set()
 7        nums.sort()
 8        for i in range(len(nums) - 2):
 9            # skip the same number to avoid duplicate triplets
10            # check i > 0 because, at 0, there is no i-1, which will cause error
11            if i > 0 and nums[i] == nums[i - 1]:
12                continue
13            # initialize two pointers
14            l, r = i + 1, len(nums) - 1
15            while l < r:
16                # calculate sum of the 3 numbers
17                s = nums[i] + nums[l] + nums[r]
18                # if the sum is less than 0, increase the left pointer to increase the sum
19                if s < 0:
20                    l += 1
21                # if the sum is more than 0, decrease the right pointer to decrease the sum
22                elif s > 0:
23                    r -= 1
24                else:
25                    # if the sum is 0, we have found a triplet; add it to the result
26                    res.add((nums[i], nums[l], nums[r]))
27                    # move both pointers inward to continue searching for other possible triplets
28                    l += 1
29                    r -= 1
30        # convert each tuple in the set back to a list
31        return [list(x) for x in res]

Time Complexity: O(n^2), where n is the number of elements in the input list. This is because we have a single loop running n times, and inside this loop, we're potentially moving two pointers across the array in the worst-case scenario. The sorting operation at the beginning also takes O(n log n) time, but O(n^2) dominates O(n log n) so we say the time complexity is O(n^2). Space Complexity: O(n), where n is the number of unique triplets that sum up to zero.

Author: Sadman Kabir Soumik

Posts in this Series