头图
LeetCode随机一题:No.1567 乘积为正数的最长子数组长度。属于数组类题目,难度中等。

原题请戳这里:
1567. 乘积为正数的最长子数组长度

题目描述

给你一个整数数组 nums ,请你求出乘积为正数最长子数组的长度。 一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。 请你返回乘积为正数的最长子数组长度。

  • 示例 1: 输入:nums = [1,-2,-3,4] 输出:4 解释:数组本身乘积就是正数,值为 24 。
  • 示例 2: 输入:nums = [0,1,-2,-3,-4] 输出:3 解释:最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。 注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。
  • 示例 3: 输入:nums = [-1,-2,-3,0,1] 输出:2 解释:乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3] 。
  • 示例 4: 输入:nums = [-1,2] 输出:1
  • 示例 5: 输入:nums = [1,2,3,5,-6,4,0,10] 输出:4
  • 在测试中发现LeetCode使用的性能测试例:长度为100000的数组,除了nums[3000]为-1,其余均为1。

解题思路

问题焦点:

  1. 一个不包含0的数组,乘积是否为正数,取决于其中负数的个数,负数为偶数个时数组的乘积为正。
  2. 乘积是否为正数,与数字的绝对值大小无关,与数组中的数字的符号有关,所以,可以把所有正数化为1,所有负数转化为-1。
  3. 题目中要求是连续数字组成的数组,那么出现0的地方一定自然切分开。而切分开的子数组中如果有唯一一个-1,意味着这个子数组的乘积为负(其它数字都是1或者没有其他数字),需要从-1的位置切分开。
  4. 在遍历子数组找符合条件的子数组长度时,先按照数组的长度排序(Python排序很方便),从最长的数组开始找,可以尽快结束遍历。

解题流程如下图所示:


首先写一些方法,实现每个小功能点

1. format_nums转化数字的方法,输入的n为数字,将正数转化为1,负数转化为-1,0不变
def format_nums(n):
    if n > 0:
        return 1
    if n < 0:
        return -1
    return 0
2. 切分子数组,先按0切分,再按唯一的-1切分。

首先是被调用的cut_by_single_negative方法,它负责检查数组中是否只有唯一的一个-1,是的话就将数组切分开,返回值统一为list的list。

# 子数组已经没有0了,检查是否存在唯一一个-1
# 如果有且仅有一个-1,意味着这个子数组乘积肯定为负数,需要再次切分
# 有多个负数的话,就没办法了
def cut_by_single_negative(sub_ls):
    ret_ls = []
    if sub_ls.count(-1) == 1:
        idx = sub_ls.index(-1)
        ret_ls.append(sub_ls[:idx])
        ret_ls.append(sub_ls[idx+1:])
    else:
        ret_ls.append(sub_ls) 
    return ret_ls

可以验证一下这个方法:

>>>cut_by_single_negative([1, 1, 1, 1, -1, 1])
[[1, 1, 1, 1], [1]]

然后是外层的调用方法cut_by_zero,它按照0来切分数组。

# 对输入的数组按0自然切分为子数组
# 对每个子数组再检查一下是否只有一个-1,有的话也要切分
def cut_by_zero(nums):
    # 没有0的数组,检查-1
    if 0 not in nums:
        return cut_by_single_negative(nums)

    # 没有什么技巧,遍历数组,临时存放在tmp中
    ret_ls = []
    tmp = []
    for i in nums:
        if i == 0:
            # 如果遍历到0,则检查一下tmp中有没有唯一的-1
            if len(tmp) != 0:
                ret_ls += cut_by_single_negative(tmp)
                tmp = []
        else:
            tmp.append(i)
    # 不要忘记尾巴
    if len(tmp) != 0:
        ret_ls += cut_by_single_negative(tmp)
    return ret_ls

验证一下这个方法:

>>>cut_by_zero([1, 1, 1, 1, -1, 1, 0, 1])
[[1, 1, 1, 1], [1], [1]]

在实现这个方法中的遍历流程时,曾使用记录下标的方法替代临时list变量tmp,以降低额外的内存开销,但按照提交结果来看,内存节省有限,而时间开销上涨较多,推测与Python的list实现机制有关,故此处保留使用临时变量的方法。

3.is_positive方法,判断一个不包含0的子数组的乘积是否为正数,只要判断一下负数的个数是否为偶数即可。
def is_positive(sub_nums):
    negative_count = sub_nums.count(-1)
    if negative_count%2 == 0:
        return True
    else:
        return False

同样验证一下

>>>print(is_positive([1, 1, 1, 1]))
>>>print(is_positive([-1, -1, 1, -1]))
True
False
4.getMaxLenofSub找到一个不包含0的子数组里满足条件的最长子数组,入参sub_nums是切分后的一个子数组,它里面没有0,-1的个数不为1。通过先判断一些特殊情况来实现尽早返回。
def getMaxLenofSub(sub_nums):
    len_sub = len(sub_nums)
    # 如果这个子数组本身乘积为正,返回数组长度
    if is_positive(sub_nums): 
        return len_sub
    # 处理特殊情况,子数组长度只有1的时候,一定只有一个1
    if len(sub_nums) == 1:
        return 1
    # 满足条件的子数组,最长就是子数组的长度
    # 从最大长度开始向下递减,只要找到一个满足条件的子数组,即为最长的子数组
    for l in range(len_sub-1, 0, -1):
        for index in range(len_sub-l+1):
            if is_positive(sub_nums[index:index+l]):
                return l
    return 1

验证一下:

>>>print(getMaxLenofSub([1, 1, 1, 1]))
>>>print(getMaxLenofSub([-1, -1, 1, -1]))
4
3
5.最后是总体的流程,先做数字的转化,然后切分为子数组,然后按照子数组的长度排序,以便后面尽早的结束遍历的流程。最后是遍历所有的子数组,找到符合条件的数组长度。
def getMaxLen(nums):
    # 先把正负整数替换为1和-1
    nums = [format_nums(x) for x in nums]
    # 按0切分为子数组
    ls = cut_by_zero(nums)
    # 按子数组的长度排序
    ls.sort(key=lambda x:len(x),reverse=True)
    
    # 记录下当前最长的满足条件的子数组长度,初始值为0
    max_len_of_all = 0
    # 遍历所有子数组
    for sub_nums in ls:
        # 如果遍历到的子数组的长度小于max_len_of_all
        # 意味着不可能得到更长的符合条件的子数组了
        # 而数组又是按长度排序的,所以可以断定max_len_of_all即为符合条件最大值
        if len(sub_nums) < max_len_of_all:
            return max_len_of_all
        # 从子数组里找出符合条件的最大长度,并和max_len_of_all比较
        sub_max = getMaxLenofSub(sub_nums)
        if sub_max > max_len_of_all:
            max_len_of_all = sub_max

    return max_len_of_all

验证

题目中提到的几个示例:

>>>nums = [1,-2,-3,4]
>>>getMaxLen(nums)
4

>>>nums = [0,1,-2,-3,-4]
>>>getMaxLen(nums)
3

>>>nums = [-1,-2,-3,0,1] 
>>>getMaxLen(nums)
2

>>>nums = [-1,2]
>>>getMaxLen(nums)
1

>>>nums = [1,2,3,5,-6,4,0,10]
>>>getMaxLen(nums)
4

最后是性能测试用例,长度为10万的数组,找出符合条件的最长子数组长度:

%%time
nums = [1]*100000
nums[3000] = -1
getMaxLen(nums)

CPU times: user 14 ms, sys: 1.27 ms, total: 15.3 ms
Wall time: 15.1 ms

提交

提交到LeetCode的结果如下图所示,每次提交的结果可能会有轻微浮动。


我的Python版本

>>> import sys
>>> print(sys.version)
3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]

_流浪猫猫_
144 声望16 粉丝

个人订阅号Python拾贝,不定期更新