Double pointer algorithm - Introduction to the algorithm

Posted by clearstatcache on Sun, 30 Jan 2022 08:29:01 +0100

Double pointer algorithm - Introduction to the algorithm

Overview of double pointer algorithm

The double pointer algorithm should be a special traversal algorithm, which not only uses a single pointer to traverse, but uses double pointers. Note that the pointer here does not refer to pointers such as int *ptr. The double pointer algorithm can be roughly divided into two categories: one is the traversal of the relative direction of two pointers, which is called collision pointer, and the other is the traversal of two pointers in the same direction, which is called fast and slow pointer, Next, three details of the double pointer algorithm are introduced with examples: ① type determination ② pointer movement method ③ end condition

Collision pointer

Reverse string

Title Description: write a function that reverses the input string. The input string is given as a character array s.

Do not allocate additional space to another array. You must modify the input array in place and use the additional space of O(1) to solve this problem.

Input: s = ["h","e","l","l","o"]
Output:["o","l","l","e","h"]

Solution:

class Solution {
public:
    void reverseString(vector<char>& s) {
        int start = 0, end = s.size()-1;
        while (start < end) {
            swap(s[start], s[end]);
            start++;
            end--;
        }
        s.assign(s.begin(), s.end());
    }
};

Analysis: type determination: modifying the array in place means to solve the problem by traversal. Obviously, the collision pointer can solve this kind of problem well. The start and end pointers, collision traversal and exchange. The pointer movement method is to move step by step. When the end condition is start > end, the array traversal ends and the problem is solved very cleanly, Next, let's look at an advanced version of this problem

Rotation array

Title Description: give you an array, rotate the elements in the array to the right K positions, where k is a non negative number.

input: nums = [1,2,3,4,5,6,7], k = 3
 output: [5,6,7,1,2,3,4]
explain:
Rotate one step to the right: [7,1,2,3,4,5,6]
Rotate right for 2 steps: [6,7,1,2,3,4,5]
Rotate right for 3 steps: [5,6,7,1,2,3,4]

Solution:

class Solution {
public:
    void reverse(vector<int>& nums, int start, int end) {
        while (start < end) {
            swap(nums[start], nums[end]);
            ++start;
            --end;
        }
    }
    void rotate(vector<int>& nums, int k) {
        int lens = nums.size();
        k %= lens;
        reverse(nums, 0, lens-1);
        reverse(nums, 0, k-1);
        reverse(nums, k, lens-1);
        nums.assign(nums.begin(),nums.end());
    }
};

Analysis: if we follow the simple approach, we may need to open a new space to store the position after rotation, but through observation, we can find that rotating k%len positions to the right is equivalent to rotating [0,lens-1],[0,k-1],[k,lens-1] respectively, which greatly simplifies the problem and saves time and space, crisp and neat! So the double pointer is very flexible.

Speed pointer

Intermediate node of linked list

**Title Description: * * given a non empty single linked list whose head node is head, return the intermediate node of the linked list. If there are two intermediate nodes, the second intermediate node is returned.

Input:[1,2,3,4,5]
Output: node 3 in this list (Serialization form:[3,4,5])
The returned node value is 3. (The serialization expression of this node in the evaluation system is [3,4,5]). 
Notice that we returned a ListNode Object of type ans,So:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, as well as ans.next.next.next = NULL.

Solution:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
            ListNode* slow;
            ListNode* fast;
            slow = fast = head;
            while (fast != NULL && fast->next != NULL) {//fast != NULL must be added. Otherwise, when fast is empty, the memory at fast - > next overflows and an error is reported.
                slow = slow->next;
                fast = fast->next->next;
            }
            return slow;
    }
};

Analysis: of course, if we don't consider the problem of space, we can create a new array vector < linknode * > to save the whole linked list, and then output the intermediate nodes. However, if we want the spatial complexity of o(1), we can use the fast and slow pointer algorithm. The slow pointer takes one step at a time, and the fast pointer takes two steps at a time. When the fast pointer reaches the end, The slow pointer is at its midpoint. The judgment type is fast and slow pointer. The pointer movement method is that the pace of the fast pointer is twice that of the slow pointer. The end condition is that the next node of the fast pointer is empty. Let's look at an advanced version of this topic

Delete the penultimate node of the linked list

Title Description: give you a linked list, delete the penultimate node of the linked list, and return the head node of the linked list.

Input: head = [1,2,3,4,5], n = 2
 Output:[1,2,3,5]

Solution:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* fast;
        fast = head;
        ListNode* start = new ListNode(0,head);//Create dummy nodes to facilitate deletion
        ListNode* slow = start;
        for (int i = 1; i < n; i++) fast = fast->next;
        while (fast->next) {
            slow = slow->next;
            fast = fast->next;
        }
        ListNode* t = slow->next;
        slow->next = t->next;
        delete t;
        return start->next;//Dummy nodes are not included
    }
};

Analysis: obviously, the simplest idea is to traverse once to get the length of the linked list, and then traverse again to find the location of L-n and delete the node. What if you are asked that you can only traverse once? We can use our fast and slow pointers. We can let the fast pointer go n-1 steps first (because there is an interval of n-1 steps between N nodes), and then the fast and slow pointers go at the same time. When the fast pointer reaches the end, the slow pointer is just n-1 steps slower than the fast pointer, that is, the position is the node to be deleted * * (because the dummy node is set, the slow pointer is actually in the previous node * *).

Other double pointers

Because the usage of double pointer is flexible, and some double pointer types are difficult to determine. Here are some other double pointer problems

Reverses the word in the string

Move 0

summary

When we traverse with a single pointer, which exceeds the time or space complexity, we can think about whether we can use the double pointer algorithm to optimize our algorithm. Generally speaking, the collision pointer starts from one end to the other, moves step by step, and the end condition is start < end. Generally speaking, the starting state of the fast and slow pointer is the same starting point, Moving mode (the slow pointer generally moves gradually, and the specific situation of the fast pointer depends on how fast it is, whether it is a rush or a big step), and the end condition is that the fast pointer reaches the end point.

The above questions are from leetcode website: https://leetcode-cn.com/

Topics: Algorithm data structure leetcode