前言
有一個整數,想知道它的二進位制表示中有多個1,你會怎麼做?本文將帶大家深入學習下二進位制以及它的各種運算,一步步的研究出這個問題的解決方案,歡迎各位感興趣的開發者閱讀本文。
前置知識
在解決這個問題之前,我們需要先了解下什麼是二進位制。
二進位制
在計算機的世界裡,只有0和1,也就是二進位制。
符號數
在二進位制中,數被分為有符號數和無符號數。
對於有符號數而言,符號的正、負機器是無法識別的,但由於“正、負”恰好是兩種截然不同的狀態,如果用“0”表示正,用“1”表示“負”,這樣符號也被數字化了,並且規定將它放在有效數字的前面,即組成了有符號數。
因此,在二進位制中使用最高位來表示符號。
- 最高位是0,表示正數。
- 最高位是1,表示負數。
二進位制的最高位就是其第一位,例如:10000001100,它的最高位就是1。
對於無符號數而言,它表示的數其範圍都是正數,所有位都用於表示數的大小。
有符號數的性質
對於有符號數而言,它有6個性質:
- 二進位制的最高位是符號位:0表示正數,1表示負數
- 正數的原碼、反碼、補碼都一樣
- 負數的反碼 = 它的原碼符號位不變,其它位取反(0 -> 1; 1 -> 0)
- 0的反碼、補碼都是0
- 負數的補碼 = 它的反碼 + 1
- 在計算機運算的時候,都是以補碼的方式來運算的
原碼、反碼、補碼
上述性質中,我們提到了原碼、反碼、補碼,接下來我們來學習下他們究竟是什麼樣的數字。
原碼,分為兩種情況:
- 一個正數,按照絕對值大小轉換成的二進位制數
- 一個負數,按照絕對值大小轉換成的二進位制數,然後最高位補1
反碼,也分為兩種情況:
- 一個正數,它的反碼與它的原碼是相同的
- 一個負數,它的反碼為該數的原碼除符號位外,各位取反
補碼,也分為兩種情況:
- 一個正數,它的補碼與它的原碼也是相同的
- 一個負數,它的補碼為對該數的原碼除符號位外各自取反後,在最後一位加1
進位制轉換
我們要對二進位制進行運算,需要先將十進位制數轉為二進位制,因此我們需要先學習下十進位制轉二進位制的方法。
十進位制轉二進位制
將十進位制轉為二進位制主要分為三種情況:
- 正整數轉二進位制
計算規則為:除二取餘(直至商為0),然後倒序排列,高位補零。
知道規則後,我們舉個例子,求一下80所對應的二進位制數,如下圖所示:
計算機內部表示數的位元組單位是定長的(字長),如:8、16、32、64位。因此當計算出來的二進位制的位數不夠時,需要在高位進行補0。
上述例子中,我們將80轉為二進位制數後,它的值為:1010000,字長為7,如果計算機的字長是64位,那麼標準寫法就是在它的最高位前面補57個0,我們用計算器來驗證下,如下所示:
- 負整數轉二進位制
在計算機中,負數是以原碼的補碼形式進行表達的,通過前面的學習,我們知道了想求負數的補碼,就得先求出它的原碼。
我們以-80
為例來計算下它的二進位制碼,步驟如所示:
- 求原碼,如下圖所示:
- 求補碼,如下圖所示:
至此,我們得到了-80
的二進位制碼:10110000
在正整數轉二進位制部分,我們講了計算機是固定字長的,當計算出來的二進位制位數不夠時,正整數會在高位補0,負整數則會在高位補1。
我們用計算器來驗證下我們計算出來的-80的二進位制碼是否正確,如下所示:
- 小數轉二進位制
在二進位制中,小數被稱為浮點數,我們在將十進位制小數轉換為二進位制小數時,需要以小數點為界限,將其拆分為整數部分和小數部分。
整數部分轉為二進位制,我們在前面已經講過了(即除2取餘)
小數部分轉為二進位制的方法為:乘2取整數部分,繼續用小數部分乘2,直至小數部分為0(大多數情況下不會為0,需要確立精度)
我們以80.13
為例來計算下它的二進位制碼,如下圖所示:
計算機中用二進位制來表示小數時,大部分十進位制小數都不能精確的用二進位制來表示,當表示這種小數時,最大精確多少位,取決於計算機的字長。
上圖中,我們計算出了
80.13
的二進位制碼為01010000.00100
,我們精確到了小數點後5位。我們用計算器來驗證下是否正確。
二進位制轉十進位制
同樣的,二進位制轉十進位制也分為三種情況:
- 正整數轉十進位制
從二進位制的最低位開始,給每一位標上序號,取出不為0位置的數的序號,將其作為2的次方進行計算,最後將結果相加。
我們以01010000
為例,求一下它的十進位制數,如下圖所示:
- 負整數轉十進位制
前面我們學習了十進位制負整數轉二進位制的方法,那麼二進位制轉十進位制,則需要倒著來算,我們以10110000
為例,步驟如下所示:
- 根據補碼求原碼
- 除去符號位,對其他位按照正整數轉二進位制的規則進行計算,最後補上負號,如下圖所示:
- 小數轉十進位制
給小數點後每一位標上負序號(從-1開始),取出不為0位置的數的序號,將其作為2的次方進行計算,最後將結果相加。
我們以01010000.00100
為例,求出它的十進位制數,如下圖所示:
經過前面的學習,我們知道了十進位制小數轉二進位制時,大多數情況是無法得到精確值的,我們用80.13舉例時,精確到了小數點後5位。
同樣的,我們將二進位制小數轉換為十進位制數時,也是無法得到準確值的,最終值也取決於精度,此處我們保留2位小數,四捨五入後就為
80.13
。
有符號數的運算
瞭解完前置知識,接下來我們舉幾個例子來看下有符號數是如何進行運算的。
左移運算子
<<
稱為左移運算子,它的運算規則為:
- 移除二進位制數最高位的0
- 在二進位制數的末尾補上一個0
我們以01010000
為例,假設他的字長為32位(多餘的0省略),對其左移一位,它的運算過程如下圖所示:
左移1位後,我們計算出來的結果為:010100000
,轉換成十進位制後結果為2^5 + 2^7 = 32 + 128 = 160
。
01010000
的十進位制數為80
,左移動1位後,值變為了160
,經過觀察後,我們發現左移後的值正好是原值的2倍,等價於乘2操作。
大多數情況下我們可以將其當作乘2使用,但在一些特殊情況下它並不代表真正的乘2,例如,我們需要將其左移25位,如下圖所示(我們把省略的0補上):
左移25位後,我們發現它左側的0已被刪完,最高位變成了1,這個數也從原來的正數,變為了負數。如果我們左移26位,左側的0不夠,會開始從右側取值,最終的值又變成了正數。
因此,我們會發現這樣一條規律:
- 當左移的位數,超過剩餘字長時,它的值並非乘2
注意:如果任意一個二進位制數,左移的位數大於等於字長(例如字長為32,我們左移32位,右邊補32個0),那麼與之對應的十進位制數豈不是都為0了?
當然不是,二進位制數進行左移時,當移動的位數大於等於它的字長時,位數會先求餘數,然後再進行左移。
例如:我們需要對一個字長為32位的二進位制數進行左移32位,那麼就需要先求它的餘數,即:
32 % 32 = 0
,需要左移0位。左移33位的值和左移1位是一樣的。
右移運算子
>>
稱為右移運算子,它的運算規則分為正數與負數兩種情況。
正數:
移除最低位的數,在最高位補0。
我們以01010000
為例(十進位制值為80),對其右移4位,計算過程如下圖所示:
計算出來的二進位制結果為
00101
,將其轉位十進位制,結果為:2^0 + 2^2 = 1 + 4 = 5
,即:80 >> 4 = 5
我們用計算器來驗證下:
負數:
經過前面的學習,我們知道了負數是以原碼的補碼形式儲存的,它的右移規則為:
- 對補碼進行右移,在最高位補1
- 求出其反碼
- 對反碼+1,求出原碼
我們以10110000
為例(十進位制為-80
),將其右移4位,計算過程如下所示:
計算出來的二進位制結果為
11100
,我們將其專為十進位制:2^0 + 2^2 = 1 + 4 = 5
,補齊符號位,結果為:-5
。即:-80 >> 4 = -1
。我們用計算器來驗證下,如下圖所示:
與運算子
&
稱為與運算子,它的運算規則為:
- 符號左右兩側的數同時為1,結果就為1,否則為0
即:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1
接下來,看個十進位制的例子,來看下它的運算過程,如下所示:
15 & 13
,它的運算步驟為:
- 將十進位制轉為二進位制
- 對二進位制進行與運算
運算過程如下圖所示:
或運算子
|
稱為或運算子,它的運算規則為:
- 符號左右兩側的數,有一個為1,其值就為1
即:0 | 1 = 1; 1 | 0 = 1; 0 | 0 = 0; 1 | 1 = 1
我們繼續以前個章節的數字為例,來看下15 | 13
的運算步驟:
- 將十進位制轉二進位制
- 對二進位制進行或運算
運算過程如下圖所示:
異或運算子
^
稱為異或運算子,它的運算規則為:
- 符號左右兩側的數,值不同,則該位結果為1,否則為0
即:0^0=0; 0^1=1; 1^0=1; 1^1=0
我們繼續以15和13為例,看下15^13
的運算步驟:
- 將十進位制轉二進位制
- 對二進位制進行異或運算
運算過程如下圖所示:
問題求解
有了上述知識做鋪墊後,接下來我們進入正題:有一個十進位制整數,求它的二進位制數中1的個數。
分析
在解決這個問題之前,我們先來分析這樣一個場景:
如果一個整數不等於0,那麼該整數的二進位制表示中至少有一位是1。
先假設這個數的最右邊一位是1,那麼減去1時,最後一位變成0而其他所有位都保持不變。也就是最後一位相當於做了取反操作,由1變成了0。
接下來,假設這個數最右邊的一位是0的情況:
如果該整數的二進位制表示中,最右邊的1
,位於第m
位,那麼減去1時:
- 第
m
位由1變成了0 - 第
m
位之後的所有0都變成1 - 整數中第m位之前的所有位都保持不變
我們舉個例子:
一個二進位制數01010000
,它的第4位是從最低位數起的第一個1,減去1後:
- 第4位變0
- 它後面的四個0全變1
- 它前面的所有位保持不變
因此得到的結果為01001111
,轉成十進位制後為79
。
結論
前面我們分析的兩種情況中,我們發現把一個整數減去1,都是把最右邊的1變成0。如果它的最右邊還有0,則所有的0都變成1,而它左邊的所有位都保持不變。
接下來,我們把一個整數和它減去1的結果做位與運算,相當於把它最右邊的1變成0。我們還是以前面的01010000
為例,它減去1的結果是01001111
。我們再對它們二者進行位與運算,得到的結果是:01000000
。
經過觀察後,我們發現:把01010000
最右側的1變成了0,結果剛好就是01000000
。
思路與編碼
看到這裡,我想各位開發者已經看出了此問題的解題思路了?。
沒錯,思路就是:把一個整數減去1,再和原整數做位與運算,會把該整數最右側的1變成0。
那麼,一個整數的二進位制表示中有多少個1,就可以進行多少次這樣的操作,直至整個數變為0,我們對每一次操作進行計數,就得到了這個問題的答案。
基於這種思路,我們就可以愉快的進行編碼了,程式碼如下所示:
export default class BinaryOperation {
/**
* 獲取二進位制中1的個數
* @param decimalVal 十進位制數
*/
public getBinaryOneNum(decimalVal: number): number {
let count = 0;
// 十進位制數不為0,代表其二進位制表示中仍存在1
while (decimalVal !== 0) {
// 二進位制中所存在的1的總數+1
count++;
// 對十進位制數與其-1後的數進行位與運算
// 得出結果後替換原十進位制數,進行下一輪計算,直至十進位制數為0
decimalVal = decimalVal & (decimalVal - 1);
}
// 返回結果
return count;
}
}
最後,我們寫個測試用例,驗證下上述程式碼能否正常工作,如下所示:
import BinaryOperation from "../BinaryOperation.ts";
const binaryOperation = new BinaryOperation();
const result = binaryOperation.getBinaryOneNum(80);
const result2 = binaryOperation.getBinaryOneNum(-80);
console.log(`80的二進位制表示中有${result}個1`);
console.log(`-80的二進位制表示中有${result2}個1`);
執行結果如下圖所示:
示例程式碼地址:BinaryOperation.ts、BinaryOperation-test.ts
執行結果與我們手動算出來的二進位制數中1的個數一致?
-80
我們在前面的章節中算過它的二進位制表示為10110000
,我們講過二進位制具體在計算機中佔多少位,取決於它的字長,我們在書寫時只需要標明它的符號位,高位上多出的1可以省略。此處,我們算出
-80
的二進位制表示中有27個1,我們加上此處的5個0,可以知道它的字長為27 + 5 = 32
。
寫在最後
至此,文章就分享完畢了。
我是神奇的程式設計師,一位前端開發工程師。
如果你對我感興趣,請移步我的個人網站,進一步瞭解。
- 文中如有錯誤,歡迎在評論區指正,如果這篇文章幫到了你,歡迎點贊和關注?