1. 问题背景与题目解析
今天我们来拆解LeetCode第2529题"正整数和负整数的最大计数"。这是一道典型的数组统计问题,考察的是对基础数据结构的操作能力。题目要求我们统计一个已排序数组中正整数和负整数的数量,并返回两者中的较大值。
题目描述:
给定一个按非递减顺序排列的整数数组nums,返回数组中正整数的数目和负整数的数目中的最大值。换句话说:
- 如果nums中有更多的正数,返回正数的数量
- 如果nums中有更多的负数,返回负数的数量
- 如果两者数量相同,返回这个数量
示例:
输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:有3个正数(1,2,3)和3个负数(-2,-1,-1),所以返回3的最大值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 直观解法:线性扫描
最直观的解法就是遍历整个数组,分别统计正数和负数的数量。这种方法的时间复杂度是O(n),空间复杂度是O(1)。
python复制def maximumCount(nums):
pos = neg = 0
for num in nums:
if num > 0:
pos += 1
elif num < 0:
neg += 1
return max(pos, neg)
这种解法虽然简单直接,但没有利用到题目给出的"数组已排序"这一重要条件。对于已排序的数组,我们通常可以考虑使用二分查找来优化时间复杂度。
2.2 优化解法:二分查找
由于数组是有序的,我们可以使用二分查找来快速定位第一个非负数的位置和第一个正数的位置:
- 使用二分查找找到最后一个负数的位置(即第一个非负数的前一个位置)
- 使用二分查找找到第一个正数的位置
- 负数数量 = 最后一个负数的索引 + 1
- 正数数量 = 数组长度 - 第一个正数的索引
- 返回两者中的较大值
这种解法的时间复杂度可以优化到O(log n),因为二分查找的时间复杂度是对数级的。
python复制def maximumCount(nums):
# 找到第一个非负数的位置
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] < 0:
left = mid + 1
else:
right = mid
neg = left
# 找到第一个正数的位置
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] <= 0:
left = mid + 1
else:
right = mid
pos = len(nums) - left
return max(neg, pos)
3. 边界条件与特殊情况处理
在实际编码中,我们需要考虑以下几种边界情况:
-
全为正数的数组:如[1,2,3]
- 负数数量为0
- 正数数量为数组长度
- 应返回数组长度
-
全为负数的数组:如[-3,-2,-1]
- 负数数量为数组长度
- 正数数量为0
- 应返回数组长度
-
包含0的数组:如[-1,0,1]
- 负数数量为1
- 正数数量为1
- 应返回1
-
空数组:[]
- 应返回0
-
只有0的数组:[0,0,0]
- 负数数量为0
- 正数数量为0
- 应返回0
我们的二分查找解法已经能够正确处理这些边界情况,因为:
- 对于全正数组,第一个非负数位置是0,所以neg=0
- 对于全负数组,第一个非负数位置是len(nums),所以neg=len(nums)
- 对于包含0的数组,0会被视为非负数处理
4. 算法复杂度分析
让我们比较两种解法的复杂度:
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 线性扫描 | O(n) | O(1) | 简单直接,适用于任何情况 |
| 二分查找 | O(log n) | O(1) | 数组较大且已排序时更优 |
虽然二分查找在理论复杂度上更优,但对于小规模数组(n<100),线性扫描可能实际更快,因为二分查找的常数因子较大。在实际面试中,可以先提出线性扫描解法,然后优化为二分查找,展示你的算法优化能力。
5. 实际编码中的注意事项
在实现二分查找时,有几个容易出错的地方需要注意:
-
循环条件:使用left < right还是left <= right?
- 我们使用left < right可以避免无限循环
- 当left == right时循环终止
-
中间值计算:mid = (left + right) // 2
- 使用整数除法
- 也可以使用mid = left + (right - left) // 2来避免可能的整数溢出
-
边界更新:
- 当nums[mid] < target时,更新left = mid + 1
- 否则更新right = mid
-
处理重复元素:
- 我们的解法可以正确处理包含重复元素的数组
- 因为二分查找会找到第一个满足条件的位置
-
零的处理:
- 题目要求统计正整数和负整数
- 零不计入任何一方
- 在二分查找中,0被视为非负数
6. 测试用例设计
为了验证我们的解法是否正确,应该设计全面的测试用例:
python复制test_cases = [
([-2,-1,-1,1,2,3], 3), # 正负数数量相等
([-3,-2,-1], 3), # 全负数
([1,2,3], 3), # 全正数
([-1,0,1], 1), # 包含0
([], 0), # 空数组
([0,0,0], 0), # 全0
([-5,-3,-2,0,1,2], 3), # 更多负数
([-1,1,2,3,4], 4), # 更多正数
([-1,0,0,1], 1), # 多个0
([-1,-1,-1,0,1], 3) # 重复负数
]
for nums, expected in test_cases:
assert maximumCount(nums) == expected, f"Failed for {nums}"
print("All test cases passed!")
7. 其他编程语言实现
虽然我们以Python为例,但这个问题在其他语言中的解法思路是相同的。以下是几种常见语言的实现:
7.1 Java实现
java复制public int maximumCount(int[] nums) {
int neg = findFirstNonNegative(nums);
int pos = nums.length - findFirstPositive(nums);
return Math.max(neg, pos);
}
private int findFirstNonNegative(int[] nums) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
private int findFirstPositive(int[] nums) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
7.2 C++实现
cpp复制#include <vector>
#include <algorithm>
using namespace std;
int findFirstNonNegative(const vector<int>& nums) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
int findFirstPositive(const vector<int>& nums) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
int maximumCount(vector<int>& nums) {
int neg = findFirstNonNegative(nums);
int pos = nums.size() - findFirstPositive(nums);
return max(neg, pos);
}
7.3 JavaScript实现
javascript复制function maximumCount(nums) {
const findFirstNonNegative = (nums) => {
let left = 0, right = nums.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] < 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
};
const findFirstPositive = (nums) => {
let left = 0, right = nums.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] <= 0) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
};
const neg = findFirstNonNegative(nums);
const pos = nums.length - findFirstPositive(nums);
return Math.max(neg, pos);
}
8. 算法优化与变种
8.1 单次二分查找优化
我们可以进一步优化,只进行一次二分查找来同时确定负数和正数的分界:
python复制def maximumCount(nums):
# 找到第一个非负数的位置
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] < 0:
left = mid + 1
else:
right = mid
neg = left
# 在非负数部分找到第一个正数的位置
left, right = neg, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] <= 0:
left = mid + 1
else:
right = mid
pos = len(nums) - left
return max(neg, pos)
这种优化在数组中有大量负数时效果更明显,因为第二个二分查找的范围缩小了。
8.2 统计非零元素
如果题目要求改为统计非零元素的最大数量,我们可以这样修改:
python复制def maximumNonZeroCount(nums):
# 找到第一个非零元素的位置
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] == 0:
left = mid + 1
else:
right = mid
first_non_zero = left
# 找到最后一个非零元素的位置
left, right = 0, len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] != 0:
left = mid + 1
else:
right = mid
last_non_zero = left - 1
if first_non_zero > last_non_zero:
return 0 # 全零数组
neg = 0
pos = 0
for i in range(first_non_zero, last_non_zero + 1):
if nums[i] < 0:
neg += 1
else:
pos += 1
return max(neg, pos)
8.3 流式处理版本
如果数据是以流的形式输入的(无法随机访问),我们只能使用线性扫描的方法:
python复制def maximumCountStream(stream):
pos = neg = 0
for num in stream:
if num > 0:
pos += 1
elif num < 0:
neg += 1
return max(pos, neg)
9. 实际应用场景
这类统计问题在实际开发中有多种应用场景:
- 用户评价分析:统计正面评价和负面评价的数量
- 财务数据监控:统计盈利和亏损的交易数量
- 传感器数据分析:统计高于和低于阈值的读数数量
- A/B测试结果:统计两种方案的支持者数量
- 日志分析:统计不同级别(ERROR/WARN/INFO)的日志数量
理解这类基础算法问题有助于我们处理实际业务中的各种统计需求。虽然问题本身看起来简单,但优化后的解法在处理大规模数据时能显著提高效率。
10. 总结与个人心得
通过这道题目,我有以下几点体会:
-
不要忽视简单问题:即使是看似简单的问题,也可能有优化的空间。这道题的线性扫描解法很简单,但利用排序特性可以优化到O(log n)。
-
二分查找的变种很多:标准的二分查找是查找特定值,但实际问题中常常需要查找第一个满足某种条件的位置,需要灵活调整比较条件和边界更新逻辑。
-
边界条件很重要:特别是处理包含0的情况和全正/全负数组时,容易出错。好的测试用例能帮助我们发现这些问题。
-
算法选择要考虑实际场景:虽然二分查找理论复杂度更好,但对于小数组,线性扫描可能更简单高效。在实际工程中,我们需要根据数据规模选择最合适的算法。
-
代码可读性很重要:即使是一个简单的算法,清晰的变量命名和适当的注释也能大大提高代码的可维护性。比如将二分查找封装成辅助函数,主逻辑就非常清晰。
这道题目很好地展示了如何利用基础算法解决实际问题,也提醒我们在面试中要充分利用题目给出的所有条件(如本题中的数组已排序),才能给出最优解。
