2026-04-19:固定长度子数组中的最小逆序对数目。用go语言,给你一个整数数组 nums(长度为 n)和一个整数 k。所谓“逆序对”,指的是在数组中下标满足 i < j 且 nums[i] > nums[j] 的任意一对位置 (i, j)。
对某个连续片段(即子数组)而言,统计它内部所有满足上述条件的逆序对数量,这个数量就是该子数组的“逆序对数量”。
现在考虑 nums 中所有长度恰好为 k 的连续子数组,要求找出其中逆序对数量的最小值并返回。
1 <= n == nums.length <= 100000。
1 <= nums[i] <= 1000000000。
1 <= k <= n。
输入:nums = [5,3,2,1], k = 4。
输出:6。
解释:
只有一个长度为 k = 4 的子数组:[5, 3, 2, 1]。
在该子数组中,逆序对为:(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), 和 (2, 3)。
逆序对总数为 6,因此最小逆序对数量是 6。
题目来自力扣3768。
大体步骤如下:
一、核心前置知识铺垫
- 逆序对:子数组内满足
i<j 且 nums[i]>nums[j]的数对,我们要统计每个长度为k的连续子数组的逆序对总数,找最小值。 - 滑动窗口:数组中所有长度为k的连续子数组,本质是一个固定大小为k的窗口从左向右滑动,每次移动一位。
- 树状数组:高效完成单点更新和前缀和查询的数据结构,用来快速统计逆序对(避免暴力枚举超时)。
- 离散化:因为数组元素值最大可达1e9,无法直接用树状数组存储,所以把原数组的值映射为连续的小整数(1,2,3...)。
二、分步骤详细执行过程
步骤1:离散化处理(压缩数值范围)
原数组:[5, 3, 2, 1]
- 克隆原数组并排序、去重,得到排序去重数组:
[1,2,3,5] - 把原数组的每个值,替换为它在排序数组中的位置+1(树状数组下标从1开始):
- 5 → 4
- 3 → 3
- 2 → 2
- 1 → 1
- 离散化后的数组:
[4, 3, 2, 1]✅ 作用:把超大数值变成1~4的连续整数,适配树状数组。
步骤2:初始化核心变量
- 初始化树状数组:长度为
离散化后最大值+1 = 5,初始值全0。 - 逆序对计数器
inv = 0:记录当前窗口的逆序对总数。 - 答案变量
ans = 最大值:存储所有窗口的最小逆序对。 - 固定窗口大小:
k=4。
步骤3:滑动窗口遍历数组(逐个元素处理)
我们遍历离散化后的数组 [4,3,2,1],每一步分为:元素入窗口 → 计算逆序对 → 窗口成型则更新答案 → 元素出窗口。
第1次遍历(处理第一个元素:4)
- 元素入窗口:把数字4加入树状数组,树状数组标记4的位置计数+1。
- 累加逆序对:当前窗口内比4大的数字个数为0,逆序对总数
inv = 0。 - 窗口判断:当前窗口长度=1 < 4,未形成有效窗口,不执行后续操作。
第2次遍历(处理第二个元素:3)
- 元素入窗口:把数字3加入树状数组,树状数组标记3的位置计数+1。
- 累加逆序对:当前窗口内比3大的数字只有4,逆序对总数
inv = 1。 - 窗口判断:当前窗口长度=2 < 4,未形成有效窗口,不执行后续操作。
第3次遍历(处理第三个元素:2)
- 元素入窗口:把数字2加入树状数组,树状数组标记2的位置计数+1。
- 累加逆序对:当前窗口内比2大的数字有4、3,逆序对总数
inv = 1+2=3。 - 窗口判断:当前窗口长度=3 < 4,未形成有效窗口,不执行后续操作。
第4次遍历(处理第四个元素:1)
- 元素入窗口:把数字1加入树状数组,树状数组标记1的位置计数+1。
- 累加逆序对:当前窗口内比1大的数字有4、3、2,逆序对总数
inv = 3+3=6。 - 窗口判断:窗口长度=4,刚好形成第一个有效窗口。
- 更新最小答案:当前逆序对总数6是最小值,
ans=6。 - 元素出窗口:窗口左侧第一个元素4移出树状数组,树状数组标记4的位置计数-1。
- 修正逆序对:减去移出元素4带来的逆序对数量,逆序对总数更新。
步骤4:遍历结束,输出结果
整个数组遍历完成,唯一的有效窗口逆序对总数为6,最终返回6。
三、关键逻辑补充说明
- 元素入窗口时:用树状数组快速查询当前窗口中比新元素大的数字个数,直接累加到逆序对总数。
- 窗口成型后:用当前逆序对总数更新最小值。
- 元素出窗口时:用树状数组快速查询比移出元素小的数字个数,从逆序对总数中减去(因为这个元素离开了窗口,它贡献的逆序对也要删掉)。
- 提前终止:如果逆序对总数变为0(理论最小值),直接结束计算,优化效率。
四、时间复杂度分析
- 离散化:排序+去重+映射,时间复杂度为 O(n log n)。
- 滑动窗口遍历:遍历n个元素,每个元素执行2次树状数组操作(更新+查询),树状数组单次操作复杂度为 O(log m)(m是离散化后的数值范围,m≤n)。 总遍历复杂度:O(n log n)。
✅ 总的时间复杂度:O(n log n) (这是处理n≤1e5数据的最优复杂度,能完美通过题目限制)
五、额外空间复杂度分析
额外空间指除输入数组外,代码主动开辟的空间:
- 离散化用的排序数组:O(n)。
- 树状数组:O(n)。
- 其他临时变量:O(1)。
✅ 总的额外空间复杂度:O(n)
总结
- 执行流程:离散化压缩数值 → 滑动窗口遍历元素 → 树状数组高效统计逆序对 → 维护最小逆序对数量;
- 时间复杂度:O(n log n),适配1e5规模数据;
- 额外空间复杂度:O(n)。
Go完整代码如下:
package main
import (
"fmt"
"math"
"slices"
"sort"
)
type fenwick []int
func (t fenwick) update(i, val int) {
for ; i < len(t); i += i & -i {
t[i] += val
}
}
func (t fenwick) pre(i int) (res int) {
for ; i > 0; i &= i - 1 {
res += t[i]
}
return
}
func minInversionCount(nums []int, k int) int64 {
// 离散化
sorted := slices.Clone(nums)
slices.Sort(sorted)
sorted = slices.Compact(sorted)
for i, x := range nums {
nums[i] = sort.SearchInts(sorted, x) + 1 // 树状数组下标从 1 开始
}
t := make(fenwick, len(sorted)+1)
inv := 0
ans := math.MaxInt
for i, in := range nums {
// 1. 入
t.update(in, 1)
inv += min(i+1, k) - t.pre(in) // 窗口大小 - (<=x 的元素个数) = (>x 的元素个数)
left := i + 1 - k
if left < 0 { // 尚未形成第一个窗口
continue
}
// 2. 更新答案
ans = min(ans, inv)
if ans == 0 { // 已经最小了,无需再计算
break
}
// 3. 出
out := nums[left]
inv -= t.pre(out - 1) // < out 的元素个数
t.update(out, -1)
}
return int64(ans)
}
func main() {
nums := []int{5, 3, 2, 1}
k := 4
result := minInversionCount(nums, k)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
from typing import List
class Fenwick:
def __init__(self, n: int):
self.n = n
self.bit = [0] * (n + 1)
def update(self, i: int, val: int) -> None:
"""将位置i增加val(i从1开始)"""
while i <= self.n:
self.bit[i] += val
i += i & -i
def pre(self, i: int) -> int:
"""前缀和:1到i的和"""
res = 0
while i > 0:
res += self.bit[i]
i &= i - 1
return res
def min_inversion_count(nums: List[int], k: int) -> int:
"""
计算所有长度为k的子数组中逆序对的最小值
"""
# 离散化
sorted_nums = sorted(set(nums))
# 映射到1,2,3,...(树状数组下标从1开始)
num_to_idx = {val: idx + 1 for idx, val in enumerate(sorted_nums)}
nums = [num_to_idx[x] for x in nums]
# 树状数组
bit = Fenwick(len(sorted_nums))
inv = 0 # 当前窗口的逆序对数
ans = float('inf')
for i, num in enumerate(nums):
# 1. 入:将当前元素加入窗口
bit.update(num, 1)
# 窗口大小 = min(i+1, k)
window_size = min(i + 1, k)
# 大于num的元素个数 = 窗口大小 - 小于等于num的元素个数
inv += window_size - bit.pre(num)
left = i + 1 - k
if left < 0:
# 尚未形成第一个窗口
continue
# 2. 更新答案
ans = min(ans, inv)
if ans == 0:
# 已经是最小值,无需继续计算
break
# 3. 出:移除窗口最左边的元素
out_num = nums[left]
# 小于out_num的元素个数(这些元素与out_num构成逆序对)
inv -= bit.pre(out_num - 1)
bit.update(out_num, -1)
return int(ans)
def main():
nums = [5, 3, 2, 1]
k = 4
result = min_inversion_count(nums, k)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Fenwick {
private:
vector<int> bit;
public:
Fenwick(int n) : bit(n + 1, 0) {}
void update(int i, int val) {
for (; i < bit.size(); i += i & -i) {
bit[i] += val;
}
}
int pre(int i) {
int res = 0;
for (; i > 0; i &= i - 1) {
res += bit[i];
}
return res;
}
};
long long minInversionCount(vector<int>& nums, int k) {
// 离散化
vector<int> sorted = nums;
sort(sorted.begin(), sorted.end());
sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
for (int i = 0; i < nums.size(); i++) {
nums[i] = lower_bound(sorted.begin(), sorted.end(), nums[i]) - sorted.begin() + 1; // 树状数组下标从1开始
}
Fenwick bit(sorted.size());
int inv = 0;
int ans = INT_MAX;
for (int i = 0; i < nums.size(); i++) {
int in = nums[i];
// 1. 入
bit.update(in, 1);
inv += min(i + 1, k) - bit.pre(in); // 窗口大小 - (<=x的元素个数) = (>x的元素个数)
int left = i + 1 - k;
if (left < 0) { // 尚未形成第一个窗口
continue;
}
// 2. 更新答案
ans = min(ans, inv);
if (ans == 0) { // 已经最小了,无需再计算
break;
}
// 3. 出
int out = nums[left];
inv -= bit.pre(out - 1); // < out的元素个数
bit.update(out, -1);
}
return (long long)ans;
}
int main() {
vector<int> nums = {5, 3, 2, 1};
int k = 4;
long long result = minInversionCount(nums, k);
cout << result << endl;
return 0;
}