1. 问题理解与定义
首先我们需要明确题目中的几个关键概念:
稳定子数组:题目中定义的稳定子数组是指一个连续子数组 nums[l..r],其中不存在任何逆序对。换句话说,对于子数组中的任意两个元素 nums[i] 和 nums[j],如果 i < j,那么必须满足 nums[i] <= nums[j]。
逆序对:在数组中,如果一个元素对 (nums[i], nums[j]) 满足 i < j 且 nums[i] > nums[j],则称这个元素对为一个逆序对。
子数组:数组中的连续元素序列,长度至少为1。
我们的目标是统计给定数组中所有满足"稳定"条件的子数组的数量。
2. 暴力解法分析与优化思路
2.1 暴力解法实现
最直观的解法是枚举所有可能的子数组,然后检查每个子数组是否是稳定的:
go复制func countStableSubarrays(nums []int) int {
n := len(nums)
count := 0
for l := 0; l < n; l++ {
for r := l; r < n; r++ {
stable := true
for i := l; i <= r && stable; i++ {
for j := i + 1; j <= r; j++ {
if nums[i] > nums[j] {
stable = false
break
}
}
}
if stable {
count++
}
}
}
return count
}
这种解法的时间复杂度是O(n^4),对于较大的数组显然效率太低。
2.2 优化思路
观察稳定子数组的定义,我们可以发现一个重要性质:如果一个子数组是稳定的,那么它一定是非递减的。也就是说,稳定子数组实际上就是单调非递减的连续子数组。
基于这个观察,我们可以将问题转化为:统计数组中所有单调非递减的连续子数组的数量。
3. 线性时间解法
3.1 滑动窗口法
我们可以使用滑动窗口技术来优化解法。基本思路是维护一个窗口[l, r],保证窗口内的子数组是单调非递减的:
go复制func countStableSubarrays(nums []int) int {
n := len(nums)
count := 0
l := 0
for r := 0; r < n; r++ {
if r > 0 && nums[r] < nums[r-1] {
l = r
}
count += r - l + 1
}
return count
}
这个算法的时间复杂度是O(n),空间复杂度是O(1),非常高效。
3.2 算法解释
- 初始化左指针l=0,计数器count=0
- 遍历数组,右指针r从0到n-1:
- 如果当前元素nums[r]比前一个元素小,说明破坏了单调性,需要移动左指针到r
- 对于每个r,以r结尾的稳定子数组数量为r-l+1
- 将这些数量累加到count中
4. 边界条件与测试用例
4.1 测试用例设计
为了验证我们的解法,我们需要考虑各种边界情况:
go复制func TestCountStableSubarrays(t *testing.T) {
tests := []struct {
name string
nums []int
want int
}{
{"empty array", []int{}, 0},
{"single element", []int{1}, 1},
{"all increasing", []int{1, 2, 3, 4}, 10},
{"all decreasing", []int{4, 3, 2, 1}, 4},
{"mixed", []int{1, 3, 2, 4}, 7},
{"all equal", []int{2, 2, 2}, 6},
{"large array", make([]int, 1000), 500500},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := countStableSubarrays(tt.nums); got != tt.want {
t.Errorf("countStableSubarrays() = %v, want %v", got, tt.want)
}
})
}
}
4.2 特殊边界处理
- 空数组:应该返回0
- 单元素数组:唯一子数组就是它自己,返回1
- 全等数组:所有子数组都稳定,数量为n*(n+1)/2
- 全递增数组:同上
- 全递减数组:只有n个单元素子数组稳定
5. 性能分析与优化
5.1 时间复杂度分析
- 暴力解法:O(n^4)
- 滑动窗口法:O(n)
5.2 空间复杂度分析
两种方法都是O(1)的额外空间,只使用了几个变量。
5.3 实际测试对比
对于n=1000的数组:
- 暴力解法:约10^12次操作,完全不可行
- 滑动窗口法:仅1000次操作,瞬间完成
6. 扩展思考
6.1 相关问题
这个问题可以扩展为:
- 统计严格递增的子数组数量
- 统计满足其他单调条件的子数组数量
- 统计二维数组中的稳定子矩阵数量
6.2 算法变种
如果题目改为统计严格递增的子数组数量,我们只需要修改判断条件:
go复制if r > 0 && nums[r] <= nums[r-1] {
l = r
}
6.3 实际应用场景
这种统计单调子数组的问题在实际中有多种应用:
- 股票分析中寻找价格持续上涨的时间段
- 信号处理中寻找平稳变化的信号片段
- 数据分析中识别趋势稳定的数据区间
7. Go语言实现细节
7.1 代码组织
完整的Go实现可以这样组织:
go复制package main
import "fmt"
func countStableSubarrays(nums []int) int {
n := len(nums)
count := 0
l := 0
for r := 0; r < n; r++ {
if r > 0 && nums[r] < nums[r-1] {
l = r
}
count += r - l + 1
}
return count
}
func main() {
testCases := []struct {
nums []int
want int
}{
{[]int{1, 2, 3, 4}, 10},
{[]int{4, 3, 2, 1}, 4},
{[]int{1, 3, 2, 4}, 7},
{[]int{2, 2, 2, 2}, 10},
{[]int{}, 0},
{[]int{5}, 1},
}
for _, tc := range testCases {
got := countStableSubarrays(tc.nums)
if got != tc.want {
fmt.Printf("countStableSubarrays(%v) = %d, want %d\n", tc.nums, got, tc.want)
} else {
fmt.Printf("PASS: countStableSubarrays(%v) = %d\n", tc.nums, got)
}
}
}
7.2 性能优化技巧
虽然我们的算法已经是线性时间,但在Go中还可以考虑:
- 使用range遍历数组更符合Go习惯
- 避免不必要的边界检查
- 对于非常大的数组,可以考虑并行处理
8. 错误排查与常见问题
8.1 常见错误
- 忘记处理空数组的情况
- 在滑动窗口法中错误地更新左指针
- 计数时漏掉某些子数组
8.2 调试技巧
- 打印窗口变化过程:
go复制fmt.Printf("l=%d, r=%d, count=%d\n", l, r, count)
- 使用小测试用例手动验证
- 检查边界条件(空数组、单元素数组等)
9. 进一步学习资源
- 《算法导论》中的分治算法章节 - 逆序对计数
- LeetCode类似题目:
-
- Number of Sub-arrays With Odd Sum
-
- Number of Subarrays with Bounded Maximum
-
- Go语言官方文档中的切片和数组操作
10. 个人实践心得
在实际编码中,我发现以下几点特别重要:
- 先理解清楚问题定义,明确什么是"稳定子数组"
- 从暴力解法开始,然后寻找优化空间
- 滑动窗口法在这种连续子数组问题中非常有效
- 编写全面的测试用例,特别是边界情况
- Go语言的简洁语法很适合实现这类算法
一个容易忽略的细节是:当数组元素全部相同时,所有子数组都满足条件,此时结果应该是n*(n+1)/2。我在最初实现时曾漏掉这个情况,导致测试不通过。
