有序数组专项

294 阅读12分钟

有序数组的单一元素

题目

image.png

版本1 正确

    public int singleNonDuplicate(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }

        // 在有序数组中, 寻找只出现一次的元素, 其它元素出现两次
        // 有序数组, 一定要考虑二分法
        // 数组一定为奇数个
        // 寻找数组中间的元素, 如果该元素和前后元素都不相等, 那么就是我们要寻找的,
        // 如果该元素和前面一个或者后面一个的元素相等, 那么就可以排除另一半的数组

        return digui(nums, 0, nums.length - 1);

    }

    public int digui(int [] nums, int start, int end) {
        if (end == start) {
            return nums[start];
        }


        int mid = start + (end - start) / 2;
        // mid将start到end这一部分数组分成了两部分, 两部分可能都是奇数, 可能都是偶数

        if ((mid - start) % 2 == 0) {
            // 如果两边是偶数
            if (nums[mid] == nums[mid - 1]) {
                // 舍去后面
                return digui(nums, start, mid - 2);
            } else if (nums[mid] == nums[mid + 1]) {
                // 舍去前面
                return digui(nums, mid + 2, end);
            } else {
                return nums[mid];
            }
        } else {
            // 如果两边是奇数
            if (nums[mid] == nums[mid - 1]) {
                // 舍去前面
                return digui(nums, mid + 1, end);
            } else if (nums[mid] == nums[mid + 1]) {
                // 舍去后面
                return digui(nums, start, mid - 1);
            } else {
                return nums[mid];
            }
        }

    }

正确的原因

(1) 注意mid将数组分成两部分, 需要根据所有两边剩余的是奇数还是偶数, 然后结合和哪边的元素相等, 才能判断应该舍弃哪部分, 应该怎么传递索引

在排序数组中查找数字

题目

image.png

版本1 正确

    public static int search(int[] nums, int target) {
        if (nums.length == 0) {
            return 0;
        }

        // 在升序数组中, 查找target出现的次数
        // 因为重复的数字可能非常多, 因此使用两次二分查找, 反而效率更快

        int left = findLeft(nums, target, 0, nums.length - 1);
        int right = findRight(nums, target, 0, nums.length - 1);
        return right - left + 1;


    }

    public static int findLeft(int [] nums, int target, int start, int end) {
        // 注意左侧边界需要返回start, 并且是 > 才终止
        if (start > end) {
            return start;
        }
        // 寻找左侧边界
        int mid = start + (end - start) / 2;

        if (nums[mid] == target) {
            // 逐渐逼近左边界
            return findLeft(nums, target, start, mid - 1);
        } else if (nums[mid] > target) {
            return findLeft(nums, target, start, mid - 1);
        } else {
            return findLeft(nums, target, mid + 1, end);
        }
    }

    public static int findRight(int [] nums, int target, int start, int end) {
        // 注意右侧边界需要返回end, 并且是 > 才终止
        if (start > end) {
            return end;
        }
        // 寻找右侧边界
        int mid = start + (end - start) / 2;

        if (nums[mid] == target) {
            // 逐渐逼近右边界
            return findRight(nums, target, mid + 1, end);
        } else if (nums[mid] > target) {
            return findRight(nums, target, start, mid - 1);
        } else {
            return findRight(nums, target, mid + 1, end);
        }
    }

调整数组顺序使奇数位于偶数前面

题目

image.png

版本1 正确

    public static int[] exchange(int[] nums) {

        // 类似快排双指针
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {

            while (right > 0 && nums[right] % 2 == 0) {
                right --;
            }

            while (left < nums.length && nums[left] % 2 != 0) {
                left ++;
            }

            // 交换一下left和right
            if (left <= right) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                right --;
                left ++;
            }

        }

        return nums;

    }

正确的原因

(1) 右边指针一直移动的时候, 要注意边界, 左边指针同理

(2) 元素发生交换需要满足条件left <= right

数组中出现次数超过一半的数字

题目

image.png

版本1 正确

    public int majorityElement(int[] nums) {
        // 求数组中超过一半的元素

        int vote = 0;
        int target = 0;
        for (int i = 0; i < nums.length; i ++) {
            if (vote == 0) {
                vote ++;
                target = nums[i];
            } else if (target == nums[i]) {
                vote ++;
            } else {
                vote --;
            }

            if (vote > nums.length / 2) {
                break;
            }
        }

        return target;
    }

正确的原因

(1) 投票法, 如果vote为0, 认为下一个数是众数, 如果遇见相同的数字, 则vote ++, 否则vote -- 最后的数字, 一定就是出现超过一半的数字.

二维数组的查找

题目

image.png

版本1 正确

    public static boolean findNumberIn2DArray(int[][] matrix, int target) {
        if (matrix.length == 0 || matrix[0].length == 0) {
            return Boolean.FALSE;
        }

        if (target < matrix[0][0] || target > matrix[matrix.length - 1][matrix[0].length - 1]) {
            return Boolean.FALSE;
        }
        
        // 从数组右上角开始寻找
        int i = 0;
        int j = matrix[0].length - 1;
        
        while (i < matrix.length && j>= 0) {
            if (matrix[i][j] == target) {
                return Boolean.TRUE;
            } else if (matrix[i][j] > target) {
                // 左移
                j --;
            } else {
                i ++;
            }
        }
        return Boolean.FALSE;
        
    }

正确的原因

(1) 注意这个二维数组 可以使用二分查找提高查找速度, 但是其实本身不用, 因为从右上角开始遍历, 是可以确定遍历步骤的.

把数组排成最小的数

题目

image.png

版本1 正确

    public static String minNumber(int[] nums) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if ((String.valueOf(o1) + String.valueOf(o2)).compareTo(String.valueOf(o2) + String.valueOf(o1)) > 0) {
                    // 返回1就代表交换, 这里o1 > o2, 所以返回1, 让o1和o2交换
                    return 1;
                } else {
                    return -1;
                }
            }
        });
        // priorityQueue此时得到的是小顶堆, 最小值在队列头部
        for (int i = 0; i < nums.length; i ++) {
            priorityQueue.offer(nums[i]);
        }


        // 元素全部添加完毕后, 就得到了排序的结果
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < nums.length; i ++) {
            sb.append(String.valueOf(priorityQueue.poll()));
        }
        return sb.toString();
    }

正确的原因

(1) 其实本质还是给数组里的元素排序, 只不过排序的依据换成了根据字典序列进行排序.

(2) compartor的返回值, 返回>0的时候, 就代表让o1和o2交换, 这样思路就明确了. 当o1 > o2的时候, 返回1, 得到的就是小顶堆, 当o2 > o1的时候, 返回1, 就是大顶堆, 默认就是大顶堆.

数字序列中某一位数字

题目

image.png

版本1 正确

    public int findNthDigit(int n) {

        // 求第n位第数字
        // 首先计算n所在的位数范围
        long start = 1;
        int digit = 1;
        long count = 9;
        while (n > count) {
             n -= count;
            start = start * 10;
            digit = digit + 1;
            count = start * 9 * digit;
        }
        // n - 1是为了排除0
        // num就是n所在的那个数字
        long num = start + (n - 1) / digit;
        // 返回n具体在哪一位
        return Long.toString(num).charAt((n - 1) % digit) - '0';
    }

正确的原因

(1) 明确思路, 不同位数拥有的数量是有关系的, 可以逐步计算n属于哪个数字, 然后再计算n到底对应的是哪一位

(2) 例如start和count应该使用long类型, digit使用int, 在计算n -= count;的时候, 一定要这么写, 不能写成 n = n - count;

旋转数组最小的数字

题目

image.png

版本1 正确

    public int minArray(int[] numbers) {

        // 数组部分有序的, 利用二分法在数组中寻找最小值
        // 排序数组旋转一次后, 最小值一定在右侧排序数组
        // 即右侧排序数组的最大值, 等于左侧排序数组的最小值
        // 每次比较mid和right的值, 不比较mid和left是因为当numbers[mid] < numbers[left]的时候, 无法判断应该在哪个区间
        int left = 0;
        int right = numbers.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (numbers[mid] > numbers[right]) {
                // 说明mid处于左侧的有序数组, 最小值一定在mid右侧
                left = mid + 1;
            } else if (numbers[mid] < numbers[right]) {
                // mid位于右侧的有序数组, 那么mid到right一定是递增的, 最小值就为mid, 或者在mid左侧
                right = mid;
            } else {
                // 如果相等, 只能证明right节点是一定没用的, 其它的不能证明
                right --;
            }
        }
        return numbers[left];

    }

正确的原因

(1) 只旋转一次的数组, 有一个特点, 左侧有序数组的最小值, 是大于等于右侧有序数组的最大值的.

(2) 利用二分法, 每次比较mid和right的值, 可以逐渐缩小最小值所在的范围

和为S的连续整数序列

题目

image.png

版本1 正确

    public int[][] findContinuousSequence(int target) {

        // 有序数组就是1....target - 1
        // 结果要求是连续的元素
        // 要连续子数组的和为某个值, 因为还需要得到具体数组, 而不是有多少的个数, 因此不采用前缀和
        // 采用滑动窗口
        int left = 1;
        int right = 2;
        // 因为不能是一个元素
        List<int []> ans = new ArrayList<>();
        while (left < right) {
            int sum = (left + right) * (right - left + 1) / 2;
            if (sum == target) {
                int [] temp = new int[right - left + 1];
                for (int i = 0; i < right - left + 1; i ++) {
                    temp[i] = left + i;
                }
                ans.add(temp);
                // 平移
                left ++;
                right ++;
            } else if (sum > target) {
                left ++;
            } else {
                right ++;
            }
        }

        int [][] res = new int[ans.size()][];
        for (int i = 0; i < ans.size(); i ++) {
            res[i] = ans.get(i);
        }

        return res;
    }

正确的原因

(1) 因为是有序数组, 采用滑动窗口, 记录窗口范围里的和, 窗口范围里的和可以直接计算得到.

合并两个有序数组

题目

image.png

版本1 正确

    public static void merge(int[] nums1, int m, int[] nums2, int n) {

        // 合并两个有序数组, nums1数组中有足够的空间
        // 因此我们可以在两个数组的尾巴开始, 挑选最大值放到nums1的最后.
        // 这样就不用担心nums1数字被覆盖的问题了

        int right1 = m - 1;
        int right2 = n - 1;
        for (int i = m + n - 1; i >=0; i --) {
            if (right1 < 0) {
                nums1[i] = nums1[right2];
                right2 --;
            } else if (right2 < 0) {
                nums1[i] = nums1[right1];
                right1 --;
            } else if (nums1[right1] >= nums2[right2]) {
                nums1[i] = nums1[right1];
                right1 --;
            } else {
                nums1[i] = nums1[right2];
                right2 --;
            }
        }
    }

正确的原因

(1) 从两个数组中, 选出最大值, 放到nums1的末尾, 这样就不存在需要新建数组空间, 并且也不会需要移动nums1元素的问题了

有序数组中差绝对值之和

题目

image.png

版本1 正确

    public int[] getSumAbsoluteDifferences(int[] nums) {

        // nums是有序的
        // 任何一个元素i将数组分成两部分, 前半部分都是<= nums[i]
        // 后半部分 >= nums[i]
        // 我得到前半部分的和, 用 n * nums[i] - sum
        // 后半部分是sum - n * nums[i]
        // 就得到了题目要求的结果

        // 得到nums的前缀和
        int [] prefixSum = new int[nums.length];
        for (int i = 0; i < nums.length; i ++) {
            if (i == 0) {
                prefixSum[i] = nums[i];
            } else {
                prefixSum[i] = prefixSum[i - 1] + nums[i];
            }
        }

        int [] ans = new int[nums.length];

        for (int i = 0; i < nums.length; i ++) {
            // 前半部分的和
            int leftNum = i;
            int leftSum = 0;
            if (leftNum > 0) {
                leftSum = leftNum * nums[i] - prefixSum[i - 1];
            }

            // 后半部分
            int rightNum = nums.length - 1 - i;
            int rightSum = 0;
            if (rightNum > 0) {
                rightSum = prefixSum[nums.length - 1] - prefixSum[i] - rightNum * nums[i];
            }

            ans[i] = leftSum + rightSum;
        }

        return ans;
    }

正确的原因

(1) 首先是有序数组, i可以将前后分割成可以直接计算结果的数组

(2) 利用前缀和避免遍历

(3) 每次结果计算两次即可

删除有序数组中的重复项

题目

image.png

版本1 正确

    public int removeDuplicates(int[] nums) {

        // 采用快慢指针, 快指针比慢指针一开始多走一步, 如果快慢指针的值相同了, 都为A, 那么快指针继续走, 慢指针停下来
        // 然后快慢指针不同时, 快指针为C, 赋值一次, 然后此时nums[slow] = C, 此时j如果继续遇见C, 依旧会跳过C

        int slow = 0;
        int fast = 1;
        while (fast < nums.length) {
            if (nums[fast] == nums[slow]) {
                // slow停止, fast继续走
                while (fast < nums.length && nums[fast] == nums[slow]) {
                    fast ++;
                }
                if (fast != nums.length) {
                    // fast遇见和slow不同的元素了, 并且fast没有走出边界
                    slow ++; // 一定要先slow++, 因为相同的元素需要保留一个
                    nums[slow] = nums[fast];
                }

            } else {
                slow ++;
                fast ++;
            }
        }

        return slow + 1;
    }

正确的原因

(1) 利用快慢指针, 快指针用来跳过重复的元素, 慢指针用来标示最后一个不重复的元素是啥.

(2) 思路挺巧妙的, 本身想着fast遇见新的重复元素该怎么办, 但是因为slow更新了, 所以fast不会受影响.

早餐组合

题目

image.png

版本1 正确

    public int breakfastNumber(int[] staple, int[] drinks, int x) {

        // 对食物数组进行计数排序, 统计每个食物值出现的次数
        int [] arr = new int[x + 1];
        for (int i = 0; i < staple.length; i ++) {
            if (staple[i] <= x) {
                arr[staple[i]] ++;
            }

        }

        // 然后对arr求前缀和, 这样对于任意价格的食物, 都可以知道小于等于该价格的种类有多少
        // 前缀和中记录的是次数
        for (int i = 1; i < arr.length; i ++) {
            arr[i] = arr[i] + arr[i - 1];
        }

        // 遍历一遍饮料, 统计结果
        long count = 0;
        for (int i = 0; i < drinks.length; i ++) {
            int drink = drinks[i];
            if (drink <= x) {
               count += arr[x - drink];
            }
        }

        int ans = (int) (count % 1000000007L);
        return ans;
    }

正确的原因

(1) 这个思路类似求连续数组和为S的子数组个数那道题, 都是利用前缀和, 计算符合条件的数据的次数.

(2) 注意方式吧, 下次记住

翻转单词顺序

题目

image.png

版本1 正确

    public static String reverseWords(String s) {
        if (s.length() == 0) {
            return "";
        }
        // 到序遍历一遍字符串
        // 遇见空格就跳过
        StringBuilder sb = new StringBuilder();

        int slow = s.length() - 1;
        int fast = slow;
        for (int i = s.length() - 1; i >=0; i --) {
            if (s.charAt(i) == ' ') {
                slow --;
                fast --;
            } else {
                // slow停止 fast继续移动
                while (fast >= 0 && s.charAt(fast) != ' ') {
                    fast --;
                }
                sb.append(s.substring(fast + 1, slow + 1));
                sb.append(" ");
                i = fast + 1;
                slow = fast;
            }
        }
        String ans = sb.toString();
        if (ans.length() >= 2) {
            return ans.substring(0, ans.length() - 1);
        }
        return ans;

    }

正确的原因

(1) 为了翻转字符串, 可以倒着遍历, 没必要正着遍历再翻转

(2) i = fast + 1; i赋值的时候要注意, 本身for循环i就减少1

(3) 取出ans最后一个括号的时候, 要注意长度

前k个高频元素

题目

image.png

版本1 正确

    public int[] topKFrequent(int[] nums, int k) {
        if (k == 0 || nums.length == 0) {
            return new int[0];
        }
        // 出现频率前k高的元素
        // 利用map统计每个元素出现的次数, 然后利用堆找到频次前k大的元素
        // 维护一个小顶堆

        Map<Integer, Integer> num2FreqMap = new HashMap<>();
        for (int i = 0; i < nums.length; i ++) {
            num2FreqMap.put(nums[i], num2FreqMap.getOrDefault(nums[i], 0) + 1);
        }

        // 维护一个大小为k的小顶堆
        PriorityQueue<int []> priorityQueue = new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                // 维护小顶堆
                return o1[1] - o2[1];
            }
        });

        // 遍历所有元素
        for(Map.Entry<Integer, Integer> entry : num2FreqMap.entrySet()) {
            int [] data = new int[2];
            data[0] = entry.getKey();
            data[1] = entry.getValue();

            // 添加入队列
            if (priorityQueue.size() == k) {
                // 和堆顶元素做比较
                if (data[1] > priorityQueue.peek()[1]) {
                    priorityQueue.poll();
                    priorityQueue.offer(data);
                }
            } else {
                // 队列小于k, 直接添加即可
                priorityQueue.offer(data);
            }

        }

        // 队列中最后剩下的就是频次前k的元素
        int [] ans = new int[priorityQueue.size()];

        for (int i = priorityQueue.size() - 1; i >= 0; i --) {
            ans[i] = priorityQueue.poll()[0];
        }

        return ans;
    }

正确的原因

(1) 利用hashMap统计次数, 维护一个小顶堆来记录频次前k的元素.

版本2 利用快排

    // 注意全局变量用static, 在最后结果的时候会多统计
    List<int []> ans = new ArrayList<>();
    public int[] topKFrequent(int[] nums, int k) {
        if (k == 0 || nums.length == 0) {
            return new int[0];
        }
        // 出现频率前k高的元素
        // 利用map找到统计每个元素出现的频次, 然后对于频次数组利用快排缩小问题规模
        // 直到找到前k频次的数字

        Map<Integer, Integer> nums2FreqMap = new HashMap<>();
        for (int i = 0; i < nums.length; i ++) {
            nums2FreqMap.put(nums[i], nums2FreqMap.getOrDefault(nums[i], 0) + 1);
        }

        // 构造频次数组
        int [][] freqArray = new int[nums2FreqMap.size()][2];
        int index = 0;
        for (Map.Entry<Integer, Integer> entry : nums2FreqMap.entrySet()) {
            int [] temp = new int[2];
            temp[0] = entry.getKey();
            temp[1] = entry.getValue();
            freqArray[index] = temp;
            index ++;
        }

        // 利用快排的思想来解决
        quickSort(freqArray, 0, freqArray.length - 1, k);

        // 排序完成后, ans的结果就是频次最大的k个元素
        int [] ansArray = new int[k];
        for (int i = 0; i < ans.size(); i ++) {
            ansArray[i] = ans.get(i)[0];
        }

        return ansArray;
    }


    public void quickSort(int [][] freqArray, int start, int end, int k) {

        // base case
        // 注意这里是start > end
        if (start > end || k == 0) {
            return;
        }

        // 选贼第一个元素作为基准元素
        int base = freqArray[start][1];
        // 注意这个left要是start, 而不是start + 1
        int left = start;
        int right = end;
        // 注意这里是小于, 不是小于等于
        while (left < right) {
            // 右指针先动
            while (right > left && freqArray[right][1] >= base) {
                right --;
            }

            // 注意这里freqArray[left][1] <= base 也是小于等于
            while (left < right && freqArray[left][1] <= base) {
                left ++;
            }

            // 交换left和right的元素
            if (left < right) {
                int [] temp = freqArray[left];
                freqArray[left] = freqArray[right];
                freqArray[right] = temp;
            }

        }

        // 交换基准元素
        int [] temp = freqArray[left];
        freqArray[left] = freqArray[start];
        freqArray[start] = temp;

        // 然后计算基准元素两边的数组大小
        // 结果允许按任意顺序返回前k个数字, 因此如果基准将右边分成了小于k的大小, 那么就可以记录结果了

        int lenLeft = left - start;
        // 右边数组包含base元素
        int lenRight = end - left;

        // 这里要考虑base的元素数目, 所以要 + 1
        if (lenRight + 1 > k) {
            // 继续对右边数组排序
            quickSort(freqArray, left + 1, end, k);
        } else {
            // 记录右边数组的所有元素
            for (int i = left; i <= end; i ++) {
                ans.add(freqArray[i]);
            }

            // 然后对于剩下的在左边数组中排序
            quickSort(freqArray, start, left - 1, k - lenRight - 1);

        }

    }

正确的原因

(1) 注意快排的写法

(2) 注意应该舍去哪一部分元素.

数组中的第K个最大元素

题目

image.png

版本1 快排分区

    static int ans;
    public static int findKthLargest(int[] nums, int k) {

        // 返回第k大的数字
        quickSort(nums, 0, nums.length - 1, k);

        return ans;


    }

    public static void quickSort(int [] nums, int start, int end, int k) {
        if (start > end) {
            return;
        }

        int left = start;
        int right = end;
        int base = nums[start];
        while (left < right) {
            // 右指针先动
            while (left < right && nums[right] >= base) {
                right --;
            }

            while (left < right && nums[left] <= base) {
                left ++;
            }

            if (left < right) {
                // 交换一次
                int temp = nums[right];
                nums[right] = nums[left];
                nums[left] = temp;
            }
        }

        // 交换基准
        nums[start] = nums[left];
        nums[left] = base;

        // 判断base
        int lenRight = end - left;
        if (lenRight == k - 1) {
            ans = nums[left];
            return;
        }

        // 如果不是刚好第k个
        // 注意这里等于要去右边数组寻找
        if (lenRight >= k) {
            // 在右边数组继续寻找第k个
            quickSort(nums, left + 1, end, k);
        } else {
            // 在左边数组寻找第k - lenRight - 1个
            quickSort(nums, start, left - 1, k - lenRight - 1);
        }
    }

正确的原因

(1) 利用快排的思想对问题进行分区, 降低问题的规模.

最小的k个数字

题目

image.png

版本1 正确 快排分区

    List<Integer> ans = new ArrayList<>();
    public int[] getLeastNumbers(int[] arr, int k) {

        // 找出最小的k个数字
        // 利用快排进行分区

        quickSort(arr, 0, arr.length - 1, k);
        int [] ansArray = new int[k];
        for (int i = 0; i < ans.size(); i ++) {
            ansArray[i] = ans.get(i);

        }

        return ansArray;



    }

    public void quickSort(int [] arr, int start, int end, int k) {

        if (start > end) {
            return;
        }

        int left = start;
        int right = end;
        int base = arr[start];
        while (left < right) {
            // 右指针先动
            while (left < right && arr[right] >= base) {
                right --;
            }

            while (left < right && arr[left] <= base) {
                left ++;
            }

            if (left < right) {
                // 交换一次
                int temp = arr[right];
                arr[right] = arr[left];
                arr[left] = temp;
            }

        }

        // 交换基准元素
        arr[start] = arr[left];
        arr[left] = base;

        // 开始分区
        int lenLeft = left - start;
        if (lenLeft + 1 > k) {
            // 继续对左边的数组分区
            quickSort(arr, start, left - 1, k);
        } else {
            // 添加结果
            for (int i = start; i <= left; i ++) {
                ans.add(arr[i]);
            }

            // 对右边的数组分区
            quickSort(arr, left + 1, end, k - lenLeft - 1);
        }

    }

乘积小于k的数组个数

题目

image.png

版本1 滑动窗口

    public static int numSubarrayProductLessThanK(int[] nums, int k) {
        if (nums.length == 0 || k <= 1) {
            return 0;
        }

        // 乘积小于k的连续子数组的个数
        // 只需要统计符合要求的连续子数组的个数, 利用滑动窗口
        // 当窗口内的值小于k时, 移动右指针
        // 当窗口内的值大于k时, 移动左边指针, 直到窗口内的值小于k
        // 记录一次结果
        int left = 0;
        int right = 0;
        int windowMulti = 1;
        int count = 0;
        while (right < nums.length) {
            windowMulti = windowMulti * nums[right];
            while (windowMulti >= k) {
                // 移动左指针
                windowMulti /= nums[left];
                left ++;
            }

            // 统计一次结果
            count += right - left + 1;
            right ++;
        }

        return count;
    }

正确的原因

(1) 如果窗口内的乘积小于k, 就累加一次次数, 每次都移动右指针, 如果窗口内的乘积大于k, 一直移动左边指针.

最短无序连续子数组

题目

image.png

版本1 正确 利用额外的空间

    public static int findUnsortedSubarray(int[] nums) {

        // nums中有一段是无序的, 我们可以从头尾分别遍历, 很容易的得到当前无序的数组长度
        // 但是此时这一段无序的数组的长度, 就算排序好了, 可能和头尾的有序数组连接不上, 因此没有用处
        // 例如[1, 3, 6, 5, 2, 8, 9], 我们判断出无序数组是[5, 2], 排序完后, 整个数组为[1, 3, 6, 2, 5, 8, 9]
        // 此时结果并不是有序的
        // 因此我们需要得到无序数组中的最大值和最小值, 并且寻找他们在排序结束后, 应该在的索引下标
        // 两个下标的差值就是我们的答案, 例如无序数组[5, 2]最小值2在最后排序数组的索引应该1, 最大值5在最后索引应该为4
        // 答案就是4 - 1 + 1 = 4;

        // 采用两个栈, 来得到无序数组中, 最小值和最大值, 在排序后数组中应该存在的位置
        Stack<Integer> stack = new Stack<>();
        // 求无序数组中最小值应该在的位置
        // 求最小值, 初始值就赋予可能的最大值
        int minIndex = nums.length - 1;
        for (int i = 0; i < nums.length; i ++) {
            // 如果当前元素比栈顶的元素小, 需要弹出元素
            // 需要弹出的时候, 就代表来到来无序数组的部分
            while (!stack.isEmpty() && nums[i] < nums[stack.peek()]) {
                stack.pop();
                // 每次弹出元素, 才表示在无序数组
                // 每次弹出元素后, 此时栈顶的元素就有可能 < nums[i], 需要记录一次
                // 取此时栈顶元素的时候, 需要判断栈顶是否为空
                if (!stack.isEmpty()) {
                    minIndex = Math.min(stack.peek() + 1, minIndex);
                } else {
                    minIndex = Math.min(0, minIndex);
                }
            }
            // 默认添加元素
            stack.push(i);
        }

        stack.clear();
        // 求最大值, 初始值就赋予可能的最小值
        int maxIndex = 0;
        // 同理求无序数组最大值的索引
        for (int i = nums.length - 1; i >= 0; i --) {
            // 如果当前元素比栈顶的元素大, 需要弹出元素
            // 需要弹出的时候, 就代表来到来无序数组的部分
            while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
                stack.pop();
                // 每次弹出元素后, 此时栈顶的元素就有可能 > nums[i], 需要记录一次
                if (!stack.isEmpty()) {
                    maxIndex = Math.max(stack.peek() - 1, maxIndex);
                } else {
                    maxIndex = Math.max(nums.length - 1, maxIndex);
                }
            }

            // 默认添加元素
            stack.push(i);
        }
        
        
        return maxIndex - minIndex > 0 ? maxIndex - minIndex + 1 : 0;
    }

正确的原因

(1) minIndex的初始值应该为nums.length - 1, maxIndex的初始值应该为0, 明确这一点, 别搞混了.

(2) 需要明确必须是在弹出元素的时候, 才需要更新minIndex的值, 并且需要考虑栈为空的情况

(3) 注意求最小值的索引的时候, 从前往后遍历, 求最大值索引的时候, 从后往前遍历

(4) 最后返回结果的时候, 如果maxIndex - minIndex < 0 就表示数组本身是有序的, 此时应该返回0

版本2 不实用额外的空间, 最优的解法

    public static int findUnsortedSubarray(int[] nums) {

        // nums中有一段是无序的, 我们可以从头尾分别遍历, 很容易的得到当前无序的数组长度
        // 但是此时这一段无序的数组的长度, 就算排序好了, 可能和头尾的有序数组连接不上, 因此没有用处
        // 例如[1, 3, 6, 5, 2, 8, 9], 我们判断出无序数组是[5, 2], 排序完后, 整个数组为[1, 3, 6, 2, 5, 8, 9]
        // 此时结果并不是有序的
        // 因此我们需要得到无序数组中的最大值和最小值, 并且寻找他们在排序结束后, 应该在的索引下标
        // 两个下标的差值就是我们的答案, 例如无序数组[5, 2]最小值2在最后排序数组的索引应该1, 最大值5在最后索引应该为4
        // 答案就是4 - 1 + 1 = 4;

        // 寻找到无序数组中的最小值和最大值
        // 然后通过遍历数组的方式, 寻找到最小值在排序数组中的位置

        int min = Integer.MAX_VALUE;
        // 遍历一遍数组, 寻找无序数组的最小值
        // flag表示, 是否进入到无序数组的范围中
        boolean flag = Boolean.FALSE;
        for (int i = 0; i < nums.length; i ++) {
            if (i > 0 && nums[i] < nums[i - 1]) {
                flag = Boolean.TRUE;
            }
            // 此时nums[i]是无序数组的一部分了
            if (flag) {
                min = Math.min(min, nums[i]);
            }
        }

        // 同理求最大值
        int max = Integer.MIN_VALUE;
        flag = Boolean.FALSE;
        for (int i = nums.length - 1; i >= 0; i --) {
            if (i < nums.length - 1 && nums[i] > nums[i + 1]) {
                flag = Boolean.TRUE;
            }
            // 此时nums[i]是无序数组的一部分了
            if (flag) {
                max = Math.max(max, nums[i]);
            }
        }

        // 寻找min在数组中正确的索引位置
        int minIndex = 0;
        for (int i = 0; i < nums.length; i ++) {
            if (nums[i] <= min) {
                minIndex ++;
            } else {
                break;
            }
        }

        // 寻找max在数组中正确的索引
        int maxIndex = nums.length - 1;
        for (int i = nums.length - 1; i >= 0; i --) {
            if (nums[i] >= max) {
                maxIndex --;
            } else {
                break;
            }
        }


        return maxIndex - minIndex > 0 ? maxIndex - minIndex + 1 : 0;
    }

正确的原因

(1) 在求解无序数组中最大最小值的时候, 需要判断是否进入了无序数组的范围, 才进行更新

(2) 因为数组中可能存在相同的值, 例如下图, 因此在求最小值, 最大值应该的排序位置时, 应该是大于等于, 而不是单纯的大于

            if (nums[i] >= max) {
                maxIndex --;
            } else {
                break;
            }

image.png

搜索旋转排序数组

题目

image.png

版本1 正确

    public int search(int[] nums, int target) {
        // 在旋转排序数组中, 寻找目标元素
        // [1, 2, 3, 4, 5, 6]旋转完后变成[4, 5, 6, 1, 2, 3]
        // 利用二分查找加快搜寻的速度
        // 对于旋转后的数组, 选择任意一个位置将数组分割开来, 一定有一半是有序的
        // 就可以根据有序的那一半来帮助判断, 目标元素应该在哪一半中

        // 如何判断哪一半是有序数组呢?
        // 数组旋转后, 左边数组的最小值, 一定是右边数组的最大值
        // 因此如果每次采用mid对于数组进行分割的话, 就可以比较nums[mid]和nums[0]和nums[nums.length - 1]这三个值的大小
        // 注意, 这里比的一定是nums[0], nums[mid]和nums[nums.length - 1], 而不是和二分查找的左右边界的值比较
        // 如果nums[mid] >= nums[0], 那么left到mid这一段一定是有序数组
        // 如果nums[mid] <= nums[nums.length - 1], 那么mid到right这一段一定是有序数组

        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] >= nums[0]) {
                // left到mid是有序数组
                // 注意判断target是否在有序数组需要两个条件, 1. 要大于有序数组的最小值 2. 小于有序数组最大值
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else if (nums[mid] <= nums[nums.length - 1]) {
                // mid到right是有序数组
                // 同理
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }

正确的原因

(1) 注意如何判断mid分割开的两个数组, 哪个是有序的, 哪个是无序的

(2) 对于有序数组, 需要判断target是否在有序数组的范围内, 不能只判断一边, 忽略另一边, 即需要nums[left] <= target && target < nums[mid]而不是单单判断target < nums[mid]