# 位元運算 ## 進位制 人類使用進位制以有限數字表達無限大小,常見的進位制有:十進位、二進位、八進位、十六進位等,我們平常使用的就是十進位制(因為人類有十根手指)。 關於進位制的計算方式,會是以當前位數(以0為初始,從右往左數)作為上標,再將每位相加。我以十進位舉例如下: ``` 314 = 3 x 10^2 + 1 x 10^1 + 4 x 10^0 ``` 以同樣的方式我們可以計算其他進位制的大小,我給幾個數字,你可以先試看看: ``` 0b1011 = 11 // ob開頭表示二進位(binary) 0o125 = 85// 0o開頭表示八進位(octal) // 十六進位數以數字0~9和英文字母a(10)~f(15)組成 oxA7 = 167 // 0x開頭表示十六進位(hexadecimal) ``` 在計算機中,我們使用二進位表示一切,包括圖片,原因是二進位最好實現,可以簡單的用1和0分別表示電路通電和不通電(高電壓和低電壓)。 ## 位元運算 計算機中的每一位二進位數稱作**位元(bit)**,而為了方便管理,將每八位元定義為**位元組(byte)**。 位元運算就是將數字逐位元的計算,不常用,但在某些地方會有奇效。 | 位元運算子 | 名稱 | | ----- | -- | | & | AND | | \ | OR | | ^ | XOR | | ~ | 補數 | | << | 左移 | | >> | 右移 | >[!Note]使用位元運算時請注意運算子的優先順序! ### & 和 | | & | a為0 | a為1 | | --- | ---- | ----- | | b為0 | 0 | 0 | | b為1 | 0 | 1 | | \| | a為0 | a為1 | | -- | ---- | ----- | | b為0 | 0 | 1 | | b為1 | 1 | 1 | 可以發現它們的名字和邏輯運算子相同,兩者確實有一定的相似性,但不是同種東西。 因為我們會將數字轉成二進位來看,因此只會有0和1的比較。當進行AND運算時,除非兩者皆為1,否則為0 ; 進行OR運算時,除非兩者皆為0,否則為1。 ```cpp= // << 和 >>的優先級大於&和|,因此需要括號防止邏輯錯誤 cout << (5 & 9) << "\n"; // 5 & 9 => 0101 & 1001 => 1 cout << (7 | 11) << "\n"; // 7 | 11 => 0111 | 1011 => 1111 => 15 ``` output ``` 1 15 ``` 在除數是2的冪次時,&可以代替%進行更有效率的取餘數,像是判斷某數是否為偶數時一般是寫```a % 2 == 0```,但可以用AND寫成```(a & 1) == 0```。 ```cpp= int num = 5; if ((num & 1) == 0) // 0101 & 0001 -> 0001 = 1 cout << num << "是偶數" << "\n"; else cout << num << "是奇數" << "\n"; ``` output ``` 5是奇數 ``` ### ^ XOR有一種很特殊的轉換方式,因此常用於訊息加密,規則如下 | ^ | a為0 | a為1 | | --- | ---- | ----- | | b為0 | 0 | 1 | | b為1 | 1 | 0 | 你可以理解為 : 當兩數相異且不為零時等於1,否則為0。 從這規則可以推導出XOR的3大特性 : 1. 相同數字經過 XOR 後會等於0 ``` a ^ a => 0 ``` 2. 任何數和0 XOR 後會等於自己 ``` a ^ 0 => a ``` 3. XOR運算具有交換律 ``` a ^ 0 => 0 ^ a ``` ---- 根據上述特性,我們主要可對XOR進行兩種應用 #### 1. 進階兩數交換 當我們想將兩個變數內的值進行互換時,最直覺的方法是使用第三個變數儲存,這在之前就說明過了,雖然簡單,但有一個缺點,為了暫存值我們被迫創建了一個不怎麼會使用的變數,從而消耗了更多的記憶體,而這是可避免的,方法是使用XOR來代替。 ```cpp= int a = 3, b = 5; a ^= b; // a = 011(a) ^ 101(b) => 0110 => 6 , b = 5 b ^= a; // b = 101(b) ^ 110(a) => 0011 => 3 , a = 6 a ^= b; // a = 110(a) ^ 011(b) => 0101 => 5 , b = 3 cout << "a = " << a << "\n"; cout << "b = " << b << "\n"; ``` 如果要追求簡潔,你甚至可以寫成 ```cpp= int a = 3, b = 5; a ^= b ^= a ^= b; cout << "a = " << a << "\n"; cout << "b = " << b << "\n"; ``` output ``` a = 5 b = 3 ``` 上述程式有一個小bug,就是當a和b相等時,使用XOR交換會導致兩者的值皆變為零,因此請加上一個a和b不相等的判斷: ```cpp= void swap(int &a, int &b) { if (a != b) { a ^= b ^= a ^= b; // 這樣就完美啦! } } ``` #### 2. Single Number 這是一種**演算法**問題,有很多種解法,而使用XOR是最優解的其中之一。 問題描述 : 給定一個由整數組成的列表,陣列中大部分的數皆兩兩成對,唯有一個例外,請找出該陣列中只出現過一次的數值。 ``` example : [1, 2, 2, 4, 3, 1, 3] => single number = 4 ``` 想想看剛才提到的XOR三大特性,該怎麼運用他們解決 ? <details> <summary>Answer Code</summary> ```cpp= int nums = {1, 2, 2, 4, 3, 1, 3}; int singleNum = 0; for (const int& num : nums) singleNum ^= num; cout << singleNum << "\n"; ``` </details> 讓我們看看為什麼可以這樣寫 : 首先,根據XOR的交換律,```[1, 2, 2, 4, 3, 1, 3] => [1, 1, 2, 2, 3, 3, 4]``` ; 再來,把這些數字都進行XOR,根據**相同數經過XOR會得到0**和**任何數經過XOR都會得到自己**的特性,可以寫成下列樣式 ``` 1 ^ 1 => 0 2 ^ 2 => 0 3 ^ 3 => 0 0 ^ 4 => 4 ``` 因此,我們成功的找出了single number。 題目連結 - [136. Single Number](https://leetcode.com/problems/single-number/description/) ##### AC Code (0ms, 20.81MB) <details> <summary>Code</summary> ```cpp= class Solution { public: int singleNumber(vector<int>& nums) { int ans = 0; for(const int &num : nums){ ans ^= num; } return ans; } }; ``` </details> ### >> 和 << 就只是乘除2的冪次而已,稍快於*和/ ```cpp= int num = 10; cout << num << " << 2 = " << (num << 2) << "\n"; // 10 => 01010 => 00010 => 2 cout << num << " >> 2 = " << (num >> 2) << "\n"; // 10 => 0001010 => 0101000 => 4 ``` output ``` 10 << 2 = 40 10 >> 2 = 2 ``` 將數字轉成二進位來看更明顯,左移是將每位元往左移動,跑出去的零丟到後面,也就是乘2 ; 右移則是將每位元往右移動,比較特別,整除時是除以2,但不整除時會有一種叫位元遮罩的東西,不管是0 和 1都會捨棄。 ## ~ 補數運算的規則很簡單,0變成1 ; 1變成0。 | 補數 | a為0 | a為1 | | --- | ---- | ----- | | 結果 | 1 | 0 | 值得一提的是,它負責處理電腦二進位的減法邏輯,是負數的轉換方法,稱為「**二補數**」。 以int型態的11舉例,其二進位為 ``` 00000000 00000000 00000000 00001011 ``` 而-11的二進位數字就是將11進行補數運算後的結果 ``` 11111111 11111111 11111111 11110100 ``` ## 總結 計算機以二進位表示一切,而二進位數字間有獨特的運算方式,稱作**位元運算**。 位元運算共有六種,分別為```&、|、^、~、<<、>>```,其中```&```用來進行二冪次的取餘數,```^```用來進行兩數交換,```~```可以轉換二進位制下的負數,```|```則是幾乎不會使用到。