關(guān)于位運(yùn)算看這個(gè)就夠了

1:背景

在現(xiàn)代計(jì)算機(jī)中所有的數(shù)據(jù)都是以二進(jìn)制的形式存儲(chǔ)在設(shè)備中。即0、1兩種狀態(tài),計(jì)算機(jī)對(duì)二進(jìn)制數(shù)據(jù)進(jìn)行的運(yùn)算(+、-、*、/)都是叫位運(yùn)算,即將符號(hào)位共同參與運(yùn)算的運(yùn)算。

我們每一種語(yǔ)言最終都會(huì)通過(guò)編譯器轉(zhuǎn)換成機(jī)器語(yǔ)言來(lái)執(zhí)行,所以直接使用底層的語(yǔ)言就不需要便編譯器的轉(zhuǎn)換工作從而得到更高的執(zhí)行效率,當(dāng)然可讀性可能會(huì)降低,這也是為什么匯編在大部分情況下有更快的速度。項(xiàng)目中合理的運(yùn)用位運(yùn)算能提高我們代碼的執(zhí)行效率。

在iOS系統(tǒng)中位運(yùn)算多見(jiàn)于枚舉中,其他地方很少見(jiàn),因?yàn)槲贿\(yùn)算是底層的計(jì)算機(jī)語(yǔ)言,而在iOS開(kāi)發(fā)中不管是Objective—C還是Swift都屬于高級(jí)的編程語(yǔ)言,大量的位運(yùn)算都被蘋(píng)果封裝了起來(lái),我們只關(guān)心調(diào)用的接口不用關(guān)心內(nèi)部的實(shí)現(xiàn)。

typedef NS_OPTIONS(NSUInteger, NSLayoutFormatOptions) {
    NSLayoutFormatAlignAllLeft = (1 << NSLayoutAttributeLeft),
    NSLayoutFormatAlignAllRight = (1 << NSLayoutAttributeRight),
    NSLayoutFormatAlignAllTop = (1 << NSLayoutAttributeTop),
    NSLayoutFormatAlignAllBottom = (1 << NSLayoutAttributeBottom),
    NSLayoutFormatAlignAllLeading = (1 << NSLayoutAttributeLeading),
    NSLayoutFormatAlignAllTrailing = (1 << NSLayoutAttributeTrailing),
.
.
.
.
    }

10:計(jì)算機(jī)計(jì)算原理

加法和乘法

舉一個(gè)簡(jiǎn)單的例子來(lái)看下CPU是如何進(jìn)行計(jì)算的,比如這行代碼

int a = 35;
int b = 47;
int c = a + b;

計(jì)算兩個(gè)數(shù)的和,因?yàn)樵谟?jì)算機(jī)中都是以二進(jìn)制來(lái)進(jìn)行運(yùn)算,所以上面我們所給的int變量會(huì)在機(jī)器內(nèi)部先轉(zhuǎn)換為二進(jìn)制在進(jìn)行相加

35:  0 0 1 0 0 0 1 1
47:  0 0 1 0 1 1 1 1
————————————————————
82:  0 1 0 1 0 0 1 0

再來(lái)看下乘法,執(zhí)行如下的代碼

int a = 3;
int b = 2;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  2
————————————————————
6:  0 0 0 0 0 1 1 0

*********************************************

int a = 3;
int b = 4;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  4
————————————————————
12:  0 0 0 0 1 1 0 0

*********************************************

int a = 3;
int b = 8;
int c = a * b;

3:  0 0 0 0 0 0 1 1  *  8
————————————————————
24:  0 0 0 1 1 0 0 0

通過(guò)以上運(yùn)算可以看出當(dāng)用a乘b,且如果b滿(mǎn)足2^N的時(shí)候 就相當(dāng)于把a(bǔ)的二進(jìn)制數(shù)據(jù)向左移動(dòng)N位,放到代碼中 我們可以這樣來(lái)寫(xiě) a << N,所以上面3 * 2、3 * 4、3 * 8其實(shí)是可以寫(xiě)成3<<1、3<<2、3<<3,運(yùn)算結(jié)果都是一樣的。

那假如相乘的兩個(gè)數(shù)都不滿(mǎn)足2N怎么辦呢?其實(shí)這個(gè)時(shí)候編譯器會(huì)將其中一個(gè)數(shù)拆分成多個(gè)滿(mǎn)足2N的數(shù)相加的情況,打個(gè)比方

int a = 15;             int a = 15
int b = 13;      =>     int b = (4 + 8 + 1)
int c = a * b;          int c = a * b   

最后其實(shí)執(zhí)行相乘運(yùn)算就會(huì)變成這樣 15 * 4 + 15 * 8 + 15 * 1,按照上文說(shuō)的移位來(lái)轉(zhuǎn)換為位運(yùn)算就會(huì)變成15 << 2 + 15 << 3 + 15 << 0

減法和除法

減法也是與加法同理只不過(guò)計(jì)算機(jī)內(nèi)減法操作就是加上一個(gè)數(shù)的負(fù)數(shù)形式,且在操作系統(tǒng)中都是以補(bǔ)碼的形式進(jìn)行操作(因?yàn)檎龜?shù)的源碼補(bǔ)碼反碼都與本身相同)。首先, 因?yàn)槿四X可以知道第一位是符號(hào)位, 在計(jì)算的時(shí)候我們會(huì)根據(jù)符號(hào)位, 選擇對(duì)真值區(qū)域的加減. 但是對(duì)于計(jì)算機(jī), 加減乘數(shù)已經(jīng)是最基礎(chǔ)的運(yùn)算, 要設(shè)計(jì)的盡量簡(jiǎn)單. 計(jì)算機(jī)辨別"符號(hào)位"顯然會(huì)讓計(jì)算機(jī)的基礎(chǔ)電路設(shè)計(jì)變得十分復(fù)雜! 于是人們想出了將符號(hào)位也參與運(yùn)算的方法. 我們知道, 根據(jù)運(yùn)算法則減去一個(gè)正數(shù)等于加上一個(gè)負(fù)數(shù), 即: 1-1 = 1 + (-1) = 0 , 所以機(jī)器可以只有加法而沒(méi)有減法, 這樣計(jì)算機(jī)運(yùn)算的設(shè)計(jì)就更簡(jiǎn)單了.

除法的話(huà)其實(shí)和乘法原理相同,不過(guò)乘法是左移而除法是右移,但是除法的計(jì)算量要比乘法大得多,其大部分的消耗都在拆分?jǐn)?shù)值,和處理小數(shù)的步驟上,所以如果我們?cè)谶M(jìn)行生成變量的時(shí)候如果遇到多位的小數(shù)我們盡量把他換成string的形式,這也是為什么浮點(diǎn)運(yùn)算會(huì)消耗大量的時(shí)鐘周期(操作系統(tǒng)中每進(jìn)行一個(gè)移位或者加法運(yùn)算的過(guò)程所消耗的時(shí)間就是一個(gè)時(shí)鐘周期,3.0GHz頻率的CPU可以在一秒執(zhí)行運(yùn)算3.010241024*1024個(gè)時(shí)鐘周期)

11:位運(yùn)算符

使用的運(yùn)算符包括下面:

含義 運(yùn)算符 例子
左移 << 0011 => 0110
右移 >> 0110 => 0011
按位或 0011
------- => 1011
1011
按位與 & 0011
------- => 0011
1011
按位取反 ~ 0011 => 1100
按位異或 (相同為零不同為一) ^ 0011
------- => 1000
1011

100:顏色轉(zhuǎn)換

背景

上面說(shuō)了iOS中經(jīng)常見(jiàn)到的位運(yùn)算的地方是在枚舉中,那么顏色轉(zhuǎn)換應(yīng)該是除了枚舉之外第二比較常用位運(yùn)算的場(chǎng)景。打個(gè)比方設(shè)計(jì)師再給我們出設(shè)計(jì)稿的時(shí)候通常會(huì)在設(shè)計(jì)稿上按照16進(jìn)制的樣子給我們標(biāo)色值。但是iOS中的UIColor并不支持使用十六進(jìn)制的數(shù)據(jù)來(lái)初始化。所以我們需要將十六進(jìn)制的色值轉(zhuǎn)換為UIColor。

原理分析

UIColor中通常是用傳入RGB的數(shù)值來(lái)初始化,而且每個(gè)顏色的取值范圍是十進(jìn)制下的0~255,而設(shè)計(jì)同學(xué)又給的是十六進(jìn)制數(shù)據(jù),所以在操作系統(tǒng)中需要把這兩種進(jìn)制的數(shù)據(jù)統(tǒng)一成二進(jìn)制來(lái)進(jìn)行計(jì)算,這就用到了位運(yùn)算。這里用一個(gè)十六進(jìn)制的色值來(lái)舉例子比如0xffa131我們要轉(zhuǎn)換就要先理解其組成

  • 0x或者0X:十六進(jìn)制的標(biāo)識(shí)符,表示這個(gè)后面是個(gè)十六進(jìn)制的數(shù)值,對(duì)數(shù)值本身沒(méi)有任何意義

  • ff 顏色中的R值,轉(zhuǎn)換為二進(jìn)制為 1111 1111

  • a1 顏色中的G值,轉(zhuǎn)換為二進(jìn)制為 1010 0001

  • 31 顏色中的B值,轉(zhuǎn)換為二進(jìn)制為 0011 0001

  • 上述色彩值轉(zhuǎn)換為二進(jìn)制后為1111 1111 1010 0001 0011 0001(每一位十六進(jìn)制的對(duì)應(yīng)4位二進(jìn)制,如果位數(shù)不夠記得高位補(bǔ)零)

通常來(lái)講十六進(jìn)制的顏色是按照上面的RGB的順序排列的,但是并不固定,有時(shí)候可能會(huì)在其中加A(Alpha)值,具體情況按照設(shè)計(jì)為準(zhǔn),本文以通用情況舉例。

綜上,我們只需把對(duì)應(yīng)位的值轉(zhuǎn)換為10進(jìn)制然后/255.0f就可得到RGB色彩值,從而轉(zhuǎn)換為UIColor

轉(zhuǎn)換代碼

先列出代碼,后續(xù)解析

- (UIColor *)colorWithHex:(long)hexColor alpha:(float)opacity
{
    //將傳入的十六進(jìn)制顏色0xffa131 轉(zhuǎn)換為UIColor
    
    float red = ((hexColor & 0xFF0000) >> 16)/255.0f;
    float green = ((hexColor & 0xFF00) >> 8)/255.0f;
    float blue = (hexColor & 0xFF)/255.0f;
    return [UIColor colorWithRed:red green:green blue:blue alpha:opacity];
}

大概原理可以看出將RGB每個(gè)值都解析出來(lái)然后變成UIColor,先拿第一步轉(zhuǎn)換紅色值來(lái)說(shuō),我們按照運(yùn)算順序一步步來(lái)講(默認(rèn)將參數(shù)代入,用0xffa131代替hexColor)

  • 0xffa131 & 0xFF0000

    我們知道紅色值是前兩位也就是ff,所以這一步我們既然要取出紅色值就要把其他位全部置零來(lái)排除干擾,這步操作便是如此,在計(jì)算機(jī)系統(tǒng)內(nèi)是二進(jìn)制來(lái)實(shí)現(xiàn)的,即:

    1111 1111 1010 0001 0011 0001
    ------------------------------------------- => & => 1111 1111 0000 0000 0000

    1111 1111 0000 0000 0000 0000
    這部操作做完后可以看出將除了R值之外的G值B值全部置零了,但是離最終結(jié)果還差點(diǎn),因?yàn)?xFF是1111 1111,而我們的結(jié)果后面多出了16個(gè)0,所以便有了第二步操作

  • >> 16

    將上一步得到的結(jié)果右移16位即得到0000 0000 0000 0000 1111 1111高位的零可以忽略,這也是最終的結(jié)果

  • / 255.0f

    這一步應(yīng)該都知道UIColor中傳入的數(shù)值范圍在0~1,所以我們要做下轉(zhuǎn)換

  • 后續(xù)的G值和B值都是一樣的,只是大家注意位數(shù)就可以了,值得注意的是兩個(gè)二進(jìn)制數(shù)進(jìn)行位運(yùn)算一定保證兩個(gè)數(shù)的位數(shù)相同,位數(shù)不夠的那個(gè)數(shù)高位要用0補(bǔ)齊

101:枚舉

關(guān)于枚舉中使用位運(yùn)算我們之前也講過(guò),下面我們自己來(lái)寫(xiě)一個(gè)枚舉(偽代碼)

typedef NS_OPTIONS(NSUInteger, TestOptions) {
     TestOptionOne     =    1 << 0, (000001)
    
     TestOptionTwo     =    1 << 1, (000010)
    
     TestOptionThree   =    1 << 2, (000100)
    
     TestOptionFour    =    1 << 3, (001000)

     TestOptionFive    =    1 << 4, (010000)

     TestOptionSix     =    1 << 5, (100000)
.
.
.
.

  • 解析
    上面的枚舉我后面用括號(hào)表明了位移后對(duì)應(yīng)的二進(jìn)制的值。這樣寫(xiě)枚舉的好處是我可以對(duì)其中選項(xiàng)多選比如TestOptionOne | TestOptionTwo (000001 | 000010 => 000011) 或者有其他的自定義組合。

110:加密

在iOS中我們可以利用異或來(lái)進(jìn)行加解密,異或的特性如下

A ^ B = C => C ^ A = B => C ^ B = A 

上文我們可以把A認(rèn)為是需要加密的數(shù)據(jù),B認(rèn)為是密鑰 C是加密后的數(shù)據(jù)
比如:

#include <stdio.h>
main()
{
   char a[]="MyPassword";        /*要加密的密碼*/
   char b[]="cryptographic";     /*密鑰*/
   int i;
   /*加密代碼*/
   for(i=0;a[i]!='\0';i++)
a[i]=a[i]^b[i];
   printf("You Password encrypted: %s\n",a);
   /*解密代碼*/
   for(i=0;a[i]!='\0';i++)
a[i]=a[i]^b[i];
   printf("You Password: %s\n",a);
  
}

111:其他應(yīng)用

  • 記得iOS總有一道面試題在不使用第三個(gè)變量的情況下交換兩個(gè)變量的值,這里用到異或的上面加解密中的特性。我有x、y兩個(gè)個(gè)變量,做如下位運(yùn)算操作
void exchange(int x , int y) 
{ 
    x ^= y; 
    y ^= x; 
    x ^= y; 
} 
  • 判斷一個(gè)數(shù)的奇偶性,其實(shí)我們可以用%2來(lái)判斷,代碼量不高,但是之前講過(guò),除法運(yùn)算的時(shí)鐘周期非常多,所以代碼雖然不多并不代表效率高,我們可以用如下運(yùn)算來(lái)完成:
void test(int x)
{
    if (x&1) {
        printf("奇數(shù)");
    } else {
        printf("偶數(shù)");
    }
}

原理很簡(jiǎn)單,因?yàn)槎M(jìn)制是滿(mǎn)二進(jìn)一,一旦超過(guò)1就會(huì)變0并進(jìn)一位,這時(shí)候和00001做&操作一定會(huì)為0,反之不為零。這樣寫(xiě)效率會(huì)更高。

  • 計(jì)算兩個(gè)數(shù)的平均值,通常我們都是(x+y)/2,先不考慮效率問(wèn)題,這樣還會(huì)引起一個(gè)其他的問(wèn)題,那就是x+y的值很有可能溢出大于INT_MAX,所以我們采用位運(yùn)算的辦法來(lái)解決即可:
int average(int x, int y) 
{    
    return (x&y)+((x^y)>>1); 
} 

1000:總結(jié)

其實(shí)位運(yùn)算的應(yīng)用遠(yuǎn)遠(yuǎn)不止這些,在算法方面適當(dāng)?shù)氖褂眠€是很有幫助的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容