DSA sheet · Recursion · Divide and conquer pattern

Median of Two Sorted Arrays

This question comes right after binary search and merge sort in the divide and conquer pattern. The teacher says merge sort is a prerequisite, because the whole solution is the merge step of merge sort: two arrays that are already sorted, walked with two pointers. The new trick is that we don't build the merged array. We just count our way to the middle and stop there.

She mentions that the best solution uses binary search, but in this video she teaches the merge approach. So this page covers: the brute force she rejects, her merge walk (in full), the same walk written as recursion, and, beyond the video, the binary search "split" solution, explained step by step for when you're ready.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

1. Recursion in five ideas

Work can happen on the way down (before the smaller call, often carried along in parameters) or on the way back up (after the smaller call returns). Part C does all its work on the way down. Merge sort, below, does its merging on the way back up.

2. What is a median?

Put all the numbers in sorted order. The median is the middle one.

For a total of total numbers (indexes 0 to total − 1), the middle index is total // 2. When total is even, the other middle one is just before it, at total // 2 − 1.

3. The merge step from merge sort (the prerequisite)

Merge sort is divide and conquer: it splits an array into two halves, sorts each half by recursion, then merges the two sorted halves on the way back up. Look at how it splits the teacher's numbers:

               [4 6 7 1 2 5]
               /           \
        [4 6 7]             [1 2 5]       ← each half sorted (by recursion)
               \           /
             merge two sorted lists
                     ↓
               [1 2 4 5 6 7]

The last step is exactly our problem: two lists that are each sorted, to be combined in order. Merging uses two pointers, i on the first list and j on the second. Each time, compare the two front elements, take the smaller one, and move only that pointer forward. When one list runs out, take the rest of the other.

stepcomparetakemerged so far
14 vs 11 (j moves)1
24 vs 22 (j moves)1 2
34 vs 54 (i moves)1 2 4
46 vs 55 (j moves, list 2 is now used up)1 2 4 5
5–6—6, 7 (the rest of list 1)1 2 4 5 6 7

The teacher also says this merge idea will come back later, for example when merging sorted linked lists. It's worth learning well.


Part A · Brute force: join, sort, pick the middle

LeetCode 4 · Median of Two Sorted Arrays

1The question in simple words

You get two arrays, nums1 (size m) and nums2 (size n). Each one is sorted on its own. Return the median of all m + n numbers together, as a decimal number.

nums1nums2all together, sortedmedian
[1, 3][2]1 2 3 (odd, 3 numbers)2.0
[1, 2][3, 4]1 2 3 4 (even, 4 numbers)(2 + 3) / 2 = 2.5
[4, 6, 7][1, 2, 5]1 2 4 5 6 7(4 + 5) / 2 = 4.5

2What the constraints tell us

3Intuition

The simplest idea: put both arrays into one list, then find the median of that list.

4Building the logic: why simply joining is not enough

In the first two examples, joining happens to give a sorted list. That's only luck. "Each array is sorted" doesn't mean the last number of nums1 is smaller than the first number of nums2. With nums1 = [4, 6, 7] and nums2 = [1, 2, 5], joining gives [4, 6, 7, 1, 2, 5]: not sorted, and its "middle" (7 and 1) means nothing. A median only makes sense on a sorted list, so we must sort the joined list first: [1, 2, 4, 5, 6, 7] → (4 + 5) / 2 = 4.5 ✓.

5Approach steps

  1. merged = sorted(nums1 + nums2).
  2. total = len(merged), mid = total // 2.
  3. Odd → return merged[mid]. Even → return (merged[mid − 1] + merged[mid]) / 2.

6Code (Python)

Brute force: join and sort
class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        merged = sorted(nums1 + nums2)   # join, then sort the whole thing
        total = len(merged)
        mid = total // 2
        if total % 2 == 1:               # odd: one middle element
            return float(merged[mid])
        return (merged[mid - 1] + merged[mid]) / 2   # even: average of two

7Code line by line

linewhat it means
merged = sorted(nums1 + nums2)+ joins the lists (an empty list is fine). sorted fixes the order.
mid = total // 2The middle index (the right one of the two middles when the count is even).
return float(merged[mid])Odd count: the single middle. float because a decimal is expected.
(merged[mid - 1] + merged[mid]) / 2Even count: average of the two middles. In Python, / always gives a float.

8Dry run

nums1 = [4, 6, 7], nums2 = [1, 2, 5] → joined [4, 6, 7, 1, 2, 5] → sorted [1, 2, 4, 5, 6, 7]. total = 6 (even), mid = 3 → (merged[2] + merged[3]) / 2 = (4 + 5) / 2 = 4.5 ✓. (No recursion in this part.)

9Complexity & remember

Remember the brute forceJoin + sort + pick the middle. Correct, but it wastes the "already sorted" information and uses extra space.

Part B · Merge walk, counting to the middle (the teacher's solution)

1The question

The same question. Now we want to use the fact that both arrays are sorted, and avoid building a new array.

2What the constraints tell us

One array can be empty, so the pointer on it is "out of range" from the very start. The code must handle a pointer running off the end of its array. That case turns out to be the main detail of this solution.

3Intuition: how the problem is split

The merge from Part 0 hands us numbers in sorted order, one at a time: smallest first, then the next smallest, and so on. The median sits at a known position in that order. So:

4Building the logic from examples

How many numbers must we take?

The teacher counts positions on [1, 2, 4, 5, 6, 7] (total 6, even). The two middles are at indexes 2 and 3, i.e. the 3rd and 4th numbers taken. So we take 4 numbers: the 4th is curr, the 3rd is prev.

totalmiddle index (total // 2)numbers to take (total // 2 + 1)answer
5 (odd)23curr (the 3rd number)
6 (even)34(prev + curr) / 2 (3rd and 4th)

So the counting loop runs for count = 0, 1, …, total // 2: that's total // 2 + 1 rounds. The teacher writes it as count <= total / 2 and explains the "equal to" part: for total = 5, 5 / 2 is 2 in integer division, and we need rounds 0, 1 and 2.

Remember the previous number before taking a new one

At the start of every round, copy curr into prev. Then find the new curr. When the loop stops, prev is automatically the number right before the middle one.

Taking the smaller front number

While both pointers are still inside their arrays, compare nums1[i] with nums2[j]. Take the smaller one and move only that pointer. (If they're equal, either is fine. The teacher takes nums1 with <=.)

When one pointer runs off the end

The teacher shows this with examples:

So there are three cases each round: both in range → compare; only i in range → take nums1[i]; otherwise → take nums2[j].

Doubt 1: in the first example the teacher says the total is 5. Is that right?
→ [1, 2] and [3, 4, 5, 6] have 2 + 4 = 6 numbers, so we need the 3rd and 4th (3 and 4 → 3.5), not just the 3rd. It's a small slip of the tongue. The point she's making is unaffected: once i runs off the end, keep taking from nums2.
Doubt 2: in the last else, how do we know j is in range?
→ We only take total // 2 + 1 numbers, never more than m + n. So in any round there's at least one number left somewhere. If the first two cases failed, i is out of range, so j must still be in range.
Doubt 3: why not build the merged array and then pick the middle?
→ That works too (O(m + n) time), but it needs O(m + n) extra space and keeps merging after the middle for no reason. The pointers already tell us which number is next in sorted order, so a count and two variables are enough.

5Approach steps

  1. m, n = sizes; total = m + n; i = j = 0; prev = curr = 0.
  2. Repeat total // 2 + 1 times: prev = curr, then: both in range → take the smaller of nums1[i], nums2[j] into curr and move that pointer; only i in range → curr = nums1[i], i += 1; else → curr = nums2[j], j += 1.
  3. Odd total → return curr. Even total → return (prev + curr) / 2.

6Code (Python)

Merge walk to the middle (teacher's approach)
class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        m, n = len(nums1), len(nums2)
        total = m + n
        i = j = 0
        prev = curr = 0
        for count in range(total // 2 + 1):   # count = 0 .. total // 2
            prev = curr                       # keep the old middle candidate
            if i < m and j < n:               # both arrays still have numbers
                if nums1[i] <= nums2[j]:
                    curr = nums1[i]
                    i += 1
                else:
                    curr = nums2[j]
                    j += 1
            elif i < m:                       # nums2 used up
                curr = nums1[i]
                i += 1
            else:                             # nums1 used up
                curr = nums2[j]
                j += 1
        if total % 2 == 1:                    # odd: the middle number
            return float(curr)
        return (prev + curr) / 2              # even: average of the two middles

7Code line by line

linewhat it means
total = m + nHow many numbers there are altogether, so we know where the middle is.
i = j = 0One pointer at the front of each array.
for count in range(total // 2 + 1):Take exactly as many numbers as needed to reach the middle index. (Teacher: count <= total/2.)
prev = currBefore finding the new number, save the old one. It may be the first of the two middles.
if i < m and j < n:Both arrays have numbers left → compare them.
if nums1[i] <= nums2[j]: curr = nums1[i]; i += 1nums1's front is the smaller (or equal) → it's the next number in sorted order.
else: curr = nums2[j]; j += 1nums2's front is smaller.
elif i < m:nums2 is used up: the only numbers left are in nums1.
else:nums1 is used up: take from nums2.
if total % 2 == 1: return float(curr)Odd total: the last number taken is the middle.
return (prev + curr) / 2Even total: the average of the last two taken.

8Dry run: nums1 = [4, 6, 7], nums2 = [1, 2, 5]

total = 6 → take 6 // 2 + 1 = 4 numbers.

counti, j at startcasetaken (curr)prev
00, 04 vs 1 → nums210
10, 14 vs 2 → nums221
20, 24 vs 5 → nums142
31, 26 vs 5 → nums254
loop ends (6 and 7 are never looked at) · total even → (4 + 5) / 2 = 4.5 ✓

9Complexity & remember

Remember the merge walkTake the smaller front number total // 2 + 1 times, saving prev = curr each round. Handle "one array used up". Odd → curr, even → (prev + curr) / 2.

Part C · The same walk as recursion

This page is in the recursion notebook, so let's write Part B's loop as a recursive function. We use the same conversion rules as for binary search: the loop's changing variables (i, j, how many are left to take, prev, curr) become parameters, the loop's end becomes the base case, and each round becomes one call.

1–2Question & constraints

The same. One new Python concern: the walk takes up to 2000 / 2 + 1 = 1001 numbers, so there can be 1001 nested calls. That's more than Python's default limit of about 1000. We must raise it with sys.setrecursionlimit.

3Intuition: how each call splits the problem

The question each call answers is: "Starting at positions i and j, take left more numbers in sorted order. What are the last two?" Each call splits off one number (the smaller front) and hands the rest, the same question with left − 1, to the next call. This is linear recursion: one call per call, and the input shrinks by 1 each time.

4Building the logic

Doubt: the loop had three cases. Why only two here?
→ "Both in range and nums1 is smaller" and "only nums1 in range" both end in the same action (take nums1[i]), so they're merged into one condition with or. Python checks j == len(nums2) first, so nums2[j] is never read out of range. And i < len(nums1) is checked before nums1[i].

5Approach steps

  1. Call walk(i = 0, j = 0, left = total // 2 + 1, prev = 0, curr = 0).
  2. walk: if left == 0 → return (prev, curr).
  3. If nums1 should give the next number → return walk(i + 1, j, left − 1, curr, nums1[i]).
  4. Else → return walk(i, j + 1, left − 1, curr, nums2[j]).
  5. Finally: odd → curr, even → (prev + curr) / 2.

6Code (Python)

Merge walk as parameterized recursion
import sys
sys.setrecursionlimit(5000)    # up to 1001 nested calls when m + n = 2000

class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        total = len(nums1) + len(nums2)
        prev, curr = self.walk(nums1, nums2, 0, 0, total // 2 + 1, 0, 0)
        if total % 2 == 1:
            return float(curr)
        return (prev + curr) / 2

    def walk(self, nums1, nums2, i, j, left, prev, curr):
        if left == 0:                                  # base case: took enough
            return prev, curr
        if j == len(nums2) or (i < len(nums1) and nums1[i] <= nums2[j]):
            return self.walk(nums1, nums2, i + 1, j, left - 1, curr, nums1[i])
        return self.walk(nums1, nums2, i, j + 1, left - 1, curr, nums2[j])

7Code line by line

linewhat it means
sys.setrecursionlimit(5000)Allow deeper nesting than the default ~1000, because the walk can be 1001 calls deep.
self.walk(..., 0, 0, total // 2 + 1, 0, 0)Start both pointers at 0 and ask for total // 2 + 1 numbers.
if left == 0: return prev, currThe loop's end. The finished answer travels back up unchanged.
if j == len(nums2) or (...):nums1 gives the next number: nums2 is used up, or nums1's front is smaller or equal.
self.walk(..., i + 1, j, left - 1, curr, nums1[i])One round of the loop: move i, one fewer to take, old curr becomes prev, nums1[i] becomes curr.
self.walk(..., i, j + 1, left - 1, curr, nums2[j])The same, taking from nums2.

8Dry run: nums1 = [4, 6, 7], nums2 = [1, 2, 5]

Recursion tree (a single chain). Each node shows walk(i, j, left, prev, curr) and what it returns:

walk(0,0, left=4, prev=0, curr=0) → (4,5)
   |  1 < 4 → take 1 from nums2
walk(0,1, left=3, prev=0, curr=1) → (4,5)
   |  2 < 4 → take 2 from nums2
walk(0,2, left=2, prev=1, curr=2) → (4,5)
   |  4 <= 5 → take 4 from nums1
walk(1,2, left=1, prev=2, curr=4) → (4,5)
   |  5 < 6 → take 5 from nums2
walk(1,3, left=0, prev=4, curr=5) → (4,5)   ← base case
  1. Going down: each call compares the two fronts, takes one number, and calls the next with left one smaller. Each call is pushed and waits.
  2. The 5th call has left = 0 → returns (4, 5).
  3. Coming up: every call passes (4, 5) back unchanged. Nothing is computed on the way up.
  4. total = 6 is even → (4 + 5) / 2 = 4.5 ✓.
after 3 calls
walk(0,0,4)walk(0,1,3)walk(0,2,2)
deepest: 5 calls
walk(0,0,4)walk(0,1,3)walk(0,2,2)walk(1,2,1)walk(1,3,0) → (4,5)

9Complexity & remember

RememberA loop that takes one item per round becomes a recursion that is as deep as the number of rounds. Watch Python's ~1000 limit.

Part D · Going further: split both arrays by binary search

Not taught in this video. The teacher only mentions that, because the arrays are sorted, binary search gives the most optimized code. Here it is, written as divide and conquer recursion. Learn Parts A–C first.

1The question

The same question, but now in O(log(min(m, n))) time, which is what LeetCode's follow-up asks for.

2What the constraints tell us

m and n up to 1000, so log₂ 1000 ≈ 10 steps. Either array may be empty, so the code must deal with "nothing on this side".

3Intuition: think about the split, not the merge

Picture the merged sorted list cut into a left half and a right half. If the count is odd, the left half gets the extra one:

merged:   1  2  4 | 5  6  7          left half = 3 numbers
                    ↑ the cut

The median comes straight from the numbers next to the cut: the largest on the left (odd count), or that number averaged with the smallest on the right (even count).

Now the key observation. The left half is "the smallest half numbers overall". Since each array is sorted, its smallest numbers are at its front. So the left half is always some front part of nums1 plus some front part of nums2:

nums1:   4        |  6  7        i = 1 number from nums1
nums2:   1  2     |  5           j = 2 numbers from nums2
                                 i + j = 3 = half  ✓

So the whole problem becomes: how many numbers (i) does nums1 put into the left half? Once i is chosen, j = half − i is forced. And i can only be 0, 1, …, m, so we can binary search on i.

4Building the logic: how to tell whether a split is right

Call the four numbers next to the cut:

Inside each array the order is already correct (a_left ≤ a_right, b_left ≤ b_right). The split is right only if everything on the left ≤ everything on the right, and that needs the two cross checks:

checkif it fails…so…
a_left <= b_righta number from nums1 on the left is bigger than one from nums2 on the right → we took too many from nums1search smaller i: hi = i − 1
b_left <= a_rightnums2 put a number on the left that's bigger than one nums1 put on the right → we took too few from nums1search bigger i: lo = i + 1
both passcorrect split → median from max(a_left, b_left) and min(a_right, b_right)
Doubt 1: what if the cut is at the very start or end of an array, so nums1[i − 1] or nums1[i] doesn't exist?
→ Use −∞ for a missing left neighbour and +∞ for a missing right neighbour (float('-inf'), float('inf')). "Nothing on the left" can never be too big, and "nothing on the right" can never be too small, so the checks still work. This also covers an empty array.
Doubt 2: why binary search on the smaller array?
→ j = half − i must be a valid count for nums2 (0 to n). If nums1 is the shorter one, then for every i from 0 to m, j stays inside 0…n, so we never read outside nums2. It's also faster: log of the smaller size. So we swap the arrays first if needed.
Doubt 3: where is the "not found" base case, like left > right in binary search?
→ A correct split always exists, because the merged list really can be cut. So the search always stops at the "both checks pass" case before the range gets empty. That's the base case here.

half = (m + n + 1) // 2. The "+1" puts the extra number on the left when the total is odd, so then the median is simply max(a_left, b_left).

5Approach steps

  1. Make nums1 the shorter array (swap if needed). half = (m + n + 1) // 2.
  2. cut(lo, hi): i = (lo + hi) // 2, j = half − i.
  3. Read a_left, a_right, b_left, b_right (with ±∞ at the edges).
  4. Both checks pass → odd: max(a_left, b_left); even: (max(a_left, b_left) + min(a_right, b_right)) / 2.
  5. a_left > b_right → cut(lo, i − 1). Else → cut(i + 1, hi).

6Code (Python)

Binary search on the split, recursive (beyond the video)
class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        if len(nums1) > len(nums2):            # binary search the shorter one
            nums1, nums2 = nums2, nums1
        half = (len(nums1) + len(nums2) + 1) // 2
        return self.cut(nums1, nums2, 0, len(nums1), half)

    def cut(self, a, b, lo, hi, half):
        i = (lo + hi) // 2                     # how many from a go left
        j = half - i                           # the rest come from b
        a_left = a[i - 1] if i > 0 else float('-inf')
        a_right = a[i] if i < len(a) else float('inf')
        b_left = b[j - 1] if j > 0 else float('-inf')
        b_right = b[j] if j < len(b) else float('inf')
        if a_left <= b_right and b_left <= a_right:      # correct split
            if (len(a) + len(b)) % 2 == 1:
                return float(max(a_left, b_left))
            return (max(a_left, b_left) + min(a_right, b_right)) / 2
        if a_left > b_right:                   # too many from a
            return self.cut(a, b, lo, i - 1, half)
        return self.cut(a, b, i + 1, hi, half) # too few from a

7Code line by line

linewhat it means
if len(nums1) > len(nums2): swapSo that i ranges over the shorter array and j always stays valid.
half = (m + n + 1) // 2The size of the left half (it gets the extra number when the total is odd).
i = (lo + hi) // 2; j = half - iDivide: try the middle choice for i. j is then fixed.
a_left = ... else float('-inf')The four neighbours of the cut, with ±∞ when a side is empty.
if a_left <= b_right and b_left <= a_right:Everything left ≤ everything right → this is the median cut (base case).
return self.cut(a, b, lo, i - 1, half)Conquer only the half of the i-range that can contain the answer: fewer from a.
return self.cut(a, b, i + 1, hi, half)More from a.

8Dry run

nums1 = [1, 3, 8, 9, 15] (m = 5), nums2 = [7, 11, 18, 19, 21, 25] (n = 6). nums1 is already the shorter. total = 11 (odd), half = 6. (Merged: 1 3 7 8 9 11 15 18 19 21 25 → the median is 11.)

cut(lo=0, hi=5)  i=2, j=4                         → 11
   |   left:  nums1 [1 3]        nums2 [7 11 18 19]
   |   a_left=3  a_right=8   b_left=19  b_right=21
   |   b_left 19 > a_right 8  →  too few from nums1, go right
cut(lo=3, hi=5)  i=4, j=2                         → 11
       left:  nums1 [1 3 8 9]    nums2 [7 11]
       a_left=9  a_right=15  b_left=11  b_right=18
       9 <= 18 ✓ and 11 <= 15 ✓  →  correct split
       odd → max(9, 11) = 11   ← base case
  1. cut(0, 5): try i = 2 → j = 4. The left side would hold 19, but the right side holds 8. 19 > 8 is wrong, so nums1 must give more. Throw away i = 0…2 and call cut(3, 5).
  2. cut(3, 5): try i = 4 → j = 2. Both cross checks pass. The total is odd → return max(9, 11) = 11.
  3. cut(0, 5) returns 11 unchanged. Answer 11.0 ✓.
deepest: 2 calls
cut(0,5) i=2cut(3,5) i=4 → 11

An example with an edge: nums1 = [5, 6, 7, 8, 9], nums2 = [4] → swapped so a = [4], b = [5, 6, 7, 8, 9], half = 3. cut(0, 1): i = 0, j = 3 → a_left = −∞, a_right = 4, b_left = 7 > 4 → go right. cut(1, 1): i = 1, j = 2 → a_left = 4, a_right = +∞, b_left = 6, b_right = 7 → both pass → even → (max(4, 6) + min(+∞, 7)) / 2 = (6 + 7) / 2 = 6.5 ✓ (the same as the teacher's example in Part B).

9Complexity & remember

Remember the splitLeft half = front of nums1 (i numbers) + front of nums2 (half − i). Binary search i on the shorter array until a_left ≤ b_right and b_left ≤ a_right. Use ±∞ at the edges.

Part E · Revision page

A · join + sortB · merge walk (video)C · walk as recursionD · binary search split
uses "already sorted"?noyesyesyes
how the problem is split—stop the merge at the middle; never look at the second halfeach call takes one number, the rest is a smaller callchoose how many of nums1 go into the left half
timeO((m+n) log(m+n))O(m + n)O(m + n)O(log min(m, n))
spaceO(m + n)O(1)O(m + n) stackO(log min(m, n)) stack
If you remember only 5 lines 1. Median: odd → the middle; even → the average of the two middles.
2. Two sorted arrays → the merge step of merge sort (take the smaller front, move that pointer).
3. Take only total // 2 + 1 numbers, keeping prev and curr.
4. When one array is used up, keep taking from the other.
5. Optimal: binary search the number of nums1 elements in the left half, checking the cross pairs.
Mistakes to avoid ✗ joining the arrays and assuming the result is sorted
✗ looping total // 2 times instead of total // 2 + 1 (one short)
✗ forgetting prev = curr before taking the new number
✗ reading nums1[i] / nums2[j] after that array is used up (or when it's empty)
✗ integer division for the even average: use / 2, not // 2
✗ in the recursive walk, forgetting sys.setrecursionlimit (up to 1001 calls deep)
test it yourself (paste under any Solution above)
s = Solution()
print(s.findMedianSortedArrays([1, 3], [2]))           # 2.0
print(s.findMedianSortedArrays([1, 2], [3, 4]))        # 2.5
print(s.findMedianSortedArrays([4, 6, 7], [1, 2, 5]))  # 4.5
print(s.findMedianSortedArrays([1, 2], [3, 4, 5, 6]))  # 3.5
print(s.findMedianSortedArrays([5, 6, 7, 8, 9], [4]))  # 6.5
print(s.findMedianSortedArrays([], [7]))               # 7.0

Based on this video: Median of Two Sorted Arrays | Divide and Conquer (merge approach)