差分数组详解:从概念到算法实践
差分数组详解:从概念到算法实践
差分数组是一种高效处理数组区间修改和查询的算法技巧。与前缀和数组类似,差分数组可以在O(1)时间内完成对数组某个区间的增减操作。本文将详细介绍差分数组的基本概念、实现方法,并通过三个力扣题目展示其在实际算法题中的应用。
差分数组思想
差分数组的核心思想是通过记录相邻元素的差值,来实现对数组区间快速修改。假设我们有一个数组nums
,我们需要频繁地对这个数组的某些区间进行增减操作。例如,要求给区间nums[2..6]
全部加1,再给nums[3..9]
全部减3,再给nums[0..4]
全部加2,等等。
常规的做法是使用for循环逐个修改区间内的元素,但这种方法的时间复杂度是O(N),当修改操作频繁时效率会很低。差分数组提供了一种更高效的方法。
我们先对nums
数组构造一个diff
差分数组,其中diff[i]
表示nums[i]
和nums[i-1]
之差:
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
通过这个diff
差分数组,我们可以反推出原始数组nums
:
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
关键在于,当我们想对区间nums[i..j]
的元素全部加3时,只需要让diff[i] += 3
,然后再让diff[j+1] -= 3
即可。这样做的原理是:diff[i] += 3
意味着给nums[i..]
所有的元素都加了3,然后diff[j+1] -= 3
又意味着对于nums[j+1..]
所有元素再减3,综合起来就是对nums[i..j]
中的所有元素都加3。
通过这种方式,我们只需要花费O(1)的时间修改diff
数组,就可以完成对nums
的区间修改。多次修改diff
后,通过diff
数组反推,即可得到nums
修改后的结果。
经典差分类
为了方便使用,我们可以将差分数组的操作封装成一个类:
// 差分数组工具类
class Difference {
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums) {
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
/* 给闭区间 [i,j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
/* 返回结果数组 */
public int[] result() {
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
注意increment
方法中的if语句:
public void increment(int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
当j+1 >= diff.length
时,说明是对nums[i]
及以后的整个数组都进行修改,那么就不需要再给diff
数组减val
了。
算法实践
接下来,我们通过三个力扣题目来实践差分数组的应用。
力扣 370 题「区间加法」
直接复用刚才实现的Difference
类:
int[] getModifiedArray(int length, int[][] updates) {
// nums 初始化为全 0
int[] nums = new int[length];
// 构造差分解法
Difference df = new Difference(nums);
for (int[] update : updates) {
int i = update[0];
int j = update[1];
int val = update[2];
df.increment(i, j, val);
}
return df.result();
}
力扣 1109 航班预定统计
这个题目需要对题目进行联想和抽象:
函数签名如下:
int[] corpFlightBookings(int[][] bookings, int n)
题目实质是:给你输入一个长度为n
的数组nums
,其中所有元素都是 0。再给你输入一个bookings
,里面是若干三元组(i,j,k)
,每个三元组的含义就是要求你给nums
数组的闭区间[i-1,j-1]
中所有元素都加上k
。请你返回最后的nums
数组是多少?
我们可以直接复用差分数组类:
int[] corpFlightBookings(int[][] bookings, int n) {
// nums 初始化为全 0
int[] nums = new int[n];
// 构造差分解法
Difference df = new Difference(nums);
for (int[] booking : bookings) {
// 注意转成数组索引要减一哦
int i = booking[0] - 1;
int j = booking[1] - 1;
int val = booking[2];
// 对区间 nums[i..j] 增加 val
df.increment(i, j, val);
}
// 返回最终的结果数组
return df.result();
}
力扣 1094 拼车
函数签名如下:
boolean carPooling(int[][] trips, int capacity);
例如:
trips = [[2,1,5],[3,3,7]], capacity = 4
这就不能一次运完,因为trips[1]
最多只能上 2 人,否则车就会超载。差分数组的思路是:trips[i]
代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于capacity
,就说明可以不超载运输所有旅客。
车站个数最多为 1000,因此差分数组长度设置为 1001:
boolean carPooling(int[][] trips, int capacity) {
// 最多有 1000 个车站
int[] nums = new int[1001];
// 构造差分解法
Difference df = new Difference(nums);
for (int[] trip : trips) {
// 乘客数量
int val = trip[0];
// 第 trip[1] 站乘客上车
int i = trip[1];
// 第 trip[2] 站乘客已经下车,
// 即乘客在车上的区间是 [trip[1], trip[2] - 1]
int j = trip[2] - 1;
// 进行区间操作
df.increment(i, j, val);
}
int[] res = df.result();
// 客车自始至终都不应该超载
for (int i = 0; i < res.length; i++) {
if (capacity < res[i]) {
return false;
}
}
return true;
}