【OfferX】数组

Java中操作数组的坑

sort

当使用sort时,如果按照下面的方式实现排序,可能存在溢出的情况

Arrays.sort(nums, (int a,int b)->{ return a-b;});

因为如果a-b的值超过int的范围,就会发生反转,这里应当如下使用:

Arrays.sort(nums, Comparator.comparingInt((a)->a));

如果是二维数组,如下:

Arrays.sort(nums, Comparator.comparingInt((int[] a)->a[0]));

使用自定义的比较器:

Arrays.sort(nums,(int a,int b)->{ if(a==b)return 0;return a>b?-1:1;});

单调栈

题目:84. Largest Rectangle in Histogram(Hard)

:使用一个单调非递减的栈记录,每当遇到更小的元素时,说明左侧的所有元素不可能再跨过这个边界,因此它们的面积已经确定。
确定的面积等于当前下标i减去元素最左侧的边界。

class Solution {
    public int largestRectangleArea(int[] heights) {
        // the height is upon the minimal bar in the range
        int[] arr = heights;
        int n = arr.length;
        int[] stack = new int[n];
        int size = 0;
        
        int max = 0;
        for(int i=0;i<n;++i){
            while(size>0 && arr[i] < arr[stack[size-1]]){
                max = Math.max(max,arr[stack[--size]] * (i- (size==0?-1:stack[size-1]) -1));
            }
            stack[size++]=i;
        }
        while(size-->0){
            max = Math.max(max, arr[stack[size]] * (n - (size==0?-1:stack[size-1]) - 1));
        }
        return max;
    }
}

k-sum问题

TwoSum

题目1. Two Sum

在一个无序的数组中,查找等于指定数的两个数,必须返回两个数的下标而不是两个数本身

:使用HashMap记录下标,并且在HashMap插入的过程中进行判断。即将子问题视为以A[i]为第二个元素的元素查找问题,因此只需要查找T-A[i]即可。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        //  1ms, the fastest
         Map<Integer, Integer> map = new HashMap<>(nums.length);
    for (int i = 0; i < nums.length; i++) {
        int complement = target - nums[i];
        if (map.containsKey(complement)) {
            return new int[] { map.get(complement), i };
        }
        map.put(nums[i], i);
    }
}

在排序数组上的解:167. Two Sum II - Input array is sorted

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int i=0,j=numbers.length-1;
        int s;
        while(i<j){
            if((s=numbers[i]+numbers[j])==target){
                return new int[]{i+1,j+1};
            }else if(s<target){
                ++i;
            }else{
                --j;
            }
        }
        return null;
    }
}

TwoSum的所有非重复的解

首先将数组排序,然后,每当查找到一个解时,将其加入到解集中,然后跳过该元素。

 // the key point is to find all unique solutions with prefix x
    // and to do this,we always goes to different index.
    void twoSum(int i,int j,List res){
        while(i<j){
            int d=nums[i]+nums[j];
            if(d==0){
                res.add(Arrays.asList(nums[i],nums[j]));
                while(++i<j && nums[i]==nums[i-1]);
                if(i>=j)return;
            }else if(d<0){
                ++i;
            }else{
                --j;
            }
        }
    }

BST上的TwoSum

题目653. Two Sum IV - Input is a BST

解1:将BST转换成双链表,然后进行普通的TwoSum求解

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    TreeNode left;
    TreeNode right;
    
    public boolean findTarget(TreeNode root, int k) {
        if(root==null)return false;
        walk(root);
        left=root;
        right=root;
        while(left.left!=null)left=left.left;
        while(right.right!=null)right=right.right;
        
        int s;
        while(left!=right && (s=(left.val+right.val))!=k){
            if(s<k) left=left.right;
            else right=right.left;
        }
        return left!=right;
    }
    
    void walk(TreeNode n){
        if(n==null)return;
        walk(n.left);
        // this time the left adjacent for n is found
        if(left!=null)left.right=n;
        n.left = left;
        // as the left for n is handled, n is the left most for n.right
        left = n;
        walk(n.right);
    }   
}

这里非常值得注意我们是如何序列化树的

void walk(TreeNode n){
        if(n==null)return;
        walk(n.left);
        // 1.当n.left处理完成之后,n的最左侧元素已经确定
        if(left!=null)left.right=n;
        n.left = left;
        // 2.当n的最左侧元素都处理之后,n就是n.right的最左侧元素
        left = n;
        walk(n.right);
} 

注意代码中的1和2两个部分,我们抽象处一个概念:相邻元素是否已经确认。在上述遍历过程中,进行到1时,显然由于n.left已经处理完毕,所以n的左侧相邻元素就确定了;进行到2时,由于n的左子树已经处理完毕,所以n必然是其右子树的左相邻节点。

解2:使用set记录所有已经出现的元素,每次判断节点K-n是否存在即可
代码略。

3Sum Closest

题目16. 3Sum Closest

:更新思路: 当sum<target时,增加j;当sum>target时,减小k

class Solution {
    public int threeSumClosest(int[] nums, int target) {
        if(nums.length<3){
            throw new IllegalArgumentException();
        }
        Arrays.sort(nums);
        int n = nums.length;
        int result = nums[0]+nums[1]+nums[2];
        int diffAbs = Math.abs(target - result);
        for(int i=0;i<n-2;++i){
            int j = i+1;
            int k = n-1;
            while(j<k){
                int sum = nums[i] + nums[j] + nums[k];
                if(sum==target){
                    return sum;
                }else{
                    int thisDiffAbs = Math.abs(sum - target);
                    if(thisDiffAbs < diffAbs){
                        diffAbs = thisDiffAbs ;
                        result = sum;
                    }
                    if(sum < target){
                        ++j;
                    }else{ /* sum > target */
                        --k;
                    }
                }
                
            }
        }
        return result;
    }
}

4Sum

kSum

移除重复元素

题目: 80. Remove Duplicates from Sorted Array II

:使用j保存最后一次填充的元素位置,则如果一个元素A[i]可以填充到j上,当且仅当 A[i]!=A[j-1] && A[i]!=A[j-2], 实际上由于是排序数组,所以只需要A[i]!=A[j-2]即可

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

相同的做法可以用于移除一个元素:27. Remove Element

class Solution {
    public int removeElement(int[] nums, int val) {
        int j=0;
        for(int i=0;i<nums.length;++i){
            if(nums[i]!=val){
                nums[j++]=nums[i];
            }
        }
        return j;
    }
}

连续子数组问题

问题特征:求一个数组的满足指定条件连续子数组,其中,如果[i,j]区间的值满足条件,则 [i+1,j]区间的值也满足条件,也就是非固定的滑动窗口,可以在O(1)时间内实现窗口的滑动。

大于K的最小和

被K整除的所有和

大于K的最小积

重复序列问题

问题特征:给定一个数组序列,将其重复K次,求连续子数组(可能加上大小限制)的某个最值

结题

滑动窗口

11.前缀和和离散化

一般来说,我们需要寻找满足指定条件的区间和,比如满足 S[i,j]=K或者S[i,j]<=K。不失一般性,我们令S[i]表示S[0~i]的前缀和,则S[i,j] = S[i] - S[j-1], 显然,j的范围是[-1,i-1], 则S[j-1]=S[i] - K或者S[j-1]>=S[i] - K,从而我们将给定的问题转化为在前缀和数组中的查找问题。

对于不等式S[j-1]>=S[i] - K,我们可以对数组进行离散化,所谓的离散化是指对于相同的值,返回同一个顺序值。

离散化的步骤是,首先对数组进行排序,然后使用unique对数组连续部分进行去重,然后对前缀和进行遍历

离散化的作用不仅在于确定元素的相对位置,还确定了不在数组中的元素的位置,使得在前缀数组中统计大于等于这些元素的数据可行

1.中位数序列

题目

给定一个序列,包含[1,N]共N个元素,求所有包含M(1<=M<=N)为中位数的子序列个数,对于偶数个元素,取中间偏左的元素作为中位数。

:我们知道中位数实际上就是元素x,小于x的元素与大于x的元素相同或者少一个,也就是说 greater - less = 0 or 1

对于这道题,由于元素不重复,所以M一定是序列中的一个元素。
我们设S[i]表示区间[0,i]中大于M的个数与小于M的个数只差,S[i]即前缀和。我们可以求出任何一个区间。当S[i]=0或S[i]=1且M存在于区间中时,该区间就是一个目标区间。

我们的目标是求解区间i,j, 使得S[i,j]=0或者1,也就是S[i] - S[j]==0 或者S[i]-S[j]=1,所以, 对于一个前缀区间来说,我们知道当前前缀和S[i],我们要查找的目标是S[j]=S[i]或者S[j]=S[i]-1.

我们只需要在遍历前缀区间的过程中,保存S[i]的计数即可。

2.和的统计

题目327. Count of Range Sum(Hard)

给定一个数组,统计[i,j] (i<=j)的和在[lower,upper]之间的区间个数

:转化为前缀和 lower<=S[i] - S[j] <=upper, 即 S[i] - upper <= S[j] <= S[i] - lower的查找问题,由于涉及不等式,我们对前缀和进行离散化,然后使用两个二分查找分别查找大于等于S[i]-upper和小于等于S[i]-lower的统计值。

class SolutionOfRangeSumCount {
        int[] bitDownto;
        int[] bitUpto;
        int n;

        void updateDownto(int i) {
            while (i > 0) {
                ++bitDownto[i];
                i -= i & -i;
            }
        }

        void updateUpto(int i) {
            while (i <= n) {
                ++bitUpto[i];
                i += i & -i;
            }
        }

        int queryDownto(int i) {
            int res = 0;
            while (i <= n) {
                res += bitDownto[i];
                i += i & -i;
            }
            return res;
        }

        int queryUpto(int i) {
            int res = 0;
            while (i > 0) {
                res += bitUpto[i];
                i -= i & -i;
            }
            return res;
        }
public int count(int[] arr, int lower, int upper) {
            // length of n will generate n+1 prefix sum, the first will not generate any result but should be used to query
            int[] sums = new int[arr.length + 1];
            for (int i = 1; i < sums.length; ++i) {
                sums[i] = sums[i - 1] + arr[i - 1];
            }

            n = sums.length;

            bitDownto = new int[n + 1];
            bitUpto = new int[n + 1];

            int[] enumerated = Arrays.copyOf(sums, sums.length);
            Arrays.sort(enumerated);
            int len = unique(enumerated);
            int cnt = 0;
            for (int i = 0; i < sums.length; ++i) {
                // 1-based index
                // sums[Li] <= sums[i] - upper
                // sums[Ri] >= sums[i] - lower
                int Li = greaterOrEquals(enumerated, len, sums[i] - upper) + 1;
                int Ri = lessOrEquals(enumerated, len, sums[i] - lower) + 1;

                int cLi = Li <= n ? queryDownto(Li) : 0;
                int cRi = Ri > 0 ? queryUpto(Ri) : 0;

                cnt += cLi + cRi - i;

                Li = greaterOrEquals(enumerated, len, sums[i]) + 1;
                Ri = greaterOrEquals(enumerated, len, sums[i]) + 1;
                updateDownto(Li);
                updateUpto(Ri);
            }
            return cnt;
        }
        int unique(int[] arr) {
            if (arr.length == 0) return 0;
            // 循环不变式: arr[i]等于最后一个元素, arr[j]当前未处理的元素,j>i
            int i = 0;
            for (int j = 1; j < arr.length; ++j) {
                if (arr[j] != arr[i]) {
                    arr[++i] = arr[j];
                }
            }
            return i + 1;
        }

        // argmin(arr[i]>=e)
        // if all arr[i]<e, return n
        int greaterOrEquals(int[] arr, int len, int e) {
            int i = 0, j = len;
            while (i < j) {
                int m = i + (j - i) / 2;
                if (arr[m] >= e) j = m;
                else i = m + 1;
            }
            return i;
        }

        // argmin(arr[i]<=e),
        // if all arr[i]>e, return -1
        int lessOrEquals(int[] arr, int len, int e) {
            int i = -1,j = len - 1;
            while (i < j) {
                int m = i + (j + 1 - i) / 2;
                if (arr[m] <= e) i = m;
                else j = m - 1;
            }
            return i;
        }
}

3.2逆序对数(离散化)

题目493. Reverse Pairs(Hard)

找出数组中i<j且 A[i]<2*A[j]的所有数对

解2:使用归并排序,我们知道归并排序的复杂度是O(nlogn), 归并排序的优势是什么?就是两个分区的元素顺序可以在已经排序的情况下进行统计。

连续分区

题目659. Split Array into Consecutive Subsequences(Medium)

给定一个数组,判断这个数组是否能够分成1到多个子区间,每个子区间都是连续的且至少包含3个元素

:我们可以观察到一个性质,如果一个数x能够与x-1进行合并,则将x与x-1合并能够产生满足上面性质的区间的选择,总是优于不将x与x-1进行合并。此外,如果我们使用sum记录以x-1结尾的子区间的个数,则x总是应当优先与sum=2的区间进行合并,这样能够产生一个3个元素的区间。如果没有sum=2的区间,则应当与sum=1的区间进行合并。否则,应当与sum=3的区间合并,再否则,x不能与任何x-1合并,则x构成新区间的起点。
因此,我们使用sum[0]表示以x-1结尾的子区间,sum[0][0]表示长度为1,sum[0][1]表示长度为2, sum[0][2]表示长度大于等于3.类似的,sum[1]表示以x结尾的子区间。我们按照上面讨论的规则将x与x-1进行合并,每当x遍历完成之后,我们知道x+1不可能与x-1进行合并,因此sum[0][2]的所有区间都是有效的,只需要sum[0][0],sum[0][1]不存在尚未合并的区间即可。
:这题的解决思路比较新颖,我思考了数个小时(包括从下班到晚上)。我觉得相比其他常规题目,这更像是一个Hard难度的题目。

class Solution {
    public boolean isPossible(int[] nums) {
        Arrays.sort(nums);
        return canBeConsecutive(nums,0,nums.length-1);
    }
    
    // arr must be sorted
    boolean canBeConsecutive(int[] arr, int i, int j) {
        if (j - i + 1 < 3) return false;
        // sums[0] ends with x-1, sums[1] ends with x
        // sums[i][0:1,1:2,2:>=3] sum[i][j] ends with x,and with j+1 elements
        int[][] sums = new int[2][3];
        int x = arr[i];
        for(int k=i;k<=j;++k) {
            if (arr[k] == x) {
                if (sums[0][1] > 0) {
                    --sums[0][1];
                    ++sums[1][2];
                } else if (sums[0][0] > 0) {
                    --sums[0][0];
                    ++sums[1][1];
                } else if (sums[0][2] > 0) {
                    --sums[0][2];
                    ++sums[1][2];
                } else {
                    ++sums[1][0];
                }
            } else if (arr[k] == x + 1) {
                if(sums[0][0]>0 || sums[0][1]>0)return false;
                sums[0][0]=sums[1][0];
                sums[0][1]=sums[1][1];
                sums[0][2]=sums[1][2];
                sums[1][0] = sums[1][1]=sums[1][2]=0;
                x=arr[k];
                // repeat the process
                --k;
            }else{
                if(sums[0][0]>0 || sums[0][1]>0||sums[1][0]>0 ||sums[1][1]>0)return false;
                sums[0][0] = sums[0][1] = sums[0][2]=0;
                sums[1][0]=1;
                sums[1][1]=sums[1][2]=0;
                x = arr[k];
            }
        }
        if(sums[0][0]>0 || sums[0][1]>0||sums[1][0]>0 ||sums[1][1]>0)return false;
        return true;
    }
}

均匀分布

在这一类题目中,要求我们返回的数据均匀分布。
可以使用随机数生成器,也可以使用轮询。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值