LeetCode/주제별 보충

BST: 4. Median of Two Sorted Arrays ★★★★★

hyunkookim 2024. 12. 21. 20:49

4. Median of Two Sorted Arrays

 

GPT

 

좋아요! 다시 처음부터 차근차근, 이 문제를 해결하기 위해 "왼쪽 부분의 최대값 ≤ 오른쪽 부분의 최소값" 조건이 필요한 이유와 이 조건을 중심으로 문제를 푸는 방식을 전체적으로 설명하겠습니다.


문제의 목표

  1. 두 개의 정렬된 배열 nums1과 nums2가 주어집니다.
  2. 이 두 배열을 병합한 후 **중앙값(중간값, Median)**을 구해야 합니다.
  3. 단, 병합하지 않고 O(log(m + n)) 시간 복잡도 안에 해결해야 합니다.

중앙값(Median)의 정의

중앙값은 정렬된 배열에서 중앙에 위치한 값입니다. 배열의 길이에 따라 정의가 달라집니다.

  1. 배열의 길이가 홀수인 경우:
    • 중앙에 위치한 하나의 값이 중앙값.
    • 예: [1, 3, 5] → 중앙값은 3.
  2. 배열의 길이가 짝수인 경우:
    • 중앙에 위치한 두 값의 평균이 중앙값.
    • 예: [1, 3, 5, 7] → 중앙값은 (3 + 5) / 2 = 4.

문제의 핵심

배열을 병합하지 않고도 중앙값을 구하는 방법을 찾아야 합니다.

병합하지 않고 중앙값을 구하려면?

  1. 병합된 배열의 왼쪽 부분오른쪽 부분을 나눌 수 있어야 합니다.
  2. 이 두 부분은 다음 조건을 만족해야 합니다:
    • 왼쪽 부분의 모든 값 ≤ 오른쪽 부분의 모든 값.
    • 이 조건이 성립하면, 두 부분의 경계에 중앙값이 위치합니다.

병합된 배열을 나누는 예시

예를 들어, 배열 nums1 = [1, 3], nums2 = [2, 4]가 있다고 합시다.

  • 병합된 배열: [1, 2, 3, 4].
  • 왼쪽 부분: [1, 2].
  • 오른쪽 부분: [3, 4].

이때:

  • 왼쪽 부분의 최대값 = 2.
  • 오른쪽 부분의 최소값 = 3.
  • 조건 2 ≤ 3이 성립합니다.
  • 중앙값 = (2 + 3) / 2 = 2.5.

병합 없이 중앙값 찾기: 이진 탐색 활용

두 배열을 병합하지 않고 이진 탐색을 통해 중앙값을 찾습니다. 핵심은 두 배열을 적절히 나누는 위치를 찾는 것입니다.

이진 탐색을 사용하는 이유

  • O(log(m + n)) 시간 복잡도를 만족하기 위해 이진 탐색을 사용합니다.
  • 두 배열 중 하나(길이가 더 짧은 배열)에 대해 탐색 범위를 좁혀 나갑니다.
  • 나눔 기준을 적절히 찾으면 병합된 배열의 중앙값을 직접 계산할 수 있습니다.

나눔 기준: 왼쪽과 오른쪽 부분의 조건

두 배열을 각각 왼쪽과 오른쪽 부분으로 나누어 다음 조건을 만족시켜야 합니다:

  • 왼쪽 부분의 최대값 ≤ 오른쪽 부분의 최소값.

이 조건의 의미

  1. 왼쪽 부분의 최대값:
    • 나뉜 두 배열의 왼쪽 부분에서 가장 큰 값.
  2. 오른쪽 부분의 최소값:
    • 나뉜 두 배열의 오른쪽 부분에서 가장 작은 값.
  3. 조건이 성립한다면:
    • 병합된 배열이 적절히 나누어졌고, 중앙값을 계산할 수 있습니다.

조건이 성립하지 않는 경우

  • 왼쪽 부분의 최대값 > 오른쪽 부분의 최소값:
    • 나눔이 잘못되었습니다.
    • 이 경우 탐색 범위를 조정합니다:
      • 왼쪽 값이 너무 크면 더 왼쪽으로 이동 (r = mid1 - 1).
      • 오른쪽 값이 너무 작으면 더 오른쪽으로 이동 (l = mid1 + 1).
class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        # 항상 nums1이 짧은 배열이 되도록 설정
        if len(nums1) > len(nums2):
            nums1, nums2 = nums2, nums1
        
        m, n = len(nums1), len(nums2)
        total_len = m + n
        half_len = total_len // 2
        
        l, r = 0, m
        while l <= r:
            mid1 = (l + r) // 2
            mid2 = half_len - mid1  # 두 배열을 나누는 기준
            
            left1 = nums1[mid1 - 1] if mid1 > 0 else float('-inf')
            right1 = nums1[mid1] if mid1 < m else float('inf')
            left2 = nums2[mid2 - 1] if mid2 > 0 else float('-inf')
            right2 = nums2[mid2] if mid2 < n else float('inf')
            
            # 조건 확인
            if left1 <= right2 and left2 <= right1:
                # 조건을 만족하면 중앙값 계산
                if total_len % 2 == 0:
                    return (max(left1, left2) + min(right1, right2)) / 2
                else:
                    return min(right1, right2)
            
            # 조건을 만족하지 않으면 탐색 범위 조정
            elif left1 > right2:
                r = mid1 - 1
            else:
                l = mid1 + 1

 

요약

  1. 두 배열을 병합하지 않고도 중앙값을 찾기 위해 두 배열을 나누는 기준을 찾습니다.
  2. 이 기준은 왼쪽 부분의 최대값 ≤ 오른쪽 부분의 최소값이라는 조건으로 표현됩니다.
  3. 조건이 성립하지 않으면 이진 탐색을 통해 나눔 기준을 조정합니다.
  4. 조건이 성립하면 중앙값을 계산합니다:
    • 홀수: 오른쪽 부분의 첫 번째 값.
    • 짝수: 왼쪽 부분의 최대값과 오른쪽 부분의 최소값의 평균.

 

https://youtu.be/q6IEA26hvXc?si=hRCnN8N1wU1nk9Rb

 

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        A, B = nums1, nums2
        total = len(nums1) + len(nums2)
        half = total // 2

        if len(B) < len(A):  # len(A) < len(B) 이 되게..
            # Ensure that A is always the smaller array for efficient binary search
            A, B = B, A

        l, r = 0, len(A) - 1
        while True:
            i = (l + r) // 2  # A
            """
            # i최대값+1 = len(nums1), j최대값+1 = len(nums2)
            # total = len(nums1) + len(nums2)
            # half = total // 2 이므로.
            # i, j 가 각 A, B 의 중간값이므로, 
            # (i+1) + (j+1) = half 가 되어야 함
            # 따라서, j = half - i - 2 수식이.. 자동으로..
            
            # i+1 represents the number of elements in the left half of A.
            # j+1 represents the number of elements in the left half of B.
            # The total elements in the left half of both arrays = `half`.
            # Therefore, j = half - i - 2 ensures proper partitioning.
            """
            j = half - i - 2  # B, -2는 배열 index 가 0부터 시작하므로, 
            """
            # A[i], B[j] 는 A와 B의 작은쪽의 최대값
            # A[i+1], B[j+1] 는 A와 B의 큰쪽의 최소값
            # 따라서, i,j 와 i+1, j+1이 유효 범위에 없으면
            # A[i], B[j] 는 A와 B의 작은쪽의 최대값, 즉, 음수 무한대를 가져야 함
            # A[i+1], B[j+1] 는 A와 B의 큰쪽의 최소값, 즉 양수 무한대 가져야 함
            
            # A[i], B[j] represent the maximum of the smaller side of A and B.
            # A[i+1], B[j+1] represent the minimum of the larger side of A and B.
            # If i or j go out of bounds, we use negative infinity (for max) 
            # or positive infinity (for min) as a fallback.
            """
            Aleft = A[i] if i >= 0 else float("-inf")  # A의 왼쪽 절반의 최대값 (없으면 -∞)
            Aright = A[i + 1] if i + 1 < len(A) else float("inf")  # A의 오른쪽 절반의 최소값 (없으면 ∞)
            Bleft = B[j] if j >= 0 else float("-inf")  # B의 왼쪽 절반의 최대값 (없으면 -∞)
            Bright = B[j + 1] if j + 1 < len(B) else float("inf")  # B의 오른쪽 절반의 최소값 (없으면 ∞)

            # partition is correct
            if Aleft <= Bright and Bleft <= Aright: 
                """
                # odd 홀수
                # 전체 길이가 홀수인 경우, 오른쪽 절반의 최소값이 중앙값이 됨
                # If the total length is odd, the median is the minimum value 
                # of the right halves.
                """
                if total % 2:
                    return min(Aright, Bright)
                """
                # even 짝수
                # 전체 길이가 짝수인 경우, 왼쪽 절반의 최대값과 오른쪽 절반의 최소값의 평균이 중앙값
                # If the total length is even, the median is the average of the 
                # maximum value of the left halves and the minimum value of the right halves.
                """
                return (max(Aleft, Bleft) + min(Aright, Bright)) / 2
            elif Aleft > Bright: 
                """
                # A의 작은쪽의 최대값 < B의 큰쪽의 최소값 여야 하는데, 
                # 반대 상황이니
                # i 를 줄여야 하는 상황
                # If A's left maximum is greater than B's right minimum,
                # i needs to move left, reducing the range.
                """
                r = i - 1
            else:
                """
                # j를 줄여야 하는데, j=half-i 이므로, i를 늘려야 하는 상황
                # If B's left maximum is greater than A's right minimum,
                # i needs to move right, increasing the range.
                """
                l = i + 1