عرض عملي: سباق بين البحث الخطي والبحث الثنائي
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In the last lesson we counted steps and predicted that binary search would pull far ahead of linear search as arrays grow. Now we measure it. We build sorted arrays of 1,000, 100,000, and 10,000,000 elements, time both searches with System.nanoTime(), and print a table. We also make the planned mistake from this module on purpose: running binary search on unsorted data.
This demo is the practice run for the module's lab, "Race the algorithms". Follow it step by step, then do the lab on your own.
Why this matters: real engineering mixes reasoning (counting) with measurement (timing). Each checks the other. You will also learn why timings must be repeated and read carefully, a habit that prevents wrong conclusions in performance work.
This lesson builds on linear search, binary search, step counting, and Arrays.sort from the arrays utility class.
Learning Objectives
By the end of this lesson you will be able to:
- Build a large sorted array with a loop.
- Time a piece of code with
System.nanoTime()by subtracting two readings. - Search for a missing value to force the worst case.
- Repeat measurements and explain why the first run can be an outlier.
- Present results in a table and describe the trend in words.
- Demonstrate the wrong-answer bug of binary search on unsorted data, then fix it by sorting.
- State the limits of such measurements.
Written Explanation
Step 1: build a sorted array
We fill the array with even numbers, a[i] = i * 2. It is sorted by construction, and every odd number is guaranteed to be missing, so searching for 1 forces both algorithms to do the maximum work for the size.
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = i * 2;At 10,000,000 elements the array holds 40 million bytes of numbers (4 bytes per int), about 40 MB. That is fine for a normal laptop with the default Java settings, but if your machine reports an OutOfMemoryError, use 1,000,000 instead and say so in your results.
Step 2: time the search
System.nanoTime() returns a time in nanoseconds (billionths of a second). Its documentation says it can only be used to measure elapsed time: the starting point is arbitrary, so a single reading means nothing, but the difference between two readings from the same program does. It also warns that precision is not the same as resolution: values may change less often than every nanosecond, and the resolution is at least as good as currentTimeMillis(). So tiny durations are only rough.
long t0 = System.nanoTime();
int r1 = linearSearch(a, 1);
long t1 = System.nanoTime();
int r2 = binarySearch(a, 1);
long t2 = System.nanoTime();
System.out.println(n + " | " + (t1 - t0) + " | " + (t2 - t1));We store the results r1 and r2 and later use them, for example by printing, so that the work is not quietly skipped. Both searches use the methods from the earlier lessons without counters.
Step 3: repeat, warm up, and read with care
The first run of Java code is often slower, because the Java runtime has not yet optimised it. Run the same measurement several times and look at the later runs. For the demo we also run both methods thousands of times on a small array before the real test (a "warm-up"). That is an informal technique, not a rigorous benchmark.
Results on one Windows machine with JDK 21 (your numbers will differ in size, and between runs):
| n | run | linear (ns) | binary (ns) |
|---|---|---|---|
| 1,000 | 1 | 600 | 47,800 |
| 1,000 | 2 | 1,100 | 1,300 |
| 1,000 | 3 | 500 | 1,000 |
| 100,000 | 1 | 25,100 | 1,600 |
| 100,000 | 2 | 24,400 | 1,600 |
| 100,000 | 3 | 23,900 | 1,300 |
| 10,000,000 | 1 | 3,511,000 | 20,600 |
| 10,000,000 | 2 | 4,063,200 | 6,300 |
| 10,000,000 | 3 | 4,142,400 | 6,400 |
Reading the table with care:
- n = 1,000. Both are tiny, around a microsecond (a thousand nanoseconds), and the numbers jump around. Binary search's first run at 47,800 ns is an outlier, more than forty times its later runs: this is the sort of one-off delay that makes a single timing misleading. At this size, timing cannot separate the two.
- n = 100,000. Linear search needs about 24,000 ns, binary about 1,500 ns. The gap is now clear.
- n = 10,000,000. Linear search needs about 4 million ns (4 ms), binary search about 6,000 ns. Binary search is hundreds of times faster here.
Step 4: explain the trend
Linear search grows with the data: 100 times more elements (1,000 to 100,000) made it roughly 20 to 50 times slower in this run, and another 100 times (to 10,000,000) made it about 170 times slower. It does not grow exactly in proportion because very small timings are noisy and because memory behaviour changes with array size, but the direction is clear and matches the count: 1,000 versus 100,000 versus 10,000,000 comparisons.
Binary search barely changes. Halving predicts at most about 10 looks at 1,000, about 17 at 100,000, and about 24 at 10,000,000 (the 1,000 case was counted earlier; the other two come from halving). Its times change by a small factor while the data grows ten thousand times. That agreement between the counts and the timings is the point of doing both.
A good written explanation says: (1) what was measured, (2) the table, (3) the trend (linear grows with n, binary almost flat), (4) a link to the step counts, and (5) a note about the limits (noise, one machine, one kind of target).
Step 5: the planned mistake
Now break it on purpose:
int[] u = {40, 10, 30, 20, 50, 60, 5};
System.out.println("unsorted: " + binarySearch(u, 5));
int[] s = u.clone();
Arrays.sort(s);
System.out.println(Arrays.toString(s) + " -> " + binarySearch(s, 5));Output:
unsorted: -1
[5, 10, 20, 30, 40, 50, 60] -> 0The value 5 is in the array, at index 6, but binary search on the unsorted array reports -1. The looks are: mid = 3 holds 20, larger than 5, so everything from index 3 on is discarded (including the 5 at index 6); mid = 1 holds 10, larger than 5, so the right side is discarded again; mid = 0 holds 40, larger than 5, and the range is empty. The search never looked at the end, where the 5 was hiding. After Arrays.sort, the same call finds 5 at index 0.
The lesson is that there is no error message. The program runs, prints a plausible answer, and is wrong. A search that returns "not found" for a value that exists is much more dangerous than one that crashes.
Examples
Example 1: a minimal timing harness (simple)
public static void main(String[] args) {
int n = 1_000_000;
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = i * 2;
long t0 = System.nanoTime();
int pos = linearSearch(a, 1);
long t1 = System.nanoTime();
System.out.println("result " + pos + ", " + (t1 - t0) + " ns");
}The printed result is -1 and a time in nanoseconds that varies from run to run. Seeing the result printed is also your correctness check.
Example 2: the full race table (practical)
Wrap the three sizes in a loop and print one line per run, as in the table above. If you want milliseconds, divide by 1,000,000.0 (a double), not by 1,000,000 with integer division, because integer division throws away the fraction and would turn 6,300 ns into 0 ms.
Example 3: spotting a bad benchmark (realistic)
A teammate reports: "Binary search at 1,000 elements took 47,800 ns, but at 100,000 it took only 1,600 ns, so bigger arrays are faster." What is wrong?
- It uses a single cold run for the first number, and the first run includes start-up effects.
- Timings at this size are close to the clock's resolution.
- Binary search does not get faster with more data. The step counts say 9 looks versus 17, so more work is expected, not less.
The fix is to repeat the measurement, discard the first run or warm up first, and compare the trend over sizes that are far apart. This is the same habit of "check the claim against the counts" that you used earlier.
Backend Insight
Backend teams use measurements all the time: how long does an endpoint take, how does it behave as data grows? Two lessons from this demo carry over. First, measure realistic sizes: a query that is fast with 1,000 test rows can be slow with 10 million, just as linear search was. Second, do not trust one timing: repeat, look at the spread, and compare against what the reasoning predicts. Serious performance tests use proper benchmarking tools, which come later in the course path; the tiny harness here is for learning the idea, not for rigorous results.
Common Mistakes
Mistake 1: Timing with a single run
One reading can be an outlier, as the 47,800 ns run showed. Repeat at least three times and look at the pattern.
Mistake 2: Searching for a value that is present
If the target is at the start of the array, linear search finishes at once and looks as fast as binary search. For the race, search for a missing value so each algorithm does its worst case.
Mistake 3: Binary search on unsorted data
It runs without errors and gives wrong answers. Sort first or confirm the array is sorted by construction.
Mistake 4: Reading nanoseconds as exact
System.nanoTime() has nanosecond units but not necessarily nanosecond resolution. Small differences (a few hundred ns) are noise.
Mistake 5: Including setup in the timing
Building the array inside the timed section measures the loop that fills it, not the search. Take t0 immediately before the search and t1 immediately after it.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why do we search for a missing value in the race?
أظهر الحل A1
A1. A missing value forces the worst case: linear search must check every element, and binary search must keep halving until the range is empty. A value near the start would make linear search look unrealistically fast.
A2. What does t1 - t0 represent in the harness above, and why is a single System.nanoTime() value meaningless on its own?
أظهر الحل A2
A2. It is the elapsed time in nanoseconds between the two readings. A single reading counts from an arbitrary origin, so only the difference between two readings in the same program has meaning.
A3. The array a[i] = i * 2 has 1,000 elements. What is its last element?
أظهر الحل A3
A3. 1,998 (index 999 times 2).
Level B: Application
B1. Write the loop that builds a sorted array of n elements with values 5, 10, 15, .... Which values are certainly missing?
أظهر الحل B1
B1.
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = (i + 1) * 5;Every value that is not a multiple of 5 (and every value below 5 or above n * 5) is missing, for example 1, 2, 3, 4, 6.
B2. Your run at n = 100,000 shows linear 24,000 ns and binary 1,500 ns. About how many times faster is binary search? Do the division.
أظهر الحل B2
B2. 24,000 / 1,500 = 16 times faster.
B3. Explain, using the counts from the previous lesson, why you expect binary timing to grow only slightly from 100,000 to 10,000,000 elements.
أظهر الحل B3
B3. Binary search needs about 17 looks at 100,000 and about 24 at 10,000,000. The data grew 100 times, but the count went up by only about 7 looks, so the time should grow very little.
Level C: Challenge
C1. Write a method static boolean isSorted(int[] a) (from the first lesson of this module) and use it in main to refuse to run the binary search timing if the array is not sorted. Test with a sorted and an unsorted array.
أظهر الحل C1
C1.
static boolean isSorted(int[] a) {
for (int i = 0; i + 1 < a.length; i++) {
if (a[i] > a[i + 1]) return false;
}
return true;
}In main: if (!isSorted(a)) { System.out.println("array is not sorted: binary search not allowed"); return; } placed before the timing. For {2, 5, 9} it lets the timing run; for {2, 9, 5} it prints the message. The method costs n - 1 comparisons, so keep it outside the timed section.
C2. Write the explanation paragraph (about five sentences) for a results table with the three sizes, mentioning the trend, the link to step counts, and one limitation.
أظهر الحل C2
C2. A sample answer: "I searched for a missing value in sorted arrays of 1,000, 100,000, and 10,000,000 elements and timed linear and binary search three times each. Linear search grew from about a microsecond to about 4 milliseconds, while binary search stayed in the range of a few microseconds. This matches the step counts: linear search compares every element, so it needs n comparisons, but binary search halves the range and needs roughly 10, 17, and 24 looks. At 1,000 elements the times were too small and noisy to tell the algorithms apart. These results come from one machine, one target, and a simple timing method, so exact numbers would change elsewhere, but the trend should not."
Mini Challenge
Run the race yourself as in the demo, with the three sizes and at least three runs each. Add a fourth size of your choice. Print a table, mark any outlier, then write the five-sentence explanation. Finish by trying the unsorted mistake with a value you know exists, and record what the program returned.
Check your work: the table has several runs per size, the target is missing, your explanation mentions both the trend and a limitation, and the unsorted experiment shows a wrong answer that disappears after sorting.
Summary & Next Lesson
Key takeaways
- Build sorted test data with a loop, search for a missing value, and measure with
System.nanoTime()readings taken immediately before and after the search. - Repeat measurements. The first run can be an outlier, and tiny times are noisy.
- On this machine linear search went from about a microsecond to a few milliseconds as the array grew to 10 million elements, while binary search stayed near microseconds, which agrees with the step counts.
- Binary search on unsorted data gives wrong answers without any error. Sort first.
- Present results as a table plus a written explanation of the trend and its limits.
Next lesson
You have finished the algorithms module. The next module begins with "A method for debugging", which teaches a calm, repeatable way to find out why a program does not do what you expect.
Self-check
- Why search for a missing value in a race?
- What do two
System.nanoTime()readings give you when subtracted? - Why should you repeat a timing?
- How did the trend differ between linear and binary search as
ngrew? - What goes wrong when binary search is run on unsorted data?
أظهر الإجابات
- It forces each algorithm to do its worst-case work.
- The elapsed time between them, in nanoseconds.
- A single run can be an outlier because of start-up effects and clock noise.
- Linear search time grew roughly with
n; binary search time stayed almost flat. - It can discard the half that holds the target and report "not found" for a value that is present, with no error message.
References
- Oracle, System (Java SE 21 API).
nanoTime()is for measuring elapsed time, has an arbitrary origin so only differences are meaningful, and offers nanosecond precision but not necessarily nanosecond resolution. - Oracle, Arrays (Java SE 21 API).
binarySearchrequires a sorted array and gives undefined results otherwise;sortsorts into ascending order. - The timing table is one real run of the demo program on JDK 21 on a Windows laptop. Timings vary between machines and runs, and the warm-up approach is informal, not a rigorous benchmark.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.