المؤشران (Two pointers)
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Suppose you have a sorted list of prices and a gift card worth exactly 28. Do any two items add up to it? The obvious answer is a nested loop over every pair, which for a million prices means hundreds of billions of checks. A better way: put two walkers on the array, one at each end, and let them move toward each other. Each step throws away part of the problem, and the search takes one pass.
This technique is called two pointers. In Java a "pointer" here is just an int index such as l and r. You will use it to find pairs, reverse an array in place, and remove duplicates, and you will learn the condition it depends on: it silently gives wrong answers without it.
Why this matters: junior technical interviews often start with exactly this kind of array question, and the idea behind it (use the order in the data to skip work) shows up again in binary search, merging, and database indexes.
This lesson builds on arrays and loops (BEG-015, BEG-013) and on Big O notation (BEG-039). It is the first lesson of this module on algorithmic techniques.
Learning Objectives
By the end of this lesson you will be able to:
- Describe two pointers as two indexes moving through an array, and say when each of the two common shapes (opposite ends, same direction) is used.
- Write the pair-sum search on a sorted array and trace it step by step.
- Explain why the technique needs sorted data, and show a case where unsorted data gives a wrong answer.
- State the cost as O(n) time and O(1) extra space, and compare it with the nested-loop O(n²).
- Reverse an array in place and remove duplicates from a sorted array in place with two indexes.
- Choose between sorting first, a
Set, and two pointers for a given problem.
Written Explanation
The idea: two walkers on a path
One walker starts at the left end, one at the right end. At each step you decide which walker moves. A walker never goes back. When they meet, the search is over.
In code the walkers are two int variables:
int l = 0; // left walker
int r = a.length - 1; // right walker
while (l < r) {
// look at a[l] and a[r], then move exactly one of them
}The loop condition l < r means "the walkers have not met yet". Both walkers move in one direction only, so together they take at most n steps in total. That is where the O(n) cost comes from, as we will see below.
The pair-sum problem
Problem: the array a is sorted from small to large. Is there a pair of items whose sum is target?
The key observation uses the order of the data:
a[l]is the smallest item still in play.a[r]is the largest.- If
a[l] + a[r]is too small, thena[l]is too small to pair with anything still in play: even the largest partnera[r]is not enough. So we can discarda[l]by movinglright. - If the sum is too big, then
a[r]is too big to pair with anything still in play: even the smallest partner is too much. Discard it by movingrleft. - If the sum equals
target, we are done.
static boolean hasPair(int[] a, int target) {
int l = 0, r = a.length - 1;
while (l < r) {
int sum = a[l] + a[r];
if (sum == target) return true;
if (sum < target) l++; else r--;
}
return false;
}Every step discards one item for good, and we never need to look at it again. That is the whole trick: each decision removes a candidate, so no pair is tested twice.
Why the cost is O(n)
At the start the gap between the pointers is n - 1. Each loop iteration moves one pointer by one position, so the gap shrinks by exactly 1. The loop stops when the gap reaches 0, so it runs at most n - 1 times. Each iteration does a fixed amount of work. That is O(n) time. The method uses only a few int variables, so extra space is O(1). The nested-loop version checks about n × n / 2 pairs, which is O(n²) (BEG-039).
Why it needs sorted data
The argument "a[l] is too small to pair with anything" is only true because everything to the right of a[l] is at least as large as a[l], and everything is at most a[r]. If the array is not sorted, a[r] is not necessarily the largest item, so discarding a[l] can throw away the real answer. The technique does not crash on unsorted data. It just returns a wrong answer, which is worse.
You have three honest options when data is not sorted:
| Option | Time | Extra space | Note |
|---|---|---|---|
| Nested loops | O(n²) | O(1) | Simple, slow for large n |
| Sort first, then two pointers | O(n log n) | depends on the sort | Sorting dominates the cost; it also rearranges the data |
Store seen numbers in a HashSet | O(n) on average | O(n) | Does not need order; uses extra memory (BEG-041) |
Second shape: both pointers go the same way
A second shape has a read pointer that scans every item and a write pointer that marks where the next kept item goes. It compacts an array in place.
Remove duplicates from a sorted array. Because the array is sorted, equal items sit next to each other. The item at index 0 is always kept. The write pointer w is the next free slot, so the last kept item is a[w - 1]. For each item at r, if it differs from a[w - 1] it is new, so copy it to a[w] and advance w.
static int removeDuplicates(int[] a) {
if (a.length == 0) return 0;
int w = 1;
for (int r = 1; r < a.length; r++) {
if (a[r] != a[w - 1]) a[w++] = a[r];
}
return w; // the new length
}The array is modified in place and no second array is needed. The method returns how many items are now valid at the front; anything after that index is leftover and should be ignored.
Reversing in place
Reversing is two pointers at its simplest: swap the two ends, then move both inward.
static void reverse(int[] a) {
int l = 0, r = a.length - 1;
while (l < r) {
int t = a[l]; a[l] = a[r]; a[r] = t;
l++; r--;
}
}This one does not need sorted data, because it never makes a decision based on values. The "needs sorted" rule applies to techniques that decide which pointer to move by comparing values.
Examples
Example 1: tracing the pair-sum search (simple)
Array {1, 3, 4, 6, 9, 11}, target 10. I added a print line inside the loop and ran it.
int[] a = {1, 3, 4, 6, 9, 11};
System.out.println(Arrays.toString(findPair(a, 10)));Output:
l=0 r=5 sum=12
l=0 r=4 sum=10
[0, 4]Step 1: 1 + 11 = 12 is too big, so 11 cannot be part of an answer; move r left. Step 2: 1 + 9 = 10, found at indexes 0 and 4. Only two checks for six items.
Searching the same array for target 2 with hasPair(a, 2) prints false: every sum is at least 4, so r walks left until the pointers meet.
Example 2: unsorted data gives a wrong answer (practical)
The array {9, 1, 6, 3} is not sorted. A valid pair exists: 1 + 3 = 4. Run hasPair on it with target 4.
int[] u = {9, 1, 6, 3};
System.out.println(hasPair(u, 4));Output: false.
Trace: l=0, r=3: 9 + 3 = 12 is too big, so move r to index 2. 9 + 6 = 15, move r to 1. 9 + 1 = 10, move r to 0. The pointers meet and the method gives up. The pair 1 and 3 was never compared, because the first step discarded 3 on the false belief that it was the largest. Sort the array to {1, 3, 6, 9} and the same method finds 1 + 3.
Example 3: counting gift-card pairs (realistic)
A shop has sorted prices {5, 8, 12, 15, 20, 25} and the manager wants to know how many pairs of different items add up to exactly 28. After finding a pair, both pointers can move, because with distinct prices neither item can pair with anything else for the same total.
static int countPairs(int[] a, int target) {
int l = 0, r = a.length - 1, c = 0;
while (l < r) {
int s = a[l] + a[r];
if (s == target) { c++; l++; r--; }
else if (s < target) l++;
else r--;
}
return c;
}Output for the prices with target 28: 1 (the pair 8 + 20). For {1, 2, 3, 4, 5, 6} with target 7 the method returns 3 (1+6, 2+5, 3+4). Note the assumption "distinct values": with repeated values a pair could use the same number again, and this simple version would undercount. Always state your assumptions.
Backend Insight
Backend code deals with large sorted sequences all the time: sorted IDs, timestamps in logs, rows returned by a database ORDER BY. Two pointers over sorted data is the basis of merging two sorted lists and of finding the items two sorted lists have in common, in one pass and without extra memory.
The design habit: ask what order the data already has and use it. If you must sort first, remember the sort costs O(n log n), so two pointers beats O(n²) but not necessarily a HashSet solution.
Common Mistakes
Mistake 1: Using two pointers on unsorted data
The method runs and returns an answer, but it may be wrong (Example 2). Check "is it sorted?" before choosing the technique. If it is not, sort first or use a HashSet.
Mistake 2: Using l <= r for a pair problem
With l <= r, the pointers can land on the same index and a number gets paired with itself. For {5} and target 10 the loop would say "found". Pairs need two different positions, so use l < r. (Reverse is safe with l < r too: the middle item of an odd-length array stays where it is.)
Mistake 3: Forgetting the empty array
removeDuplicates reads a[0], so it checks a.length == 0 first. Think about empty and one-item inputs before submitting.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Trace hasPair on the sorted array {2, 5, 8, 12, 16, 23} with target 28. Write the values of l, r, and the sum at every step.
أظهر الحل A1
A1. Start l=0, r=5: 2 + 23 = 25, too small, so l++. Then l=1, r=5: 5 + 23 = 28, found. The pair is 5 and 23.
A2. How many loop iterations can hasPair run at most for an array of 1,000 items? Why is this not about 1,000 × 1,000?
أظهر الحل A2
A2. At most 999. Each iteration moves one pointer one step and the gap starts at 999 and shrinks by 1, so at most 999 iterations happen. The nested loop would check about half a million pairs; two pointers is one linear pass.
Level B: Application
B1. Run reverse by hand on {7, 2, 5, 1} and write the array after each swap.
أظهر الحل B1
B1. Start {7, 2, 5, 1}, l=0, r=3. Swap 7 and 1: {1, 2, 5, 7}. Now l=1, r=2. Swap 2 and 5: {1, 5, 2, 7}. Now l=2, r=1, the loop ends. Result {1, 5, 2, 7} (matches my run).
B2. Run removeDuplicates by hand on {1, 1, 2, 2, 2, 5}. Write r, w, and the array at each step where something is copied, and give the returned length.
أظهر الحل B2
B2. Start: w=1, array {1, 1, 2, 2, 2, 5}. r=1: 1 equals a[0], skip. r=2: 2 differs from a[0]=1, copy to a[1], w=2. r=3 and r=4: 2 equals a[1], skip. r=5: 5 differs from a[1]=2, copy to a[2], w=3. The method returns 3. The first three items are {1, 2, 5} (matches my run); the tail is leftover and ignored.
B3. The array {9, 1, 6, 3} made hasPair fail for target 4. Explain two ways to get a correct answer and the cost of each.
أظهر الحل B3
B3. (1) Sort the array first ({1, 3, 6, 9}) and then use two pointers: total O(n log n), because the sort costs more than the single pass. (2) Use a HashSet: for each number check whether target - number is already in the set, then add the number. It is O(n) on average, but needs O(n) extra memory.
Level C: Challenge
C1. Write isPalindrome(String s) using two index pointers and charAt, without creating a reversed copy. Give the cost.
أظهر الحل C1
C1.
static boolean isPalindrome(String s) {
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) return false;
l++; r--;
}
return true;
}It is O(n) time and O(1) extra space. The empty string and one-letter strings return true because the loop never runs.
C2. For sorted {1, 2, 3, 4, 5, 6} and target 7, what does countPairs do, step by step? Why does the counting version move both pointers after a match?
أظهر الحل C2
C2. l=0, r=5: 1 + 6 = 7, match, c=1, move both to l=1, r=4. 2 + 5 = 7, match, c=2, l=2, r=3. 3 + 4 = 7, match, c=3, l=3, r=2, the loop ends. Result 3 (matches my run). After a match, a[l] has found its partner, and with distinct values it cannot match anything else, so it is safe to discard both; moving only one pointer would make the next sum differ from the target anyway, so moving both is just the faster, correct move.
Mini Challenge
Write a method int closestToTarget(int[] sorted, int target) that returns the sum of a pair that is closest to target (it may be above or below). Use two pointers: at each step remember the best sum so far, then move the pointer exactly as in hasPair.
Check your work: test on {1, 3, 4, 6, 9, 11} with target 10 (the exact answer 10 should win), with target 100 (answer 20) and with target 0 (answer 4). State the time cost as a comment above the method.
Summary & Next Lesson
Key takeaways
- Two pointers uses two indexes that move through an array instead of nested loops.
- With opposite ends, each step discards one item for good, so a pair search is O(n) time and O(1) space.
- The value-comparing version only works on sorted data. On unsorted data it can silently return the wrong answer; sort first or use a
HashSet. - The read/write shape compacts an array in place, as in removing duplicates from a sorted array.
- Use
l < rfor pair problems and check empty and one-item inputs.
Next lesson
Two pointers moves two walkers that approach each other. The next lesson, "Sliding window", keeps both pointers moving forward and treats the range between them as a window that grows and shrinks to solve subarray and substring problems.
Self-check
- What does each of the two pointers represent in Java code?
- In a sorted pair-sum search, what do you do when the sum is too big, and why is that safe?
- Why does the pair-sum loop run at most n - 1 times?
- Why can two pointers give a wrong answer on
{9, 1, 6, 3}? - Name two ways to handle an unsorted pair-sum problem and their time costs.
أظهر الإجابات
- Each is an
intindex into the array, usually named likelandr. - Move the right pointer left. The right item is the largest one left in play, and even its smallest possible partner already gives too big a sum, so it cannot be part of an answer.
- The gap between the pointers starts at n - 1 and shrinks by one in every iteration, and the loop stops when the pointers meet.
- The array is not sorted, so the right pointer item is not necessarily the largest, and the method may discard an item that belongs to the answer.
- Sort first and then use two pointers (O(n log n)), or use a
HashSet(O(n) on average, with O(n) extra space). Nested loops also work at O(n²).
References
- Oracle,
HashSet(Java SE 21 API). It states thataddreturnsfalsewhen the element is already present and thatadd,remove, andcontainsrun in constant time when the hash function disperses elements well, which supports theHashSetalternative. - Oracle,
List(Java SE 21 API). Used for the behavior ofaddandremovein later lessons of this module. The correctness arguments and O(n) counts for two pointers above are this course's own analysis, not quoted from a source; all Java examples were compiled and run with JDK 21.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.