Anthony's Blog Site

Cracking LeetCode 75 (Part 1)

February 24, 2025

Two Pointers

Photo Credit: ChatGPT 4o


This blog post aims to walk through classic coding problems listed in LeetCode 75.

Topics covered in this post are: Two Pointers, Sliding Window, Prefix Sum, and Array / String. These problems involve basic arrays, iterating, modifying arrays, or standard scanning.

All solutions are written in Python and are optimized for performance and readability.

Two Pointers

283. Move Zeroes

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """

        # Intuition: When using `for i in range(num_ints)` to check for zeros,
        # `i` will end up reaching to the appended zeros,
        # so we need to mark the "end" of checking (which moves forward when a zero is removed).
        # The checking will terminate when we reach the

        i_curr = 0  # marks the int we are checking
        i_end = len(nums) - 1  # marks the (real-time) end of the list
        while i_curr <= i_end:
            if nums[i_curr] == 0:
                nums.pop(i_curr)
                nums.append(0)
                i_end -= 1  # The end moves forward by 1.
            else:
                i_curr += 1

392. Is Subsequence

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        
        # Intuition: We go through `t`, and whenever we see a char also appearing in `s`,
        # we cross that char out in `s` (metaphorically)
        # and go on and compare the rest of chars in `t` with the next char in `s`.

        if not s:
            return True  # "" is treated as a subsequence of any strings.

        i_s = 0
        for i_t in range(len(t)):
            if t[i_t] == s[i_s]:
                i_s += 1  # we move on to check the rest of chars in `t` with the next char in `s`
            if i_s >= len(s):
                return True  # we've seen all chars in `s`

        return False

11. Container With Most Water

class Solution:
    def maxArea(self, height: List[int]) -> int:

        # Intuition: We set a left border and a right border.
        # By adjusting the left and right wall, we could control the volumn formed.
        # The volumn is also determined by the shorter one of the left & right walls.

        # We start from the left-most wall and right-most wall to get the largest width,
        # and we constantly adjust the shorter wall,
        # expecting that though the width is getting smaller,
        # we would meet a taller wall that gives us a larger volumn.

        i_left = 0
        i_right = len(height) - 1
        max_area = 0

        while i_left < i_right:
            curr_area = min(height[i_left], height[i_right]) * (i_right - i_left)
            max_area = max(max_area, curr_area)  # update

            # Shrink width:
            if height[i_left] < height[i_right]:
                i_left += 1
            else:
                i_right -= 1

        return max_area

1679. Max Number of K-Sum Pairs

class Solution:
    def maxOperations(self, nums: List[int], k: int) -> int:

        # Intuition: This looks similar to the Two Sum problem.
        # We could use Counter() to build a frequency map.

        # freq = {}
        # for num in nums:
        #     freq[num] = freq.get(num, 0) + 1
        freq = Counter(nums)

        operations = 0
        for num in nums:
            complement = k - num
            if freq[num] > 0 and freq[complement] > 0:
                # Skip if there's only one occurrence of itself:
                if num == complement and freq[num] <= 1:
                    continue
                # Do a removal operation:
                operations += 1
                freq[num] -= 1
                freq[complement] -= 1

        return operations

Sliding Window

643. Maximum Average Subarray I

class Solution:
    def findMaxAverage(self, nums: List[int], k: int) -> float:
        # max_avg = sum(nums[0:k])/k
        # for i in range(len(nums)- k + 1):
        #     avg = sum(nums[i:i+k])/k
        #     max_avg = max(avg, max_avg)
        # return max_avg

        # Reflection: computing `sum(nums[i:i+k])/k` consumes too much CPU for each new i.

        curr_sum = max_sum = sum(nums[0:k])
        for i in range(k, len(nums)):
            curr_sum = curr_sum + nums[i] - nums[i-k]
            max_sum = max(curr_sum, max_sum)
        return (max_sum / k)  # computes the avg only at the end

1456. Maximum Number of Vowels in a Substring of Given Length

class Solution:
    def maxVowels(self, s: str, k: int) -> int:
        # Intuition: Use a sliding window of length k.
        # Use set for faster lookups.

        vowels = {'a', 'e', 'i', 'o', 'u'}  # Set of vowels for fast lookup
        curr_num_vowels = 0

        # Initial count:
        for i in range(k):
            if s[i] in vowels:
                curr_num_vowels += 1
        max_num_vowels = curr_num_vowels
        
        # Move the sliding window:
        for i in range(1, len(s) - k + 1):
            if s[i-1] in vowels:
                curr_num_vowels -= 1  # removed from the window
            if s[i+k-1] in vowels:
                curr_num_vowels += 1  # added into the window
            max_num_vowels = max(max_num_vowels, curr_num_vowels)
        
        return max_num_vowels

1004. Max Consecutive Ones III

class Solution:
    def longestOnes(self, nums: List[int], k: int) -> int:
        # Intuition:
        # We create a sliding window where there are k 0's.

        left = 0
        max_length = 0
        zero_count = 0
        
        # Move the right bound of sliding window:
        for right in range(len(nums)):
            if nums[right] == 0:
                zero_count += 1
            
            # If there are more than k zeros, shrink the window from the left
            while zero_count > k:
                if nums[left] == 0:
                    zero_count -= 1
                left += 1  # Move left pointer to the right
            
            # Update:
            max_length = max(max_length, right - left + 1)
        
        return max_length

1493. Longest Subarray of 1's After Deleting One Element

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:

        # Intuition:
        # We specify the longest subarray using a `left` and a `right` pointer.
        # NOTE: To make the algorithm cleaner, we use a for loop to automate the incrementations of `right` and adjust `left` separately.

        left = 0
        max_len = 0
        zero_count = 0
        for right in range(len(nums)):
            if nums[right] == 0:
                zero_count += 1

            # Once zero_count reaches 2,
            # we shrink the window to remove the previous zero:
            while zero_count > 1:
                if nums[left] == 0:
                    zero_count -= 1
                left += 1

            max_len = max(max_len, right - left)

        return max_len

Prefix Sum

1732. Find the Highest Altitude

class Solution:
    def largestAltitude(self, gain: List[int]) -> int:
        altitude = 0
        highest = 0
        for rise in gain:
            altitude += rise
            highest = max(highest, altitude)
        return highest

724. Find Pivot Index

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        left_sum = 0
        total_sum = sum(nums)
        for i in range(len(nums)):
            if left_sum == total_sum - nums[i] - left_sum:
                return i
            left_sum += nums[i]
        return -1

Array / String

1768. Merge Strings Alternately

class Solution:
    def mergeAlternately(self, word1: str, word2: str) -> str:
        result = []  # more performant than `result = ""`
        i = 0
        while i < len(word1) or i < len(word2):
            if i < len(word1):
                result.append(word1[i])  # more performant than `result += word1[i]`
            if i < len(word2):
                result.append(word2[i])
            i += 1
        return ''.join(result)

        # NOTE: In Python, strings are immutable, meaning every time you perform an operation like result += char, a new string is created in memory. This results in repeated memory allocation and copying of the existing string contents, which becomes costly as the string grows.

1071. Greatest Common Divisor of Strings

class Solution:
    def gcdOfStrings(self, str1: str, str2: str) -> str:

        # Intuition: Simply use gcd() on the string lengths as long as the strings have a GCD.
        # Check: when str1 and str2 have a GCD (str1 = mGCD, str2 = nGCD),
        # then str1 + str2 = (m + n)GCD = str2 + str1.
        
        if str1 + str2 != str2 + str1:
            return ""
        return str1[:math.gcd(len(str1), len(str2))]  # or `str2[:math.gcd(len(str1), len(str2))]`

1431. Kids With the Greatest Number of Candies

class Solution:
    def kidsWithCandies(self, candies: List[int], extraCandies: int) -> List[bool]:
        # Intuition: For each kid, find the number of candies needed to make him/her
        # the kid with the most candies.

        max_num_candies = max(candies)  # the max num of candies
    
        return [(num_candies + extraCandies >= max_num_candies) for num_candies in candies]
        # Syntax ("list comprehesion"): [<expression> for <item> in <iterable> if <condition>]
        # which can also be done using a for loop.

605. Can Place Flowers

class Solution:
    def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
        # Intuition: Given `flowerbed` alone, the max num of additional flowers
        # can already be determined.

        num_allowed = 0
        for i in range(len(flowerbed)):
            if flowerbed[i] == 0:
                # if left is unoccupied and right is unoccupied -> plant flower:
                if (i == 0 or flowerbed[i-1] == 0) and (i == len(flowerbed) - 1 or flowerbed[i+1] == 0):
                    # print(i)
                    flowerbed[i] = 1
                    num_allowed += 1
        return n <= num_allowed

345. Reverse Vowels of a String

class Solution:
    def reverseVowels(self, s: str) -> str:

        # Intuition:
        # "Reversing" is basically swapping against the central element.
        # We can use the two-pointer method to avoid using `indices` in Solution 1.

        vowels = set("aeiouAEIOU")
        s = list(s)  # Convert string to list for mutability
        left, right = 0, len(s) - 1

        while left < right:
            # Move `left` and `right` separately until they both point at vowels.
            while left < right and s[left] not in vowels:
                left += 1
            while left < right and s[right] not in vowels:
                right -= 1
            
            # Swap:
            s[left], s[right] = s[right], s[left]
            left += 1
            right -= 1

        return "".join(s)  # Convert list back to string

151. Reverse Words in a String

class Solution:
    def reverseWords(self, s: str) -> str:
        words = s.split()
        words.reverse()  # reversed as a list
        return ' '.join(words)  # joined with ' '

238. Product of Array Except Self

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:

        # Intuition:
        # We notice to compute the product for each integer in `nums`,
        # we would have to multiply all integers before and after it.
        # e.g. For both `nums[5]` and `nums[6]`, we will need to compute the product of `nums[0]` to `nums[4]`, which are redundant calculations.

        # We use two passes, which is still O(n).
        # For each integer in `nums`,
        # the forward pass computes the product of all integers before it and
        # the backward pass computes the product of all integers after it.

        result = [1] * len(nums)
        
        # Forward pass:
        product = 1
        for i in range(len(nums)):
            result[i] = product
            product *= nums[i]  # build up the "prefix product"
        
        # Backward pass:
        product = 1
        for i in range(len(nums)-1, -1, -1):
            result[i] *= product
            product *= nums[i]  # build up the "suffix product"
        
        return result

334. Increasing Triplet Subsequence

class Solution:
    def increasingTriplet(self, nums: List[int]) -> bool:

        # Intuition: 
        # Since the question only asks for a True/False answer instead of all triple of indices, the algorithm can be simplified significantly.
        # To check for the existence of such triples, the key idea is to find the 1st and 2nd minimum values seen so far. For example, for [2, 4, 1, 3, _], to make a triple exist, we only have to make sure _ is larger than 1 and 3 without having to comparing it with 2 or 4.

        min_1 = float('inf')  # the smallest
        min_2 = float('inf')  # the 2nd smallest

        # (i < j < k) naturally holds in a for loop
        for num in nums:
            if num <= min_1:
                min_1 = num
            elif num <= min_2:  # min_1 < num <= min_2
                min_2 = num
            else:  # num > min_2
                return True

        return False

443. String Compression

class Solution:
    def compress(self, chars: List[str]) -> int:

        # Intuition: We go through the list and modify it in-place.
        # Again, `for i in range(len(chars))` won't work since `chars` will be modified inside the for loop.

        # Helper function (for avoiding boilerplate code):
        # For pass-by-reference, Python lists are mutable,
        # but Python integers are immutable (needs to be returned explicitly).
        #   i: the index right after the last duplicate of `prev_char`
        #   count: the number of occurrences of `prev_char`
        def compress_for_prev_char(chars, i, count):
            # 1. Remove all duplicates (except the first occurrence):
            chars[(i-count+1):i] = []
            i -= (count - 1)  # adjusts `i` after the deletion

            # 2. Append the count as digits to `chars` (conditionally):
            if count > 1:
                for idx, digit in enumerate(str(count)):
                    chars.insert(i + idx, digit)
                i += len(str(count))  # adjusts `i` after the insertion
            return i

        i = 0
        prev_char = ""
        count = 0
        while i <= len(chars) - 1:
            temp = chars[i]

            if not prev_char or chars[i] == prev_char:  # Simply count:
                count += 1
            else:  # Compress the list for the last char:
                i = compress_for_prev_char(chars, i, count)
                count = 1  # reset count for this new (different) char:

            # Update:
            i += 1
            prev_char = temp

            # Handle the exit:
            if i >= len(chars):
                i = compress_for_prev_char(chars, i, count)
                break

        return len(chars)