swift中的进制转换,以及玩转二进制

swift中的进制转换,以及玩转二进制

在日常开发中我们很少用到进制转换,或操作二进制的情况。但是如果你处理一些底层的代码,你会经常与二进制打交道,所以接下来我们先来了解一下二进制。

二进制(binary),是在数学和数字电路中以2为基数的记数系统,是以2为基数代表系统的二进位制。这一系统中,通常用两个不同的符号0(代表零)和1(代表一)来表示。数字电子电路中,逻辑门的实现直接应用了二进制,现代的计算机和依赖计算机的设备里都使用二进制。每个数字称为一个比特(Bit,Binary digit的缩写)【摘自百度百科】

因此在计算机的世界里只有0和1。

在swift中为我们提供了存储不同长度bit的基本类型,例如UInt8(无符号的8bit,我们常说是无符号的8位Int型),Int8(有符号的8bit)等。符号位一般是二进制的最高位表示,0:正数,1:负数。

我们以十进制+8和-8来举例8位二进制中的符号位(这里操作小端模式举例,关于大小端模式的介绍,可以看这里):

+8的二进制:00001000,最高位这里是0,因此代表正数

-8的二进制:10001000,最高位这里是1,因此代表负数

 

进制的表示法

二进制:b(Binary),swift中可以这样表示:let a = 0b0000_1001

八进制:o(Octal),swift中可以这样表示:let a = 0o11

十进制:d(Decimal),swift中可以这样表示:let a = 9

十六进制:x(Hexadecimal),swift中可以这样表示:let a = 0x9

 

接下来就来讲讲进制之间的转换原理

二进制——>十进制

00001001——>9

转换原理:(因为是小端模式,二进制的有效位是从右到左的)1*2^0 + 0*2^1 + 0*2^2 + 1*2^3 + 0*2^4+ 0*2^5+ 0*2^6+ 0*2^7 = 9

swift代码实现:

func decimal(_ v: String) -> Int {
    guard v.count > 0 else { return 0 }
    let l = v.count
    var isSkip = true
    var sum: Int = 0
    var idx = 0, skipCount = 0
    for i in v.indices {
        if let a = Int(String(v[i])) {
            if isSkip && a > 0{
                isSkip = false
            }
            if isSkip {
                skipCount += 1
                continue
            }
            sum += a*Int(pow(2, Double(l-skipCount-1-idx)))
            idx += 1
        }
    }
    return sum
}

swift自带的api

let b = 0b0000_1001
let s = String(b, radix: 10) // 转成10进制字符
print("---: \(s)")

// 输出结果:---: 9

 

十进制——>二进制

9——>00001001

转换原理:(根据二进制 转 十进制可以推导出,注意这里是小端模式,因此每次计算出来的bit顺序应该是从右到左,因为我们只要8位长度的二进制,因此这里需要计算8次)

func toBinary(_ v: UInt8) -> String {
    var str: String = ""
    var i = v
    while str.count < v.bitWidth {
        str.insert(Character("\(i % 2)"), at: str.startIndex)
        i /= 2
    }
    return str
}

swift自带api

let a: UInt8 = 9
let str = String(a, radix: 2)
print("str: \(str)")

// 输出:str: 1001
// 它输出的字符串只包括有效二进制位

 

二进制——>八进制

00001001——>11

转换原理:(因为是小端模式,二进制的有效位是从右到左的)二进制每三位一组(不够三位补0),分别计算对应的十进制值,计算结果的组合就是一个八进制数。

1、分组:000(不足三位补0),001,001

2、每组分别计算十进制值:0*2^0 + 0*2^1 + 0*2^2 = 0,1*2^0 + 0*2^1 + 0*2^2 = 1,1*2^0 + 0*2^1 + 0*2^2 = 1

3、每组的结果从左到右组合:0,1,1,最终的八进制数字为:11 

swift代码实现:

func octal(_ v: String) -> String {
    guard v.count > 0 else { return "" }
    var res = ""
    var idx = 0
    var sum = 0
    for i in v.indices.reversed() {
        if let a = Int(String(v[i])) {
            sum += a*Int(pow(2, Double(idx)))
            if idx >= 2 {
                res.insert(Character("\(sum)"), at: res.startIndex)
                idx = 0
                sum = 0
            }else {
                idx += 1
            }
        }
    }
    return res
}

print("----o: \(octal("00001011"))")

// 输出:----o: 13

swift自带api:

let a = 0b0000_1011
let str = String(a, radix: 8)
print("str: \(str)")

// 输出:str: 13

 

八进制——>二进制

11——>00001001

原理:(利用二进制 转 八进制 反推导可知,对八进制的每位分别进行转3位的二进制数,然后将每位的结果拼接)1->001,1->001,最后拼接得到二进制:001001

swift实现代码:

func octalToBinary(_ v: String) -> String {
    guard v.count > 0 else { return "" }
    var res = ""
    for i in v.indices {
        if let a = Int(String(v[i])) {
            var n = a
            var s = ""
            for _ in 0..<3 {
                s.insert(Character("\(n % 2)"), at: s.startIndex)
                n /= 2
            }
            res.append(s)
        }
    }
    return res
}

print("----b: \(octalToBinary("11"))") // 八进制:11转二进制

// 输出:----b: 001001

swift自带api

let a = 0o11
let str = String(a, radix: 2)
print("str: \(str)")

// 输出:str: 1001

 

二进制——>十六进制

00101011——>2b

原理:二进制每四位一组(不够四位补0),分别计算对应的十进制值,计算结果的组合就是一个八进制数

1、分组:0010,1001

2、分别计算每组的十进制值:0*2^0 + 1*2^1 + 0*2^2 + 0*2^3 = 2,1*2^0 + 0*2^1 + 0*2^2 + 1*2^3 = 11(因为在16进制的表示中是从0到f的,因此11对应的是b)

3、组合结果:2b

swift代码实现:

func hex(_ v: String) -> String {
    guard v.count > 0 else { return "" }
    var res = ""
    var idx = 0, sum = 0
    for i in v.indices.reversed() {
        if let a = Int(String(v[i])) {
            sum += (a*Int(pow(2, Double(idx))))
            if idx >= 3 {
                res.insert(Character(String(format: "%x", sum)), at: res.startIndex)
                idx = 0
                sum = 0
            }else {
                idx += 1
            }
        }
    }
    if sum > 0 {
        res.insert(Character("\(sum)"), at: res.startIndex)
    }
    return res
}

print("----hex: \(hex("101011"))") // 二进制:101011转十六进制

// 输出:----hex: 2b

swift自带api:

let a = 0b0010_1011
let str = String(a, radix: 16)
print("str: \(str)")

// 输出:str: 2b

 

十六进制——>二进制

2b——>00101011

原理:(根据二进制 转 十六进制 可推导出,对十六进制的每位分别进行转4位的二进制数,然后将每位的结果拼接)2——>0010,b——>1011

swift实现代码:

func hexToBinary(_ v: String) -> String {
    guard v.count > 0 else { return "" }
    var res = ""
    let map = ["1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "a": 10, "b": 11, "c": 12, "d": 13, "e": 14, "f": 15]
    for i in v.indices {
        if let a = map[String(v[i])] {
            var n = a
            var s = ""
            for _ in 0..<4 {
                s.insert(Character("\(n % 2)"), at: s.startIndex)
                n /= 2
            }
            res.append(s)
        }
    }
    return res
}

print("----b: \(hexToBinary("2b"))") // 十六进制:2b转二进制

// 输出:----b: 00101011

swift自带api

let a = 0x2b
let str = String(a, radix: 2)
print("str: \(str)")

// 输出:str: 101011

 

八进制——>十进制

0o1101——>577

原理:(类似于二进制 转 十进制,因为是小端模式,分别从右往左对八进制每位进行加权计算,然后将所有加权的值累加即为 十进制结果)

1*8^0 + 0*8^1 + 1*8^2 + 1*8^3  = 557

swift实现代码:

func octalToDecimal(_ v: String) -> Int {
    guard v.count > 0 else { return 0 }
    var idx = 0, sum = 0
    for i in v.indices.reversed() {
        if let a = Int(String(v[i])) {
            sum += (a*Int(pow(8, Double(idx))))
            idx += 1
        }
    }
    return sum
}

print("----d: \(octalToDecimal("1101"))") // 八进制:1101转十进制

// 输出:----d: 577

swift自带api:

let a = 0o1101
let str = String(a, radix: 10)
print("str: \(str)")

// 输出:str: 577

 

十进制——>八进制

577——>1101

原理:(根据八进制 转 十进制 可推导出,对十进制数 除以 8,余数作为二进制有效位,商>0,继续对商进行上面的操作,直到商==0为止)

swift实现代码:

func decimalToOctal(_ v: Int) -> String {
    guard v > 0 else { return "0" }
    var str = ""
    var a = v
    while a > 0 {
        str.insert(Character("\(a % 8)"), at: str.startIndex)
        a /= 8
    }
    return str
}

print("---o: \(decimalToOctal(577))") // 十进制:577 转 八进制

// 输出:---o: 1101

swift自带api:

let a = 577
let str = String(a, radix: 8)
print("str: \(str)")

// 输出:str: 1101

 

八进制——>十六进制

1271——>2b9

原理:(中转法)现将八进制 转成 二进制 或 十进制,然后通过二进制 或 十进制 再转 十六进制。

swift自带api:

let a = 0o1271
let str = String(a, radix: 16)
print("str: \(str)")

// 输出:str: 2b9

 

十六进制——>八进制

2b9——>1271

原理:(中转法)现将十六进制 转成 二进制 或 十进制,然后通过二进制 或 十进制 再转 八进制。

swift自带api:

let a = 0x2b9
let str = String(a, radix: 8)
print("str: \(str)")

// 输出:str: 1271

 

以上就是全部的进制之间的转化过程,接下来介绍一下对二进制操作常用到的位运算符

“&”运算符

两个二进制对应位都为1,该位的结果才为1,否则为0。如下:

第一个二进制 0 1 0 1 1 0 0 1
第二个二进制 0 0 1 1 1 0 1 0
&运算后的结果 0 0 0 1 1 0 0 0

 

 

 

 

&运算符在日常开发中用处比较多,举个最长用到的例子,在OC开发中,我们定义枚举,经常会利用&来判断一个包含多个枚举值的情况:

NS_ENUM(NSInteger, MyType) {
    
    MyType1 = 1 << 0,
    MyType2 = 1 << 1,
    MyType3 = 1 << 2,
};

enum MyType type = MyType1 | MyType2;

if ((type & MyType1) == MyType1) {
    NSLog(@"-----type中包含MyType1枚举值");
}else {
    NSLog(@"-----type中不包含MyType1枚举值");
}

if ((type & MyType2) == MyType2) {
    NSLog(@"-----type中包含MyType2枚举值");
}else {
    NSLog(@"-----type中不包含MyType2枚举值");
}

if ((type & MyType3) == MyType3) {
    NSLog(@"-----type中包含MyType3枚举值");
}else {
    NSLog(@"-----type中不包含MyType3枚举值");
}


// 输出:
// -----type中包含MyType1枚举值
// -----type中包含MyType2枚举值
// -----type中不包含MyType3枚举值

我们也可以利用&运算符来判断一个数是奇数还是偶数,只要二进制位第0位=1,那这个数必定是奇数,如果=0,那就是偶数。为什么?根据上面介绍的二进制转十进制可知:第一位的值要么是0(0*2^0),要么是1(1*2^0),而后面位数对应的值都是2的倍数(偶数),因此一个偶数+1=奇数。这样我们只需要判断第一位是1还是0,就可以判断出这个数是奇还是偶 。如何判断二进制第一位是1还是0呢,我们可以让这个数与1进行&运算,即x & 0000 0001 = 1(奇数),x & 0000 0001 = 0(偶数),如下:

let x = 123
if x & 1 == 0 {
    print("偶数")
}else {
    print("奇数")
}

 

“|”运算符

两个二进制对应位只要有一个为1,该位的结果就为1,否则为0。如下:

第一个二进制 0 1 0 1 1 0 0 1
第二个二进制 0 0 1 1 1 0 1 0
|运算后的结果 0 1 1 1 1 0 1 1

 

 

 

 

|运算符常用于将多个枚举值合并,例如上面的例子中,将两个枚举值合并到一个type变量中。

 

“^”(异或)运算符

两个二进制对应位不相同结果则为1,相同则为0。如下:

第一个二进制 0 1 0 1 1 0 0 1
第二个二进制 0 0 1 1 1 0 1 0
^运算后的结果 0 1 1 0 0 0 1 1

 

 

 

 

^运算符也有一些使用的场景,例如我们可以利用该运算符实现 不占用额外空间交换两个变量的值。如下:

var x = 1024, y = 4201

x = x^y
y = x^y // x
x = x^y // y

print("x: \(x), y: \(y)")

// 输出:x: 4201, y: 1024

我们用一个简单的两个二进制数解释上面的例子:

数字1: 0 0 0 0 0 0 0 1
数字2: 0 0 0 0 0 0 1 0
^结果 0 0 0 0 0 0 1 1

 

 

 

 

^结果与 数字1 再次进行^运算

数字1 0 0 0 0 0 0 0 1
^结果 0 0 0 0 0 0 1 1
结果 0 0 0 0 0 0 1 0

 

 

 

 

它的结果正是数字2。同样的道理,让^结果与 数字2 再次进行 ^运算,结果将会是数字1。

另外^运算还有个特性,那就是任何一个数与自己^运算,相当于将自己至为0。

var x = 100123
x ^= x
print("x: \(x)")

// 输出:x: 0

 

“~”(取反)运算符

 对一个二进制取反就是对二进制中的每位取反,例如0取反就是1,1取反就是0。如下:

一个二进制 0 1 0 1 1 0 0 1
~结果 1 0 1 0 0 1 1 0

 

 

 

我们可以利用取反运算符和&运算结合,从而实现从多个合并的枚举中删除指定的枚举。如下:

NS_ENUM(NSInteger, MyType) {
    
    MyType1 = 1 << 0,
    MyType2 = 1 << 1,
    MyType3 = 1 << 2,
};

enum MyType type = MyType1 | MyType2;
if ((type & MyType1) == MyType1) {
    NSLog(@"-----type中包含MyType1枚举值");
    // 删除枚举值 MyType1
    type &= ~MyType1;
    
    if ((type & MyType1) == 0) {
        NSLog(@"枚举MyType1从type变量中移除");
    }
}

我们也可以通过该运算符计算一个数的相反数,如下:(一个数x的相反数=~x + 1)

let x = -123
print("x: \(~x + 1)") // 计算x的相反数

// 输出:x: 123

 

“<<”左移运算符

对二进制的每位进行左移,右侧空出的位用0补,运算符后面跟着移动的位数,例如:x << 2,对x进行左移2位。

一个二进制 0 0 0 0 1 1 1 1
<<4结果 1 1 1 1 0 0 0 0

 

 

 

由于二进制每位之间都相差2^n倍数,因此对一个数进行左移运算,相当于这个数乘以2^n(n:移动的位数)

例如:3 << 1:相当于:3 * 2^1 = 6。因此要求一个数的2的幂数,可以这样计算:1 << n:相当于:1 * 2^n

 

“>>”右移运算符

与左移运算符相反,它是对对二进制的每位进行右移,左侧空出的位用0补,运算符后面跟着移动的位数,例如:x >> 2,对x进行右移2位。

右移运算相当于这个数除以2^n(n:移动的位数)

一个二进制 1 1 1 1 0 0 0 0
>>4结果 0 0 0 0 1 1 1 1

 

 

 

左移与右移运算也在开发中常用到,例如我们可以通过对一个数右移一位来实现除以2的操作。

另外还有用于取指定位的值,例如:要取一个八位无符号的二进制的前4位的数值,如下:

1、二进制:1111 0000

2、对二进制右移4位:0000 1111

3、计算十进制数值:1*2^0 + 1*2^1 + 1*2^2 + 1*2^3 = 15

在取一个图片中指定像素点的色值的时候,我们就会用到移位算法。一个色值是包括r、g、b、a四个分量,根据不同的颜色通道的mode,它们的排列顺序也不同。一般以r、g、b、a顺序居多。颜色的每个分量采用8位二进制数表示,因此一个色值的位数可以是32位。根据它们的排列顺序,我们就可以通过移位运算,来分别取出每个分量值。例如:

----------------x----------------

---r----   ---g---   ---b---   ---a---

00000100   00001001  00000010  00000001

r:x >> 24

g:(x << 8) >> 24

b:(x << 16) >> 24

a:(x << 24) >> 24

其实对于上面的取值还有一些技巧,比如在取b分量的值时,我们可以通过x&00000000_00000000_11111111_00000000来快速取值,这样就不需要移位了。一般我们会采用十六进制来表示这样的一串二进制数。我们知道十六进制中最大值是F,而每个十六进制数占4位,因此8位的最大值就是八个1,用十六进制表示就是:FF。

下面将展示不通过移位方法来去各个分量的值。

r:x & 0xFF000000

g:x & 0x00FF0000

b:x & 0x0000FF00

a:x & 0x000000FF

这种计算方法会减少很多移位的步骤,因此这样计算的效率肯定比移位方法要高。

 

“>>>”(无符号右移)运算符(某些开发语言中不支持该运算符,例如:swift、OC)

这个运算符跟>>运算符类似,都是将二进制位右移,而>>>在移动时不会考虑符号位,会将符号位用0补。而>>在移动的时候会考虑符号位,并不会用0来补充符号位。因此通过>>>运算后得到的值肯定是一个正数。

 

额外知识

1、关于如何完整打印出二进制位的字符串,下面是问题描述:

Int8:代表8位二进制位的整型,其二进制位字符串完整输出应该是长度为8的字符串,例如:00001111。

知识点,关于Int(Int8、Int16、Int32、Int64)的属性介绍:

let a = Int8(12) // 拿Int8为例
print("非完整的二进制字符串:\(String(a, radix: 2))") // 二进制字符串:1100
print("bit位数:\(a.bitWidth), ") // 8
print("bit位左侧(头部)0的个数:\(a.leadingZeroBitCount)") // 4
print("bit位右侧(尾部)0的个数:\(a.trailingZeroBitCount)") // 2
print("非0比特位的个数:\(a.nonzeroBitCount)") // 2

综上知识点,我们可以打印完整的二进制位字符串的方法如下:

let a = Int8(12) // 拿Int8为例
let bitStr = String(repeating: "0", count: a.leadingZeroBitCount) + String(a, radix: 2)
print("完整的二进制字符串:\(bitStr)") // 00001100

 

2、如果获取下一个2的幂数,问题描述如下:

2的幂数列表为:2^02^12^22^32^4...,即:1  2  4  8 16...

给出一个数,返回这个数的下一个2的幂数,例如:1->1、2->2、3->4、4->4、5->8、6->8、7->8、8->8、9->16、10->6、11->16....

// 例如:给出一个数n
let n = 6
let nextPowerOf2 = 1 << (n.bitWidth - (n - 1).leadingZeroBitCount)
print("nextPowerOf2: \(nextPowerOf2)") // 8

 

posted @ 2022-06-26 19:19  zbblogs  阅读(787)  评论(0编辑  收藏  举报