排他的論理和(XOR)とは?ビット演算の重要な仕組みを徹底解説

プログラミングや情報処理を学んでいると、排他的論理和(Exclusive OR)、通称XOR(エックスオア)という演算に出会います。

この演算は一見地味に見えますが、実は暗号化データの整合性チェック高速な値の交換など、様々な場面で活躍する超重要な演算なんです。「2つの値が異なる時に真(1)になる」というシンプルなルールから、驚くほど多彩な応用が生まれます。

例えば、変数の値を一時変数なしで交換する、データの破損を検出する、簡単な暗号を作る、といったテクニックは全てXORの性質を利用しています。

この記事では、排他的論理和の基本から、ビット演算としての使い方、実践的な応用例、そして知っておくと便利な性質まで、初心者の方にも分かりやすく解説していきます!


スポンサーリンク
  1. 排他的論理和(XOR)とは?基本概念
    1. 簡単に言うと
    2. 記号と表記
    3. 読み方
  2. 真理値表:XORの動作を完全理解
    1. 基本的な真理値表
    2. 言葉での説明
    3. 他の論理演算との比較
  3. プログラミングでのXOR演算
    1. 基本的な使い方
    2. ビット単位のXOR
    3. より複雑な例
  4. XORの重要な性質:数学的な特徴
    1. 1. 交換法則が成り立つ
    2. 2. 結合法則が成り立つ
    3. 3. 自分自身とのXORは0
    4. 4. 0とのXORは元の値
    5. 5. 同じ値で2回XORすると元に戻る
    6. 6. 1とのXORはビット反転
  5. 実践的な応用例:XORの活用テクニック
    1. 応用1:変数の値を交換(スワップ)
    2. 応用2:簡易暗号化
    3. 応用3:重複要素の検出
    4. 応用4:ビットの反転
    5. 応用5:パリティチェック
    6. 応用6:2つの数が同じか判定
    7. 応用7:配列内の単独要素を見つける
  6. XORの視覚的理解
    1. ビットパターンで見るXOR
    2. マスクパターンの応用
    3. ビットフラグの切り替え
  7. プログラミング言語別の実装
    1. C/C++
    2. Python
    3. Java
    4. JavaScript
    5. PHP
    6. SQL
  8. よくある間違いと注意点
    1. 間違い1:論理演算子と混同
    2. 間違い2:順序の誤解
    3. 間違い3:符号付き整数での問題
    4. 間違い4:浮動小数点数でのXOR
    5. 間違い5:暗号化での誤用
  9. XORの数学的性質:まとめ
    1. 基本性質
    2. 派生的な性質
    3. 群論的な解釈
  10. 高度な応用例
    1. 応用1:Gray Code(グレイコード)
    2. 応用2:チェックサム計算
    3. 応用3:ビットマスクの最適化
    4. 応用4:リンクリストの反転(XOR Linked List)
  11. まとめ:XORは奥深い演算

排他的論理和(XOR)とは?基本概念

排他的論理和(はいたてきろんりわ)、英語でExclusive OR、略してXORは、論理演算の一種です。

簡単に言うと

「2つの値が異なる時に真(1)、同じ時に偽(0)になる演算」

日常的な例:
「AさんまたはBさんのどちらか一方だけが来る」という状況を考えましょう。

  • A君だけ来た → 真(条件を満たす)
  • B君だけ来た → 真(条件を満たす)
  • 2人とも来た → 偽(両方はダメ)
  • 誰も来ない → 偽(誰もいない)

これが排他的論理和の考え方です。「どちらか一方だけ」がポイントなんですね。

記号と表記

数学的表記:

  • A ⊕ B(⊕マークが一般的)
  • A XOR B

プログラミング言語:

  • C/C++/Java/JavaScript等:A ^ B
  • Python:A ^ B
  • SQL:A XOR B

読み方

  • 日本語:エックスオア、エクスクルーシブオア、排他的論理和
  • 英語:XOR(エックスオア)、Exclusive OR(エクスクルーシブ・オア)

真理値表:XORの動作を完全理解

真理値表で、XORの動作を正確に把握しましょう。

基本的な真理値表

2つの入力A、Bに対するXOR:

ABA ⊕ B(結果)
000
011
101
110

ルール:

  • 異なる値同士 → 結果は1(真)
  • 同じ値同士 → 結果は0(偽)

言葉での説明

0 ⊕ 0 = 0

  • 両方とも0(偽)→ 結果は0

0 ⊕ 1 = 1

  • 異なる値 → 結果は1

1 ⊕ 0 = 1

  • 異なる値 → 結果は1

1 ⊕ 1 = 0

  • 両方とも1(真)→ 結果は0

他の論理演算との比較

理解を深めるため、他の論理演算と比較してみましょう。

OR(論理和)との比較:

ABA OR BA XOR B
0000
0111
1011
1110

違い:

  • OR:「どちらか一方、または両方」→ 1,1の時も1
  • XOR:「どちらか一方だけ」→ 1,1の時は0

AND(論理積)との比較:

ABA AND BA XOR B
0000
0101
1001
1110

違い:

  • AND:「両方とも真」の時だけ1
  • XOR:「どちらか一方だけ真」の時に1

プログラミングでのXOR演算

実際にコードで使ってみましょう。

基本的な使い方

C/C++/Java/JavaScript:

int a = 5;   // 0101 (2進数)
int b = 3;   // 0011 (2進数)
int c = a ^ b;  // 0110 = 6

printf("5 ^ 3 = %d\n", c);  // 出力: 6

Python:

a = 5   # 0b0101
b = 3   # 0b0011
c = a ^ b  # 0b0110 = 6

print(f"5 ^ 3 = {c}")  # 出力: 6

論理値(bool)でのXOR:

# Pythonの例
true_val = True
false_val = False

print(true_val ^ false_val)   # True (異なる)
print(true_val ^ true_val)    # False (同じ)
print(false_val ^ false_val)  # False (同じ)

ビット単位のXOR

数値のXOR演算は、各ビットごとにXORを適用します。

例:5 XOR 3

    5 = 0101 (2進数)
    3 = 0011 (2進数)
  -------------
5^3 = 0110 (2進数) = 6 (10進数)

ビットごとに見ると:

  • 右から1ビット目:1 ⊕ 1 = 0
  • 右から2ビット目:0 ⊕ 1 = 1
  • 右から3ビット目:1 ⊕ 0 = 1
  • 右から4ビット目:0 ⊕ 0 = 0

結果:0110 = 6

より複雑な例

// C言語の例
unsigned char data = 0xAB;  // 10101011
unsigned char mask = 0x0F;  // 00001111

unsigned char result = data ^ mask;
// 10101011
// 00001111
// --------
// 10100100 = 0xA4

printf("0xAB ^ 0x0F = 0x%02X\n", result);  // 0xA4

XORの重要な性質:数学的な特徴

XORには非常に興味深い性質があります。これらを理解すると、応用範囲が広がります。

1. 交換法則が成り立つ

順序を入れ替えても結果は同じ:

A ⊕ B = B ⊕ A

例:

5 ^ 3 = 6
3 ^ 5 = 6  // 同じ結果

2. 結合法則が成り立つ

複数のXORは、どの順番でまとめても同じ:

(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)

例:

(5 ^ 3) ^ 7 = 5 ^ (3 ^ 7)
   6 ^ 7    =  5 ^ 4
     1      =    1  // 同じ結果

3. 自分自身とのXORは0

同じ値同士のXORは必ず0:

A ⊕ A = 0

例:

5 ^ 5 = 0
123 ^ 123 = 0

ビットレベルで見ると:

  0101
⊕ 0101
------
  0000  // すべてのビットが同じなので、全て0

この性質は超重要!
暗号化、データ検証、値の交換など、多くの応用の基礎になります。

4. 0とのXORは元の値

0とXORすると値が変わらない:

A ⊕ 0 = A

例:

5 ^ 0 = 5

ビットレベル:

  0101
⊕ 0000
------
  0101  // 0とXORしてもビットは変わらない

これも重要な性質!
初期化や、XORマスクの基本になります。

5. 同じ値で2回XORすると元に戻る

最も美しい性質:

(A ⊕ B) ⊕ B = A

証明:

(A ⊕ B) ⊕ B 
= A ⊕ (B ⊕ B)  // 結合法則
= A ⊕ 0         // B ⊕ B = 0
= A             // A ⊕ 0 = A

具体例:

int original = 42;
int key = 17;

int encrypted = original ^ key;  // 暗号化
int decrypted = encrypted ^ key; // 復号化

// decrypted == original (元に戻る!)

これが暗号化の基礎です!

6. 1とのXORはビット反転

1とXORすると、そのビットが反転:

0 ⊕ 1 = 1  // 0が1に反転
1 ⊕ 1 = 0  // 1が0に反転

例:

unsigned char data = 0x0F;  // 00001111
unsigned char mask = 0xFF;  // 11111111

data ^ mask = 0xF0;  // 11110000 (全ビット反転)

実践的な応用例:XORの活用テクニック

XORの性質を活かした実用的なテクニックを紹介します。

応用1:変数の値を交換(スワップ)

通常の方法(一時変数使用):

int a = 5;
int b = 3;

int temp = a;  // 一時変数が必要
a = b;
b = temp;

XORを使った方法(一時変数不要):

int a = 5;
int b = 3;

a = a ^ b;  // a = 5 ^ 3 = 6
b = a ^ b;  // b = 6 ^ 3 = 5 (元のaの値)
a = a ^ b;  // a = 6 ^ 5 = 3 (元のbの値)

// 結果: a = 3, b = 5 (交換完了!)

動作原理:

最初: a = 5, b = 3

ステップ1: a = a ^ b = 5 ^ 3 = 6
           a = 6, b = 3

ステップ2: b = a ^ b = 6 ^ 3 = 5 (元のa)
           a = 6, b = 5

ステップ3: a = a ^ b = 6 ^ 5 = 3 (元のb)
           a = 3, b = 5

注意点:

  • 同じメモリ位置を指している場合は使えない
  • 可読性が低いので、通常は一時変数を使う方が良い
  • コンパイラの最適化で速度差はほぼない

応用2:簡易暗号化

XORを使った単純な暗号化:

#include <stdio.h>
#include <string.h>

void xor_encrypt_decrypt(char *data, char *key, size_t data_len) {
    size_t key_len = strlen(key);

    for (size_t i = 0; i < data_len; i++) {
        data[i] = data[i] ^ key[i % key_len];  // 鍵を繰り返し使用
    }
}

int main() {
    char message[] = "Hello World";
    char key[] = "SECRET";

    printf("Original: %s\n", message);

    // 暗号化
    xor_encrypt_decrypt(message, key, strlen(message));
    printf("Encrypted: %s\n", message);  // 文字化けした文字列

    // 復号化(同じ操作をもう一度)
    xor_encrypt_decrypt(message, key, strlen(message));
    printf("Decrypted: %s\n", message);  // 元に戻る!

    return 0;
}

出力例:

Original: Hello World
Encrypted: ??\?▒▒?????
Decrypted: Hello World

重要:
これは教育目的の例です。実際のセキュリティ用途では、AESなどの強力な暗号化アルゴリズムを使ってください。

応用3:重複要素の検出

配列内で1つだけ異なる要素を見つける:

問題:
配列に1から100までの数字があり、1つだけ欠けています。どの数字が欠けているでしょうか?

XORを使った解法:

int find_missing_number(int arr[], int n) {
    int xor_all = 0;
    int xor_arr = 0;

    // 1からn+1までの全数値をXOR
    for (int i = 1; i <= n + 1; i++) {
        xor_all ^= i;
    }

    // 配列の全要素をXOR
    for (int i = 0; i < n; i++) {
        xor_arr ^= arr[i];
    }

    // 欠けている数字 = xor_all ^ xor_arr
    return xor_all ^ xor_arr;
}

// 使用例
int arr[] = {1, 2, 3, 5, 6, 7, 8, 9, 10};  // 4が欠けている
int missing = find_missing_number(arr, 9);
printf("Missing number: %d\n", missing);  // 4

原理:

  • 全ての数字をXORすると、重複する数字は消える(A ⊕ A = 0)
  • 残るのは欠けている数字だけ

応用4:ビットの反転

特定のビットだけを反転:

unsigned char data = 0b10101010;  // 170

// 下位4ビットを反転
unsigned char mask = 0b00001111;
data = data ^ mask;
// 結果: 0b10100101 (下位4ビットが反転)

printf("Result: 0x%02X\n", data);  // 0xA5

用途:

  • フラグの切り替え
  • ビットマスク操作
  • グラフィックス処理

応用5:パリティチェック

データの整合性確認:

// 8ビットデータのパリティビット計算
int calculate_parity(unsigned char data) {
    int parity = 0;

    for (int i = 0; i < 8; i++) {
        parity ^= (data >> i) & 1;  // 各ビットをXOR
    }

    return parity;  // 1ビットの数が奇数なら1、偶数なら0
}

// 使用例
unsigned char data = 0b10110101;  // 1が5個(奇数)
int parity = calculate_parity(data);
printf("Parity: %d\n", parity);  // 1

用途:

  • データ転送のエラー検出
  • RAIDシステムのパリティ計算
  • 通信プロトコル

応用6:2つの数が同じか判定

0判定による効率的な比較:

int are_equal(int a, int b) {
    return (a ^ b) == 0;  // 同じなら0、異なるなら0以外
}

// 使用例
printf("%d\n", are_equal(5, 5));   // 1 (true)
printf("%d\n", are_equal(5, 3));   // 0 (false)

用途:

  • 高速な比較処理
  • ハッシュ関数の実装
  • データ構造の最適化

応用7:配列内の単独要素を見つける

問題:
配列内のすべての要素は2回現れますが、1つだけ1回だけ現れます。それを見つけてください。

int find_single_number(int arr[], int n) {
    int result = 0;

    for (int i = 0; i < n; i++) {
        result ^= arr[i];  // すべてをXOR
    }

    return result;  // ペアは相殺され、単独要素だけが残る
}

// 使用例
int arr[] = {2, 3, 5, 4, 5, 3, 4};  // 2だけが単独
int single = find_single_number(arr, 7);
printf("Single number: %d\n", single);  // 2

原理:

  • A ⊕ A = 0(ペアは消える)
  • 0 ⊕ B = B(残った値)

XORの視覚的理解

ビット演算としてのXORを視覚的に理解しましょう。

ビットパターンで見るXOR

例1:単純な8ビット

データ1:  11001100  (204)
データ2:  10101010  (170)
        ----------
XOR結果:  01100110  (102)

説明:
位置1: 1⊕1=0
位置2: 1⊕0=1
位置3: 0⊕1=1
位置4: 0⊕0=0
...

マスクパターンの応用

特定ビットの操作:

unsigned char data = 0b11110000;  // 240

// 下位4ビットを反転
data ^= 0b00001111;
// 結果: 0b11111111 (255)

// 上位4ビットを反転
data ^= 0b11110000;
// 結果: 0b00001111 (15)

// 全ビットを反転
data ^= 0b11111111;
// 結果: 0b11110000 (240) - 元に戻る

ビットフラグの切り替え

フラグの管理:

#define FLAG_READ    0b00000001  // ビット0
#define FLAG_WRITE   0b00000010  // ビット1
#define FLAG_EXECUTE 0b00000100  // ビット2

unsigned char permissions = 0b00000101;  // 読み取り+実行

// 書き込み権限を切り替え(トグル)
permissions ^= FLAG_WRITE;
// 結果: 0b00000111 (読み取り+書き込み+実行)

// もう一度切り替え
permissions ^= FLAG_WRITE;
// 結果: 0b00000101 (読み取り+実行) - 元に戻る

プログラミング言語別の実装

各言語でのXORの使い方をまとめます。

C/C++

// 整数のXOR
int a = 5;
int b = 3;
int result = a ^ b;  // 6

// ビット反転にも使える
unsigned char data = 0xAA;
data ^= 0xFF;  // 全ビット反転 -> 0x55

// 複合代入演算子
int x = 10;
x ^= 5;  // x = x ^ 5

Python

# 整数のXOR
a = 5
b = 3
result = a ^ b  # 6

# ブール値のXOR
print(True ^ False)   # True
print(True ^ True)    # False

# 大きな数でも可能
big1 = 123456789
big2 = 987654321
print(big1 ^ big2)    # 1040208568

# 複合代入
x = 10
x ^= 5  # x = x ^ 5

Java

// 整数のXOR
int a = 5;
int b = 3;
int result = a ^ b;  // 6

// ブール値のXOR
boolean bool1 = true;
boolean bool2 = false;
boolean boolResult = bool1 ^ bool2;  // true

// ビット演算
byte data = (byte)0xAA;
data ^= 0xFF;  // 全ビット反転

JavaScript

// 整数のXOR
let a = 5;
let b = 3;
let result = a ^ b;  // 6

// 注意: JavaScriptは32ビット整数として扱う
console.log(0xFFFFFFFF ^ 0x00000000);  // -1

// 複合代入
let x = 10;
x ^= 5;  // x = x ^ 5

PHP

// 整数のXOR
$a = 5;
$b = 3;
$result = $a ^ $b;  // 6

// 文字列のXOR(文字単位)
$str1 = "Hello";
$str2 = "World";
$xor_str = $str1 ^ $str2;  // バイナリ文字列

// 複合代入
$x = 10;
$x ^= 5;

SQL

-- MySQLの例
SELECT 5 XOR 3;  -- 結果: 1(真)
SELECT 5 XOR 5;  -- 結果: 0(偽)

-- 論理値での使用
SELECT (1 < 2) XOR (3 < 4);  -- 結果: 0(両方真なので偽)
SELECT (1 > 2) XOR (3 < 4);  -- 結果: 1(一方だけ真なので真)

よくある間違いと注意点

XORを使う際の落とし穴を押さえておきましょう。

間違い1:論理演算子と混同

誤り:

if (a ^ b) {  // ビット演算
    // これは a ^ b の結果が0以外の時
}

正しい(論理XORを意図する場合):

if ((a && !b) || (!a && b)) {  // 論理XOR
    // aとbの一方だけが真の時
}

// または
if (!a != !b) {  // より簡潔
    // 真偽値が異なる時
}

間違い2:順序の誤解

ビット単位の演算順序:

int result = a ^ b & c;  // これは a ^ (b & c)
int correct = (a ^ b) & c;  // 意図した順序

演算子の優先順位:

  1. &(AND)
  2. ^(XOR)
  3. |(OR)

推奨:
括弧を使って明示的に順序を指定する。

間違い3:符号付き整数での問題

負の数のXOR:

int a = -5;
int b = 3;
int result = a ^ b;  // -8

// ビットパターン(32ビット環境)
// a = 11111111111111111111111111111011 (2の補数)
// b = 00000000000000000000000000000011
//   = 11111111111111111111111111111000 = -8

注意点:
符号付き整数のビット演算では、2の補数表現を理解する必要があります。

間違い4:浮動小数点数でのXOR

誤り:

double a = 5.5;
double b = 3.3;
// double c = a ^ b;  // コンパイルエラー!

理由:
XORはビット演算なので、浮動小数点数には直接使えません。

解決策(バイト列として扱う):

double a = 5.5;
double b = 3.3;

unsigned char *pa = (unsigned char *)&a;
unsigned char *pb = (unsigned char *)&b;

for (int i = 0; i < sizeof(double); i++) {
    pa[i] ^= pb[i];
}

間違い5:暗号化での誤用

危険な例:

// これだけでは安全な暗号化にならない
void weak_encrypt(char *data, char key) {
    while (*data) {
        *data ^= key;  // 同じ鍵を繰り返し使用
        data++;
    }
}

問題点:

  • 鍵が短すぎる
  • 統計的な攻撃に弱い
  • 既知平文攻撃に脆弱

正しいアプローチ:
AES、ChaCha20などの検証済み暗号アルゴリズムを使用する。


XORの数学的性質:まとめ

重要な性質を一覧にします。

基本性質

1. 恒等元(Identity):

A ⊕ 0 = A

2. 自己逆元(Self-inverse):

A ⊕ A = 0

3. 交換法則(Commutative):

A ⊕ B = B ⊕ A

4. 結合法則(Associative):

(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)

5. 逆元(Inverse):

A ⊕ B ⊕ B = A
(A ⊕ B) ⊕ A = B

派生的な性質

6. 二重適用で元に戻る:

(A ⊕ B) ⊕ B = A

7. 連鎖的なXOR:

A ⊕ B ⊕ C ⊕ ... ⊕ A ⊕ B ⊕ C ⊕ ... = 0
(各要素が偶数回現れる場合)

8. 全ビット反転:

A ⊕ 1...1 = ~A(NOT A)

群論的な解釈

XORは加法群を形成します:

  • 閉包性:A ⊕ B の結果も同じ集合の要素
  • 結合法則:(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)
  • 単位元:0
  • 逆元:各要素自身(A ⊕ A = 0)
  • 可換性:A ⊕ B = B ⊕ A

これがアーベル群(可換群)です。


高度な応用例

さらに進んだ使い方を紹介します。

応用1:Gray Code(グレイコード)

グレイコードの生成:

// n番目のグレイコード
unsigned int binary_to_gray(unsigned int n) {
    return n ^ (n >> 1);
}

// グレイコードから通常の2進数へ
unsigned int gray_to_binary(unsigned int gray) {
    unsigned int binary = 0;

    while (gray) {
        binary ^= gray;
        gray >>= 1;
    }

    return binary;
}

// 使用例
for (int i = 0; i < 16; i++) {
    printf("%2d: Binary=%04b, Gray=%04b\n", 
           i, i, binary_to_gray(i));
}

グレイコードの特徴:

  • 隣接する値が1ビットだけ異なる
  • エンコーダやセンサーで使用
  • エラー耐性が高い

応用2:チェックサム計算

XORを使った簡単なチェックサム:

unsigned char calculate_xor_checksum(unsigned char *data, size_t len) {
    unsigned char checksum = 0;

    for (size_t i = 0; i < len; i++) {
        checksum ^= data[i];
    }

    return checksum;
}

// 使用例
unsigned char data[] = {0x12, 0x34, 0x56, 0x78};
unsigned char checksum = calculate_xor_checksum(data, 4);
printf("Checksum: 0x%02X\n", checksum);

// 検証
data[4] = checksum;  // チェックサムを追加
unsigned char verify = calculate_xor_checksum(data, 5);
printf("Verification: 0x%02X\n", verify);  // 0なら正常

応用3:ビットマスクの最適化

複数のフラグを効率的に管理:

#define PERM_NONE    0x00
#define PERM_READ    0x01
#define PERM_WRITE   0x02
#define PERM_EXEC    0x04
#define PERM_ALL     0x07

// フラグの切り替え
void toggle_permission(unsigned char *perms, unsigned char flag) {
    *perms ^= flag;
}

// 使用例
unsigned char user_perms = PERM_READ | PERM_EXEC;  // 読み取り+実行

toggle_permission(&user_perms, PERM_WRITE);  // 書き込みを追加
toggle_permission(&user_perms, PERM_WRITE);  // 書き込みを削除(トグル)

応用4:リンクリストの反転(XOR Linked List)

メモリ効率的な双方向リスト:

struct XORNode {
    int data;
    struct XORNode *npx;  // 前のノードと次のノードのXOR
};

// XOR演算子でポインタを計算
struct XORNode *XOR(struct XORNode *a, struct XORNode *b) {
    return (struct XORNode *)((uintptr_t)a ^ (uintptr_t)b);
}

// 挿入、削除、走査などの操作が可能
// 通常の双方向リストより省メモリ

利点:

  • ポインタを1つしか持たない
  • メモリ使用量が少ない

欠点:

  • 可読性が低い
  • ポインタ演算が複雑
  • デバッグが困難

まとめ:XORは奥深い演算

排他的論理和(XOR)は、シンプルながら非常に強力な演算です。

この記事のポイント:

  • XORは「異なる時に真」という単純なルール
  • A ⊕ A = 0という性質が最重要
  • 同じ値で2回XORすると元に戻る
  • 変数の交換が一時変数なしでできる
  • 簡易暗号化の基礎として使える
  • パリティチェックでエラー検出
  • ビット操作の基本テクニック
  • 数学的に美しい性質(アーベル群)
  • 様々な分野で応用される
  • プログラミング言語で^演算子として使用

XORは、低レベルプログラミング、暗号化、データ圧縮、エラー検出、最適化など、様々な場面で活躍します。

最初は「異なる時に1」という単純なルールですが、その性質を理解すると、驚くほど多彩な応用が可能になるんです。

プログラミングスキルを向上させたい方は、ぜひXORの性質を使いこなせるようになってください。きっとコードがより効率的で、エレガントになりますよ!

コメント

タイトルとURLをコピーしました