基础算法(1)——双指针

1. 概念

常见的双指针有两种形式,一种是对撞指针,一种是快慢指针

1.1 对撞指针

一般用于顺序结构中,也称为左右指针

对撞指针从两端向中间移动,一个指针从最左端开始,另一个从最右端开始,逐渐往中间逼近

对撞指针的终止条件一般是两个指针相遇或错开(也可能在循环内部找到结果直接跳出循环)

1.2 快慢指针

又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动,这种方法对于处理环形链表或数组很有用

快慢指针的实现方式有很多种,最常用的一种就是:在一次循环中,每次让慢的指针向后移动一位,快的指针向后移动两位,实现一快一慢

2. 移动零

属于 “数组划分、数组分块” 类型的题,该题是数组分为两块,根据一种划分方式,将数组的内容分成左右两部分,这种类型的题一般就是使用 “双指针” 完成

题目描述:

难点:同时保持非零元素的相对位置

解法思路:

利用数组下标来充当指针,两个指针分别的作用:

cur :从左向右扫描数组,遍历数组;dest:已处理的区间内,非零元素的最后一个位置。如下图:

代码思路:

cur 从前往后遍历的过程中

1. 遇到 0 元素:cur++

2. 遇到非零元素:swap(dest + 1, cur); dest++,cur++;

代码实现:

class Solution {
    public void moveZeroes(int[] nums) {
        int dest = 0;
        int cur = 0;
        int n = nums.length;
        while (cur < nums.length) {
            if (nums[cur] != 0) {
                int tmp = nums[cur];
                nums[cur] = nums[dest];
                nums[dest] = tmp;
                dest++;
            }
            cur++;
        }
    }
}

tip:该方法思路也是快排里面最核心的一步,如下快排示意图:

3. 复写零

题目描述:

难点:对输入数组就地进行上述修改

解法思路:

如果 从前向后 进行原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数被覆盖掉,因此应选择 从后往前 的复写策略

先根据 “异地” 操作,然后优化成双指针下的 “就地” 操作:

从而得到 “就地” 处理的思路:

1) 先找到最后一个 复写 的元素下标(双指针法)

cur 和 dest 从 0 下标开始遍历数组

        a) 判断 cur 位置的值

        b) 若为 0,则 dest 后移 2 位,反之后移 1 位

        c) 判断 dest 是否 >= 数组长度,若有,则跳出循环

         d) 若没有,cur++

2) 处理边界情况,这里就分为了两种情况

        a) 如例 [1,0,2,3,0,4,5,0] 这个数组,最后一位复写的数字不为 0,此时第一步结束之后,dest 等于数组长度,只需处理 dest 等于数组长度 - 1 即可

        b) 如例 [1,0,2,3,0,4] 这个数组,最后一位复写的数字为 0,此时第一步结束之后,dest 大于数组长度,这时若在让 dest 等于数组长度 - 1的话,就会导致复写第二次 0 的时候发生数组越界异常,因此这种最后一位复写数字为 0 的情况,直接处理为:

        arr[n - 1] = 0;

        dest = n - 2;

        cur--;

3) 从后向前完成复写操作

代码实现:

class Solution {
    public void duplicateZeros(int[] arr) {
        int n = arr.length;
        int dest = 0;
        int cur = 0;

        // 1. 先找到最后一个复写的元素下标
        while (cur < n) {
            if (arr[cur] == 0) dest += 2;
            else dest++;
            if (dest >= n) break; // cur 当前位置就是最后一个要复写的位置
            cur++;
        }

        // 下面是一个错误示例
        // // 2. 处理边界情况 
        // if (arr[cur] == 0) {
        //     // 第一步结束之后,若最后一位要复写的数字是 0,直接执行下面操作(这是错误的!!)
               // 并不是所有的最后一位复写数字是0的都执行下面操作,例如:{1,5,2,0,6,8,0,6,0}
        //     arr[n - 1] = 0;
        //     dest = n - 2;
        //     cur--;
        // } else {
        //     // 第一步结束后,dest = 数组长度
        //     dest = n - 1;
        // }

        // 2. 处理边界情况(第一步结束之后,dest 肯定 >= n)
        if (dest == n) {
            dest--;
        } else {
            // 特殊情况:当数组为[1,0,2,3,0,4]这样最后一位复写数字是0,且dest会大于n的情况,执行下面操作
            arr[n - 1] = 0;
            dest = n - 2;
            cur--;
        }
        
        // 3. 从后往前遍历数组
        while (cur >= 0) {
            if (arr[cur] == 0) {
                arr[dest--] = 0;
                arr[dest--] = 0;
            } else {
                arr[dest--] = arr[cur];
            }
            cur--;
        }
    }
}

4. 快乐数

题目描述:

题目解析:

为方便叙述,将 【对于一个正整数,每次将该数替换为它每个位置上的数字的平方和】 这个操作记为 x 操作

题目中说,当我们不断重复 x 操作时,计算一定会【死循环】,死循环的方式有两种:

情况一:一直在 1 中死循环,如下图:

情况二:在历史的数据中【死循环】,但始终变不到 1,如下图:

由于上述两种情况只会出现一种,因此我们只需确定循环是在【情况一】还是【情况二】中进行就能解决该题

扩展:为什么不会出现【不是死循环,而是一直计算下去】的情况

【鸽巢原理】(也称为抽屉原理):现有 n 个巢,n + 1 个鸽子,得出结论【至少有一个巢里面的鸽子数大于 1】

在该题中,根据题目给出提示,n 的取值范围为 [12^{31}-1](也就是最大能取到 2147483647),现在我们假设 n 能取到 9999999999,该数字进行 x 操作 = 9 ^ 2 * 10 = 810,也就是说变化的区间在 [1,810] 之间

根据【鸽巢原理】,取极限情况,一个数在进行 x 操作 810 次后都没有进入循环,但是一旦到 811 次之后,必然会形成一个循环

因此,变化的过程最终肯定会走到一个圈里面,所以可以用【快慢指针】来解决该问题

解法思路:

根据题目分析可知,当重复执行 x 操作时,数据最终会死循环,而快慢指针有一个特性,就是在一个圆圈中,快指针总是会追上慢指针的,也就是说他们总会相遇到一个位置上,此时就可以判断,若相遇位置为 1,则 n 为快乐数;若相遇位置不为 1,则 n 不是快乐数

代码实现:

class Solution {
    //返回 n 这个数每一位的平方和
    public int bitSum(int n) {
        int sum = 0;
        while (n != 0) {
            int val = n % 10;
            sum += val * val;
            n /= 10;
        }
        return sum;
    }
    public boolean isHappy(int n) {
        int fast = bitSum(n);
        int slow = n;
        while (fast != slow) {
            fast = bitSum(bitSum(fast));
            slow = bitSum(slow);
        }
        return fast == 1;
    }
}

5. 盛水最多的容器

题目描述:

解法一:暴力求解(会超时)

算法思路:枚举出能构成的所有容器,找出其中最大值

两个 for 循环,时间复杂度 O(n^2)

代码实现:

class Solution {
    public int maxArea(int[] height) {
        int area = 0;
        for (int i = 0; i < height.length; i++) {
            for (int j = i + 1; j < height.length; j++) {
                area = Math.max(area, (j - i) * Math.min(height[i], height[j]));
            }
        }
        return area;
    }
}

解法二:对撞指针

算法思路:

先找到单调性的规律:

重复上述过程,每次都可以舍去大量不必要的枚举过程,直到 left 和 right 相遇

代码实现:

//方法二:利用单调性的规律,通过双指针解决问题
class Solution {
    public int maxArea(int[] height) {
        int area = 0;
        int left = 0;
        int right = height.length - 1;
        while (left < right) {
            area = Math.max(area, (right - left) * Math.min(height[left], height[right]));
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return area;
    }
}

6. 有效三角形的个数

题目描述:

解法一:暴力求解

算法思路:

三层 for 循环枚举出所有的三元组,并判断是否能构成三角形

若是直接进行判断,利用 a+b>c, a+c>b, b+c>a 这三个条件进行判断的话,会超时(时间复杂度3N^3

所以进行优化:将数组从小到大排序,这样只需比较 较小的两个数相加是否大于较大的数 即可(时间复杂度N\log N + N^3,远小于3N^3)

代码实现:

class Solution {
    public int triangleNumber(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int sum = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    if (nums[i] + nums[j] > nums[k]) {
                        sum ++;
                    }
                }
            }
        }
        return sum;
    }
}

解法二:排序 + 双指针

算法思路

代码实现:

class Solution {
    public int triangleNumber(int[] nums) {
        //1.将数组排序
        Arrays.sort(nums);
        //2.根据单调性,利用双指针解决问题
        int n = nums.length - 1; //通过 n 来固定最大值
        int sum = 0;
        while (n >= 2) {
            //通过双指针快速统计处符合要求的三元组个数
            int left = 0;
            int right = n - 1;
            while (left < right) {
                if (nums[left] + nums[right] > nums[n]) {
                    sum += right - left;
                    right--;
                } else {
                    left++;
                }
            }
            n--;
        }
        return sum;
    }
}

7. 查找总价格为目标值的两个商品

题目描述:

解法一:暴力解法(会超时)

算法思路:两层 for 循环列出所有两个数字的组合,判断是否等于目标值

注:在第二层 for 循环时,挑选数字不从第一个数开始选,因为前面的数字已经判断过了,直接从第一层循环挑选数字的下一个位置开始

代码实现:

class Solution {
    public int[] twoSum(int[] price, int target) {
        int n = price.length;
        for (int i = 0; i < n; i++) { //第一层循环从前往后列举第一个数
            for (int j = i + 1; j < n; j ++) { //第二层循环从 i 位置之后列举第二个数
                if (price[i] + price[j] == target) {
                    return new int[] {price[i], price[j]};
                }
            }
        }
        return null;
    }
}

解法二:对撞指针

算法思路:

本题为升序的数组,因此可以用 对撞指针 优化时间负责度

具体实现流程,相当于上一个算法题的简化,不展开

代码实现:

class Solution {
    public int[] twoSum(int[] price, int target) {
        int[] arr = new int[2];
        int left = 0;
        int right = price.length - 1;
        while (left < right) {
            if (price[left] + price[right] < target) {
                left++;
            } else if (price[left] + price[right] > target) {
                right--;
            } else {
                arr[0] = price[left];
                arr[1] = price[right];
                return arr;
            }
        }
        return null;
    }
}

8. 三数之和

题目描述:

解法一:排序 + 暴力枚举 + 利用 HashSet 去重(时间复杂度 O(N^3)

三层 for 循环

解法二:排序 + 双指针

本题和两数之和类似,稍微不同的是题目要求找到所有【不重复】的三元组,具体步骤如下:

1) 排序

2) 固定一个数 a(此处固定最大值或最小值都可以,只是遍历方式不同)

3) 若是固定最小值,则在此数后面的区间内使用【双指针算法】快速找到两个数之和等于 -a 即可;若是固定最大值,则在此数前面的区间进行操作

本题需要注意的是:需要有去重操作

a) 找到一个结果后,left 和 right 指针要【跳过重复】的元素

b) 当使用完一次双指针算法之后,固定的 a 也要【跳过重复】的元素

代码实现:

1. 固定最小值:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        //1.排序
        Arrays.sort(nums);

        List<List<Integer>> ret = new ArrayList<>();
        int n = nums.length - 1;
        //2.固定值 a
        for (int i = 0; i < n;) { //注:此处为了去重,将 i++ 操作放到 for 循环尾部
            int left = i + 1;
            int right = n;
            while (left < right) {
                if (nums[left] + nums[right] < -nums[i]) {
                    left++;
                } else if (nums[left] + nums[right] > -nums[i]) {
                    right--;
                } else {
                    ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right])));
                    left++;
                    right--;
                    //去重
                    while (left < right && nums[left] == nums[left - 1]) {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                }
            }
            i++;
            //去重
            while (i < n && nums[i] == nums[i - 1]) {
                i++;
            }
        }
        return ret;
    }
}

2. 固定最大值:

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        //1.排序
        Arrays.sort(nums);
        //2.固定最大值(数组最后一位)
        int n = nums.length - 1;
        if (nums[n] < 0) { //若数组中最大值都小于 0 直接返回 null
            return null;
        }
        List<List<Integer>> ret = new ArrayList<>();
        while (n > 0) {
            int left = 0;
            int right = n - 1;
            //3.遍历数组剩余元素,将符合条件的添加到 ret
            while (left < right) {
                if (nums[left] + nums[right] + nums[n] > 0) {
                    right--;
                } else if (nums[left] + nums[right] + nums[n] < 0) {
                    left++;
                } else {
                    //将相加等于 0 的三个数创建为一个 ArrayList,并添加到 ret 中
                    ret.add(new ArrayList<Integer>(Arrays.asList(nums[left], nums[right], nums[n])));
                    left++;
                    right--;
                    //去重操作
                    while (left < right && nums[right] == nums[right + 1]) {
                        right--;
                    }
                    while (left < right && nums[left] == nums[left-1]) {
                        left++;
                    }
                }
            }
            n--;
            //去重操作
            while (n >= 2 && nums[n] == nums[n + 1]) {
                n--;
            }
        }
        return ret;
    }
}

9. 四数之和

题目描述:

解法一:排序 + 暴力枚举(四个 for 循环) + 利用HashSet 去重(会超时)

解法二:排序 + 双指针

代码实现:

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> ret = new ArrayList<>();
        //1.排序
        Arrays.sort(nums);
        //2.固定两个数
        int n = nums.length;
        int a = 0;
        while (a < n) {
            int b = a + 1;
            while (b < n) {
                int left = b + 1;
                int right = n - 1;
                long aim = (long)target - nums[a] - nums[b];
                while (left < right) {
                    int sum = nums[left] + nums[right];
                    if (sum < aim) {
                        left++;
                    } else if (sum > aim) {
                        right--;
                    } else {
                        ret.add(Arrays.asList(nums[left], nums[right], nums[b], nums[a]));
                        left++;
                        right--;
                        while (left < right && nums[left] == nums[left - 1]) {
                            left++;
                        }
                        while (left < right && nums[right] == nums[right + 1]) {
                            right--;
                        }
                    }
                }
                b++;
                while (b < n && nums[b] == nums[b - 1]) {
                    b++;
                }
            }
            a++;
            while (a < n && nums[a] == nums[a - 1]) {
                a++;
            }
        }
        return ret;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值