コーディング · アルゴリズム
ビット演算(ビット操作)とは?演算子・テクニック・例題をわかりやすく解説
結論
ビット演算は、整数の2進数の桁(ビット)を、ビット演算子で直接操作することです。演算子には、AND(&)、OR(|)、XOR(^)、NOT(~)、シフト << と >> があります。これらを使うと、個々のビットの判定・セット・クリア・反転ができ、小さな数の集合を1つの整数(ビットマスク)に格納でき、立っているビットの数え上げや、ペアになっていない1つの数を追加メモリ一定で見つける、といった問題も解けます。固定長の整数では、各演算は高速なCPU命令1つです。
ビット演算が解く問題
整数はビットの並びであり、多くの問題は実はそのビットに関するものです。rwx のようなファイルのパーミッション、機能フラグ、ネットワークマスク、ピクセルの色、コンパクトなオプションの集合、チェックサム、ハッシュなどです。ビット演算子を使うと、多くのビットを1回の操作で読み書きでき、メモリ使用量が小さく、非常に高速です。
面接でもビットのテクニックがよく出題されます。たとえば、ほかのすべての数が2回ずつ現れる中で1回だけ現れる数を見つける問題のように、空間 O(1) のエレガントな解法が得られるためです。
演算子
例では、a = 12(2進数で 1100)と b = 10(2進数で 1010)を使います。
| 演算子 | 名前 | ビットごとの規則 | 例 | 結果 |
|---|---|---|---|---|
a & b | AND | 両方のビットが1なら1 | 1100 & 1010 | 1000 = 8 |
a | b | OR | どちらかのビットが1なら1 | 1100 | 1010 | 1110 = 14 |
a ^ b | XOR | ビットが異なれば1 | 1100 ^ 1010 | 0110 = 6 |
~a | NOT | すべてのビットを反転 | ~12 | -13(2の補数) |
a << n | 左シフト | ビットを左に動かし、空いたところを0で埋める | 12 << 1 | 24(2ⁿ 倍) |
a >> n | 右シフト | ビットを右に動かす | 12 >> 2 | 3(2ⁿ で切り捨て除算) |
a, b = 12, 10
print(bin(a), bin(b)) # 0b1100 0b1010
print(a & b, a | b, a ^ b) # 8 14 6
print(~a, a << 1, a >> 2) # -13 24 3
print(f"{a ^ b:04b}") # 0110
print(int("1010", 2)) # 101ビットの操作
ビットの位置は、右端(最下位ビット)を0として数えます。マスク 1 << i は、ビット i だけが立っています。
| 目的 | 式 |
|---|---|
| ビット i を判定する | (x >> i) & 1 または x & (1 << i) != 0 |
| ビット i をセットする | x | (1 << i) |
| ビット i をクリアする | x & ~(1 << i) |
| ビット i を反転する | x ^ (1 << i) |
| 最下位の立っているビットだけを取り出す | x & -x |
| 最下位の立っているビットをクリアする | x & (x - 1) |
| x は2のべき乗か?(x > 0) | x & (x - 1) == 0 |
例題:立っているビットの数え上げ
ブライアン・カーニハン(Brian Kernighan)の方法は、最下位の立っているビットを残りがなくなるまでクリアし続けるので、ビット位置の数ではなく、1のビットの数だけ繰り返します。x = 44(2進数で 101100)のトレースです。
| ステップ | x(2進数) | x - 1(2進数) | x & (x - 1) | カウント |
|---|---|---|---|---|
| 1 | 101100 | 101011 | 101000 | 1 |
| 2 | 101000 | 100111 | 100000 | 2 |
| 3 | 100000 | 011111 | 000000 | 3 |
def count_bits(x):
count = 0
while x:
x &= x - 1 # drop the lowest set bit
count += 1
return count
print(count_bits(44)) # 3
print((44).bit_count()) # 3
print((44).bit_length()) # 6int.bit_count() はPython 3.10以降で使えます。int.bit_length() は、符号を除いて、その数を2進数で書くのに必要なビット数を返します。
Pythonでの定番テクニック
from functools import reduce
from operator import xor
# Every number appears twice except one: pairs cancel out with XOR.
print(reduce(xor, [4, 1, 2, 1, 2])) # 4
# Swap two integers without a temporary variable (for illustration only).
x, y = 5, 9
x ^= y; y ^= x; x ^= y
print(x, y) # 9 5
# Bitmask as a set: enumerate all subsets of 3 items.
items = ["a", "b", "c"]
for mask in range(1 << len(items)):
print(mask, [items[i] for i in range(len(items)) if mask >> i & 1])
# 0 []
# 1 ['a']
# 2 ['b']
# 3 ['a', 'b']
# 4 ['c']
# 5 ['a', 'c']
# 6 ['b', 'c']
# 7 ['a', 'b', 'c']マスク 0 は空集合、5(2進数で 101)は ['a', 'c']、7 は3つの品物すべてです。ビットマスクDPは同じ考え方で、マスクを状態として使い、小さな入力での巡回セールスマン問題など、最大でおよそ20個の要素に対する問題に使います。
ほかにも2つの定番があります。x & 1 は奇数なら1です。0 から n までのすべての数と、リストのすべての値のXORを取ると、欠けている1つの数が見つかります。
時間計算量と空間計算量
| 演算 | 時間 | 備考 |
|---|---|---|
マシンのワードサイズの整数に対する &、|、^、~、シフト | O(1) | C、Java、Go、RustではCPU命令1つです。 |
Pythonの int に対する同じ演算 | O(ビット数) | Pythonの整数は大きさに制限がありません。小さな値なら、それでも非常に高速です。 |
| カーニハン法によるビットの数え上げ | O(立っているビットの数) | Python 3.10以降では int.bit_count() のほうが高速です。 |
| マスクですべての部分集合を列挙 | O(n × 2ⁿ) | 2ⁿ 個のマスクで、それぞれ n 回のビット判定を行います。 |
ビットマスクは要素1つにつき1ビットを使うので、64個のフラグも1つの64ビット整数に収まります。64個の別々の真偽値変数やリストの要素を使う場合と比べて、小さくて済みます。
よくある間違い
- 負の数:Pythonの整数は、符号ビットが無限に並んでいるかのように動作するので、
~5は-6で、bin(-5)は2の補数の文字列ではなく-0b101です。32ビットのパターンを見たいときは、値をマスクします:-5 & 0xFFFFFFFF。 - Pythonでの負の数の右シフトは算術シフトです。
-8 >> 1は-4で、-1 >> 10は-1のままです。 - 固定幅の言語でのオーバーフロー:JavaやCで32ビットintに
1 << 31を行うと、負の値または未定義の動作になります。64ビット型を使い、1L << 40や1ULL << 40と書いてください。 - 論理演算子とビット演算子の取り違え:
andとor(C系の言語では&&と||)は真偽値を返しますが、&と|はビットごとに動作します。 - 可読性:
x * 2をx << 1に置き換えても、現代のコンパイラでもPythonでも速くはならず、コードが読みにくくなるだけです。
実世界での用途
- Unixのファイルパーミッション:
chmod 754は3ビットのグループ3つを設定します(rwx = 4 + 2 + 1)。 - ネットワーク:IPv4のサブネットマスク。
ip & maskで、CIDRブロックのネットワークアドレスが得られます。 - フラグとオプション:Pythonの
re.IGNORECASE | re.MULTILINEやenum.Flagは、ORでオプションを組み合わせます。 - グラフィックスと色:24ビットのRGB値は、赤・緑・青を
(r << 16) | (g << 8) | bとして詰め込んだものです。 - データ構造とアルゴリズム:ブルームフィルタ、ビットセット、フェニック木(
i & -iを使います)、ハッシュ、チェックサム。 - コーディング面接の問題:単一の数、ビットの数え上げ、2のべき乗、ビットの反転、欠けている数、ビットマスクによる部分集合、
+を使わない2つの整数の和。
よくある質問
x & (x - 1) は何をしますか?
x の最下位の立っているビットをクリアします。立っているビットの数え上げや、正の x が2のべき乗かどうかの判定(結果が 0 なら真)に使われます。
Pythonで ~5 が -6 になるのはなぜですか?
整数は2の補数で表現され、~x は -x - 1 に等しくなります。Pythonはこれを大きさに制限のない整数に適用するので、5 のすべてのビットを反転すると -6 になります。
Pythonで数の1のビットを数えるにはどうすればよいですか?
Python 3.10以降では x.bit_count() を、それより古いバージョンでは bin(x).count("1") を使います。負の数では、bit_count() は絶対値のビットを数えます。
ビット演算は算術演算より速いですか?
固定長の整数では、ビット演算は1命令ですが、定数との加算や乗算もコンパイラの最適化後は同様です。本当の利点は、コンパクトな格納と空間が一定のアルゴリズムであり、通常の算術の置き換えではありません。