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
解:更新思路: 当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逆序对数(离散化)
找出数组中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;
}
}
均匀分布
在这一类题目中,要求我们返回的数据均匀分布。
可以使用随机数生成器,也可以使用轮询。