一 子数组最大累乘积
题目:给定一个 double 类型的数组 arr,其中的元素可正、可负、可 0,返回子数组累乘的最大乘积。
例如:arr = [-2.5,4,0,3,0.5,8,-1],子数组 [3,0.5,8] 累乘可以获得最大的乘积 12,所以返回 12。
子数组问题套路
在某种标准/条件下求子数组中最大/最小/最优的解法:在标准下,求解以每个 i 位置作为开头或者结尾的子数组的答案,那么全局的答案一定在其中。
分析:
本题求以每个位置 i 结尾的子数组的最大累乘积是多少,如果你每个位置都求出来了,答案一定在其中。
必须以 i 位置结尾的子数组的最大累乘积是多少,列可能性:
1、以 i 结尾的子数组自己是一个子数组,不往左扩,即只含 i;
2、以 i 结尾的子数组往左扩;
- 当 arr[i] > 0,最大累乘积是 i - 1 位置的最大累乘积 * arr[i];
- 当 arr[i] < 0,最大累乘积是 i - 1 位置的最小累乘积 * arr[i];
所以对每个位置,我们都需要得到以该位置结尾的最大累乘积和最小累乘积。
public class MaxMulitiple {
public static double maxMulitiple(double[] arr){
if(arr == null || arr.length == 0){
return 0;
}
double max = arr[0]; // 记录以i位置结尾的最大累乘积
double min = arr[0]; // 记录以i位置结尾的最小累乘积
double res = arr[0]; // 最后的结果
// 记录以i位置结尾的三种情况下的累乘积
double possible1 = 0;
double possible2 = 0;
double possible3 = 0;
// 从1开开始作为子数组的结尾,依次往后遍历每个元素作为子数组结尾的情况
for(int i = 1; i < arr.length; i++){
// 情况1:i位置自己是一个子数组
possible1 = arr[i];
// 情况2: arr[i] > 0.i位置作为子数组的结尾,往左扩 i-1位置最大累乘积
possible2 = max * arr[i];
// 情况3: arr[i] < 0.i位置作为子数组的结尾,往左扩 i-1位置最小累乘积
possible3 = min * arr[i];
// 三种情况最大和最小
max = Math.max(Math.max(possible1,possible2), possible3);
min = Math.min(Math.min(possible1,possible2), possible3);
res = Math.max(max, res);
}
return res;
}
}
二 需要排序的最短子数组长度
题目:给定一个无序数组 arr,求出需要排序的最短子数组长度。例如:arr = [1,5,3,4,2,6,7] 返回 4,因为只有 [5,3,4,2] 需要排序。
要求:时间复杂度 O(N)、额外空间复杂度O(1)。
假设数组为 [a b c d e f g h i j k l m n],如果 abc 是有序的,mn是 有序的,至于中间的 defghijkl 是无序的,我们可以得知,如果是正常升序序列,左边的一定是小于右边的任意数值,右边的一定大于左边的任意数值。
分析:分别从左往右和从右往左遍历,找出左右两边失效的位置,则这两个失效位置中间的数组即为需要排序的最短子数组。
步骤:
1、从左向右遍历,找出不合适数的最右范围:从左往右遍历,如果 maxLeft > 当前元素,则记录它的位置到 invalidRight ,一直遍历到最右边【可知 invalidRight 是最后一个不满足排序要求的数,其右边都满足大于 invalidRight 】
2、从右向左遍历,找出不合适数的最左范围:从右往左遍历,如果当前元素 > minRight,则记录它的位置为 invalidLeft,一直遍历到最左边 【可知 invalidLeft 是最后一个不满足排序要求的数,其左边都满足小于 minRight】
3、invalidRight - invalidLeft + 1 就是需要排序的最短子数组长度。
public class GetMinLengthForSort {
public static int getMinLengthForSort(int[] arr){
if(arr == null || arr.length < 2){
return 0; // 不需要排序
}
int maxLeft = arr[0]; // 左边最大
int minRight = arr[arr.length - 1]; // 右边最小
// 这两个指针分别记录左右两边无效的位置
int invalidLeft = 0;
int invalidRight = -1; // 数组原本有序时:invalidRight - invalidLeft + 1 = 0
// 1、从左到右遍历:找出不合适数的最右范围
// 遍历过的最大值大于当前值,那么当前值肯定是无效的,那么排序时这个最大值在当前位置或者是更右的位置
for(int i = 1; i < arr.length; i++){
if(maxLeft > arr[i]){
// 如果已经遍历过的最大值大于当前值,则记录最右边无效位置
invalidRight = i;
}else{
// 如果已经遍历过的最大值小于等于当前值,则遍历过的最大值更新为当前值
maxLeft = arr[i];
}
}
// 2、从右向左遍历:找出不合适数的最左范围
// 遍历过的最小值小于当前值,那么当前值就是无效的,那么排序时这个最小值在当前位置或者是更左的位置
for(int i = arr.length - 2; i >= 0; i--){
if(minRight < arr[i]){
// 如果已经遍历的最小值小于当前值,则记录最左边无效的位置
invalidLeft = i;
}else{
// 更新最小值
minRight = arr[i];
}
}
// invalidRight是不合适数的最右范围,invalidLeft 是不合适数的最左范围
// invalidRight - invalidLeft + 1 就是不合适数的个数
return invalidRight - invalidLeft + 1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(getMinLengthForSort(arr));
}
}
三 最长的可整合子数组的长度
题目:最长的可整合子数组的长度。给定一个整型数组 arr,请返回其中最大可整合子数组的长度。例如:[5,5,3,2,6,4,3] 的最大可整合子数组为[5,3,2,6,4],所以返回 5。
先给出可整合数组的定义:如果一个数组在排序之后,每相邻两个数差的绝对值都为 1,则该数组为可整合数组。例如:[5,3,4,6,2] 排序之后为 [2,3,4,5,6],符合每相邻两个数差的绝对值都为 1,所以这个数组为可整合数组。
分析:
1、暴力:
列出所有的子数组【N^2】,然后再对每个子数组复制,再排序(NlogN),再遍历看满不满足,共 O(N^3logN) 【子数组:数组中一个或连续的多个整数组成一个子数组】;
2、优化:
以后看到复杂的标准先自己改为简洁的标准。新的标准:一个数组如果是可整合的:
1)无重复值;
2)最大值减去最小值等于数组个数减 1 的话,则是可整合的。
则尝试所有的子数组的复杂度为 O(N^2),判断每个子数组是否是可整合的复杂度为 O(1)(遍历时只需要记录 min、max 就可以判断是否时可整合数组了)。
public class LongestIntegrationLength {
public static int getLongest(int[] arr){
if(arr == null || arr.length == 0){
return 0;
}
int maxLen = 0;
HashSet<Integer> set = new HashSet<>();
// 尝试以每个 i 开头的子数组
for(int i = 0; i < arr.length; i++){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int j = i; j < arr.length; j++){
if(set.contains(arr[j])){
// 如果包含重复数字,则直接进行一下位作为数组的开头
break;
}
set.add(arr[j]);
max = Math.max(max, arr[j]);
min = Math.min(min, arr[j]);
// 数组的个数为:j - i + 1,再减去1,即为:j - i
if(max - min == j - i){
// 最大减最小等于当前数组的长度,说明每个数字之间的跨度都是1,可以整合
maxLen = Math.max(maxLen, j - i + 1);
}
}
// 每次以一个新的数字作为子数组开头的时候,都要先清空set
set.clear();
}
return maxLen;
}
public static void main(String[] args) {
int[] arr = { 5, 5, 3, 2, 6, 4, 3 }; // 5, 3, 2, 6, 4
System.out.println(getLongest(arr)); // 5
}
}
四 最短无序连续子数组
题目:给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。你找到的子数组应是最短的,请输出它的长度。
示例 1:
输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
说明 :
输入的数组长度范围在 [1, 10,000]。
输入的数组可能包含重复元素 ,所以升序的意思是
双指针思路
1、数组 temp 为 nums 复制而来,然后对 temp 从小到大进行排序;
2、然后利用双指针, 一个从头, 一个从尾, 向中间靠拢; 如果相等, 就 ++ 或者 --; 如果 left 和 right 指针指向的数值都不相等, 就 break;
3、需要排序的大小就为 [left , right] ,大小为:right - left + 1。
public class FindUnsortedSubarray_581 {
public static int findUnsortedSubarray(int[] nums) {
if(nums == null || nums.length < 1){
return -1;
}
int[] temp = nums.clone();
// 将temp数组从小到大排序
Arrays.sort(temp);
// 双指针:一个指向头,一个指向尾
int left = 0;
int right = nums.length - 1;
while(left < right){
boolean flag = true;
// 找到不相等的位置后,对应的指针就停了,等另一个指针也不满足条件停下来时,就break了
if(nums[left] == temp[left]){
left++;
flag = false;
}
if(nums[right] == temp[right]){
right--;
flag = false;
}
// 两个指针指向的位置都不相等时,flag才会为true,则 break
if(flag == true){
break;
}
}
return left >= right ? 0 : right - left + 1;
}
public static void main(String[] args) {
int[] arr = {1, 3, 2, 2, 2};
System.out.println(findUnsortedSubarray(arr)); // 4
}
}
五 连续子数组的最大和
题目:输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如输入的数组为{1,-2,3,10,-4,7,2,-5 },和最大的子数组为{3,10,-4,7,2},因此输出为该子数组的和为18。
看到该题目,很多人都能想到最直观的方法,即枚举出数组的所有子数组并求出他们的和。一个长度为n的数组,总共有n(n+1)/2 个子数组。计算出所有的子数组的和,最快也需要O()的时间。通常最直观的方法不会是最优的方法,面试官将提示我们还有更快的方法。
1、举例分析数组的规律
我们试着从头尾逐个累加示例数组中的每个数字。初始化和为0.第一步加上第一个数字,此时和为1.接下来第二步加上数字-2,和就变成了-1.第三步加上数字3.我们注意到由于此前累计的和为-1,小于0,那如果用-1加3,得到的和为2,比3本身还小。也就是说从第一个数字开始的子数组的和会小于从第三个数字开始的子数组的和。因此我们不用考虑从第一个子数组,之前累计的和也被抛弃。
我们从第三个数字重新开始累加,此时得到的和为3.接下来第四步加10,得到和为13.第五步加上-4,和为9.我们发现-4是一个负数,因此累加-4之后得到的和比原来的还小。因此我们要把之前得到的和13保存下来,它有可能是最大的子数组的和。第六步加上数字7,9加7的结果是16,此时和比之前最大的和13还要大,把最大的子数组的和由13更新为16.第七步加上2,累加得到的和为18,同时我们也要更新最大子数组的和。第八步加上最后一个数字-5,由于得到结果为13,小于此前得到的最大和18,因此最终最大的子数组的和为18,对应的子数组是{3,10,-4,7,2}。
package com.zju.offer.arrays;
/**
* 连续子数组的最大和
*/
public class FindMaxSumOfSubArray {
public int findMaxSumOfSubArray(int[] array){
if(array.length == 0){
return 0;
}
int greatest = 0x80000000;
int curSum = 0;
for (int i = 0; i < array.length; i++) {
if(curSum <= 0){
// 如果curSum为负数,则将surSum更新为array[i]
curSum = array[i];
}else{
// 如果curSum为正数,则将array[i]累加到curSum
curSum += array[i];
}
if(curSum > greatest){
// 更新最大值
greatest = curSum;
}
}
return greatest;
}
// 测试
public static void main(String[] args) {
FindMaxSumOfSubArray find = new FindMaxSumOfSubArray();
int[] array = {-1,-2,-3,-10,-4,-7,2,-5};
int maxSumOfSubArray = find.findMaxSumOfSubArray(array);
System.out.println(maxSumOfSubArray);
}
}
解法二:应用动态规划法
我们还可以适用动态规划的思想来分析这个问题。如果用函数 f(i) 表示以第 i 个数字结尾的子数组的最大和,那么我们需要求出 max[f(i)],其中0
下面采用牛客网上的一个答案分析:
F(i):以 array[i] 为末尾元素的子元素数组的和的最大值,子数组的元素的相对位置不变;
- F(i) = max(F(i - 1) + array[i], array[i])
res:所有子数组的和的最大值
- res = max(res, F(i))
package com.zju.offer.arrays;
/**
* 连续子数组的最大和
*/
public class FindMaxSumOfSubArray {
// 使用动态规划法实现
public int findGreatestSunOfSubArray(int[] array){
if(array.length == 0){
return 0;
}
// 记录当前子数组中的最大值
int res = array[0];
// 包含array[i]的连续数组的最大值
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max + array[i], array[i]);
res = Math.max(max, res);
}
return res;
}
// 测试
public static void main(String[] args) {
FindMaxSumOfSubArray find = new FindMaxSumOfSubArray();
int[] array = {-1,-2,-3,-10,-4,-7,2,-5};
int maxSumOfSubArray = find.findGreatestSunOfSubArray(array);
System.out.println(maxSumOfSubArray);
}
}