[算法]数组类 – 初步及优化

> 数组类问题一般可优化为O(n)复杂度的问题
> 常用技术
    - 快慢指针滑动
    - 首尾指针对撞

[toc]

最大连续1的个数

LeetCode485
① [暴力] 单次遍历,连续1直到末位情况需再次计算

    public int findMaxConsecutiveOnes(int[] nums) {
        int max = 0;
        int cnt = 0;
        for (int item : nums) {
            if (item == 1) ++cnt;
            else {
                max = max > cnt ? max : cnt;
                cnt = 0;
            }
        }
        return max > cnt ? max : cnt;
    }

② [双指针] 慢指针指向1子串首元素,快指针指向1子串末元素下一元素,差为数目

    public int findMaxConsecutiveOnes(int[] nums) {
        int slow = 0;
        int fast = 0;
        int max = 0;
        while (fast < nums.length) {
            if (nums[fast] == 0) {
                max = Math.max(max, fast - slow);
                slow = ++fast;
            }
            else ++fast;
        }
        return Math.max(max, fast - slow);
    }

反转字符串

LeetCode344
[双指针]

    public void reverseString(char[] s) {
        int slow = 0;
        int fast = s.length - 1;
        char tmp;
        while(slow < fast) {
            tmp = s[fast];
            s[fast--] = s[slow];
            s[slow++] = tmp;
        }
    }

数组拆分

LeetCode561
① [转义] 每个min对中相差最小即两两相邻可使和最大,排序后挑选即可

    public int arrayPairSum(int[] nums) {
        Arrays.sort(nums);
        int sum = 0;
        for (int i = 0; i < nums.length; i += 2)
            sum += nums[i];
        return sum;
    }

② [★] 待续

两数之和

LeetCode1

已知被减数,遍历指向的当前减数,可计算另一减数
必须第二次遍历或额外内存寻找另一减数下标

① [暴力求解] 每次遍历第一减数,并从此向后寻找零一减数,直到满足条件返回下标

    public static 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 null;
    }

② [两次遍历] 哈希表是高效查找算法,第一次遍历存储第一减数下标,第二次遍历通过计算出该数查找下标,注意排除拿到同一下标情况

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

③ [一次遍历] 查找当前数是否为已计算结果

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

两数之和 – 有序数组

LeetCode167

[双指针] 利用排序性质,首指针足够小,尾指针足够大,要么解唯一,要么无解

    public static int[] twoSum(int[] nums, int target) {
        int h = 0;
        int t = nums.length - 1;
        while(h < t) {
            int s = nums[h] + nums[t];
            if (s == target) return new int[] {h + 1, t + 1};
            else if (s < target) h++;
            else t--;
        }
        return null;
    }

买卖股票的最佳时机

LeetCode121
[双指针]

    public static int maxProfit(int[] prices) {
        int slow = 0;
        int fast = 1;
        int temp = 0;
        int max = 0;
        while (fast < prices.length) {
            if (prices[slow] > prices[fast])
                slow = fast;
            else {
                temp = prices[fast] - prices[slow];
                max = max > temp ? max : temp;
            }
            fast++;
        }
        return max;
    }

判断子序列

LeetCode392
[双指针] 依次遍历两字符串下标匹配,O(n)复杂度

    public boolean isSubsequence(String s, String t) {
        int index = -1;
        for (char c : s.toCharArray()) {
            index = t.indexOf(c, index + 1);
            if (index == -1) return false;
        }
        return true;
    }

移动零

LeetCode283
① [暴力] 从零开始到最后一个非零数依次前移去除所夹0,末位补0

    public static void moveZeroes(int[] nums) {
        int head = 0;
        int tail = nums.length - 1;
        while (nums[tail] == 0)
            if (tail > 0) tail--;
            else return;
        while (head < tail) {
            while (nums[head] == 0) {
                for (int cur = head; cur < tail; cur++)
                    nums[cur] = nums[cur + 1];
                nums[tail] = 0;
                tail--;
            }
            head++;
        }
    }

② [改进] 第一次遍历筛选出非零数,第二次遍历将非零数依次填值,剩余置零

    public void moveZeroes(int[] nums) {
        int len = nums.length;
        List<Integer> list = new ArrayList<>(len);
        for (int i = 0; i < len; i++)
            if (nums[i] != 0) list.add(nums[i]);
        for (int i = 0; i < list.size(); i++)
            nums[i] = list.get(i);
        Arrays.fill(nums, list.size(), len, 0);
    }

③ [改进] 快指针筛选非零数,慢指针依次填入非零数,快指针遍历完后其余填零

    public static void moveZeroes(int[] nums) {
        int slow = 0;
        int fast = 0;
        for (; fast < nums.length; fast++)
            if (nums[fast] != 0) nums[slow++] = nums[fast];
        for(; slow < nums.length; slow++)
            nums[slow] = 0;
    }

④ [改进] 慢指针找到首个零项,快指针从慢指针出发找到第一个非零项,值交换

    public static void moveZeroes(int[] nums) {
        int slow = 0;
        int fast = 0;
        while (fast < nums.length) {
            if (nums[slow] != 0) fast = ++slow;
            else {
                if (nums[fast] == 0) fast++;
                else {
                    nums[slow] = nums[fast];
                    nums[fast] = 0;
                }
            }
        }
    }

⑤ [改进] 慢指针记录首个0与其后快指针记录的首个非零数交换,慢指针依次填值,首位非零值与自身交换位置不动

    public static void moveZeroes(int[] nums) {
        int slow = 0;
        int fast = 0;
        for (; fast < nums.length; fast++)
            if (nums[fast] != 0) {
                int temp = nums[slow];
                nums[slow++] = nums[fast];
                nums[fast] = temp;
            }
    }

移除元素

LeetCode27
① [迁移] 相当于移动零问题,slow所指即为除去该值数组末尾

    public int removeElement(int[] nums, int val) {
        int fast = 0;
        int slow = 0;
        for(;fast < nums.length; fast++){
            if(nums[fast] != val){
                int temp = nums[slow];
                nums[slow++] = nums[fast];
                nums[fast] = temp;
            }
            return slow;
        }
    }

② [改进] 快指针筛选,慢指针只需按序填值即可

    public int removeElement(int[] nums, int val) {
        int fast = 0;
        int slow = 0;
        for(;fast < nums.length; fast++)
            if(nums[fast] != val)
                nums[slow++] = nums[fast];
        return slow;
    }

③ [改进] 减小数组减少遍历次数,末尾值替换该值

    public int removeElement(int[] nums, int val) {
        int i = 0;
        int n = nums.length;
        while(i < n)
            if(nums[i] == val)
                nums[i] = nums[--n];
            else i++;
        return n;
    }

删除重复项Ⅰ

LeetCode26
① [线性表] 线性表删除思想需要移动大量尾部元素

    public int removeDuplicates(int[] nums) {
        int tail = nums.length;
        int i = 1;
        while (i < tail) {
            if (nums[i - 1] == nums[i]) {
                for (int j = i; j < tail - 1; j++) {
                    nums[j] = nums[j + 1];
                }
                tail--;
            } else {
                i++;
            }
        }
        return tail;
    }

② [双指针] 快指针对比筛选,慢指针依次覆盖

    public int removeDuplicates(int[] nums) {
        int slow = 0;
        int fast = 1;
        for(; fast < nums.length; fast++)
            if(nums[fast] != nums[slow])
                nums[++slow] = nums[fast];
        return ++slow;
    }

③ [改进] 取消开始位置不同值的原地复制

    public int removeDuplicates(int[] nums) {
        int slow = 0;
        int fast = 1;
        for(; fast < nums.length; fast++)
            if(nums[fast] != nums[slow])
                if(fast != slow)
                    nums[++slow] = nums[fast];
        return ++slow;
    }

删除重复项Ⅱ

LeetCode80
① [双指针] 快指针筛选与慢指针元素对比,慢指针依次填值,直到同一元素计数到2停止更新

    public int removeDuplicates(int[] nums) {
        int slow = 0;
        int fast = 1;
        int count = 1;
        for (; fast < nums.length; fast++) {
            if (nums[fast] != nums[slow]) {
                nums[++slow] = nums[fast];
                count = 1;
            } else {
                if (count != 2) {
                    nums[++slow] = nums[fast];
                    count++;
                }
            }
        }
        return ++slow;
    }

② [改进] 慢指针记录最后一个写入元素,快指针记录待插入慢指针之后元素。快指针与慢指针前一值相同,说明待插元素与前两个元素均相同不需插入,快指针向前遍历寻找新值即可。若不同,则说明待插值要么是第二个值,要么是新值,插在慢指针之后并更新慢指针。每个值最多可出现两次,则前两个必在结果集中,从第三个开始遍历即可。

    public int removeDuplicates(int[] nums) {
        int slow = 1;
        int fast = 2;
        for(; fast < nums.length; fast++)
            if(nums[slow - 1] != nums[fast])
                nums[++slow] = nums[fast];
        return ++slow;
    }

③ [改进] 若当前数大于前二位置数,说明是出现新值或第二个同样值,慢指针依次填值

    public int removeDuplicates(int[] nums) {
        if(nums.length <= 2) return nums.length;
        for (int i = 2; i < nums.length; i++)
            if (nums[i] != nums[i - 2])
                nums[i++] = n;
        return i;
    }

④ [推广] 删除重复项,每个元素最多出现N次

    public int removeDuplicates(int[] nums) {
        int i = 0;
        for (int n : nums)
            if (i < N || N > nums[i - N])
                nums[i++] = n;
        return i;
    }

颜色分类

LeetCode80
① [双指针/基数排序] 快指针遍历两次数组,每轮快指针从慢指针出发递增并筛选该值,交换该值到慢指针位置填入

    public static void sortColors(int[] nums) {
        int cur = 0;
        int slow = 0;
        for (; cur <= 1; cur++) {
            for (int fast = slow; fast < nums.length; fast++) {
                if (cur == nums[fast]) {
                    int temp = nums[slow];
                    nums[slow++] = nums[fast];
                    nums[fast] = temp;
                }
            }
        }
    }

② [改进] 三路快速排序。一次向前遍历,遇0交换到最左,遇2交换到最右。相当于一个快指针遍历,两个慢指针分别从两端向中间依次填入最小值和最大值

    public static void sortColors(int[] nums) {
        int i0 = 0;
        int i1 = 0;
        int i2 = nums.length - 1;
        int tmp;
        while (i1 <= i2) {
            if (nums[i1] == 0) {
                tmp = nums[i0];
                nums[i0++] = nums[i1];
                nums[i1++] = tmp;
            }
            else if (nums[i1] == 2) {
                tmp = nums[i2];
                nums[i2--] = nums[i1];
                nums[i1++] = tmp;
            }
            else i1++;
        }
    }

寻找数组的中心索引

LeetCode724

注意下标0处在右侧加和为0时也为中心索引,length - 1同理

① [暴力] 递增索引,每次验证左右部分和

    public int pivotIndex(int[] nums) {
        for(int i = 0; i < nums.length; i++) {
            int left = 0;
            int right = 0;
            for(int j = 0; j < i; j++)
                left += nums[j];
            for(int k = nums.length - 1; k > i; k--)
                right += nums[k];
            if(left == right) return i;
        }
        return -1;
    }

② [改进] 减少右侧和计算,右侧和 = 总和 – 左侧和 – 当前索引值。同理有 2 * 左侧和 = 总和 – 当前索引值

    public int pivotIndex(int[] nums) {
        int sum = 0;
        int lef = 0;
        for(int num: nums) sum += num;
        for(int i = 0; i < nums.length; i++) {
            if(lef == sum - lef - nums[i]) return i;
            lef += nums[i];
        }
        return -1;
    }

至少是其他数字两倍的最大数

LeetCode747
① [两次遍历] 一次找最大值,第二次验证条件

    public static int dominantIndex(int[] nums) {
        int max = 0;
        for (int i = 0; i < nums.length; ++i)
            if (nums[i] > nums[max])
                max = i;
        for (int i = 0; i < nums.length; ++i)
            if (max != i && nums[max] <= 2 * nums[i])
                return -1;
        return maxIndex;
    }

② [一次遍历] 一次遍历筛选最大值和次大值

    public static int dominantIndex(int[] nums) {
        if (nums.length < 1) return -1;
        if (nums.length == 1) return 0;
        int max = 0;
        int smax = Integer.MIN_VALUE;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[max]) {
                smax = nums[max];
                max = i;
            }
            else if (nums[i] > smax && nums[max] != nums[i])
                smax = nums[i];
        }
        return nums[max] >= smax * 2 ? max : -1;
    }

加一

LeetCode66

    public int[] plusOne(int[] digits) {
        for(int i = digits.length - 1; i >= 0; i--) {
            if(digits[i] == 9) digits[i] = 0;
            else{
                digits[i]++;
                return digits;
            }
        }
        int arr = new int[digits.length + 1];
        arr[0] = 1;
        return arr;
    }

对角线遍历

LeetCode498
① [数字规律] 列出遍历顺序下标寻找规律。仅适用于方阵,M×N矩阵报错

    public static int[] findDiagonalOrder(int[][] matrix) {
        if (matrix == null) return new int[0];
        if (matrix.length == 0) return new int[0];
        if (matrix[0].length == 0) return new int[0];
        int count = 0, n = matrix.length;
        int[] nums = new int[n * matrix[0].length];
        for (int i = 0; i < 2 * n - 1; i++) {
            if (i < n)
                if (i % 2 == 1)
                    for (int j = 0; j <= i; j++)
                        nums[count++] = matrix[j][i - j];
                else
                    for (int j = i; j >= 0; j--)
                        nums[count++] = matrix[j][i - j];
            else
                if (i % 2 == 1)
                    for (int j = i - n + 1; j <= n - 1; j++)
                        nums[count++] = matrix[j][i - j];
                else
                    for (int j = n - 1; j >= i - n + 1; j--)
                        nums[count++] = matrix[j][i - j];
        }
        return nums;
    }

② [移动下标] 依次移动下标,坐标和为遍历层数,层数为偶数向右上移动,奇数左下移动,每到层末边界改变移动方向。

主对角线到达层末元素后改变方向与次对角线改变方向不同
须首先判断是否主对角线,再判断是否次对角线层末元素
颠倒则超出数组边界
    public int[] findDiagonalOrder(int[][] matrix) {
        if (matrix == null) return new int[0];
        if (matrix.length == 0) return new int[0];
        if (matrix[0].length == 0) return new int[0];
        int r = 0, c = 0;
        int row = matrix.length;
        int col = matrix[0].length;
        int[] arr = new int[row * col];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = matrix[r][c];
            if ((r + c) % 2 == 0) {
                if (c == col - 1) r++;
                else if (r == 0) c++;
                else { r--; c++; }
            }
            else {
                if (r == row - 1) c++;
                else if (c == 0) r++;
                else { r++; c--; }
            }
        }
        return arr;
    }

旋转数组

LeetCode189
① [暴力求解] 每次批量移动一位移动k次

依次移动,单独处理末位和头部
    public void rotate(int[] nums, int k) {
        int l = nums.length;
        if (l > 0) {
            for (int i = 0; i < k; i++) {
                int t = nums[l - 1];
                for (int j = l - 1; j > 0; j--)
                    nums[j] = nums[j - 1];
                nums[0] = t;
            }
        }
保存被覆盖值,供下次循环赋值
    public void rotate(int[] nums, int k) {
        int p = nums[l - 1];
        for (int j = 0; j < l; j++) {
            int t = nums[j];
            nums[j] = p;
            p = t;
        }
    }

② [额外内存] 开辟另外一个数组,直接放入正确位置

    public void rotate(int[] nums, int k) {
        if (nums.length > 1) {
            int[] arr = new int[nums.length];
            for (int i = 0; i < nums.length; i++)
                arr[(i + k) % nums.length] = nums[i];
            for (int i = 0; i < nums.length; i++)
                nums[i] = arr[i];
        }
    }

③ [★] 待续

长度最小的子数组

LeetCode209
① [暴力求解] 枚举下标所有情况

    public static int minSubArrayLen(int s, int[] nums) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            for (int j = i; j < nums.length; j++) {
                int sum = 0;
                for (int k = i; k <= j; k++)
                    sum += nums[k];
                if (sum >= s) {
                    int val = j - i + 1;
                    if (min > val) min = val;
                }
            }
        }
        return min == Integer.MAX_VALUE ? 0 : min;
    }

② [优化] 待续
③ [滑动窗口] 慢指针原地等待,快指针向前遍历加和,直到和满足条件。慢指针前移后重复上述操作,记录最小序列长度。快指针到达末尾停止。

    public int minSubArrayLen(int s, int[] nums) {
        int l = 0, r = 0;
        int sum = 0, tmp = 0;
        int min = Integer.MAX_VALUE;
        while (l < nums.length) {
            if (sum < s && r < nums.length)
                sum += nums[r++];
            else sum -= nums[l++];
            tmp = r - l;
            if (min > tmp && sum >= s)
                min = tmp;
        }
    }

详细说明

输入 [2, 3, 1, 2, 4, 3]
window  l       r       sum     min     tmp
2       0       1       2       MAX     1
23      0       2       5       MAX     2
231     0       3       6       MAX     3
2312    0       4       8       4       4
312     1       4       6       4       3
3124    1       5       10      4       4
124     2       5       7       3       3
24      3       5       6       3       2
243     3       6       9       3       3
43      4       6       7       2       2
3       5       6       3       2       1
        6       6       0       2       0

④ [优化] 仅在缩小窗口(慢指针前移)时计算比较上次长度

    public int minSubArrayLen(int s, int[] nums) {
        int l = 0, r = 0;
        int sum = 0, tmp = 0;
        int min = Integer.MAX_VALUE;
        while (l < nums.length) {
            if (sum < s && r < nums.length)
                sum += nums[r++];
            else {
                if (sum < s) break;
                tmp = r - l;
                if (min > tmp)
                    min = tmp;
                sum -= nums[l++];
            }
        }
        return min == Integer.MAX_VALUE ? 0 : min;
    }

详细说明

输入 [2, 3, 1, 2, 4, 3]
window      l       r       sum     min     tmp
2           0       1       2       MAX     0
23          0       2       5       MAX     0
231         0       3       6       MAX     0
2312        0       4       8       MAX     0
312         1       4       6       4       4
3124        1       5       10      4       4
124         2       5       7       4       4
24          3       5       6       3       3
243         3       6       9       3       3
43          4       6       7       3       3
3           5       6       3       2       2

最大子序和

LeetCode53
不断更新最大和,其中负和对最大和无贡献,清零重计

    public int maxSubArray(int[] nums) {
        int cur = 0;
        int sum = 0;
        int max = nums[0];
        while (cur < nums.length) {
            sum += nums[cur];
            max = sum > max ? sum : max;
            sum = sum > 0 ? sum : 0;
            cur++;
        }
        return max;
    }

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号