【问题标题】:A bitmask solution to test if an element is included in a finite set?测试元素是否包含在有限集中的位掩码解决方案?
【发布时间】:2021-04-15 05:28:00
【问题描述】:

这是一个代码挑战。我在 SO 上遇到了this question,关于测试一个数字是否包含在给定的集合中。典型的解决方案是:

const givenSet = [9, 91, 20, 18, 17, 37]
function isIncluded(x) {
  return givenSet.includes(x)
}

但是,我想知道这个问题是否存在使用位掩码的解决方案?有人可以在下面填写CREATE_BITMASKJUDGEMENT吗?

const givenSet = [9, 91, 20, 18, 17, 37]
const bitmask = CREATE_BITMASK(givenSet)
function isIncluded(x) {
  return JUDGEMENT(x, bitmask)
}

其中givenSet 由适合Int32 的任意正整数组成。

我的即时观察是,如果x 等于给定集合中的n,那么我们可以应用异或来获得x ^ n === 00 是一个我们可以利用的特殊数字。但我没有更多的想法。

供您参考,这里是Bitwise Operator Laws


更新:@coderLMN 指出,上述形式可能具有误导性。

基本上,我正在寻求一次性解决方案,无需遍历给定集合中的每个元素。

我真正要问的是,是否可以将要求编码(给定的集合)一个单一的数据结构(很可能是位掩码)。

一个有效的解决方案可以采用任何形式,只要它不需要为每个单独的测试迭代给定的集合。不要限制自己。

【问题讨论】:

  • “基本上我正在寻找一次性解决方案,而不需要遍历给定集合中的每个元素。”听起来您要的是 Set 对象。见developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… ...
  • 不,这更像是一道数学题。真正的问题是,我们可以在多大程度上将多个元素(给定的正整数集)编码为压缩数据结构(位掩码),同时保留必要的信息(足以区分传入元素是否包含在原始集合中)。
  • 我认为 coderLMN 是正确的,因为不可能将它们编码为 one 位掩码。但我想仍然可以将它们压缩成两三个这样的结构?还在想。
  • 如果您的整数范围是 0...n,其中 n 在合理范围内并且您没有重复值,那么您可以使用类型化数组(例如,Uint32Array)位的索引代表数字。例如,如果设置了第 12 位,则数字 12 是您设置的一部分。如果设置了第 320 位(即 Uint32Array 中第 10 个元素的第 0 位),则数字 320 是该集合的一部分。通过这种方式,可以直接确定一个数字是否是集合的一部分,而无需扫描数组。否则,我倾向于同意 coderLMN。

标签: javascript bitmask


【解决方案1】:

理论上是不可能的。

假设您有一组CREATE_BITMASKBITWISE_OPJUDGEMENT,它们适用于givenSet 中的所有整数,那么您必须:

(9  BITWISE_OP bitmask) === JUDGEMENT
(91 BITWISE_OP bitmask) === JUDGEMENT
(20 BITWISE_OP bitmask) === JUDGEMENT
(18 BITWISE_OP bitmask) === JUDGEMENT
(17 BITWISE_OP bitmask) === JUDGEMENT
(37 BITWISE_OP bitmask) === JUDGEMENT

考虑到整数适合Int32 的要求,bitmask 必须是相同的格式,这意味着无论BITWISE_OP 是什么,都不可能让它对不同的整数进行运算以获得相同的结果。

【讨论】:

  • 感谢您的意见。但我的描述具有误导性,并不反映我的意图。我已经更新了这个问题。有兴趣再试一次吗?
  • 我的观点还是一样,因为无论您的修订版中的“单一数据结构”(位掩码)是什么,在数学上都无法实现多对一([9, 91, 20, 18...] -> JUDGEMENT) 映射与“一次通过操作”(尤其是 BITWISE_OP)。从信息论的角度来看,位掩码可能是一种压缩的数据结构,它必须包含列表中的所有信息,但比较操作必须以一种或另一种方式处理(迭代)所有这些信息。
  • 好点。我会让这个问题搁置几天。如果没有新内容出现,我会标记你的接受。
【解决方案2】:

如果正整数的范围在合理的最大值范围内并且重复值不是一个因素,那么可以使用类型化数组(例如,Uint32Array),其中该数组中位的索引表示一个整数价值。

例如,如果设置了第 12 位,则数字 12 是您设置的一部分。如果设置了第 320 位(即 Uint32Array 中第 10 个元素的第 0 位),则数字 320 是该集合的一部分。这样,通过索引数组而不是扫描数组,可以直接判断一个数是否是集合的一部分。

下面的代码包含一个类,给定一个最大整数值,定义一个表示一组整数的位数组。方法setBit 将整数添加或删除到集合中,方法getBit 确定整数是否在整数集合中。

class BitArray {

  constructor( maxN ) {
    this.max = maxN;
    this.bitArray = new Uint32Array( ( maxN / 32 |0 ) + 1 );
  }
  
  getBit( n ) {
    if ( 0 <= n && n <= this.max ) {
      return 0 < ( this.bitArray[ n / 32 |0 ] & ( 1 << ( n % 32 ) ) );
    }
    return NaN;
  }
  
  setBit( n, val ) {
    if ( 0 <= n && n <= this.max ) {
      if ( val ) {
        this.bitArray[ n / 32 |0] |=  1 << ( n % 32 );
      } else {
        this.bitArray[ n / 32 |0 ] &=  ~( 1 << ( n % 32 ) );
      }
    }    
  }
  
}

let ints = new BitArray( 100 );
[9, 91, 20, 18, 17, 37].forEach( n => ints.setBit( n, true ) );


console.log( `5: ${ ints.getBit( 5 ) }` );
console.log( `37: ${ ints.getBit( 37 ) }` );
console.log( `91: ${ ints.getBit( 91 ) }` );
console.log( `500: ${ ints.getBit( 500 ) }` );

编辑整数集中具有大整数的稀疏数组

作为一些关于内存使用的 cmets 的后续行动,一个解决方案是进行两层查找,第一层实现为稀疏数组。第一层是一个数组,它返回整数块中的整数个数,以及对第二层的引用,即原始答案中的上述位图数组。

例如,假设第一层按 15 位块映射整数,即每块 2^15 或 32768 个整数。第一层由索引为 0...131071 的对象数组组成,元素初始化为 emtpy / undefined。一旦位被设置在第一层元素的范围内,那么 tier1 就成为一个对象,其中 count 指示块中设置的位数,以及对跟踪块中整数集的第二层位图数组的引用一级范围。由于 32768 / 32(每个 Uint32 的位数)是 1024,那么第二层位图是Uint32Array(1024),足以表示一个由 32768 个整数组成的块。

通常预计会有大量整数要跟踪(否则线性搜索可能会更快),但举例来说,假设要跟踪的整数是...

[ 0, 1, 2, 1000000, 1000000000 ]

...在这种情况下,第一层和第二层的表示将是...

tier1[ 0 ] = { count: 3, tier2: [ 7, 0, 0, ..., 0 ] }   // Range 0 - 32767
tier1[ 1 ] = undefined                                  // Range 32768 - 65535
tier1[ 2 ] = undefined                                  // Range 65536 - 98304
        o
        o
        o
tier1[ 29 ] = undefined                                           // Range 950272 - 983039
tier1[ 30 ] = { count: 1, tier2: [ 0, 0, 0, ..., 512, ..., 0 ] }  // Range 983040 - 1015807
tier1[ 31 ] = undefined                                           // Range 1015808 - 1048575

        o
        o
        o
tier1[ 30516 ] = undefined                                          // Range 999948288 - 999981055
tier1[ 30517 ] = { count: 1, tier2: [ 0, 0, 0, ..., 32 ..., 0 ] }   // Range 999981056 - 1000013823
tier1[ 30518 ] = undefined                                          // Range 1000013824 - 1000046591
        o
        o
        o
tier1[ 131071 ] = undefined                                         // Range 4294934528 - 4294967295

现在,要确定一个特定整数是否是整个集合的一部分,只需计算 tier1 索引,如果不是未定义,则查看 tier2 位图。

例如,在寻找 1000000 时,tier1 索引由 1000000 / 32768(因为它们是每个块的 32768 个整数)计算得出,四舍五入到索引 30。然后为 1000000 寻找 tier2 的关联 tier1[ 30 ] 位索引- 983040 或位 16960,并返回该位值(在本例中为 true)。

这仍然通过计算两层索引提供对集合中整数的直接访问(无需扫描数组),并适应稀疏整数集,特别是因为 tier1 数组将利用 JavaScript 内置的稀疏数组功能。 ..

请注意,可以调整块的大小(在此示例中为 32768 个整数)以适应预期的数据分布和密度,以平衡内存利用率。

【讨论】:

  • 好主意!不够通用,但对于某些用例来说,这绝对是一个有效的解决方案。
  • 一点观察:如果给定的集合是均匀分布且密集的,那么这种编码可以看作是压缩。但是,如果给定的集合中间稀疏而两端密集,例如[0, 1, 2, 10000, 100001],那么这种编码实际上会占用更多空间。
  • @hackape 同意稀疏集消耗内存。问题的根源是代码挑战,因此在本质上有点理论。具体的用例将取决于要解决的实际问题,并且必须考虑您提出的因素。例如,如果需要一种非常快速的方法来确定整数是否在集合中,那么消耗更多内存可能是值得的。否则,您是正确的,内存消耗可能不值得性能提升,并且会退回到线性搜索或 Set 对象。
  • 喜欢更新的解决方案。感谢您的时间和精力!此解决方案类似于内存分页的工作方式。所以我们从实模式转到保护模式?
  • @hackape count 只是tier2 中设置的位数的计数。清除位时,如果count 达到零,则可以将teir1 设置为未定义。否则,每次清除位时,都必须检查整个 tier2 数组以查看是否设置了任何位。当然,也许这并不重要,因为在检查 tier1 范围内的整数时,您仍然会得到相同的结果(即错误),因为整个 tier2 位图数组然后为零......所以是的,安全删除count,如果tier2 没问题,可能在位设置和清除操作后没有设置位...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-08
  • 1970-01-01
  • 1970-01-01
  • 2011-12-01
  • 1970-01-01
相关资源
最近更新 更多