第四章 算法思维

186 阅读6分钟

子集

题目

image.png

版本1 正确

    public List<List<Integer>> subsets(int[] nums) {
        if (nums.length == 0) {
            return new ArrayList<>();
        }

        // 对于nums中的每一位数字, 因为数字都不相同,
        // 求的又是子集, 子集并不需要考虑顺序.
        // 对于nums = [1]
        // 子集有[][1]
        // 对于nums[1, 2]
        // 相对于nums = [1], 新增了[2][1, 2], 就是在[]和[1]的基础上添加了个元素

        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> base1 = new ArrayList<>();
        List<Integer> base2 = new ArrayList<>();
        base2.add(nums[0]);
        ans.add(base1);
        ans.add(base2);

        for (int i = 1; i < nums.length; i ++) {
            List<List<Integer>> tempAns = new ArrayList<>();
            for (List<Integer> pre: ans) {
                List<Integer> temp = new ArrayList<>(pre);
                temp.add(nums[i]);
                tempAns.add(temp);
            }
            ans.addAll(tempAns);
        }

        return ans;
    }

正确的原因

(1) 找到规律, 并且在遍历赋值的时候, 注意遍历替换的问题

子集2

题目

image.png

版本1 正确

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

        // nums中包含有重复的元素, 那么子集1的方法就不适用了
        // 通过回溯法可以做到去重集合

        // 首先需要对nums排序, 让相同的元素挨到一起
        Arrays.sort(nums);
        
        
        // digui函数的定义就是nums中以temp作为前缀, 能够得到哪些子集
        // 相同的前缀, 不能选择相同的后缀
        digui(nums, 0, new ArrayList<>());
        return ans;



    }
    
    public void digui(int [] nums, int start, List<Integer> temp) {

        // base case
        if (start > nums.length) {
            return;
        }
        
        // 添加一个子集合
        ans.add(new ArrayList<>(temp));
        
        // 选择列表
        for(int i = start; i < nums.length; i ++) {
            if (i > start && nums[i] == nums[i - 1]) {
                // 注意这里一定要有i > start的条件
                // (1) 保证全新的start, 第一个元素就算是和上一个元素是重复, 也应该添加上, 而同一个start, 重复的元素跳过
                // (2) 保证i - 1不会越界
                continue;
            }
            // 做出选择
            temp.add(nums[i]);
            digui(nums, i + 1, temp);
            // 撤销选择
            temp.remove(temp.size() - 1);
        }
        
    }

正确的原因

(1) 采用回溯的写法, 一定要注意那个i > start

组合

题目

image.png

版本1 正确

    // 返回1....n中, k个数字一组的所有组合
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {


        // 数字不会重复, 相当于结果是限定了长度的子集
        digui(1, new ArrayList<>(), n, k);
        return ans;


    }

    public void digui(int start, List<Integer> temp, int n, int k) {
        if(temp.size() == k) {
            ans.add(new ArrayList<>(temp));
        }

        // 枚举所有选择
        for (int i = start; i <= n; i ++) {
            // 做出选择
            temp.add(i);
            digui(i + 1, temp, n, k);
            // 撤销选择
            temp.remove(temp.size() - 1);
        }
    }

组合2

题目

image.png

从candidates选择元素不能重复说的是如果candidates中只有一个1, 你不能在答案中出现两个1, 但是你本身candidates就有两个1的话, 是可以选择两个1的.

版本1 正确

    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {

        // 就相当于寻找candidates中所有不重复的子集, 然后子集如果满足和为target, 则作为结果
        // 组合中所有元素都不能重复
        // 需要先排序
        Arrays.sort(candidates);
        digui(0, 0, new ArrayList<>(), target, candidates);
        return ans;

    }

    public void digui(int start, int tempSum, List<Integer> temp, int target, int [] candidates) {
        if (tempSum == target) {
            ans.add(new ArrayList<>(temp));
        }

        if (tempSum > target) {
            // 因为是有序的, 无需再寻找后缀了
            return;
        }

        // 枚举所有状态
        for(int i = start; i < candidates.length; i ++) {
            if (i > start && candidates[i] == candidates[i - 1]) {
                // 跳过所有重复元素
                continue;
            }
            // 做出选择
            temp.add(candidates[i]);
            tempSum += candidates[i];
            digui(i + 1, tempSum, temp, target, candidates);

            // 撤销选择
            temp.remove(temp.size() - 1);
            tempSum -= candidates[i];
        }


    }

正确的原因

(1) 注意审题, 读懂元素不能重复是什么含义.

组合3

题目

image.png

版本1 正确

    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {

        // 寻找组合的和为n, 长度为k
        // 数字只有1-9

        digui(1, 0, new ArrayList<>(), n, k);
        return ans;

    }

    public void digui(int start, int tempSum, List<Integer> temp, int n, int k) {
        if (tempSum == n && temp.size() == k) {
            ans.add(new ArrayList<>(temp));
        }
        

        // 枚举所有状态
        for(int i = start; i < 10; i ++) {
            // 做出选择
            temp.add(i);
            tempSum += i;
            digui(i + 1, tempSum, temp, n, k);

            // 撤销选择
            temp.remove(temp.size() - 1);
            tempSum -= i;
        }
        
    }

组合4 其实应该算作排列问题 本质就是零钱兑换啊!!!!

题目

image.png

版本1 超时

    int count = 0;
    public int combinationSum4(int[] nums, int target) {

        // nums中挑选任意元素作为序列, 每个元素的数量不限
        // 序列的顺序不同, 视为不同的序列

        digui(0, target, nums);
        return count;

    }

    public void digui(int tempSum, int target, int [] nums) {
        if (tempSum > target) {
            return;
        }
        if (tempSum == target) {
            count ++;
        }


        // 枚举所有状态
        for(int i = 0; i < nums.length; i ++) {
            // 做出选择
            tempSum += nums[i];
            digui(tempSum, target, nums);

            // 撤销选择
            tempSum -= nums[i];
        }

    }
    

错误的原因

(1) 递归的数目太多了, 考虑减枝

(2) 如果要求出所有的可能性, 只能通过回溯的方式, 也就是上面写的, 但是它只求总数, 就要往动态规划的方面想了

版本2 错误

    public int combinationSum4(int[] nums, int target) {


        // nums中挑选任意元素作为序列, 每个元素的数量不限
        // 序列的顺序不同, 视为不同的序列
        // 因为只求种数, 而不需要具体的列出来, 因此考虑动态规划
        // 仔细想想, 这不就是零钱兑换2吗?
        // nums就是硬币的种类, target就是目标金额

        // 明确状态
        // 状态就是i种数字和目标金额

        // 明确dp数组的含义
        // dp[i][j] = x, 表示选择前i个数组, 凑成和为j的种数为x种
        int [][] dp = new int[nums.length + 1][target + 1];
        
        // base case
        // dp[i][0] = 1; dp[0][j] = 0;
        for (int i = 1; i <= nums.length; i ++) {
            dp[i][0] = 1;
        }
        
        // 枚举所有状态
        for(int i = 1; i <= nums.length; i ++) {
            for (int j = 1; j <= target; j ++) {
                if (j - nums[i - 1] >= 0) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i - 1]];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        
        return dp[nums.length][target];
    }

错误的原因

(1) 完全背包问题求的是组合数, 也就是不认为顺序不同, 属于不同的序列

image.png

原因就是外层遍历物品, 那么当前物品在追加到之前的结果中的时候, 一定是追加到末尾的, 例如当j = 7的时候, nums[i] = 3的时候, dp[i][j - nums[i - 1]]表示的就是[1, 1, 1, 1, 3]或者[1, 3, 3]. 3永远是会在1之后出现的.

版本3 正确

参考一个连接

    public int combinationSum4(int[] nums, int target) {


        // nums中挑选任意元素作为序列, 每个元素的数量不限
        // 序列的顺序不同, 视为不同的序列
        // 因为只求种数, 而不需要具体的列出来, 因此考虑动态规划
        // 仔细想想, 这不就是零钱兑换吗? 注意是零钱兑换1, 而不是零钱兑换2,
        // 因为零钱兑换1中, dp数组的定义是枚举某个金额下, 然后构成的硬币数目最少的情况
        // 在每个金额下, 都会枚举所有硬币作为结尾的情况
        // 在这里就可以通过枚举金额是多少, 来求和一共有多少种数目


        // 明确状态
        // 状态就是i种金额

        // 明确dp数组的含义
        // dp[i] = x, 就是目标金额是i的情况下, 所有序列的种数和为x
        int [] dp = new int[target + 1];

        // base case
        // dp[0] = 1; 目标金额为0的情况下, 所需序列的种数只有一种, 就是[]
        dp[0] = 1;

        // 枚举所有状态
        // 枚举所有金额
        for(int i = 1; i <= target; i ++) {
            // 枚举该金额下, 以每个硬币作为结尾的序列和为多少
            dp[i] = 0;
            for(int j = 0; j < nums.length; j ++) {
                if (i - nums[j] >= 0) {
                    // 以nums[j]结尾的序列数目为dp[i - nums[j]]
                    dp[i] += dp[i - nums[j]];
                }
            }
        }


        return dp[target];
    }

正确的原因

(1) 注意为什么是一个零钱兑换1的问题, 然后和零钱兑换1有啥区别

零钱兑换

题目

image.png

版本1 双状态版本

   public static int coinChange(int[] coins, int amount) {

        int [][] dp = new int[amount + 1][coins.length + 1];

        for (int i = 1; i <= amount; i ++) {
            for (int j = 1; j <= coins.length; j ++) {
                dp[i][j] = Integer.MAX_VALUE;
                // 以第J枚硬币结尾
                if (i - coins[j - 1] >= 0) {
                    for (int m = 1; m <= coins.length; m ++) {
                        // 求放入第J枚硬币后, 此时最短的硬币数量
                        if (dp[i - coins[j - 1]][m] == -1) {
                            continue;
                        }
                        dp[i][j] = Math.min(dp[i][j], dp[i - coins[j - 1]][m] + 1);
                    }

                }
                if (dp[i][j] == Integer.MAX_VALUE) {
                    dp[i][j] = -1;
                }


            }
        }
        int min = Integer.MAX_VALUE;
        for(int j = 1; j <= coins.length; j ++) {
            if (dp[amount][j] == -1) {
                continue;
            }
            min = Math.min(dp[amount][j], min);
        }

        if (min == Integer.MAX_VALUE) {
            return -1;
        }

        return min;

    }

正确的原因

(1) 将dp数组定义为双状态, 然后在计算的时候, 需要循环去掉最后一个硬币后的结果

(2) 这个版本太复杂, 但是能清楚的明白定义其实是目标金额下, 以某个硬币为结尾的硬币的序列数目

版本2 单状态

public static int coinChange(int[] coins, int amount) {

        int [] dp = new int [amount + 1];


        for (int i = 1; i <= amount; i ++) {
            // 枚举以第J枚硬币结尾的情况
            int min = Integer.MAX_VALUE;
            for(int j = 1; j <= coins.length; j ++) {

                if (i - coins[j - 1] >= 0 && dp[i - coins[j - 1]] != -1) {
                    // 说明能够以第J枚硬币结尾
                    min = Math.min(min, dp[i - coins[j - 1]] + 1);
                }
            }
            if (min == Integer.MAX_VALUE) {
                dp[i] = -1;
            } else {
                dp[i] = min;
            }
        }

        return dp[amount];

    }

解数独

题目

image.png

版本1 正确

public void solveSudoku(char[][] board) {
        // 回溯算法
        // 从[0][0]位置, 填充元素到[board.length()][board[0].length]

        digui(board, 0, 0);



    }

    public boolean digui(char [][] board, int row, int col) {

        // 应为是从左往右枚举, 因此base case是row == board.length
        if (row == board.length) {
            return Boolean.TRUE;
        }

        if (board[row][col] == '.') {
            // 选择列表 1-9个数字
            for(int i = 1; i <= 9; i ++) {

                if (checkNum(board, (char) ('0' + i), row, col)) {
                    // 做出选择
                    board[row][col] = (char) ('0' + i);
                    boolean ans = Boolean.FALSE;
                    if (col + 1 < board[0].length) {
                        ans = digui(board, row, col + 1);
                    } else {
                        ans = digui(board, row + 1, 0);
                    }
                    if (ans) {
                        return ans;
                    }
                    // 撤销选择
                    board[row][col] = '.';
                }
            }
        } else {
            if (col + 1 < board[0].length) {
                return digui(board, row, col + 1);
            } else {
                return digui(board, row + 1, 0);
            }

        }
        return Boolean.FALSE;
    }

    // 校验该数字是否合法
    public boolean checkNum(char [][] board, char num, int row, int col) {

        // 同一行 数字不能重复
        for (int j = 0; j < board[0].length; j ++) {
            if (board[row][j] == num) {
                return Boolean.FALSE;
            }
        }

        // 同一列不能重复
        for (int i = 0; i < board.length; i ++) {
            if (board[i][col] == num) {
                return Boolean.FALSE;
            }
        }

        // 3x3内只能出现一次
        int rowStart = row - row % 3;
        int colStart = col - col % 3;
        for (int i = rowStart; i < rowStart + 3; i ++) {
            for (int j = colStart; j < colStart + 3; j ++) {
                if (board[i][j] == num) {
                    return Boolean.FALSE;
                }
            }
        }

        return Boolean.TRUE;


    }

正确的原因

(1) 注意数字转char的写法, (char) ('0' + i) 不能直接是(char) i , 不然显示的就是错误的. 其实就是算出1对应的ASCII码, 就可以正确的转换成char的1了.

原因见这里

(2) 注意得到一个正确的解之后, 就可以正常返回了, 无需遍历完所有

括号生成

题目

image.png

版本1 正确

List<String> ans = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        
        digui(n, new ArrayList<>(), 0);
        return ans;
    }

    public void digui(int n, List<String> temp, int leftNum) {

        // base case
        if (temp.size() == n * 2) {
            ans.add(String.join("", temp));
        }

        // 选择列表
        // 左括号数量要小于等于n
        // 右括号数量要小于等于左括号
        for (int i = 0; i < 2; i ++) {
            if (i == 0) {
                if(leftNum < n) {
                    // 添加左括号
                    temp.add("(");
                    digui(n, temp, leftNum + 1);
                    // 撤销选择
                    temp.remove(temp.size() - 1);
                }
            } else {
                if(temp.size() - leftNum < leftNum) {
                    // 添加右括号
                    temp.add(")");
                    digui(n, temp, leftNum);
                    // 撤销选择
                    temp.remove(temp.size() - 1);
                }

            }
        }
    }

两数之和

题目

image.png

版本1 错误

    static List<int []> ans = new ArrayList<>();
    public static int[] twoSum(int[] nums, int target) {

        // 两数之和, 就是在nums中, 找到长度为2的组合, 且组合的和为target
        digui(0, nums, target, new ArrayList<>(), 0);
        return ans.get(0);


    }

    public static boolean digui(int start, int [] nums, int target, List<Integer> temp, int tempSum) {
        if (temp.size() > 2) {
            return Boolean.FALSE;
        }
        if (temp.size() == 2 && tempSum == target) {
            ans.add(new int[]{temp.get(0), temp.get(1)});
            return Boolean.TRUE;
        }

        for (int i = start; i < nums.length; i ++) {
            // 做出选择
            temp.add(i);
            tempSum += nums[i];
            boolean ans = digui(start + 1, nums, target, temp, tempSum);
            if (ans) {
                return Boolean.TRUE;
            }
            // 撤销选择
            tempSum -= nums[i];
            temp.remove(temp.size() - 1);
        }
        return Boolean.FALSE;
    }

错误的原因

(1) 注意递归到下一个函数是i + 1, 而不是start + 1, 千万别搞错了

版本2 正确

    static List<int []> ans = new ArrayList<>();
    public static int[] twoSum(int[] nums, int target) {

        // 两数之和, 就是在nums中, 找到长度为2的组合, 且组合的和为target
        digui(0, nums, target, new ArrayList<>(), 0);
        return ans.get(0);


    }

    public static boolean digui(int start, int [] nums, int target, List<Integer> temp, int tempSum) {
        if (temp.size() > 2) {
            return Boolean.FALSE;
        }
        if (temp.size() == 2 && tempSum == target) {
            ans.add(new int[]{temp.get(0), temp.get(1)});
            return Boolean.TRUE;
        }

        for (int i = start; i < nums.length; i ++) {
            // 做出选择
            temp.add(i);
            tempSum += nums[i];
            boolean ans = digui(i + 1, nums, target, temp, tempSum);
            if (ans) {
                return Boolean.TRUE;
            }
            // 撤销选择
            tempSum -= nums[i];
            temp.remove(temp.size() - 1);
        }
        return Boolean.FALSE;
    }

正确的原因

(1) 两数之和的本质就是寻找数组中所有的长度为2的组合, 同时它的值等于目标值即可

三数之和

题目

image.png

如果是两数之和怎么求呢?

版本1 正确

    public List<List<Integer>> twoSum(int[] nums, int target) {

        // 如果数组nums是有序的, 如何寻找两数之和
        Arrays.sort(nums);

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

        // 两个指针
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int leftVal = nums[left];
            int rightVal = nums[right];

            if (leftVal + rightVal > target) {
                // 如果元素重复, 加速右指针的移动
                while (left < right && nums[right] == rightVal) {
                    right --;
                }
                
            }
            if (leftVal + rightVal < target) {
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
            if (leftVal + rightVal == target) {
                // 添加到ans中的结果不能重复, 因此每次添加需要将left和right跳过重复的元素
                List<Integer> temp = new ArrayList<>();
                temp.add(leftVal);
                temp.add(rightVal);
                ans.add(temp);

                // 移动右边指针, 遇见重复的就跳过
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

                // 移动左指针
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
        }
        return ans;

    }

正确的原因

(1) 为了让结果中不出现重复的结果, 遇见重复的元素跳过即可.

(2) 元素是有序的, 可以快速移动指针

版本1 通过两数之和, 扩展到三数之和 错误

    public List<List<Integer>> threeSum(int[] nums) {

        Arrays.sort(nums);
        
        List<List<Integer>> ans = new ArrayList<>();
        
        // 枚举第一个元素
        for(int i = 0; i < nums.length; i ++) {
            
            int nowVal = nums[i];
            // 枚举出第一个元素, 然后剩下的元素进行数求和
            List<List<Integer>> tempAns = twoSum(nums, 0 - nowVal, i + 1);
            for (List<Integer> temp : tempAns) {
                temp.add(nowVal);
                ans.add(new ArrayList<>(temp));
            }
            
            // 第一个元素如果遇见重复的也需要跳过
            while (i < nums.length && nums[i] == nowVal) {
                i ++;
            }
            
        }
        
        return ans;

        

    }



    public List<List<Integer>> twoSum(int[] nums, int target, int start) {



        List<List<Integer>> ans = new ArrayList<>();
        if (start >= nums.length - 1) {
            return ans;
        }

        // 两个指针
        int left = start;
        int right = nums.length - 1;
        while (left < right) {
            int leftVal = nums[left];
            int rightVal = nums[right];

            if (leftVal + rightVal > target) {
                // 如果元素重复, 加速右指针的移动
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

            }
            if (leftVal + rightVal < target) {
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
            if (leftVal + rightVal == target) {
                // 添加到ans中的结果不能重复, 因此每次添加需要将left和right跳过重复的元素
                List<Integer> temp = new ArrayList<>();
                temp.add(leftVal);
                temp.add(rightVal);
                ans.add(temp);

                // 移动右边指针, 遇见重复的就跳过
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

                // 移动左指针
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
        }
        return ans;

    }

错误的原因

(1) 枚举第一个元素的时候, 采用for循环, 然后为了跳过重复的元素, i已经到了下一个不重复的元素, 然后因为for循环的i又自加了1, 就多跳过了一个元素

版本2 正确

    public List<List<Integer>> threeSum(int[] nums) {

        Arrays.sort(nums);

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

        // 枚举第一个元素
        int start = 0;
        while (start < nums.length) {
            int nowVal = nums[start];
            // 枚举出第一个元素, 然后剩下的元素进行数求和
            List<List<Integer>> tempAns = twoSum(nums, 0 - nowVal, start + 1);
            for (List<Integer> temp : tempAns) {
                temp.add(nowVal);
                ans.add(new ArrayList<>(temp));
            }

            // 第一个元素如果遇见重复的也需要跳过
            while (start < nums.length && nums[start] == nowVal) {
                start ++;
            }
        }

        return ans;
    }



    public List<List<Integer>> twoSum(int[] nums, int target, int start) {

        List<List<Integer>> ans = new ArrayList<>();
        if (start >= nums.length - 1) {
            return ans;
        }

        // 两个指针
        int left = start;
        int right = nums.length - 1;
        while (left < right) {
            int leftVal = nums[left];
            int rightVal = nums[right];

            if (leftVal + rightVal > target) {
                // 如果元素重复, 加速右指针的移动
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

            }
            if (leftVal + rightVal < target) {
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
            if (leftVal + rightVal == target) {
                // 添加到ans中的结果不能重复, 因此每次添加需要将left和right跳过重复的元素
                List<Integer> temp = new ArrayList<>();
                temp.add(leftVal);
                temp.add(rightVal);
                ans.add(temp);

                // 移动右边指针, 遇见重复的就跳过
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

                // 移动左指针
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
        }
        return ans;

    }

正确的原因

(1) 通过枚举第一个元素, 然后对于剩下的数组, 进行两个元素求和, 成功的将问题化解, 因此可以迁移到n个元素求和.

四数之和

题目

image.png

版本1 正确 利用n数之和的模板

    public List<List<Integer>> fourSum(int[] nums, int target) {

        int n = 4;

        return nSum(nums, target, n, 0);


    }

    // 表示从数组start位置开始求n个数之和等于target的结果
    public List<List<Integer>> nSum(int[] nums, int target, int n, int start) {

        Arrays.sort(nums);

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

        if (n == 2) {
            return twoSum(nums, target, start);
        } else {
            while (start < nums.length) {
                int nowVal = nums[start];
                // 枚举出第一个元素, 然后剩下的元素进行数求和
                List<List<Integer>> tempAns = nSum(nums, target - nowVal, n - 1, start + 1);
                for (List<Integer> temp : tempAns) {
                    temp.add(nowVal);
                    ans.add(new ArrayList<>(temp));
                }

                // 第一个元素如果遇见重复的也需要跳过
                while (start < nums.length && nums[start] == nowVal) {
                    start ++;
                }
            }
        }



        return ans;
    }



    public List<List<Integer>> twoSum(int[] nums, int target, int start) {

        List<List<Integer>> ans = new ArrayList<>();
        if (start >= nums.length - 1) {
            return ans;
        }

        // 两个指针
        int left = start;
        int right = nums.length - 1;
        while (left < right) {
            int leftVal = nums[left];
            int rightVal = nums[right];

            if (leftVal + rightVal > target) {
                // 如果元素重复, 加速右指针的移动
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

            }
            if (leftVal + rightVal < target) {
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
            if (leftVal + rightVal == target) {
                // 添加到ans中的结果不能重复, 因此每次添加需要将left和right跳过重复的元素
                List<Integer> temp = new ArrayList<>();
                temp.add(leftVal);
                temp.add(rightVal);
                ans.add(temp);

                // 移动右边指针, 遇见重复的就跳过
                while (left < right && nums[right] == rightVal) {
                    right --;
                }

                // 移动左指针
                while (left < right && nums[left] == leftVal) {
                    left ++;
                }
            }
        }
        return ans;

    }