This page briefly introduces the two-pointer technique.
Introduction
Two pointers are a simple and flexible technique. Used on its own, the technique easily solves certain problems; combined with other algorithms, it has many more applications.
As the name suggests, two pointers use two pointers at the same time. In sequences and linked lists they point to positions, while in trees and graphs they point to vertices. The pointers maintain or collect information by moving in the same direction or toward each other.
We now look at several concrete ways to use two pointers.
Maintaining interval information
Without combining it with another data structure, the simplest way to use two pointers for interval information is to maintain a quantity with suitable monotonicity, for which adding or removing one element is easy to handle, such as the sum of positive numbers or the product of positive integers.
Example 1
Example 1: LeetCode 713. Subarray Product Less Than K
Given an array of length of positive integers, denoted by , and an integer , find the number of contiguous subarrays whose product is less than .
Here .
Procedure
Let the two pointers be , and let record the product of all numbers in . Initially are both at the left end. Move to the right until for the first time, then hold fixed and move right until . For each , is the leftmost boundary to which the interval can extend. Because products of positive integers are monotone, the number of valid intervals ending at is .
Implementation
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
long long ji = 1ll, ans = 0;
int l = 0;
for (int i = 0; i < nums.size(); ++i) {
ji *= nums[i];
while (l <= i && ji >= k) ji /= nums[l++];
ans += i - l + 1;
}
return ans;
}
Two pointers can also be combined with other data structures, such as difference arrays, monotone queues, segment trees, and chairman trees. Mo's algorithm also incorporates the two-pointer technique: after sorting queries offline, it generally uses two pointers to record the current interval and updates its information as the pointers move step by step.
Example 2
Next is an example that uses two pointers on a tree together with tree difference arrays.
Example 2: Luogu P3066 Running Away From the Barn G
Given a rooted tree with vertices and weighted edges, numbered from 1 to with vertex 1 as the root, and a parameter , for every vertex find how many vertices in the subtree of are at distance not greater than from . The constraints are .
Procedure
Run a DFS from the root. Use a stack to record the tree chain from the root to the current vertex. Let pointer denote the current vertex, and let pointer denote the shallowest vertex among those whose distance from is not greater than . Record the distance to the root and determine by binary search each time. At this point, contributes to every vertex on the path from to , which can be recorded with tree differences. Do not move by brute force, or the time complexity may degrade to .
Exercises
LeetCode 1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit
Subsequence matching
Example 3: LeetCode 524. Longest Word in Dictionary through Deleting
Given a string and a string array , find and return the longest string in the dictionary that can be obtained by deleting some characters from .
Procedure
This type of problem matches strings and to determine whether is a subsequence of . Put pointer at the beginning of and pointer at the beginning of . If , the -th character of has found its first match in , so both and can be incremented. Otherwise the -th character of has not been matched, so increment only and continue searching in the remainder of . Finally, if has reached the position just past the end, the whole string has been matched and is a subsequence of ; otherwise it is not.
Implementation
string findLongestWord(string s, vector<string>& dictionary) {
sort(dictionary.begin(), dictionary.end());
int mx = 0, r = 0;
string ans = "";
for (int i = dictionary.size() - 1; i >= 0; i--) {
r = 0;
for (int j = 0; j < s.length(); ++j) {
if (s[j] == dictionary[i][r]) r++;
}
if (r == dictionary[i].length()) {
if (r >= mx) {
mx = r;
ans = dictionary[i];
}
}
}
return ans;
}
The method of putting two pointers on different objects and comparing them step by step is also useful in some dynamic-programming problems.
Using the ordering of a sequence
Two pointers often work correctly on a sequence because of one of its properties; the most common is that the sequence is ordered.
Example 4: LeetCode 167. Two Sum II — Input Array Is Sorted
Given an integer array numbers sorted in ascending order, find two numbers whose sum equals the target target.
Procedure
This is a classic application of two pointers. Binary search is also convenient, but it adds a factor of to the time complexity and produces less concise code.
To use two pointers, observe that the two numbers must occupy different positions, so one is on the left and the other on the right. Since their sum is fixed, increasing the smaller number requires decreasing the larger number. We can therefore approach the answer from both ends.
Initially suppose the answer is 1 and n. If , one of the numbers must decrease. cannot decrease any further, so decrement the pointer at and make the larger number smaller.
Similarly, if , one of the numbers must increase. cannot increase any further, so increment the pointer at 1 and make the smaller number larger.
In general, if the pointers are at with , decrement when , and increment when . Thus moves right and moves left until they approach an answer.
Implementation
vector<int> twoSum(vector<int>& numbers, int target) {
int r = numbers.size() - 1, l = 0;
vector<int> ans;
ans.clear();
while (l < r) {
if (numbers[l] + numbers[r] > target)
r--;
else if (numbers[l] + numbers[r] == target) {
ans.push_back(l + 1), ans.push_back(r + 1);
return ans;
} else
l++;
}
return ans;
}
Merging two sorted arrays in time during merge sort is another use of two pointers that relies on the arrays remaining ordered.
Exercises
Finding a cycle in a singly linked list
Procedure
There are several ways to find a cycle in a singly linked list, but the fast-and-slow two-pointer method is one of the simplest.
Initially both pointers point to the head. Move one pointer one step at a time and the other two steps at a time. If they meet, there is a cycle; otherwise there is none. The time complexity is .
If there is a cycle, how can we find its starting point?
Let us write the equations. Suppose that when the pointers meet, the slow pointer has taken steps in total and has taken steps on the cycle (when the pointers meet on the cycle, the slow pointer has not completed a full lap). The fast pointer has taken steps. Let the cycle length be . Then
At the first meeting, is the smallest positive integer 1, so . Therefore, after the pointers meet, move one pointer to the head and move both one step at a time. Their next meeting point is the start of the cycle.
Implementation
pair<bool, vector<int>> findCycle(
vector<int>
nxt) // nxt[i] is the node pointed to by i in the singly linked list (0-indexed); -1 means no outgoing edge
{
int fast = 0, slow = 0;
do // first determine whether a cycle exists
{
if (nxt[fast] == -1 || nxt[nxt[fast]] == -1) {
return make_pair(false, vector<int>()); // no cycle
}
fast = nxt[nxt[fast]];
slow = nxt[slow];
} while (fast != slow);
slow = 0;
while (slow != fast) // find the cycle entry
{
slow = nxt[slow];
fast = nxt[fast];
}
vector<int> cycle;
do // finally collect the entire cycle
{
cycle.push_back(slow);
slow = nxt[slow];
} while (slow != fast);
return make_pair(true, cycle);
}
The time complexity is .