Double pointer algorithm
Principle
Fast and slow pointer
The two-pointer technique is further divided into two categories, one is “fast and slow pointers”, and the other is “left and right pointers”. The former mainly solves problems in linked lists, such as typically determining whether a linked list contains a ring; the latter mainly solves problems in arrays (or strings), such as binary search.
The fast and slow pointers are generally initialized to point to the head node of the linked list. When advancing, the fast pointer is fast in front and the slow pointer is slow in the back, which skillfully solves some problems in the linked list.
Determine whether the linked list contains a ring.
This should belong to the most basic operation of linked lists. If readers already know this technique, they can skip it.
The characteristic of a single linked list is that each node only knows the next node, so a pointer cannot determine whether the linked list contains a ring.
If there is no ring in the linked list, then this pointer will eventually encounter a null pointer to indicate that the linked list is at the end, which is good to say, it can be judged that the linked list does not contain rings.
1 | boolean hasCycle(ListNode head) { |
However, if the linked list contains a ring, then the pointer will fall into an infinite loop, because there is no null pointer as the tail node in the ring array.
The classic solution is to use two pointers, one running fast and the other running slow. If it does not contain a ring, the fast pointer will eventually encounter null, indicating that the linked list does not contain rings; if it contains rings, the fast pointer will eventually exceed the slow pointer once and meet the slow pointer, indicating that the linked list contains rings.
1 | boolean hasCycle(ListNode head) { |
Knowing that the linked list contains a ring, return the starting position of this ring.
This problem is not difficult at all. It is a bit like a brain teaser. Let’s look directly at the code first.
1 | ListNode detectCycle(ListNode head) { |
You can see that when the fast and slow pointers meet, let either pointer point to the head node, and then let them both move forward at the same speed. When they meet again, the node position is the position where the ring starts. Why is this?
At the first encounter, if the slow pointer slow took k steps, then the fast pointer fast must have taken 2k steps, that is, k steps more than slow (that is, the length of the ring).
Let the distance between the meeting point and the starting point of the ring be m, then the distance between the starting point of the ring and the head node head is k - m, that is, if you advance k - m steps from head, you can reach the starting point of the ring.
Coincidentally, if you continue to advance k - m steps from the meeting point, you also happen to reach the starting point of the ring.
So, as long as we point either of the fast or slow pointers back to head, and then the two pointers move forward at the same speed, they will meet after k - m steps, and the place where they meet is the starting point of the ring.
Find the middle point of the linked list.
Similar to the above idea, we can also make the fast pointer advance two steps at a time, and the slow pointer advance one step at a time. When the fast pointer reaches the end of the linked list, the slow pointer is in the middle of the linked list.
1 | while (fast != null && fast.next != null) { |
When the length of the linked list is odd, slow happens to stop at the midpoint; if the length is even, the final position of slow is right-of-center:
An important function of finding points in a linked list is to merge and sort the linked list.
Recall the merging sort of arrays: find the midpoint index recursion divides the array into two parts, and finally merges two ordered arrays. For linked lists, merging two ordered linked lists is very simple, and the difficulty lies in binary.
But now you have learned to find the midpoint of the linked list, you can achieve the two points of the linked list. The specific content of merging and sorting is not specifically expanded in this article.
** 4. Find the k-th element from the bottom of the linked list **
Our idea is to use the fast and slow pointer, let the fast pointer take k steps first, and then the fast and slow pointers start to advance at the same speed. In this way, when the fast pointer reaches null at the end of the linked list, the position of the slow pointer is the k-th penultimate linked list node (for simplicity, assuming k does not exceed the length of the linked list):
1 | ListNode slow, fast; |
Left and right pointers
The left and right pointers in the array actually refer to two index values, usually initialized as left = 0, right = nums.length - 1.
** 1, binary search **
The previous “binary search” has a detailed explanation, here only write the simplest binary algorithm, designed to highlight its double pointer characteristics:
1 | int binarySearch(int[] nums, int target) { |
** 2, the sum of two numbers **
Let’s take a look at a LeetCode topic directly: https://leetcode-cn.com/problems/two-sum/
As long as the array is ordered, you should think of the double-pointer trick. The solution to this problem is somewhat similar to binary search. By adjusting left and right, you can adjust the size of sum:
1 | int[] twoSum(int[] nums, int target) { |
** 3. Reverse array **
1 | void reverse(int[] nums) { |
** 4, sliding window algorithm **
This may be the highest state of the two-pointer technique. If you master this algorithm, you can solve a large class of substring matching problems, but the “sliding window” is slightly more complex than the above algorithms.
Example
Sum of three numbers:
https://leetcode-cn.com/problems/3sum/
1 | var threeSum = function (nums) { |
Link to the original text of the principle part:双指针技巧
Thanks to God Labuladong for his series of blogs, which really inspired me a lot.