在Java中,如何获取整数二进制形式倒序排列中的1的位置?

57

我有一个遗留应用程序,它接受整数、将其转换为二进制字符串,反转该字符串,然后获取位(一)的位置作为整数列表。例如:

6 -> "110" -> "011" -> (2,3) 
7 -> "111" -> "111" -> (1,2,3)
8 -> "1000" -> "0001" -> (4)

在现代Java中,没有使用String操作,如何简洁明了地完成这个任务?将其转换为String然后再转回来似乎很浪费,而且我知道没有简单的方法来翻转一个字符串(没有String.reverse())。


9
我认真地问自己:为什么?所有的解决方案都会检查是否存在某个索引,然后以某种方式存储该索引。也许字符串转换并不是必需的,但根据今天的标准,我怀疑它对性能有多大影响。 - maio290
6
注意:new StringBuilder(s).reverse().toString() 可以用来翻转字符串 s。 - David Conrad
4
在这里使用字符串完全是一个概念上有问题的做法。性能与此关系不大(尽管它是一个后果,如果这是一个频繁的操作,性能可能会变得相关)。 - Konrad Rudolph
5
那有什么用途呢?我想不出实际用途。 - inetphantom
2
@inetphantom 正如我所说,这是一个遗留系统。实际上,这是数据库中非常特殊的“外键”。整数编码了一组要在数据库表中查找的ID号码。 - workerjoe
显示剩余5条评论
13个回答

58

依次检查每一位:

List<Integer> bits(int num) {
  List<Integer> setBits = new ArrayList<>();
  for (int i = 1; num != 0; ++i, num >>>= 1) {
    if ((num & 1) != 0) setBits.add(i);
  }
  return setBits;
}

在线演示

6 [2, 3]
7 [1, 2, 3]
8 [4]

看起来不错。我从来没用过这些二进制/按位操作,所以我总是记不住它们。 - workerjoe
5
显然,使用字符串来完成这个任务是非常糟糕的。 - TonyK
这个问题有很多好的答案!我认为其中一个使用右移操作符进行迭代的方法非常聪明。而且这是最早的答案之一。 - workerjoe
2
@workerjoe:你选择得很好;这个应该是最有效的 JIT 编译(除了 @Matthie M.'s answer,它只循环遍历设置的位,特别是在输入中只有少量位稀疏分布在整个数字上时)。这是使用简单的位运算编写的非常干净 / 易于理解。右移给你一个便宜的早期退出,并且通常比测试 num & (1<<i) 或其他东西更有效,同时也使位编号在初始化 i 方面是任意的。 - Peter Cordes

31

你可以直接测试比特位,而无需将整数转换为字符串:

List<Integer> onePositions(int input) {
  List<Integer> onePositions = new ArrayList<>();
  for (int bit = 0; bit < 32; bit++) {
    if (input & (1 << bit) != 0) {
      onePositions.add(bit + 1); // One-based, for better or worse.
    }
  }
  return onePositions;
}

比特通常从右向左计数,最右边的比特为第0位。操作1<<bit会给你一个int,其中编号为bit的比特位被设置为1(其余位为0)。然后使用&(按位与)检查此比特位在input中是否被设置,并且如果是,则记录输出数组中的位置。


1
我怀疑性能是否会受到始终检查所有32位的影响;但是你可以使用maxBit = Integer.highestOneBit(input),然后将bit <= maxBit用作循环保护(并从bit = Integer.lowestOneBit(input)开始,就此而言)。 - Andy Turner
需要在 input & (1 << bit) 周围加上括号,因为 != 的优先级高于 &。(在我看来,这是 Java 中的一个错误,因为它没有任何用处)。我认为这个答案是最好的,因为它最易读。你可以像 Matthieu 的答案中所做的那样,将 Integer.bitCount(input) 传递给 ArrayList 的构造函数,以稍微提高性能。 - user42723
通常比起左移一个 1,将被测试的数字右移更好。首先,它避免了任何变量计数移位,这可能具有优势(例如,在现代英特尔 CPU 上,如果 JIT 编译器没有将其优化为 bt 指令,则需要 3 个 uops 而不是 1)。其次,当 input >>>= 1 变为 0 时,它可以免费提供早期退出。在循环传递依赖链中使用右移作为一部分是可以的;现代 CPU 具有单周期延迟移位。 - Peter Cordes
@PeterCordes 谢谢,这是深入和有用的信息,Andy Turner 的被接受的答案完美地解决了这个问题。我的答案可能更易读,更高层次的教育性,所以我会保持原样。 - Thomas

26

我可以提出一个纯位运算的解决方案吗?

static List<Integer> onesPositions(int input)
{
    List<Integer> result = new ArrayList<Integer>(Integer.bitCount(input));

    while (input != 0)
    {
        int one = Integer.lowestOneBit(input);
        input = input - one;
        result.add(Integer.numberOfTrailingZeros(one));
    }

    return result;
}

这种解决方案在算法上是最优的:

  1. 单一内存分配,使用 Integer.bitCount 来预先适当地调整 ArrayList 的大小。
  2. 循环迭代的次数最少,每组一次1

内部循环非常简单:

  • Integer.lowestOneBit 返回一个只有输入中最低位的 int
  • input - one 取消设置输入的此位,以用于下一次迭代。
  • Integer.numberOfTrailingZeros 计算二进制下的末尾零的数量,有效地给出了最低 1 位的索引。

1 值得注意的是,一旦编译,这可能不是最优的方式,并且相反,基于 bitCount 的显式 0..n 循环更容易为 JIT 展开。


在CTZ之前,您不需要隔离最低位。相反,您可以使用add CTZ(input)/ blsr input,即使用input&= input-1;清除最低位。即使JITter不使用x86 BMI2 [blsr](https://www.felixcloutier.com/x86/blsr),也很容易使用几个指令实现。或者如果JIT仍然无法使用LEA孔洞优化,则为3,并且最终的AND根据“input”设置FLAGS,从而为循环分支节省了cmp / test。除非HotSpot JIT也搞砸了。希望HotSpot知道“tzcnt”或“bsf”具有输出依赖性... - Peter Cordes
更正,blsr 是 BMI1 指令,而非 BMI2。 - Peter Cordes
1
@PeterCordes Integer.lowestOneBit(…) 已经被实现为 return i & -i;。无论 JIT 是否理解这个习惯用法,调用这个方法永远不会比手动执行 input&=input-1; 更糟糕。 - Holger
@Holger:这本身就是真的,但我的建议可以节省input = input - one;。这缩短了循环依赖链的延迟时间。(如果使用BMI1 blsr而不是blsi / sub,或者只有1个周期,如果JIT不那么聪明)。 - Peter Cordes
@Holger:此外,在x86-64上,使用i & -i;可能比i & (i-1)更糟糕,这取决于JIT编译器。例如,可以使用lea eax,[rdi-1]i-1计算到一个单独的寄存器中,但是计算-i而不破坏原始的i通常需要mov+neg,或者使用异或零寄存器来从中减去sub。(x86-64没有任何地址模式可以从任何东西中减去一个寄存器)。因此,对于可以使用LEA的编译器而言,如果没有BMI1 peepholes(并且它是否可用),则i & (i-1)的计算成本可能更低。(我不知道当前的JVM是否这样做)。 - Peter Cordes

22

既然你提到“现代Java”,那么以下是使用(Java 8或更高版本)的实现方式:

final int num = 7;

List<Integer> digits = IntStream.range(0,31).filter(i-> ((num & 1<<i) != 0))
        .map(i -> i+1).boxed().collect(Collectors.toList());

这张地图仅在你从1开始计数而不是从0开始计数时才需要。

然后

System.out.println(digits);

打印

[1, 2, 3]

1
BitSet.valueOf(new long[] { num }).stream().map(i -> i + 1) - Holger

16

我个人肯定更倾向于安迪的回答,尽管一开始它似乎有些神秘。但由于这里没有人给出使用流(streams)的答案(即使它们在这里完全不合适):

我个人肯定更喜欢安迪的答案,即使一开始它看起来有点难懂。但是由于这里还没有人给出流(streams)方面的答案(即使它们在这里完全不适用):

public List<Integer>  getList(int x) {
    String str = Integer.toBinaryString(x);
    final String reversed = new StringBuilder(str).reverse().toString();
    return IntStream.range(1, str.length()+1)
            .filter(i -> reversed.charAt(i-1)=='1')
            .boxed()
            .collect(Collectors.toList());
}

3
不是要反转字符串,而是可以使用映射收集器,然后使用 i -> str.length() - i 来获取反转的索引。 - user
2
Nit: IntStream.rangeClosed(1, str.length()) 对我来说更整洁。 - Andy Turner
2
使用流可以在一行代码中完成,而无需使用StringBuilder:https://dev59.com/questions/p1IH5IYBdhLWcg3wAnrX#61737402(但我不确定是否特别要求使用String)。 - Christian Fries
1
伙计,你看起来是个不错的人!我喜欢你的风格。 - aran
1
@aran 很高兴听到这个消息。谢谢你。 - Eritrean

12

一个愚蠢的答案,只是为了增加趣味性:

BitSet bs = BitSet.valueOf(new long[] {0xFFFFFFFFL & input});
List<Integer> setBits = new ArrayList<>();
for (int next = -1; (next = bs.nextSetBit(next + 1)) != -1;) {
  setBits.add(next + 1);
}

(感谢pero_hero指出需要对WJS的答案进行掩蔽)


11

给定原始整数,返回一个包含每个二进制位的位置的列表。

static List<Integer> bitPositions(int v) {
     return BitSet.valueOf(new long[]{v&0xFF_FF_FF_FFL})
                .stream()
                .mapToObj(b->b+1)
                .collect(Collectors.toList());
}

或者,如果您想进行位移操作。

static List<Integer> bitPositions(int v ) {
    List<Integer> bits  = new ArrayList<>();
    int pos = 1;
    while (v != 0) {
        if ((v & 1) == 1) {
            bits.add(pos);
        }
        pos++;
        v >>>= 1;
    }
    return bits;

}


上面真的很好 +1,只有-1时,它提供了64位,因为它是一个长整型。 - pero_hero
好的观察。我会屏蔽高位字。谢谢! - WJS

9
你不需要反转实际的二进制字符串,你只需要计算索引。
String str = Integer.toBinaryString(num);
int len = str.length();
List<Integer> list = new ArrayList<>();
for (int i=0; i < len; i ++) {
  if (str.charAt(i) == '1') list.add(len - 1 - i);
}

2
你不需要转换为字符串。 - Andy Turner
@AndyTurner 是的,我只是不知道如何使用位移来实现它。 - user

7
抱歉,我只能使用英语来回答您的问题。
Pattern one = Pattern.compile("1");
List<Integer> collect = one.matcher(
             new StringBuilder(Integer.toBinaryString(value)).reverse())
            .results()
            .map(m -> m.start() + 1)
            .collect(Collectors.toList());
System.out.println(collect);

7
一个流式版本的@Matthieu M.的回答:
 List<Integer> list = IntStream.iterate(value, (v) -> v != 0, (v) -> v & (v - 1))
                .mapToObj(val -> Integer.numberOfTrailingZeros(val) + 1)
                .collect(toList());

1
清除最低位的更简单的习惯用语是 v & (v-1)。查找隔离或重置最低位的位操作技巧的简单方法是参考 x86 汇编 BMI 指令的文档 blsrblsi。我经常双重检查这些文档,而不是记忆实际的位操作公式。 - Peter Cordes
@PeterCordes 感谢您的提醒。我试图想出一个更简单的形式,但一直无法实现。现在我知道了,这其实很简单。 - pero_hero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接