前言

从现在开始,博主决定开始将自己写的算法进行写博客输出下来,以加深印象,顺便看看给初学者一点借鉴。这个系列我会先是讲解一下概念,然后与在使用好几道题目,通过讲题给大家详细讲解如何使用正在讲解的算法进行解题。那么今天这一篇讲的就是双指算法。

大致理解

对于每个算法我们没必要很清楚的记住它的概念,我们只需要大致理解。因为算法使用在实际的题目中都会有一丝丝的改变,但是大致逻辑不变。

双指针:我们这里说的双指针,并不一定就是两个指针,我们可以理解为是两个标记,指针本身就是比标记指定这个地址的数据,那么我们这里就把标记的两个元素叫做双指针,比如两个int类型的数据代表下标标记数组的元素。

使用场景:当出现一种 从一个 i 起点往后遍历,然后又需要从i,i+1,或是 i 附近又进行遍历,不断循环这个过程的场景时。我们就可以使用双指针。就不需要一个“指针”来回从头跑。这样我们通常可以把时间复杂度为O(n^2)的过程变为O(n)的代码。

一、移动零

【笑着写算法系列】双指针---O(n)时间复杂度的常客_List

题目要求:

就是将数组中的所有0,移到最右边。且不改变不为0元素的先后顺序。注意:题目要求原地修改。

参考代码以及解析:

class Solution {
    public void moveZeroes(int[] nums) {

        int left=0;
        int right=0;
        
        while(right<nums.length){
            if(nums[left]!=0){

                if(left==right){
                    right++;
                }
                left++;
            }else if(nums[right]!=0){
                int tmp=nums[left];
                nums[left]=nums[right];
                nums[right]=tmp;
                left++;
                right++;

            }else{
                right++;

            }
        }
    }
}

暴力解法就不多解释了,我们直接讲解双指针算法来解决这道题。

首先我们定义两个int 变量作为下标来标记两个数组两个位置,一个left,一个right。

然后while循环遍历,循环条件为right<nums.length。

这里我们用left定位到靠左的0元素,当不为0元素时,right和left共同++,往后找。

我们的right就是找到 left右边不为0的元素。当left和right都找到符合的元素时,就进行交换。

这样我们就可以使不为0的元素顺序不变的情况下,将0移到右边。

二、复写零

【笑着写算法系列】双指针---O(n)时间复杂度的常客_数组_02

题目要求:

保持数组的长度不变,将数组里面的0都在复写一遍。注意数组长度不变就会导致原数组后面的元素就会被挤掉。

参考代码以及解析:

class Solution {
    public void duplicateZeros(int[] arr) {
        int size=arr.length;
        int left=0;
        int right=0;
        while(true){
            if(arr[left]==0){
                right+=2;
            }else{
                right++;
            }

            if(right>=size){
                break;
            }

            left++;

        }

        if(right!=size){
            right-=2;
            arr[right]=0;
            left--;
            right--;
        }else{
            right--;
        }

        while(left>=0){
            if(arr[left]==0){
                arr[right]=0;
                arr[--right]=0;
            }else{
                arr[right]=arr[left];
            }
            right--;
            left--;
        }
    }
}

首先我们要是从前往后进行修改,毫无疑问当复写一个0的时候,会把那个位置的元素就给覆盖掉了,这样我们就会丢失掉一些数据了。所以我们需要从前往后开始复写就可以了。

那么首先我们找到原数组中最后一个不会被挤掉的元素,从这个元素开始从后往前复写0。

那我们就用right来表示当前复写数组的元素,left来找最后一个不会被挤掉的元素,直到right>=size

当我们用while循环这么寻找时,结束时的right和left会出现两个状态。

一:这时最后一个没被挤掉的元素为0,但是原数组长度就剩下一个位置,但是代码中right是+2的。

这时right=size+1

二:刚刚好left这个元素写进数组right就刚好等于数组长度了。即right等于size。

注意无论是哪一种情况,这时的left指的都是最后一个被挤掉的元素。

那么应对这两种情况我们需要进行不同处理:

第一种情况:我们先将最后一个0给写入数组中,在开始正常从后往前复写0。right先-=2,指向数组最后一位。给其赋值,再right--,left--;然后直接开始从后往前复写就好了。

第二种情况:right--后指向数组最后一个元素后,直接开始从后往前复写就好了。


三、快乐数

【笑着写算法系列】双指针---O(n)时间复杂度的常客_数组_03

题目要求:

判断一个数的每一位数的平方相加是否为1,如果不为1,继续这样平方相加。

看其最后是否能变成1,还是无限循环即:得到之前已经得到过的数。

代码及其解析:

首先我们一看到循环就该想到快慢指针了,也是双指针,为什么呢?

我们可以理解为绕圈,如果我们的路是包含了一个圈,那么快的那个指针总会再遇到满指针,需要确保的是,快指针的速度只比慢指针的速度快一步,否则可能不会相遇而是越过。

我们先是定义一个函数,这个函数就是求给定一个数的每一位数的平方和,返回这个和。

然后就开始循环只要快指针fast和慢指针slow不相等就循环,循环体就是快慢指针都在进行平方求和,快指针执行两次,慢指针执行一次,然后更新这个数,每循环一遍我们都要判断一遍这两个指针有没有可能为1,如果已经是1返回true即可,没有就继续循环

循环结束后说明fast==slow,这时只需要判断fast和slow是否为1,即可。

class Solution {

    public boolean isHappy(int n) {
        int fast=n;
        int slow=n;

        do{
            fast=func(func(fast));
            slow=func(slow);

            if(fast==1||slow==1){
                return true;
            }

        }while(fast!=slow);

        if(fast!=1){

            return false;
        }else{
            return true;
        }

    }

    private int func(int cur){
        int sum=0;
        int n;
        while(cur!=0){
            n=cur%10;
            cur/=10;
            sum+=n*n;
        }

        return sum;

    }

}

四、有效三角形的个数

【笑着写算法系列】双指针---O(n)时间复杂度的常客_List_04

题目要求:让我们在数组中找出有不同种组合可以组成三角形。即使重复,只要下标不同即可。

参考代码以及解析:

首先我们很容易想到暴力解法,三层循环直接嵌套,暴力求出。时间复杂度为O(N^3).

但是运用双指针就可以将时间复杂度为O(N^3)的过程变成O(N^2)。

首先我们可以先对数组进行排序。

我们先是选定第三条较大的边cur,然后选的较小的两条边left,right。那么我们选定最大的边就要从后往前选了。

那么剩下两条我们就可以运用双指针了。在升序情况下,我们是否会想到一种情况。

当这两个数之间隔着一段距离,这时两数相加已经大于我们选定的那个第三边cur。

那么是否就可以确认left左边的数加上right也大于cur呢。利用这个特性,所以我们排序,在运用双指针将会大大减少我们循环的次数了。直接在计数变量上加上right-left即可。

如图:当left往前移一步或是两步都是可与构成三角形的。因为3+5>5,更何况我们left往前移还是增大的,且不大于right。

【笑着写算法系列】双指针---O(n)时间复杂度的常客_数组_05

import java.util.Arrays;

class Solution {
    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int count=0;
        int cur=nums.length-1;
        int left=0;
        int right=cur-1;
        for(;cur>1;cur--){
            right=cur-1;
            left=0;

            while(left<right){

                if(nums[left]+nums[right]>nums[cur]){
                    count+=right-left;
                    right--;


                }else{
                    left++;
                }
            }

        }
        return count;
    }
}

五、三数之和

【笑着写算法系列】双指针---O(n)时间复杂度的常客_数组_06

题目要求:

在一堆数组中找出三个数和相加起来等于0,且这三个数组成的组合不能相同,即使下标不同。

那么我们等下就要考虑如何去重了。

代码及其解析:

暴力解法很容易看出来时O(N^3)。所以我们双指针就会把它优化成O(N^2)。

首先我们先将数组排序,然后用第一层循环从前往后确定一个数tmp,然后还有一层循环就是用来找两个数和为

-tmp的即可,那么找这两个数就需要到双指针了。我们一前一后left与right,相加起来如果大于-tmp则right--,如果小于则left++,找到一次就开始将这几个数放进链表。继续找直到left>=right。

class Solution1 {
    public List<List<Integer>> threeSum(int[] nums) {
        int size = nums.length;
        Arrays.sort(nums);

        List<List<Integer>> List = new ArrayList<>();

        for (int i = 0; i < size; i++) {

            if (i != 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int tmp = nums[i];
            int size1 = size-1;
            for (int j = i + 1; j < size1; ) {

                if (j != i+1 && nums[j] == nums[j - 1]) {
                    j++;
                    continue;
                }

                int total = nums[j] + nums[size1];
                if (total == -tmp) {

                    List<Integer> list = new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[j]);
                    list.add(nums[size1]);

                    List.add(list);
                    j++;
                    size1--;
                } else if (total > -tmp) {
                    size1--;
                }else{
                    j++;
                }
            }
        }
        return List;
    }

}