二進位制中1的個數

神奇的程式設計師發表於2019-01-19

前言

有一個整數,想知道它的二進位制表示中有多個1,你會怎麼做?本文將帶大家深入學習下二進位制以及它的各種運算,一步步的研究出這個問題的解決方案,歡迎各位感興趣的開發者閱讀本文。

前置知識

在解決這個問題之前,我們需要先了解下什麼是二進位制。

二進位制

在計算機的世界裡,只有0和1,也就是二進位制。

符號數

在二進位制中,數被分為有符號數無符號數

對於有符號數而言,符號的正、負機器是無法識別的,但由於“正、負”恰好是兩種截然不同的狀態,如果用“0”表示正,用“1”表示“負”,這樣符號也被數字化了,並且規定將它放在有效數字的前面,即組成了有符號數。

因此,在二進位制中使用最高位來表示符號。

  • 最高位是0,表示正數。
  • 最高位是1,表示負數。
二進位制的最高位就是其第一位,例如:10000001100,它的最高位就是1。

對於無符號數而言,它表示的數其範圍都是正數,所有位都用於表示數的大小。

有符號數的性質

對於有符號數而言,它有6個性質:

  • 二進位制的最高位是符號位:0表示正數,1表示負數
  • 正數的原碼、反碼、補碼都一樣
  • 負數的反碼 = 它的原碼符號位不變,其它位取反(0 -> 1; 1 -> 0)
  • 0的反碼、補碼都是0
  • 負數的補碼 = 它的反碼 + 1
  • 在計算機運算的時候,都是以補碼的方式來運算的

原碼、反碼、補碼

上述性質中,我們提到了原碼、反碼、補碼,接下來我們來學習下他們究竟是什麼樣的數字。

  • 原碼,分為兩種情況:

    • 一個正數,按照絕對值大小轉換成的二進位制數
    • 一個負數,按照絕對值大小轉換成的二進位制數,然後最高位補1
  • 反碼,也分為兩種情況:

    • 一個正數,它的反碼與它的原碼是相同的
    • 一個負數,它的反碼為該數的原碼除符號位外,各位取反
  • 補碼,也分為兩種情況:

    • 一個正數,它的補碼與它的原碼也是相同的
    • 一個負數,它的補碼為對該數的原碼除符號位外各自取反後,在最後一位加1

進位制轉換

我們要對二進位制進行運算,需要先將十進位制數轉為二進位制,因此我們需要先學習下十進位制轉二進位制的方法。

十進位制轉二進位制

將十進位制轉為二進位制主要分為三種情況:

  • 正整數轉二進位制

計算規則為:除二取餘(直至商為0),然後倒序排列,高位補零。

知道規則後,我們舉個例子,求一下80所對應的二進位制數,如下圖所示:

image-20211025213251590

計算機內部表示數的位元組單位是定長的(字長),如:8、16、32、64位。因此當計算出來的二進位制的位數不夠時,需要在高位進行補0。

上述例子中,我們將80轉為二進位制數後,它的值為:1010000,字長為7,如果計算機的字長是64位,那麼標準寫法就是在它的最高位前面補57個0,我們用計算器來驗證下,如下所示:

image-20211006101942620

  • 負整數轉二進位制

在計算機中,負數是以原碼的補碼形式進行表達的,通過前面的學習,我們知道了想求負數的補碼,就得先求出它的原碼。

我們以-80為例來計算下它的二進位制碼,步驟如所示:

  1. 求原碼,如下圖所示:

image-20211025213338223

  1. 求補碼,如下圖所示:

image-20211014233217872

至此,我們得到了-80的二進位制碼:10110000

在正整數轉二進位制部分,我們講了計算機是固定字長的,當計算出來的二進位制位數不夠時,正整數會在高位補0,負整數則會在高位補1。

我們用計算器來驗證下我們計算出來的-80的二進位制碼是否正確,如下所示:

image-20211014233921705

  • 小數轉二進位制

在二進位制中,小數被稱為浮點數,我們在將十進位制小數轉換為二進位制小數時,需要以小數點為界限,將其拆分為整數部分小數部分

整數部分轉為二進位制,我們在前面已經講過了(即除2取餘)

小數部分轉為二進位制的方法為:乘2取整數部分,繼續用小數部分乘2,直至小數部分為0(大多數情況下不會為0,需要確立精度)

我們以80.13為例來計算下它的二進位制碼,如下圖所示:

image-20211026003224757

計算機中用二進位制來表示小數時,大部分十進位制小數都不能精確的用二進位制來表示,當表示這種小數時,最大精確多少位,取決於計算機的字長

上圖中,我們計算出了80.13的二進位制碼為01010000.00100,我們精確到了小數點後5位。

我們用計算器來驗證下是否正確。

image-20211026004819315

二進位制轉十進位制

同樣的,二進位制轉十進位制也分為三種情況:

  • 正整數轉十進位制

從二進位制的最低位開始,給每一位標上序號,取出不為0位置的數的序號,將其作為2的次方進行計算,最後將結果相加。

我們以01010000為例,求一下它的十進位制數,如下圖所示:

image-20211028233947922

  • 負整數轉十進位制

前面我們學習了十進位制負整數轉二進位制的方法,那麼二進位制轉十進位制,則需要倒著來算,我們以10110000為例,步驟如下所示:

  1. 根據補碼求原碼

image-20211029001747628

  1. 除去符號位,對其他位按照正整數轉二進位制的規則進行計算,最後補上負號,如下圖所示:

image-20211029002527261

  • 小數轉十進位制

給小數點後每一位標上負序號(從-1開始),取出不為0位置的數的序號,將其作為2的次方進行計算,最後將結果相加。

我們以01010000.00100為例,求出它的十進位制數,如下圖所示:

image-20211029231026693

經過前面的學習,我們知道了十進位制小數轉二進位制時,大多數情況是無法得到精確值的,我們用80.13舉例時,精確到了小數點後5位。

同樣的,我們將二進位制小數轉換為十進位制數時,也是無法得到準確值的,最終值也取決於精度,此處我們保留2位小數,四捨五入後就為80.13

有符號數的運算

瞭解完前置知識,接下來我們舉幾個例子來看下有符號數是如何進行運算的。

左移運算子

<<稱為左移運算子,它的運算規則為:

  • 移除二進位制數最高位的0
  • 在二進位制數的末尾補上一個0

我們以01010000為例,假設他的字長為32位(多餘的0省略),對其左移一位,它的運算過程如下圖所示:

image-20211030191646272

左移1位後,我們計算出來的結果為:010100000,轉換成十進位制後結果為2^5 + 2^7 = 32 + 128 = 160

01010000的十進位制數為80,左移動1位後,值變為了160,經過觀察後,我們發現左移後的值正好是原值的2倍,等價於乘2操作。

大多數情況下我們可以將其當作乘2使用,但在一些特殊情況下它並不代表真正的乘2,例如,我們需要將其左移25位,如下圖所示(我們把省略的0補上):

image-20211030191558946

左移25位後,我們發現它左側的0已被刪完,最高位變成了1,這個數也從原來的正數,變為了負數。如果我們左移26位,左側的0不夠,會開始從右側取值,最終的值又變成了正數。

因此,我們會發現這樣一條規律:

  • 當左移的位數,超過剩餘字長時,它的值並非乘2

注意:如果任意一個二進位制數,左移的位數大於等於字長(例如字長為32,我們左移32位,右邊補32個0),那麼與之對應的十進位制數豈不是都為0了?

當然不是,二進位制數進行左移時,當移動的位數大於等於它的字長時,位數會先求餘數,然後再進行左移。

例如:我們需要對一個字長為32位的二進位制數進行左移32位,那麼就需要先求它的餘數,即:32 % 32 = 0,需要左移0位。左移33位的值和左移1位是一樣的。

image-20211030192330358

右移運算子

>>稱為右移運算子,它的運算規則分為正數與負數兩種情況。

正數:

移除最低位的數,在最高位補0。

我們以01010000為例(十進位制值為80),對其右移4位,計算過程如下圖所示:

image-20211030221935067

計算出來的二進位制結果為00101,將其轉位十進位制,結果為:2^0 + 2^2 = 1 + 4 = 5,即:80 >> 4 = 5

我們用計算器來驗證下:

image-20211030225942888

負數

經過前面的學習,我們知道了負數是以原碼的補碼形式儲存的,它的右移規則為:

  • 對補碼進行右移,在最高位補1
  • 求出其反碼
  • 對反碼+1,求出原碼

我們以10110000為例(十進位制為-80),將其右移4位,計算過程如下所示:

image-20211030231644452

計算出來的二進位制結果為11100,我們將其專為十進位制:2^0 + 2^2 = 1 + 4 = 5,補齊符號位,結果為: -5。即:-80 >> 4 = -1

我們用計算器來驗證下,如下圖所示:

image-20211030231953329

與運算子

&稱為與運算子,它的運算規則為:

  • 符號左右兩側的數同時為1,結果就為1,否則為0

即:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1

接下來,看個十進位制的例子,來看下它的運算過程,如下所示:

15 & 13,它的運算步驟為:

  • 將十進位制轉為二進位制
  • 對二進位制進行與運算

運算過程如下圖所示:

image-20211031182434882

或運算子

|稱為或運算子,它的運算規則為:

  • 符號左右兩側的數,有一個為1,其值就為1

即:0 | 1 = 1; 1 | 0 = 1; 0 | 0 = 0; 1 | 1 = 1

我們繼續以前個章節的數字為例,來看下15 | 13的運算步驟:

  • 將十進位制轉二進位制
  • 對二進位制進行或運算

運算過程如下圖所示:

image-20211031184228354

異或運算子

^稱為異或運算子,它的運算規則為:

  • 符號左右兩側的數,值不同,則該位結果為1,否則為0

即:0^0=0; 0^1=1; 1^0=1; 1^1=0

我們繼續以15和13為例,看下15^13的運算步驟:

  • 將十進位制轉二進位制
  • 對二進位制進行異或運算

運算過程如下圖所示:

image-20211031202538320

問題求解

有了上述知識做鋪墊後,接下來我們進入正題:有一個十進位制整數,求它的二進位制數中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

image-20211031214343399

經過觀察後,我們發現:把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`);

執行結果如下圖所示:

image-20211031215022229

示例程式碼地址:BinaryOperation.tsBinaryOperation-test.ts

執行結果與我們手動算出來的二進位制數中1的個數一致?

-80我們在前面的章節中算過它的二進位制表示為10110000,我們講過二進位制具體在計算機中佔多少位,取決於它的字長,我們在書寫時只需要標明它的符號位,高位上多出的1可以省略。

此處,我們算出-80的二進位制表示中有27個1,我們加上此處的5個0,可以知道它的字長為27 + 5 = 32

寫在最後

至此,文章就分享完畢了。

我是神奇的程式設計師,一位前端開發工程師。

如果你對我感興趣,請移步我的個人網站,進一步瞭解。

  • 文中如有錯誤,歡迎在評論區指正,如果這篇文章幫到了你,歡迎點贊和關注?

相關文章