diff --git a/merge_sorted_array.py b/merge_sorted_array.py new file mode 100644 index 00000000..fe3e3bc0 --- /dev/null +++ b/merge_sorted_array.py @@ -0,0 +1,99 @@ +''' +88 Merge Sorted Array +https://leetcode.com/problems/merge-sorted-array/description/ + +You are given two integer arrays nums1 and nums2, sorted in non-decreasing order, and two integers m and n, representing the number of elements in nums1 and nums2 respectively. + +Merge nums1 and nums2 into a single array sorted in non-decreasing order. + +The final sorted array should not be returned by the function, but instead be stored inside the array nums1. To accommodate this, nums1 has a length of m + n, where the first m elements denote the elements that should be merged, and the last n elements are set to 0 and should be ignored. nums2 has a length of n. + +nums1 = [ <-- sorted elements --->, <-- auxiliary space of 0s-->] +nums2 = [ <-- sorted elements --->] + +Example 1: +Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 +Output: [1,2,2,3,5,6] +Explanation: The arrays we are merging are [1,2,3] and [2,5,6]. The result of the merge is [1,2,2,3,5,6] with the underlined elements coming from nums1. + +Example 2: +Input: nums1 = [1], m = 1, nums2 = [], n = 0 +Output: [1] +Explanation: The arrays we are merging are [1] and []. The result of the merge is [1]. + +Example 3: +Input: nums1 = [0], m = 0, nums2 = [1], n = 1 +Output: [1] +Explanation: The arrays we are merging are [] and [1]. The result of the merge is [1]. Note that because m = 0, there are no elements in nums1. The 0 is only there to ensure the merge result can fit in nums1. + +Solution: +1. Brute Force: +Copy nums2 to the aux space of nums1. Then sort nums1. +Time: O(N + (M+N) log (M+N)) = O((M+N) log (M+N)), Space: O(1) + +2. Three-pointer (aka Two-pointer) approach: +Let A = nums1, B = nums2 +Initialize three pointers: +a = points to the last element in the sorted region of array A +b = points to the last element in the sorted region of array B = last element of array B +w = points to the last element of array A (last element in the auxiliary space of A) + +Compare the last element of A (indexed by a) vs last element of B (indexed by b). +If A[a] > B[b], copy A[w] <-- A[a]. Decrement a and w +If B[b] > A[a], copy A[w] <-- B[b]. Decrement b and w +Doing this repeatedly will exhaust the sorted elements of A or B. + +If A is exhausted, copy the remaining elements of B, i.e. copy A[w] <-- B[b]. Decrement b and w + +If B is exhausted, do nothing because (the remaining elements of A are already in A and sorted) + +https://www.youtube.com/watch?v=XJytDszgN14 + +During initialization, could we set pointers a and b to the first elements of A and B respectively? +Ans: No, because it could potentially lead to two problems: +a) overwriting elements in A +b) unsorted B (after swapping elements with A) +Short discussion: https://youtu.be/XJytDszgN14?t=393 (6:33 - 9:43) + +Time: O(M+N), Space: O(1) +''' + +def merge_sorted_array(A, B, M, N): + assert len(A) == M + N, f"Len of array A must be = M+N = {M}+{N} = {M+N}" + assert len(B) == N, f"Len of array B must be = N = {N}" + if N == 0: + return M + + a, b = M-1, N-1 + w = M + N - 1 + + while a>=0 and b>=0: + if B[b] >= A[a]: + A[w] = B[b] + b -= 1 + else: + A[w] = A[a] + a -= 1 + w -= 1 + + # if pointer a reaches the starting of A, there is nothing to be done with a. Copy the remaining elements of B to A + while b>=0: + A[w] = B[b] + b -= 1 + w -= 1 + + +def run_tests(): + tests = [([1,2,3,0,0,0], [2,5,6], 3, 3, [1,2,2,3,5,6]), ([1], [], 1, 0, [1]), ([0], [1], 0, 1, [1])] + for test in tests: + A, B, M, N, ans = test[0], test[1], test[2], test[3], test[4] + print(f"\nBefore merge: A = {A}") + merge_sorted_array(A, B, M, N) + print(f"After merge: A = {A}") + passed = (ans == A) + assert passed == True, f"Test case failed" + print(f"Pass: {passed}") + + print(f"\nDONE") + +run_tests() diff --git a/remove_duplicates_from_sorted_array_2.py b/remove_duplicates_from_sorted_array_2.py new file mode 100644 index 00000000..9f6bb302 --- /dev/null +++ b/remove_duplicates_from_sorted_array_2.py @@ -0,0 +1,151 @@ +''' +80 Remove duplicates from sorted array II +https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/description/ + +Given an integer array nums sorted in non-decreasing order, remove some duplicates in-place such that each unique element appears at most twice. The relative order of the elements should be kept the same. + +Since it is impossible to change the length of the array in some languages, you must instead have the result be placed in the first part of the array nums. More formally, if there are k elements after removing the duplicates, then the first k elements of nums should hold the final result. It does not matter what you leave beyond the first k elements. + +Return k after placing the final result in the first k slots of nums. + +Do not allocate extra space for another array. You must do this by modifying the input array in-place with O(1) extra memory. + +Custom Judge: + +The judge will test your solution with the following code: + +int[] nums = [...]; // Input array +int[] expectedNums = [...]; // The expected answer with correct length + +int k = removeDuplicates(nums); // Calls your implementation + +assert k == expectedNums.length; +for (int i = 0; i < k; i++) { + assert nums[i] == expectedNums[i]; +} + +If all assertions pass, then your solution will be accepted. + +Example 1: +Input: nums = [1,1,1,2,2,3] +Output: 5, nums = [1,1,2,2,3,_] +Explanation: Your function should return k = 5, with the first five elements of nums being 1, 1, 2, 2 and 3 respectively. +It does not matter what you leave beyond the returned k (hence they are underscores). + +Example 2: +Input: nums = [0,0,1,1,1,1,2,3,3] +Output: 7, nums = [0,0,1,1,2,3,3,_,_] +Explanation: Your function should return k = 7, with the first seven elements of nums being 0, 0, 1, 1, 2, 3 and 3 respectively. +It does not matter what you leave beyond the returned k (hence they are underscores). + +Constraints: +1 <= nums.length <= 3 * 104 +-104 <= nums[i] <= 104 +nums is sorted in non-decreasing order. + +Solution: +1. Brute Force (w/o bothering about constraints): +Traverse the array, maintain a hash map of freq count of each element. +Then populate the same array where each element is repeated not more than twice. +Time: O(N), Space: O(N) + +2. Two-pointer: +Maintain a left and right pointer. Use the right pointer for reading the array and keeping frequency count of repeated elements. Use the left pointer for writing the repeated elements into the array up to min(freq count, K) times. +K = max no. duplicates allowed +Time: O(N), Space: O(1) +https://youtu.be/zIOGLbAFBOM?t=1393 +''' +import copy + +def remove_duplicates_from_sorted1(nums, K): + ''' + K = max no. of duplicates allowed + Two pointer approach: For each increment of fast pointer, read the value of the array at fast pointer, maintain a freq count of that value. Write that value into nums[slow], i.e. nums[slow] = nums[fast]. Increment slow pointer by 1 step. + The writing part into slow pointer is allowed only if count <= K. + ''' + N = len(nums) + if N == 0: + return -1 + elif N == 1: + return 1 + slow = 1 + count = 1 + for fast in range(1, N): + if nums[fast] == nums[fast-1]: + count += 1 + else: + count = 1 + + if count <= K: + nums[slow] = nums[fast] + slow += 1 + return slow + +def remove_duplicates_from_sorted2(nums, K): + ''' + K = max no. of duplicates allowed + Two pointer: Slightly different code structure + where we increment fast pointer until we hit the next + unique element and then write at the index pointed to by + slow pointer. + ''' + N = len(nums) + if N == 0: + return -1 + elif N == 1: + return 1 + slow = 0 + count = 1 + for fast in range(1, N): + if nums[fast] == nums[fast-1]: + count += 1 + else: + n = 1 + while n <= min(count, K): + nums[slow] = nums[fast-1] + slow += 1 + n += 1 + count = 1 + + # copy the final repeated element of the sequence + n = 1 + while slow <= fast and n <= min(count, K): + nums[slow] = nums[fast] + slow += 1 + n += 1 + return slow # there are 'slow' no. of elements from index=0...slow-1 + + +def run_tests(): + tests = [([1,1,1,2,2,3], 2, [1,1,2,2,3]), + ([1,1,1,2,2,3], 1, [1,2,3]), + ([1,2], 2, [1,2]), + ([1,2], 1, [1,2]), + ([0,1,1,2,2,3,3,3,3,3], 2, [0,1,1,2,2,3,3]), + ([0,1,1,2,2,3,3,3,3,3], 3, [0,1,1,2,2,3,3,3]), + ([0,1,1,1,1,1,2,2,3,3,3,3,3], 2, [0,1,1,2,2,3,3]), + ([0,1,1,1,1,1,2,3,3,3,3,3], 2, [0,1,1,2,3,3]), + ([0,1,3,3,3,3,3], 2, [0,1,3,3]), + ([1,1,1,1,1,3], 2, [1,1,3]), + ([1,3,3,3,3], 2, [1,3,3]), + ([1,1,1,1,1], 2, [1,1]), + ([1], 2, [1]) + ] + + for test in tests: + nums, K, ans = test[0], test[1], test[2] + nums1 = nums.copy() + nums2 = nums.copy() + print(f"\nBefore remove duplicates = {nums}") + print(f"Max no. of duplicates allowed = {K}") + index1 = remove_duplicates_from_sorted1(nums1, K) + index2 = remove_duplicates_from_sorted2(nums2, K) + print(f"1. Num elements = {index1}, nums = {nums1[:index1]}") + print(f"2. Num elements = {index2}, nums = {nums2[:index2]}") + passed = (ans == nums1[:index1] == nums2[:index2]) + assert passed == True, f"Test case failed" + print(f"Pass: {passed}") + + print(f"\nDONE") + +run_tests() \ No newline at end of file diff --git a/search_2d_matrix_2.py b/search_2d_matrix_2.py new file mode 100644 index 00000000..d2b9a2ca --- /dev/null +++ b/search_2d_matrix_2.py @@ -0,0 +1,156 @@ +''' +240 Search a 2D Matrix II + +https://leetcode.com/problems/search-a-2d-matrix-ii/description/ + +Write an efficient algorithm that searches for a value target in an m x n integer matrix matrix. This matrix has the following properties: + +Integers in each row are sorted in ascending from left to right. +Integers in each column are sorted in ascending from top to bottom. + +Example 1: +Input: matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5 +Output: true + +Example 2: +Input: matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20 +Output: false + +Constraints: +m == matrix.length +n == matrix[i].length +1 <= n, m <= 300 +-109 <= matrix[i][j] <= 109 +All the integers in each row are sorted in ascending order. +All the integers in each column are sorted in ascending order. +-109 <= target <= 109 + +Solution: +1. Brute Force: +Traverse every element in the matrix and compare the element with the target +Time: O(M*N), Space: O(1) + +2. Binary search on columns: +For every row, run a binary search across all columns. Alternatively, we could first check if the target lies between the first and last ele of each row. If yes, only then do a binary search. However, it's possible that this condition is satisifed for every row. Eg. [[1, 4, 20], [1, 10, 30], [2, 6, 10]] and target = 6. In this case, the alternative method doesn't really improve time complexity. +Time: O(M log N), Space: O(1) + +3. Binary search on rows: +For every column, run a binary search across all rows +Time: O(N log M), Space: O(1) + +4. Two-pointer: +Key Idea: Start from an element where you get one increasing and one decreasing sequence when you traverse horizontally or vertically. + +traverse dir seq type +horizontal Increasing +vertical Decreasing + OR + +horizontal Decreasing +vertical Increasing + + +Method 1: Start with top-right element. +a) traversing the col from top to bottom yields an increasing order sequence +b) traversing the row from right to left yields a decreasing order sequence +We can use this observation to find the target. +Step 0: row = 0, col = M-1 +Step 1: while row and col are within bounds +if matrix[row][col] == target: + return True +elif matrix[row][col] > target: + # need to find smaller elements -> go left + col-- +else: + # need to find larger elements -> go down + row++ +Step 2: If we come out of the loop, then we have not found the target, hence return False. + +Method 2: We could also start from bottom-left: +a) traversing the col from bottom to top yields a decreasing sequence +b) traversing the row from left to right yields an increasing sequence +Then, the logic is +if matrix[row][col] == target: + return True +elif matrix[row][col] > target: + # need to find smaller elements -> go up + row-- +else: + # need to find larger elements -> go right + col++ + +Note that we could not start from top-left since if we start at: +top-left: going right on row or going down on col, both directions yield increasing sequences +For eg, if target > top-left ele, we need to go in a direction of increasing seq. But which direction to pick - right or down? We can't say for sure. This leads to ambiguity. + +Likewise, we cannot start at bottom-right since: +bottom-right: going left on col or going up on row, both directions yield decreasing sequences + +https://youtu.be/XJytDszgN14?t=2037 + +Complexity: Starting at top-right, at most, we move M steps downwards and N steps to the left before we declare find/not find the target. Hence, max steps = M + N + +Time: O(M+N), Space: O(1) +''' +def search_2d_matrix1(mat, target): + ''' + Two pointer: Start from top right + ''' + M = len(mat) + if M == 0: + return False + N = len(mat[0]) + + r, c = 0, N-1 + while r < M and c >= 0: + if mat[r][c] == target: + return True + if mat[r][c] < target: + r += 1 + else: + c -= 1 + return False + +def search_2d_matrix2(mat, target): + ''' + Two pointer: Start from bottom left + ''' + M = len(mat) + if M == 0: + return False + N = len(mat[0]) + + r, c = M-1, 0 + while r >= 0 and c < N: + if mat[r][c] == target: + return True + if mat[r][c] < target: + c += 1 + else: + r -= 1 + return False + + +def run_tests(): + tests = [([[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], 5, True), + ([[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], 20, False), + ([[0],[2],[4],[6],[8]], 6, True), + ([[0],[2],[4],[6],[8]], 5, False), + ([[0, 2, 4, 6, 8]], 6, True), + ([[0, 2, 4, 6, 8]], 5, False), + ([[1]], 1, True), + ([[1]], 0, False), + ] + for test in tests: + mat, target, ans = test[0], test[1], test[2] + found1 = search_2d_matrix1(mat, target) + found2 = search_2d_matrix2(mat, target) + print(f"\nMatrix = {mat}") + print(f"Target = {target}") + passed = (ans == found1 == found2) + assert passed == True, f"Test case failed" + print(f"Pass: {passed}") + + print(f"\nDONE") + +run_tests()