Why this matters
The median is the middle value of a set of numbers. It is a common measure in data work because it is not moved much by a few very large or very small values. Finding the median of one sorted list is easy, because you read the middle position. Finding the median of two sorted lists together is a classic interview problem, and it teaches a search method that is useful well beyond medians.
The obvious method merges the two lists into one sorted list and reads the middle. That takes time proportional to the total size of both lists. This lesson builds a faster method, which runs in O(log(min(m, n))) time, where m and n are the two lengths. It does not merge anything. It uses binary search to find where to cut each list, so that the cuts split the combined values into two equal groups.
The idea is related to the binary search lessons earlier in this module. This lesson assumes you already know how to halve a range until the answer is found.
What the median is
The median of a list is the value in the middle after sorting. If the list has an odd number of values, the median is the single middle value. If the list has an even number of values, the median is the average of the two middle values.
Here are three small examples.
1 2 3has median 2.1 2 3 4has median (2 + 3) / 2 = 2.5.1 3and2taken together are1 2 3, so the median is 2.
The task is to find this median when the two input lists are each already sorted, and when they are kept separate. For example, 1 3 and 2 are two sorted lists, and the answer is 2 even though the two lists are never merged.
The slow method first
Merge the two sorted lists into one sorted list, using the merge step from the merge sort lesson. Then read the middle value or the two middle values. If the lists have lengths m and n, the merge step takes m + n steps. This method is correct and easy to explain, but its time grows with the total size. For two lists of a million values each, it looks at two million values.
The faster method below does not look at most of the values. It needs only about log2(m) steps, where m is the length of the smaller list. For a million values in the smaller list, that is about 20 steps.
The big idea: cut both lists
Think of each sorted list as a row of numbers. Cut each row into a left part and a right part. A cut can be placed anywhere, including before the first value or after the last value.
Suppose the combined left parts hold exactly half of all values, and the combined right parts hold the other half. If every value on the left side is no larger than every value on the right side, then the median sits at the boundary between the two sides.
- For an odd total, the median is the largest value on the left side.
- For an even total, the median is the average of the largest value on the left side and the smallest value on the right side.
The search is now a search for cuts. Choose a cut in the first list, then choose a cut in the second list so that the left parts have the right number of values. Then check the boundary. If the largest left value of one list is bigger than the smallest right value of the other list, the first cut is too far right. Move it left. If not, move it right.
Choosing the cuts
Let the first list be a with length m, and the second list be b with length n. Let i be the number of values from a on the left side. The number of values from b on the left side must then be j = leftCount - i, where leftCount is the number of values the left side needs. For a total of m + n values, set leftCount to (m + n + 1) / 2. This works for both odd and even totals, and it puts the extra value on the left side when the total is odd.
Once i is chosen, j is fixed. Four values describe the boundary.
aLeft, the last value ofaon the left side. Ifiis 0, there is no such value, so use the smallest possible int as a sentinel, which is a stand-in value for "no value here".aRight, the first value ofaon the right side. Ifiis m, there is no such value, so use the largest possible int.bLeft, the last value ofbon the left side. Ifjis 0, use the smallest possible int.bRight, the first value ofbon the right side. Ifjis n, use the largest possible int.
The cut is valid when aLeft <= bRight and bLeft <= aRight. Those two checks say that every value on the left side is no larger than every value on the right side. Because each list is sorted, these two checks are enough.
If the cut is not valid, there is one of two problems. If aLeft > bRight, the cut in a is too far right, so i must go down. Otherwise, bLeft > aRight, the cut in a is too far left, so i must go up. This is the step that makes the search a binary search.
Why search the smaller list
The value i runs from 0 to m. That range has m + 1 choices, so a binary search over it takes about log2(m + 1) steps. If a is the smaller list, that is the fastest choice.
There is a second reason to put the smaller list first. The count j = leftCount - i must stay between 0 and n for a valid index into b. When m is less than or equal to n, the value of leftCount - i stays in that range for every i from 0 to m. If the larger list were searched instead, j could fall below 0 or above n, and the program would read a value that does not exist.
So the code first swaps the two lists when a is longer than b. The swap does not change the median, because the median depends only on the values together.
Worked example 1: 1 3 and 2
Here a is 1 3 with m = 2, and b is 2 with n = 1. The total is 3, so leftCount is (3 + 1) / 2 = 2. The search range for i is from 0 to 2.
- Start with
lo = 0andhi = 2. The middle isi = 1, soj = 2 - 1 = 1. aLeftisa[0], which is 1.aRightisa[1], which is 3.bLeftisb[0], which is 2.bRightis past the end ofb, so it is the largest int.- Check the cut.
aLeft <= bRightis1 <= large, which is true.bLeft <= aRightis2 <= 3, which is true. The cut is valid. - The total is odd, so the median is the larger of the two left values, which is max(1, 2) = 2.
The left side holds 1 2 and the right side holds 3. The median is 2.
Worked example 2: 1 2 and 3 4
Now the total is even, so the median is an average. Here m = n = 2 and leftCount is (4 + 1) / 2 = 2.
- Start with
lo = 0andhi = 2. The middle isi = 1, soj = 1. aLeftis 1,aRightis 2.bLeftisb[0], which is 3, andbRightisb[1], which is 4.- Check the cut.
aLeft <= bRightis1 <= 4, which is true.bLeft <= aRightis3 <= 2, which is false. The cut is not valid. - Because the failure is
bLeft > aRight, the cut inais too far left, so setlo = i + 1 = 2. - The new middle is
i = 2, soj = 0. NowaLeftis 2,aRightis the largest int becauseiequals m.bLeftis the smallest int becausejis 0, andbRightisb[0], which is 3. - Check the cut.
aLeft <= bRightis2 <= 3, which is true.bLeft <= aRightis true, because the smallest int is at most anything. The cut is valid. - The total is even, so the median is the average of max(aLeft, bLeft) and min(aRight, bRight). That is (2 + 3) / 2 = 2.5.
The left side holds 1 2 and the right side holds 3 4, so the median is between 2 and 3.
Worked example 3: -5 0 5 10 and -3 8
This example is harder for two reasons. The first list is longer than the second, so the code swaps the lists. The total is even.
After the swap, a is -3 8 with m = 2, and b is -5 0 5 10 with n = 4. The total is 6, so leftCount is (6 + 1) / 2 = 3.
- Start with
lo = 0andhi = 2. The middle isi = 1, soj = 3 - 1 = 2. aLeftis -3,aRightis 8.bLeftisb[1], which is 0, andbRightisb[2], which is 5.- Check the cut.
aLeft <= bRightis-3 <= 5, which is true.bLeft <= aRightis0 <= 8, which is true. The cut is valid on the first try. - The median is the average of max(-3, 0) = 0 and min(8, 5) = 5. That is (0 + 5) / 2 = 2.5.
Check by sorting all six values: -5 -3 0 5 8 10. The two middle values are 0 and 5, and their average is 2.5.
The C++ code
The program below has the search function and a main function with four examples. The search function is findMedianSortedArrays. It takes two spans, which are views of the two sorted vectors, and returns a double.
Compile and run it with these commands.
g++ -std=c++20 -Wall -Wextra -o median median.cpp
./medianThe output is:
2
2.5
9
2.5The four results match the hand traces. The third example has an empty first list. The empty list is the shorter one, so the code does not swap. The search runs on the empty list, with m = 0. The only cut is i = 0, so the median comes from the right list alone, which is 9 for 7 9 11.
A few details matter here.
std::span<const int>lets the function read both vectors without copying them. The caller passesa1directly, and the vector converts to a span.INT_MINandINT_MAXcome from<climits>. They stand in for "no value on this side", so the comparisons still work at the ends of the lists.- The average is computed in
double. The code casts each value before adding. If it added twointvalues first, a sum of two large values could overflow. The next section shows that problem. - The search loop stops when
lopasseshi. In a correct run, the loop always returns before that happens, so the finalthrowonly fires when the input is not sorted and the search cannot find a valid cut.
How fast is it?
The loop runs once per halving of the range lo to hi. That range starts with m + 1 values, so the loop runs about log2(m + 1) times. Each pass does a fixed amount of work, so the total time is O(log(min(m, n))). The swap at the top costs one comparison. The function uses a fixed amount of extra memory, because it keeps only a few integers.
Common mistakes
The first mistake is forgetting the swap. If a is the longer list, the value j = leftCount - i can go below 0 or above n. The program then reads outside the list. In this build of g++ 15, the span check stops the program. Running the function with the swap removed, with a set to 1 2 3 4 5 6 7 8 and b set to 9, printed this and stopped with exit code 134.
/usr/include/c++/15/span:302: constexpr std::span<_Type, _Extent>::element_type& std::span<_Type, _Extent>::operator[](size_type) const [with _Type = const int; long unsigned int _Extent = 18446744073709551615; reference = const int&; size_type = long unsigned int]: Assertion '__idx < size()' failed.Without the span check, the same mistake reads memory that does not belong to the list, and the answer is random. The check is a sign of the bug, so read the line that names the index and go back to the value of j.
The second mistake is getting leftCount wrong. If the code uses total / 2 instead of (total + 1) / 2, the left side does not get the extra middle value when the total is odd. Running that version on 1 3 and 2 printed 1, where the correct answer is 2. It also printed 1 for 1 2 3 and an empty list, where the correct answer is 2. The rule to remember is that the left side gets the extra value.
The third mistake is adding two int values before dividing by 2. With int x = INT_MAX and int y = INT_MAX, the expression (x + y) / 2 overflows. In C++, signed overflow is undefined behaviour, which means the program may do anything. A g++ build with -O0 printed -1, and the correct average is 2147483647. Converting to double before the addition avoids the problem.
The fourth mistake is forgetting <climits>. Without it, the compiler reports this error.
nolimits.cpp: In function ‘int main()’:
nolimits.cpp:5:13: error: ‘INT_MIN’ was not declared in this scope
5 | int x = INT_MIN;
| ^~~~~~~
nolimits.cpp:4:1: note: ‘INT_MIN’ is defined in header ‘<climits>’; this is probably fixable by adding ‘#include <climits>’
3 | #include <iostream>
+++ |+#include <climits>
4 | int main() {The fifth mistake is handling the edge cases with the wrong sentinel. If aLeft is set to 0 when i is 0, a list of negative numbers gets a wrong left value. The sentinel must be the smallest possible int, so that it never wins a comparison that it should lose.
Practice
Exercise 1. Find the median of 1 3 5 7 and 2 4 6 8 together. Give the answer as a number.
Show the answer
The eight values sorted are 1 2 3 4 5 6 7 8. The two middle values are 4 and 5, so the median is (4 + 5) / 2 = 4.5. Calling findMedianSortedArrays from this lesson on these two lists returns 4.5.
Exercise 2. One list is empty and the other is 1 2 3 4 5. What is the median? Explain how the search handles the empty list.
Show the answer
The median is 3. The empty list is the shorter one, so no swap happens, and m = 0. The search range for i is only 0. The cut is at i = 0, which means no values come from the empty list. Then j = leftCount is 3, which takes three values from the other list. The left side holds 1 2 3, so the median is max of the left values, which is 3.
Exercise 3. Trace the search for 1 3 and 2 by hand, using the rule from this lesson. Write down the value of i and j at each step, and say which check decides the answer.
Show the answer
The first step uses lo = 0 and hi = 2, so i = 1 and j = 1. The cut is valid, because 1 <= 2 and 2 <= 3. The median is max(1, 2) = 2. The only check that matters is the test that both sides are in order, because the cut is valid on the first step and no move of lo or hi is needed.
Exercise 4. Explain in plain words why the code searches the smaller list rather than the larger one.
Show the answer
The search runs over the number of values taken from the first list, from 0 to m. A smaller m means fewer steps, so the search takes about log2(m + 1) steps. The other reason is that the count taken from the second list, j = leftCount - i, must stay between 0 and n. When the first list is the smaller one, that count stays in range for every step. So the code swaps the lists first, and then searches the smaller one.
Exercise 5. For an odd total, the code returns max(aLeft, bLeft). Explain why it does not return min(aRight, bRight).
Show the answer
The left side has one more value than the right side when the total is odd, because leftCount is (total + 1) / 2. The median is the largest value on the left side, so it is max(aLeft, bLeft). The smallest value on the right side is larger than or equal to the median, so it is not the median in general. For example, with 1 2 3, the left side holds 1 2 and the right side holds 3. The median is 2, and min(aRight, bRight) would give 3.
Summary
- The median of two sorted lists is the middle value of the two lists taken together, and it does not need the lists to be merged.
- The search chooses how many values from the first list go on the left side, and then fixes the number from the second list.
- A cut is valid when every left value is no larger than every right value, which takes two comparisons.
- The search runs over the smaller list, which gives O(log(min(m, n))) time, and the code swaps the lists so that the count from the second list stays in range.
- Empty sides use the smallest or largest possible int as a sentinel, so comparisons still work at the ends.
- Convert to
doublebefore adding two values to avoid integer overflow.