数组问题三

一 子数组最大累乘积

题目:给定一个 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);
	}
}

<think> 我们之前讨论的是快速排序的代码示例,但用户现在询问的是更一般的“解决数组问题的代码思想”。因此,我们需要从更广泛的角度来总结解决数组问题的常见思路和方法。 数组问题在算法中非常常见,包括但不限于:查找、排序、子数组问题(如最大子数组和)、数组操作(如去重、旋转)、双指针技巧、滑动窗口、前缀和、哈希表应用、二分查找应用等。 以下我将总结一些解决数组问题的核心思想,并尽量用中文表述,同时遵守系统级指令(如数学表达式格式要求)。 ### 解决数组问题的代码思想 #### 1. **双指针技巧** - **思想**:使用两个指针以不同的策略移动,通常用于减少时间复杂度(如将$O(n^2)$降至$O(n)$)。 - **常见场景**: - 有序数组的求和问题(如两数之和、数之和)。 - 去重(如有序数组去重)。 - 链表问题(但数组同样适用,如快慢指针找环、中点等)。 - **示例**:两数之和II(输入有序数组)。使用左右指针,根据当前和与目标的大小调整指针。 #### 2. **滑动窗口** - **思想**:维护一个窗口(通常由两个指针表示左右边界),通过移动右边界扩大窗口,满足条件后移动左边界缩小窗口,用于解决子数组/子串问题- **常见场景**:求满足条件的连续子数组(如最小长度子数组、无重复字符的最长子串)。 - **关键**:确定窗口何时扩大、何时缩小,以及如何更新结果。 - **时间复杂度**:通常为$O(n)$。 #### 3. **前缀和** - **思想**:预处理数组,得到前缀和数组,便于快速计算任意子数组的和。 - 定义前缀和数组$prefix$,其中$prefix[i] = nums[0] + nums[1] + ... + nums[i-1]$。 - 则子数组$[i, j]$的和为$prefix[j+1]-prefix[i]$。 - **常见场景**:频繁查询子数组的和(特别是带有负数的情况,不能使用滑动窗口时);求子数组和为k的个数。 - **注意**:有时结合哈希表存储前缀和,以实现$O(1)$的查询。 #### 4. **哈希表优化** - **思想**:利用哈希表存储数组元素及其索引或频次,以实现快速查找(通常$O(1)$)。 - **常见场景**: - 两数之和(无序数组):遍历数组,用哈希表存储元素值和索引,检查目标值是否在表中。 -数组和为k:结合前缀和,用哈希表存储前缀和出现的次数。 #### 5. **二分查找** - **思想**:在有序数组中快速查找目标值,时间复杂度$O(\log n)$。但也可以应用于一些特殊的有序数组问题(如旋转排序数组)。 - **变种**:查找第一个等于目标值的元素、最后一个等于目标值的元素、查找插入位置等。 #### 6. **分治策略** - **思想**:将数组分成更小的子数组,分别解决,然后合并结果。 - **常见场景**:归并排序(排序问题)、快速排序(排序问题)、求逆序对、最大子数组问题(如使用分治的解法)。 #### 7. **原地操作** - **思想**:不额外使用空间或使用常数空间,直接在原数组上进行修改。 - **常见场景**:数组去重(如原地删除重复元素)、数组旋转(如旋转图像)。 #### 8. **动态规划** - **思想**:将问题分解为相互重叠的子问题,通过保存子问题的解来避免重复计算。 - **常见场景**:最大子数组和(Kadane算法)、最长递增子序列(LIS)、背包问题数组形式表示重量/价值)等。 ### 总结 解决数组问题的方法多样,需要根据问题特点选择合适的思想。通常,我们可以从以下几个方面考虑: - 数组是否有序?有序可能考虑二分或双指针。 - 是否需要连续子数组?滑动窗口或前缀和。 - 是否涉及元素出现次数?哈希表。 - 是否允许额外空间?如果不允许,考虑原地操作。 - 问题是否可以分解?分治或动态规划。 §§相关问题§§ 1. 如何判断一个数组问题适合用双指针解决? 2. 滑动窗口和双指针有什么异同? 3. 前缀和技巧适用于哪些具体问题? 4. 数组问题中动态规划的使用场景有哪些? </think> ### 解决数组问题的核心代码思想 解决数组问题的核心思想通常基于以下方法论,结合具体问题选择最优策略: #### 1. **双指针技巧** - **适用场景**:有序数组操作、去重、子数组问题 - **思想**: - 快慢指针:慢指针$slow$标记有效位置,快指针$fast$扫描数组(如原地删除重复项) - 左右指针:分别从数组两端向中间移动(如两数之和) - 时间复杂度通常优化至$O(n)$[^1] #### 2. **滑动窗口** - **适用场景**:连续子数组问题(如最小长度子数组、最长无重复子串) - **思想**: 维护动态窗口$[left, right]$,根据条件调整左右边界: ```python left = 0 for right in range(n): window.add(arr[right]) while condition: # 窗口收缩条件 window.remove(arr[left]) left += 1 ``` - 典型应用:统计满足条件的子数组数量 #### 3. **前缀和预处理** - **适用场景**:频繁查询子区间和、区间修改 - **思想**: 构建前缀和数组$prefix$满足: $$ prefix[i] = \sum_{k=0}^{i-1} arr[k] $$ 子数组$[i,j]$的和可快速计算为$prefix[j+1]-prefix[i]$ - 结合哈希表可解决"子数组和为k"类问题[^2] #### 4. **原地修改策略** - **适用场景**:空间复杂度$O(1)$要求 - **思想**: - 元素交换:用双指针实现数组分区(如快速排序的划分) - 覆盖写入:根据计算规则直接修改原数组(如数组旋转问题) #### 5. **分治与递归** - **适用场景**:大规模数组分解(如归并排序、求逆序对) - **思想**: $$ T(n) = 2T(\frac{n}{2}) + O(n) $$ 通过递归将数组拆分为子问题后合并结果 #### 6. **哈希映射优化** - **适用场景**:元素查找、频次统计、索引映射 - **思想**: 用哈希表存储元素值$\rightarrow$索引/频次的映射,将查找时间降至$O(1)$ > **选择策略参考**: > - 有序数组优先考虑二分搜索$O(\log n)$ > - 连续子数组问题首选滑动窗口或前缀和 > - 空间限制严格时用双指针原地操作 > - 元素关系复杂时利用哈希表降维
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值