0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看威廉希尔官方网站 视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一个小而美的算法技巧:差分数组

算法与数据结构 来源:labuladong 作者:labuladong 2020-09-21 15:54 次阅读

本文给大家介绍一个小而美的算法技巧:差分数组。

读完本文,你可以去解决力扣第 1109 题「航班预订统计」,难度Medium

差分数组技巧是前文前缀和技巧详解写过的前缀和技巧的兄弟。

前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。

没看过前文没关系,这里简单介绍一下前缀和,核心代码就是下面这段:

classPrefixSum{ //前缀和数组 privateint[]prefix; /*输入一个数组,构造前缀和*/ publicPrefixSum(int[]nums){ prefix=newint[nums.length+1]; //计算nums的累加和 for(inti=1;i< prefix.length; i++) {             prefix[i] = prefix[i - 1] + nums[i - 1];         }     }     /* 查询闭区间 [i, j] 的累加和 */     public int query(int i, int j) {         return prefix[j + 1] - prefix[i];     } }

prefix[i]就代表着nums[0..i-1]所有元素的累加和,如果我们想求区间nums[i..j]的累加和,只要计算prefix[j+1] - prefix[i]即可,而不需要遍历整个区间求和。

本文讲一个和前缀和思想非常类似的算法技巧「差分数组」,差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。

比如说,我给你输入一个数组nums,然后又要求给区间nums[2..6]全部加 1,再给nums[3..9]全部减 3,再给nums[0..4]全部加 2,再给…

一通操作猛如虎,然后问你,最后nums数组的值是什么?

常规的思路很容易,你让我给区间nums[i..j]加上val,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对nums的修改非常频繁,所以效率会很低下。

这里就需要差分数组的技巧,类似前缀和技巧构造的prefix数组,我们先对nums数组构造一个diff差分数组,diff[i]就是nums[i]和nums[i-1]之差:

int[]diff=newint[nums.length]; //构造差分数组 diff[0]=nums[0]; for(inti=1;i< nums.length; i++) {     diff[i] = nums[i] - nums[i - 1]; }

通过这个diff差分数组是可以反推出原始数组nums的,代码逻辑如下:

int[]res=newint[diff.length]; //根据差分数组构造结果数组 res[0]=diff[0]; for(inti=1;i< diff.length; i++) {     res[i] = res[i - 1] + diff[i]; }

这样构造差分数组diff,就可以快速进行区间增减的操作,如果你想对区间nums[i..j]的元素全部加 3,那么只需要让diff[i] += 3,然后再让diff[j+1] -= 3即可:

原理很简单,回想diff数组反推nums数组的过程,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修改后的结果。

现在我们把差分数组抽象成一个类,包含increment方法和result方法:

classDifference{ //差分数组 privateint[]diff; publicDifference(int[]nums){ assertnums.length>0; diff=newint[nums.length]; //构造差分数组 diff[0]=nums[0]; for(inti=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 语句:

publicvoidincrement(inti,intj,intval){ diff[i]+=val; if(j+1< diff.length) {         diff[j + 1] -= val;     } }

当j+1 >= diff.length时,说明是对nums[i]及以后的整个数组都进行修改,那么就不需要再给diff数组减val了。

算法实践

这里看一下力扣第 1109 题「航班预订统计」:

函数签名如下:

int[]corpFlightBookings(int[][]bookings,intn)

这个题目就在那绕弯弯,其实它就是个差分数组的题,我给你翻译一下:

给你输入一个长度为n的数组nums,其中所有元素都是 0。再给你输入一个bookings,里面是若干三元组(i,j,k),每个三元组的含义就是要求你给nums数组的闭区间[i-1,j-1]中所有元素都加上k。请你返回最后的nums数组是多少?

PS:因为题目说的n是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组(i,j,k),数组区间应该对应[i-1,j-1]。

这么一看,不就是一道标准的差分数组题嘛?我们可以直接复用刚才写的类:

int[]corpFlightBookings(int[][]bookings,intn){ //nums初始化为全0 int[]nums=newint[n]; //构造差分解法 Differencedf=newDifference(nums); for(int[]booking:bookings){ //注意转成数组索引要减一哦 inti=booking[0]-1; intj=booking[1]-1; intval=booking[2]; //对区间nums[i..j]增加val df.increment(i,j,val); } //返回最终的结果数组 returndf.result(); }

这道题就解决了。

其实我觉得差分数组和前缀和数组都是比较常见且巧妙的算法技巧,分别适用不同的常见,而且是会者不难,难者不会。所以,关于差分数组的使用,你学会了吗?!

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4608

    浏览量

    92844
  • 差分
    +关注

    关注

    0

    文章

    53

    浏览量

    21368
  • 数组
    +关注

    关注

    1

    文章

    417

    浏览量

    25939

原文标题:论那些小而美的算法技巧:差分数组/前缀和

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    数组的下标为什么可以是负数

    最近有同学发来这样段代码,并提出问题,数组的下标为什么可以是负数?     #include int main(){ const char *s = "helloworld";
    的头像 发表于 12-20 11:18 66次阅读

    指针数组和二维数组有没有区别

    。 首先是指针数组 s1。 s1 本身是数组数组有三元素,每个元素都是
    的头像 发表于 11-24 11:12 143次阅读

    C语言数组应用计算机导论A第6讲:数组

    C语言数组应用计算机导论A第6讲:数组
    发表于 11-20 15:33 0次下载

    labview字符串数组转化为数值数组

    常重要的。LabVIEW支持多种数据类型,包括数值、字符串、数组、簇等。在本例中,我们将关注字符串数组和数值数组。 字符串数组 :由系列字
    的头像 发表于 09-04 17:47 2260次阅读

    为什么电势计测量的是电池的电动势不是其端电压?

    电势计是种精密的测量仪器,它能够测量出电池的电动势,不是其端电压。
    的头像 发表于 05-21 15:27 2329次阅读

    .c文件中定义个数组遇到的疑问求解

    .c文件中定义个数组,然后在其他文件中引用,用sizeof求数组长度,那么按说是必须要声明这个
    发表于 05-14 07:03

    嵌入式中零长度数组基本操作方法

    就是长度为0的数组,也就是说不包含任何元素的数组。零长度数组在C99标准中引入,并在C11中得到进
    的头像 发表于 05-11 08:49 925次阅读
    嵌入式中零长度<b class='flag-5'>数组</b>基本操作方法

    while的循环里面,怎么样可以通过控件去让其中的数组值全部初始化?

    这种方式是让所有的数据都初始化了,我只需要部分数据初始化,就是所有的数组,大佬们求帮助!
    发表于 04-18 21:19

    深入探索KUKA KRL中的数组应用

    如果 CHAR 类型数组的所有数组元素都拥有相同的字符串,则不必单独初始化每个数组元素。忽略右侧的数组下标。(对于
    的头像 发表于 04-18 10:37 1230次阅读
    深入探索KUKA KRL中的<b class='flag-5'>数组</b>应用

    鸿蒙TypeScript入门学习第11天【Array(数组)】

    数组对象是使用单独的变量名来存储系列的值。 数组非常常用。
    的头像 发表于 04-09 14:38 1135次阅读
    鸿蒙TypeScript入门学习第11天【Array(<b class='flag-5'>数组</b>)】

    随机抽取SV数组中的元素方法实现

    如果想从关联数组中随机选取元素,需要逐个访问它之前的元素,原因是没办法能够直接访问到第N
    的头像 发表于 03-21 10:11 988次阅读
    随机抽取SV<b class='flag-5'>数组</b>中的<b class='flag-5'>一</b><b class='flag-5'>个</b>元素方法实现

    数组和链表在内存中的区别 数组和链表的优缺点

    内存中的存储方式: 数组种连续存储的数据结构,它将元素存储在相邻的内存位置中。这使得数组的访问效率高,可以通过下标来直接访问任何
    的头像 发表于 02-21 11:30 1023次阅读

    什么是分晶振 分晶振的优势 分输出与单端输出的差别

    许多优势,而其分输出与单端输出也有些明显的区别。 首先,来了解分晶振的工作原理。分晶振结构由两
    的头像 发表于 01-18 11:30 1144次阅读

    PHP中数组的使用方法!

    如何创建数组、添加/删除元素、访问数组元素、遍历数组以及使用数组方法和函数等等。 首先,我们来看下如何创建
    的头像 发表于 01-12 15:11 540次阅读

    labview怎么查数组中相同元素的个数

    要查找LabVIEW中数组中相同元素的个数,可以使用以下步骤: 创建包含要查找的数值的数组。这可以通过手动输入数组元素或从文件/其他数据
    的头像 发表于 12-28 16:42 3489次阅读