快慢双指针经常用来解决数组在遍历的过程中时间复杂度是O(n)的情况。

image.png

例如这个题目:

11. 盛最多水的容器

示例 1:
**数组中快慢双指针总结 - 图2

  1. 输入:[1,8,6,2,5,4,8,3,7]
  2. 输出:49
  3. 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49

示例 2:

输入:height = [1,1]
输出:1

示例 3:

输入:height = [4,3,2,1,4]
输出:16

示例 4:

输入:height = [1,2,1]
输出:2

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/container-with-most-water
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

根据生活常识,木桶能盛水的水量,取决于最短的一块板子,如下图所示:
image.png

可以先给这个示例进行一个编号:

1-9号

image.png

怎么才能找到盛水最多的容器呢?肯定是取决于板子的高低。

如果我们拿①号板子到⑨号板子来说的话:

①号板子的高度是1; ⑨号板子的高度是7; 那么这个范围的成水量就取决于①号板子的高度,因为在①和⑨号板子中,①号板子的高度是最低的。

如果我们拿②号板子到⑨号板子来说的话:
image.png

②号板子的高度是8; ⑨号板子的高度是7; 那么这个范围的成水量就取决于⑨号板子的高度,因为在②和⑨号板子中,⑨号板子的高度是最低的。

这个范围的盛水量是:7(高)* 7(宽)=49

这道算法题就是让我们找出可以盛水最多的容器,那么很明显,这个图中的最大值就是49。

在业界通常流传着一句话:往往第一次想到的解决办法都不是最好的,或者说都不是最优的。

能解决这道算法题有很多种办法,例如我们可以穷举每一块板子到其他所有板子的最大值。最终也可以拿到结果,无非就是复杂度高一点。

暴力解法

如下代码:

在下面这段暴力解法中,可以看到思路还是挺简单的,就是对每一根板子进行遍历,求出来每一个板子与其他所有板子的区域面积,最终选取最大的一个面积。

class Solution {
    public int maxArea(int[] height) {
        int max = 0;

        for(int i = 0;i<height.length;i++){

            //j = i+1 ;这里为什么要i+1呢?因为是不让重复的走自己走过的路
            for(int j = i+1 ; j < height.length;j++){

                int high =  Math.min(height[i],height[j]);//选出最小的板子的高度

                int wide = j - i;//宽

                int area = high * wide;//容量面积 = 高 * 宽

                max = Math.max(area,max);//保留最大值的容量
            }

        }

        return max;
    }
}

但是我们可以看到,虽然能顺利的解出这道题,但是时空复杂度是不这么完美的。如果你是一个追求完美的人,可能就需要进行一番折腾去想办法去优化了。
image.png

双指针

代码如下:

我们定义两个指针,一个指向数组的头,一个指向数组的尾。 然后 头—-> 逐渐逼近<——尾

class Solution {
    public int maxArea(int[] height) {
        //定义双指针
        int i = 0;//指针一,指向头
        int j = height.length - 1;//指针二,指向尾
        int max = 0;//记录最终最大的值

        while(j -i >=1){

            max = Math.max(Math.min(height[i],height[j]) * (j-i),max);

            //移动缩小范围
            if(height[i] <= height[j] ){
                i++;//指针一向后移动
            }else{
                j--;//指针二向前移动
            }    
        }
        return max;
    }
}

经过我们的优化,可以看到已经有效果了
image.png

1. 两数之和

为了更详细的说明接下来的问题,先拿出两数之和这个题目来说一下。
题目如下:

//给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 
//
// 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 
//
// 
//
// 示例: 
//
// 给定 nums = [2, 7, 11, 15], target = 9
//
//因为 nums[0] + nums[1] = 2 + 7 = 9
//所以返回 [0, 1]
// 
// Related Topics 数组 哈希表 
// 👍 9725 👎 0

链接:https://leetcode-cn.com/problems/two-sum

这个题我们可以使用暴力手段:

思路还是很明确的,当nums[i] + nums[j] = target时,即可返回i和j坐标

    public int[] twoSum(int[] nums, int target) {
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = i+1; j < nums.length ; j++) {
                if(nums[i] + nums[j] == target){
                    return new int[]{i,j};
                }
            }
        }

        return new int[]{};
    }

可以看到虽然是暴力的手段,单纯的看着效果还是挺不错的。
image.png

那么有没有优化的可能性呢,答案是有的,例如:可以使用hash表来存储。

    public int[] twoSum(int[] nums, int target) {
        int[] result = new int[2];
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(target - nums[i])) {
                result[1] = i;
                result[0] = map.get(target - nums[i]);
                return result;
            }
            map.put(nums[i], i);
        }
        return result;
    }

image.png

15. 三数之和

题目:

//给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复
//的三元组。 
//
// 注意:答案中不可以包含重复的三元组。 
//
// 
//
// 示例: 
//
// 给定数组 nums = [-1, 0, 1, 2, -1, -4],
//
//满足要求的三元组集合为:
//[
//  [-1, 0, 1],
//  [-1, -1, 2]
//]
// 
// Related Topics 数组 双指针 
// 👍 2785 👎 0

力扣链接:https://leetcode-cn.com/problems/3sum

暴力解法

对于二数之和的题目,我们可以

当nums[i] + nums[j] = target时,即可返回i和j坐标

那么此题中三数之和,我们肯定也可以类似二数之和的做法一样会去做:

1、三重for循环遍历nums数组; 2、找到nums[i]+nums[j]+nums[k] = 0 保存nums[i],nums[j],nums[k]; 3、继续寻找; 4、返回保存的数据即可。

那么上面这个思路就是暴力的手段了。

 public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();

        int len = nums.length;

        //利用set去重
        Set<List<Integer>> set = new HashSet<>();


        for (int i = 0; i < len; i++) {
            for (int j = i+1; j < len; j++) {
                for (int k = j+1; k < len; k++) {

                    if(nums[i]+nums[j]+nums[k] == 0){
                        List<Integer> list = new ArrayList<>();
                        list.add(nums[i]);
                        list.add(nums[j]);
                        list.add(nums[k]);

                        Collections.sort(list);

                        set.add(list);
                    }

                }
            }
        }
        return new ArrayList<>(set);
    }

很遗憾的是,暴力解法跑不过测试程序,超时了:

image.png

双指针解法

双指针可以解决数组相关算法题的大多数超时问题。

 public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();

        //如果传递的数组是空的,直接返回一个空的list集合
        if(nums.length == 0) return result;

        //排序
        Arrays.sort(nums);

        int len = nums.length;

        //遍历
        for (int i = 0; i < len; i++) {

            //跳过重复的值
            if(i>0 && nums[i] == nums[i-1])
                continue;

            /**
             * a=i
             * b=j
             * c=k
             *
             * 因为:a+b+c=0
             * 那么:b+c=-a
             * 那么nums[j] + nums[k] = -nums[i]
             *
             */
            int target = -nums[i];//目标数
            int j =  i +1;//头指针
            int k = len - 1;//尾指针

            //两个指针到中间相遇了,既结束
            while ( j < k){

                //判断 b+c = -a 是否成立
                if(nums[j] + nums[k] == target){
                    //组装数据
                    List<Integer> curr = new ArrayList<>();
                    curr.add(nums[i]);
                    curr.add(nums[j]);
                    curr.add(nums[k]);

                    //添加到结果集
                    result.add(curr);

                    //移动头指针和尾指针,缩小范围
                    j++;
                    k--;

                    //进一步减小范围
                    //1、检查左边
                    while (j < len && nums[j] == nums[j - 1]) j++;

                    //2、检查右边
                    while (k > j && nums[k] == nums[k + 1]) k--;

                    //如果符合b+c > -a
                /*
                    例如:[-4,-1,-1,0,1,2]
                    a=-4
                    b=-1
                    c=2

                     i  j         k
                     ↓  ↓         ↓
                     a  b         c
                     ↓  ↓         ↓
max data index(j) → [-4,-1,-1,0,1,2] ← min data index(k)

                    b+c=-1 != --4(4)

                    说明b+c太大了,所以我们要让b+c变小,那么就必须缩小大头
                    因为k指针是指向的数组中最大的一个范围,那么就需要缩小k所指的位置
                    则:k--
                 */
                }else if(nums[j] + nums[k] > target){
                    k--;
                }else{

                    //反之,j++;增大数据
                    j++;
                }

            }
        }

        return result;
    }

代码虽然比之前复杂了点,但是效果是很好的:
image.png