【Optimal Algorithm】—— Double Pointer Problem

Starting today, throughout the summer. I will bring you questions about various algorithms from time to time to help you overcome the difficulties of algorithms that may be encountered during the interview process.


Table of contents

(1) Basic concepts

(2) Topic explanation

1. Difficulty: easy

1️⃣ Move Zero

2️⃣ Overwrite zero

2. Difficulty: medium

1️⃣ Happy Number

2️⃣The container with the most water

3. Difficulty: difficult

2️⃣ Max score

Summarize


(1) Basic concepts

Double pointer arithmetic is a commonly used algorithmic technique, which is usually used for fast search, matching, sorting or moving operations in arrays or strings . Double pointer arithmetic iterates over the data structure using two pointers and moves the pointers as required by the problem.

Common double pointers come in two forms:

  • One is the collision pointer ;
  • One is left and right pointer .
     

Colliding pointers : generally used in sequential structures, also known as left and right pointers.
• The collision pointer moves from the ends to the middle. One pointer starts from the far left, the other starts from the far right, and then gradually approaches the middle.
• The termination condition of colliding pointers is generally that the two pointers meet or stagger (the result may also be found inside the loop and jump out of the loop), that is: ◦ left == right
          (two pointers point to the same position)
          ◦ left > right (the two pointers are staggered)



Fast and slow pointers : also known as the rabbit race algorithm , the basic idea is to use two pointers with different moving speeds to move on sequence structures such as arrays or linked lists .


This approach is very useful for working with circular linked lists or arrays.


In fact, it is not just a circular linked list or an array. If the problem we want to study has a cyclical situation, we can consider using the idea of ​​fast and slow pointers.


There are many ways to implement fast and slow pointers, the most commonly used one is:
        • In a cycle, each time the slow pointer moves backward one bit, and the fast pointer moves backward two bits to achieve a fast slowly
 

【Advantage】 

  1. The advantage of double-pointer algorithms is that they can be done in one iteration, the time complexity is usually low, and no additional space is required;
  2. By moving the pointer reasonably, unnecessary calculation and comparison can be effectively reduced, and the efficiency of the algorithm can be improved.

When applying the double-pointer algorithm, it is necessary to select an appropriate pointer movement strategy according to the characteristics and requirements of the problem to ensure the correctness and efficiency of the algorithm. At the same time, pay attention to handling boundary conditions and special cases to avoid errors and exceptions.


(2) Topic explanation

Next, we will give you a specific experience through a few topics. (topics from easy to difficult )

1. Difficulty: easy

1️⃣ Move Zero

The link is as follows : 283. Moving Zero
【Title Description】

 [Solution] ( The idea of ​​​​quick sorting : the array is divided into intervals - the array is divided into two pieces )

Algorithm idea:

  1. In this question, we can use a cur pointer to scan the entire array, and another dest pointer to record the last position of the non-zero sequence. According to the different situations that cur encounters during the scanning process, it is classified and processed to realize the division of the array.
  2. During the traversal of cur, the elements of [0, dest] are all non-zero elements, and the elements of [dest + 1, cur - 1] are all zero

【Algorithm flow】

a . Initialize cur = 0 (for traversing the array), dest = -1 (point to the last position of the non-zero element sequence.
Because we don't know where the last non-zero element is at the beginning, it is initialized to -1 )


b. cur traverses each element in turn, and the traversed elements will have the following two situations:


    i. The encountered element is 0, cur directly ++. Because our goal is to make
the elements in [dest + 1, cur - 1] all zero, so when cur encounters 0, directly ++ can let 0 be at the
position of cur - 1, thus In [dest + 1, cur - 1];
    ii. The encountered element is not 0, dest++, and exchange the elements at cur position and dest position, and let cur++ scan the next element.

  •  Because the position pointed to by dest is the last position of the non-zero element interval, if a new non-zero element is scanned, then its position should be at the position of dest + 1, so dest is first incremented by 1;
  •  After dest++, the pointed element is the 0 element (because the last element at the end of the non-zero element interval is 0), so it can be exchanged to the position of cur, and the elements of [0, dest] are all non-zero elements , the elements of [dest + 1, cur - 1] are all zeros.

【Algorithm Implementation】

class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        for(int cur = 0, dest = -1; cur < nums.size(); cur++)
        if(nums[cur]) // 处理⾮零元素
            swap(nums[++dest], nums[cur]);
    }
};

【Result display】

 【Performance Analysis】

The specific performance analysis is as follows:

  • Time Complexity : A loop is used in the code to traverse the array, so the time complexity is O(n) , where n is the length of the array.
  • Space complexity : No extra space is used in the code, only the movement is realized by exchanging array elements, so the space complexity is O(1) .

The performance of the algorithm is good, the processing speed is fast, and the space overhead is small. Since only one traversal is performed and only element exchange operations are performed, the time complexity is linear in most cases. However, in some special cases, such as when almost all elements in the array are non-zero elements , the entire array still needs to be traversed, but the number of swap operations will be reduced.

 


2️⃣ Overwrite zero
 

The link is as follows: 1089. Overwrite zero

【Title description】

 

[Solution] ( copy in place --- double pointer )

  • Algorithm idea:

① If the in-place copy operation is performed "from front to back", due to the appearance of 0, it will be copied twice, resulting in the "overwriting" of the number that has not been copied. Therefore, we choose the "back to front" replication strategy.


② But when copying from "back to front", we need to find the "last number to copy", so our general process is divided into two steps:

  • Find the last copied number first;
  • Then perform the copy operation from the back to the front
     

 

【Algorithm flow】

a. Initialize two pointers cur = 0, dest = 0;


b. Find the last copied number:
   i. When cur < n, keep executing the following loop:
        • Determine the element at the position of cur:
                ◦ If it is 0, move dest two bits backward;
                ◦ Otherwise, dest moves backward one bit.
        • Judging that dest has reached the end position, if it is over, the loop will be terminated;
        • If not, cur++ will continue to judge.


c. Determine whether dest is out of bounds to position n:
        i. If it is out of bounds, perform the following three steps:
                1. Change the value of position n - 1 to 0;
                2. Move one step to cur;
                3. Move forward two steps to dest step.


d. Traverse the original array from the cur position forward, and restore the overwritten result array in turn:
        i. Determine the value of the cur position:
                1. If it is 0: dest and dest - 1 positions are changed to 0, dest -= 2;
                2. If non-zero: modify the position of dest to 0, dest -= 1;
        ii. cur-- , copy the next position

【Algorithm Implementation】

class Solution {
public:
    void duplicateZeros(vector<int>& arr) {
        // 1. 先找到最后⼀个数
        int cur = 0, dest = -1, n = arr.size();
        while(cur < n)
        {
            if(arr[cur]) dest++;
            else dest += 2;
            if(dest >= n - 1) break;
            cur++;
        }
        // 2. 处理⼀下边界情况
        if(dest == n)
        {
            arr[n - 1] = 0;
            cur--; dest -=2;
        }
        // 3. 从后向前完成复写操作
        while(cur >= 0)
        {
            if(arr[cur]) arr[dest--] = arr[cur--];
            else
            {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
};

【Result display】

 

【Performance analysis】

The specific performance analysis is as follows:

  • Time complexity : Two loops are used in the code, the first loop is used to find the last number and handle the boundary cases, and the second loop is used to complete the copy operation from back to front. Since the second loop operates on constant multiples of the length of the array, the time complexity is O(n) , where n is the length of the array.
  • Space complexity : No extra space is used in the code, but the copy operation is realized by modifying the original array, so the space complexity is O(1) .

The performance of the algorithm is good, the time complexity is linear level, and the space overhead is small. In the worst case, the entire array needs to be traversed for the copy operation, but the linear time complexity is still maintained.


2. Difficulty: medium


1️⃣ Happy Number
 

The link is as follows: 202. Happy Number

【Title description】

 

【Solution】 (Speed ​​pointer)

The basic idea of ​​using the double pointer algorithm to judge the happy number is as follows:

  1. Define two pointers, a fast pointer (fast) moves forward two steps each time, and a slow pointer (slow) moves forward one step each time.
  2. Convert the given positive integer to string form.
  3. In a loop, continuously calculate the sum of the squares of the digits of the current number.
  4. Use the obtained sum of squares as the next number, and continue to calculate the sum of squares until the sum of squares equals 1, or a loop occurs.
  5. Determine whether a cycle occurs. If a cycle occurs and the sum of squares is not equal to 1, it means that the number is not a happy number.

【Algorithm flow】

a. Initialize the fast and slow pointers to the given positive integers.

b. In a loop, calculate the sum of the squares of the digits of the numbers pointed to by the fast and slow pointers, and assign the result to the fast pointers.

c. At the same time, the slow pointer moves one step.

d. Check whether the fast pointer and the slow pointer point to the same number, if so, it means there is a loop, exit the loop.

e. If the number pointed by the fast pointer is equal to 1, it means it is a happy number and returns true.

f. Otherwise, assign the number pointed by the slow pointer to the fast pointer, and repeat step be.

g. If no happy number is found at the end of the loop, return false.

【Algorithm Implementation】

class Solution {
public:
    int getNext(int n) {
    int sum = 0;
    while (n > 0) {
        int digit = n % 10;
        sum += digit * digit;
        n /= 10;
    }
    return sum;
}

    bool isHappy(int n) {
        int slow = n;
        int fast = getNext(n);

        while (fast != 1 && slow != fast) {
            slow = getNext(slow);
            fast = getNext(getNext(fast));
        }

        return fast == 1;
    }
    
};

【Result display】

 

【Performance analysis】

The specific performance analysis is as follows:

  1. time complexity:

    • In the process of calculating the next number, the number of digits of each number needs to be traversed, so the time complexity is O(logn), where n is the given number.
    • The fast and slow pointers iterate through the numbers in a loop until a result is found or a loop occurs. In the worst case, the number of loops is the sum of the individual digits in a number, which is O(logn).
    • Therefore, the overall time complexity is O(logn) .
  2. Space complexity: Only a constant level of extra space is used, which does not change with the input scale, so the space complexity is O(1) .


 

2️⃣The container with the most water
 

The link is as follows: 11. The container that holds the most water

【Title description】

 

【Solution】 (collision pointer)

  • First, initially the left pointer points to the start of the array, and the right pointer points to the end of the array. Calculate the water capacity of the container in the current interval, and update the maximum water capacity.
  • Next, determine the height of the elements pointed to by the two pointers, and move the pointer of the smaller element inward by one step. This ensures that each move is selecting a higher boundary line for a possible larger water holding capacity.
  • Repeat the above steps until the two pointers meet, that is, the left pointer is greater than or equal to the right pointer. At this point, the traversal is complete, and the maximum water capacity is returned.

【Algorithm flow】

a. Initialize the maximum water capacity  maxArea to 0, the left pointer points  left to the start position of the array, and the right pointer points  right to the end position of the array.

b. Enter the loop, and the judgment condition is that the left pointer is smaller than the right pointer.

       ◦ Calculate the width, minimum height, and current capacity of the current interval.

       ◦ Update the maximum water capacity, take the larger value of the current water capacity and the historical maximum water capacity.

       ◦ Determine the height of the elements pointed by the two pointers, and move the pointer of the smaller element one step inward.

c. After the cycle ends, return the maximum water capacity as the result.

【Algorithm Implementation】

class Solution {
public:
    int maxArea(vector<int>& height) {
        int res = 0;
        int left = 0;
        int right = height.size() - 1;

        while (left < right) {
            int width = right - left;
            int minHeight = min(height[left], height[right]);
            int v = width * minHeight;
            res = max(res, v);

            if (height[left] < height[right]) {
                left++;
            } 
            else {
                right--;
            }
        }

        return res;
    }
};

【Result display】

 

【Performance analysis】

The specific performance analysis is as follows:

  • Time complexity analysis : During the movement of the collision pointer, a part of the area is excluded each time, so the time complexity is O(n) , where n is the length of the array.
  • Space complexity analysis : The algorithm only uses a constant level of extra space and stores a small number of variables and pointers. Therefore, the space complexity is O(1) , independent of the input size.

3. Difficulty: difficult

2️⃣ Max score

The link is as follows: 1537. Maximum score

【Title description】

 

【Algorithm flow】

  1. Initialize the double pointers iand jto be 0 respectively, and initialize two variables sum1and sum2to record the sum of the current path, and initialize res to record the maximum score.

  2. In a loop, compare the values ​​of nums1[i]and at the current pointer position nums2[j]:

    • If  nums1[i] < nums2[j], it will  nums1[i] be added  sum1 to and the pointer will  i be moved backward one bit.
    • If  nums1[i] > nums2[j], it will  nums2[j] be added  sum2 to and the pointer will  j be moved backward one bit.
    • If  nums1[i] the sum  nums2[j] is equal, it means that the same value has been encountered, and path switching needs to be considered at this time. Compare   the sums, add the larger value to res, and  clear the sum1 sum   to   zero. Then it will   be added to res,   and  the pointer will  be moved back one bit at the same time.sum2sum1sum2nums1[i]ij
  3. After the loop ends, there may be remaining untraversed elements. At this point, the sum of the remaining paths needs to be added to res.

  4. Returns res  % num, which is 10^9 + 7the maximum score after taking the modulus of .

 

【Algorithm Implementation】

class Solution {
public:
    const int num = 1e9 + 7;

    int maxSum(vector<int>& nums1, vector<int>& nums2) {
        int n1 = nums1.size(), n2 = nums2.size();
        int i = 0, j = 0;
        long long sum1 = 0, sum2 = 0;  // 使用 long long 类型防止整数溢出
        long long res = 0;

        while (i < n1 && j < n2) {
            if (nums1[i] < nums2[j]) {
                sum1 += nums1[i++];
            } 
            else if (nums1[i] > nums2[j]) {
                sum2 += nums2[j++];
            } 
            else {                     // 遇到相同值,取两个路径中的较大值
                res += max(sum1, sum2) + nums1[i];
                sum1 = 0, sum2 = 0;
                
                i++;
                j++;
            }
        }

        // 处理剩余的元素
        while (i < n1) {
            sum1 += nums1[i++];
        }
        while (j < n2) {
            sum2 += nums2[j++];
        }

        res += max(sum1, sum2);  // 加上剩余路径的和

        return res % num;
    }
};

【Result display】

 

 【Performance analysis】

The specific performance analysis is as follows:

  • Time complexity analysis: O(n1 + n2) , where n1 and n2 are the lengths of the two input arrays, respectively. Because we iterate through both arrays at the same time, the time complexity is linear with the total length of the two arrays.
  • Space complexity analysis: O(1) , only a few variables are used to save the result, so the extra space is fixed and will not increase as the input size increases.

Summarize

The above is the entire explanation of the double pointer algorithm in this issue. If you master the above knowledge and practice more diligently, I believe that you will be able to solve such problems in the future.

Guess you like

Origin blog.csdn.net/m0_56069910/article/details/131658910