(给算法爱好者加星标,修炼编程内功)

来源:十年踪迹的博客

h5jun.com/post/range-sum-query-immutable.html

这是一道翻译小组的同学问我的题目,这道题很有意思,在 leetcode 上标记的难度为 Easy, 然而正确率出奇地低,只有不到 25%,看来这是一道看似简单实际上颇有挑战性的题目。

不可变数组的范围求和

给定一个整数数组 nums,计算出从第 i 个元素到第 j 个元素的和 ( i ≤ j ),包括 nums[ i ] 和 nums[ j ]。

例子:

const nums = Object.freeze([-2, 0, 3, -5, 2, -1]);sumRange(0, 2) -> 1sumRange(2, 5) -> -1sumRange(0, 5) -> -3

注意:

1、假定数组的值不会改变(如上面代码,nums 因为 Object.freeze 的缘故可读不可写)

2、sumRange 可能会被使用很多次,求不同范围的值

3、数组可能规模很大(比如超过 10000 个数),注意运行时间

解题思路

这道题看起来十分简单对吧,简单写一个函数应该谁都会:

简单实现

function sumRange(i, j){  var sum = 0;  for(; i <= j; i++){    sum += nums[i];  }  return sum;}

不过呢,这么写,对照上面的注意事项,尤其是后两条:

  • sumRange 可能会被使用很多次

  • 数组的规模可能会很大

如果考虑这两条,那么上面的方法可以说是十分慢的,这也是为什么很多人在 leetcode 提交代码通不过,因为简单这么算的话,跑 leetcode 的大数组 case 肯定超时。

那么,我们要怎么做才能更快呢?注意到前面说的这是不可变数组了吧?也就是说数组初始化完成之后,它的值不会改变,因此我们可以对它进行拷贝,同时“重新编码”。

具体怎么做,大家心里是不是已经隐隐有答案了?让我们思考30秒钟然后继续 ——

重构数组

我们可以重新创建一个数组类,用新的数组来计算 sumRange:

重构数组

const Immutable = Sup => class extends Sup {  constructor(...args){    super(...args);    Object.freeze(this);  }}class NumArray extends Immutable(Array){  sumRange(i, j){    let sum = 0;    for(; i <= j; i++){      sum += this[i];    }    return sum;      }}
上面的代码里面我们重构了数组,这里我用了一点点小技巧来让数组元素不可变,这个技巧在我之前的一篇译文“六个漂亮的 ES6 技巧”中被提到,很多同学不理解那篇文章的第6个技巧,在这里我使用了一下,当然这无关我们今天讨论的主题。

于是我们可以用新的数组对象来计算 sumRange:

var nums = new NumArray(-2, 0, 3, -5, 2, -1);nums.sumRange(0, 2) -> 1nums.sumRange(2, 5) -> -1nums.sumRange(0, 5) -> -3

到这里为止,我们似乎并没有改变什么,我们只是继承了 Array 类,把 sumRange 改成了对象的方法而已,它还是一样很慢。

那接下来我们要怎么做呢?

因为前面说过了,sumRange 要被调用很多次,所以我们要尽可能减少 sumRange 调用的复杂度对吗?按照前面的方式,我们用一个循环来对从 i 到 j 进行求和,有没有更快的方法?答案是:空间换时间,查表!

查表

查表不是查水表,因为 sumRange 要计算很多次,所以我们可以事先在 NumArray 构造的时候将 sumRange 需要查的值算好存入一个表中。

二维表?

R/C 0 1 2 3 4 5
0 -2 -2 1 -4 -2 -3
1 0 3 -2 0 -1
2 3 -2 0 -1
3 -5 -3 -4
4 2 1
5 -1

二维表可以将每一对 i, j 完全映射一个值,这样的话,空间复杂度变成了 O( n2 ),记得我们前面说了,这个数组可能会很大,有 10000 个元素,如果用这样的映射表,内存就溢出了。实际上,使用二维表是愚蠢的,因为我们可以很容易找到以下对应关系:

sumRange(i, j) === sumRange(0, j) - sumRange(0, i - 1); //(i > 0)

一维表

我们只需要将 NumArray 的每一个元素对应从第 1 元素开始求和,将结果保存成一个一维表,我们就可以用 O( 1 ) 时间复杂度来计算 sumRange( i, j ) !

以下是经过优化之后的 NumArray:

使用一维表

const UniqueID = Sup => class extends Sup {  constructor(...args){      super(...args);      Object.defineProperty(this, "id", {        value: Symbol(),        writable: false,        enumerable: false      });    }}const Immutable = Sup => class extends Sup {  constructor(...args){    super(...args);    Object.freeze(this);  }}const NumArray = (function(){  let sumTable = {};  return class  extends Immutable(UniqueID(Array)){    constructor(...args){      super(...args);      let sum = 0;      let table = [0];      for(let i = 0; i < this.length; i++){        sum += this[i];        table.push(sum);      }      sumTable[this.id] = table;    }    sumRange(i, j){      let table = sumTable[this.id];      return table[j + 1] - table[i];       }  }})();

上面的代码里,我们在构造 NumArray 的时候同时创建了一个私有属性 sumTable,它的第 1 个元素是 0,第 i + 1 个元素等于 sumRange(0, i),因此我们就可以快速通过:

sumRange(i, j){  let table = sumTable[this.id];  return table[j + 1] - table[i];   }

来计算出 sumRange(i, j) 的值了。

进一步优化

上面的代码通过查表大大加快了 sumRange 的执行速度,由于数组 NumArray 是不可变的,因此我们在它被构造的时候创建好 sumTable,那么 sumRange 就完全只需要查表加上一次减法运算就可以完成了。这么做提升了 sumRange 的性能,代价是构造 NumArray 对象的时候带来额外的建表开销。

不过,我们可以不在构造对象的时候建表,而在对象的 sumRange 方法第一次被使用的时候建表。这样的话,我们就将性能开销延从构造对象时迟到了第一次使用 sumRange 时,如果恰巧某种原因,NumArray 对象没有被使用,那么 sumTable 就永远也不会被创建。看下面的代码:

将创建 sumTable 的工作放在 sumRange 第一次被调用时

const UniqueID = Sup => class extends Sup {  constructor(...args){    super(...args);    Object.defineProperty(this, "id", {      value: Symbol(),      writable: false,      enumerable: false    });  }};const Immutable = Sup => class extends Sup {  constructor(...args){    super(...args);    Object.freeze(this);  }};const NumArray = (function(){  let sumTable = {};  return class  extends Immutable(UniqueID(Array)){    sumRange(i, j){      if(!sumTable[this.id]){        let table = [0], sum = 0;        for(let i = 0; i < this.length; i++){          sum += this[i];          table.push(sum);        }        sumTable[this.id] = table;      }      let table = sumTable[this.id];      return table[j + 1] - table[i];    }  }})();

以上是今天我们讨论的内容。上面的代码其实还可以优化,因为我们将建表的工作推迟到 sumRange 第一次被调用时执行,这很好,但这给 sumRange 带来了一次 if 判断操作的额外开销,实际上我们应该也有办法消除这个开销,我把这个问题留给大家吧,欢迎大家讨论。

推荐阅读

(点击标题可跳转阅读)

从一个无序数组中查询最大值的最快算法是什么?

算法数据结构-B树

觉得本文有帮助?请分享给更多人

关注「算法爱好者」加星标,修炼编程内功

好文章,我在看❤️

给定数组 求和等于固定值 算法_别人家的面试题:不可变数组快速范围求和相关推荐

  1. 给定数组 求和等于固定值 算法_[见题拆题] 大厂面试算法真题解析 - 第一期开张...

    如今想要收获大厂offer,在面试的前几轮,总是躲不开算法这座大山. 常听人说,算法很难.这话没错.算法本身是是一个艰深的方向.但是算法题却有据可循.通过有针对性的学习和练习,我们完全可以掌握解题的基 ...

  2. 往数组里添加键值对_框架都是花哨的东西!js才是根基,分享一下给原生js数组的操作...

    1Array.map()方法 此方法原数组不会改变,会返回一个新数组.必须有返回值: 语法: array 回调函数是必穿的参数,thisValue是可选参数!对象作为该执行回调同时使用,传递给函数用作 ...

  3. java如何定义一个变长数组_如何自定义一个长度可变数组

    摘要:本文主要写了如何自定义一个长度可变数组 数组是在程序设计中,为了处理方便,把具有相同类型的若干元素按无序的形式组织起来的一种形式 在定义之初,数组的长度就被定义 新建数组有很多方式 下面两个都可 ...

  4. 二叉树和等于某值路径_【python每日一练】二叉树的路径总和

    给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和. 说明: 叶子节点是指没有子节点的节点. 示例: 给定如下二叉树,以及目标和 sum = 22 ...

  5. excel相同内容单元格数值等于固定值怎么做?

    如下表,让该表格所有"苹果"内容的单价都等于D3的20元,怎么批量操作? 可以使用Excel的条件格式功能来实现该需求,具体步骤如下: 1. 选中表格中所有的"苹果&qu ...

  6. 如何动态的向数组中插入键值对_在Java中实现的一个简单“HashMap”

    如何创建Hash表 对于把K(键)-V(值)这样的键值对插入Hash表中,需要执行两个步骤: 1.使用散列函数将K转换为小整数(称为其哈希码). 2.哈希码用于查找索引(hashCode%arrSiz ...

  7. 算法_栈的Java的通用数组实现

    栈是一个常用的最简单的数据结构,这里提供了其实现.内部维护了一个数组,并且可以动态的调整数组的大小.而且,提供了迭代器支持后进先出的迭代功能.Stack的实现是所有集合类抽象数据类型实现的模板,它将所 ...

  8. 数组元素替换_LeetCode基础算法题第183篇:一维数组的重新洗牌

    技术提高是一个循序渐进的过程,所以我讲的leetcode算法题从最简单的level开始写的,然后到中级难度,最后到hard难度全部完.目前我选择C语言,Python和Java作为实现语言,因为这三种语 ...

  9. ios笔试题算法_微软笔试题-Dijkstra算法

    Dijkstra算法是典型的算法.Dijkstra算法是很有代表性的算法.Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表的方式,这里均采用永久和临 ...

最新文章

  1. 重大改变!Python 或将取代 VBA 成为 Excel 官方脚本语言
  2. ARM NEON 编程简单入门1
  3. 使用Unity引擎打造赛博朋克之城!CIGA Game Jam 2019 48小时独立游戏开发挑战
  4. 我有一个域名_一个域名可以绑定几个网站?域名解析多少子域名?
  5. Mac多功能文件搜索软件:HoudahSpot
  6. [2014]兄弟连高洛峰 php教程5.5.1,2014PHP兄弟连全套教程
  7. 每个python文件就是一个模块、模块的名字就是_Python-模块和包
  8. ios加密算法AES
  9. 1、Django下载与搭建、配置环境变量
  10. Qt多语言翻译(国际化)
  11. 返回 代码: E_INVALIDARG (0x80070057)解决方法
  12. PHP Class SoapClient not found解决方法
  13. 计及需求侧响应日前、日内两阶段鲁棒备用优化【IEEE6节点】(Matlab代码实现)
  14. dapi 基于Django的轻量级接口测试平台一
  15. Singing Contest
  16. 计算机鼓轮原理,数码裂隙灯显微镜光学系统的设计与实现
  17. Ajax怎么获取天气,Ajax获取全国天气预报的API数据
  18. 迅为龙芯开发板Loongnix系统烧写-loognix图形化安装
  19. word转图片 java_Java 利用LibreOffice将Office文档转换成 PDF,进而转图片,实现在线预览功能...
  20. linux调节字体大小加粗,支持任意大小字体freetype2显示(linux frambuffer)版

热门文章

  1. dojo/domReady! 中感叹号的作用
  2. GridView 梆定一个实体类
  3. JavaWeb学习总结(五十)——文件上传和下载
  4. T-SQL中的GROUP BY GROUPING SETS
  5. sql 2005判断某个表或某个表中的列是否存在
  6. 携程基于Storm的实时大数据平台实践
  7. PHP与Redis结合令牌桶算法进行实现限流
  8. 反思编写页面追加页面元素的方法,目的:加快开发速度 节省开发时间 需求:点击搜索清空表格内容进行增加新的数据行
  9. centos7安装详细图解_5G基站工程安装详细图解(纯干货)
  10. java 交互输入_JAVA -----------交互式程序