Go 算法专项之整数

491 阅读8分钟

背景

现在已经连续刷了三个月的 leetcode 题目,同时也看了不少算法书籍文章,明显感觉做算法题的速度和准确率提升了不少。毕竟前段时间的面试上在算法题上面栽过跟头,想着这块硬骨头还是得啃一下, 就开始去力扣刷每日一题和周赛,没想到居然已经坚持了三个月。

计划做一下 Go 语言版的数据结构与算法专项整理,让我们看到问题都迎刃而解。现在就从“整数”专项开始吧。

整数的基础知识

整数是一种基本的数据类型。我们在做整数相关的运算时,需要考虑计算结果是否超过范围,产生溢出会让我们得到错误的结果。

Go 中的整数类型取值范围:

uint8  : 0 to 255
uint16 : 0 to 65535
uint32 : 0 to 4294967295
uint64 : 0 to 18446744073709551615
int8   : -128 to 127
int16  : -32768 to 32767
int32  : -2147483648 to 2147483647
int64  : -9223372036854775808 to 9223372036854775807

可以在 math 包看到内置的整数范围常量:

// Integer limit values.
const (
   intSize = 32 << (^uint(0) >> 63) // 32 or 64

   MaxInt    = 1<<(intSize-1) - 1
   MinInt    = -1 << (intSize - 1)
   MaxInt8   = 1<<7 - 1
   MinInt8   = -1 << 7
   MaxInt16  = 1<<15 - 1
   MinInt16  = -1 << 15
   MaxInt32  = 1<<31 - 1
   MinInt32  = -1 << 31
   MaxInt64  = 1<<63 - 1
   MinInt64  = -1 << 63
   MaxUint   = 1<<intSize - 1
   MaxUint8  = 1<<8 - 1
   MaxUint16 = 1<<16 - 1
   MaxUint32 = 1<<32 - 1
   MaxUint64 = 1<<64 - 1
)

我们经常会用到比较整数大小的函数,但是 Go 没有内置该方法:

func min(a, b int) int {
   if a < b {
      return a
   }
   return b
}

func max(a, b int) int {
   if a > b {
      return a
   }
   return b
}

二进制

整数在计算机中是以二进制的形式表示的。二进制是指数字的每位都是0或1。

位运算是把数字用二进制形式表示之后,对每位上0或1的运算。

Go 语言支持的位运算有:与(&)、或(|)、异或(^)、左移(<<)和右移(>>)。

左移运算符 m << n 表示把 m 左移 n 位,即 m 乘以 2 的 n 次方。如果左移 n 位,那么最左边的 n 位将被丢弃,同时在最右边补上 n 个 0。

右移运算符 m >> n 表示把 m 右移 n 位,即 m 除以 2 的 n 次方。如果右移 n 位,则最右边的 n 位将被丢弃。但右移时处理最左边位的情形比较复杂。如果数字是一个无符号数值,则用 0 填补最左边的 n 位。如果数字是一个有符号数值,则用数字的符号位填补最左边的 n 位。也就是说,如果数字原先是一个正数,则右移之后在最左边补 n 个 0;如果数字原先是一个负数,则右移之后在最左边补 n 个 1。

// 二进制位运算示例
func binaryExample() {
   a := 60 // 60 = 00111100
   fmt.Printf("a = %d, %08b\n", a, a)
   b := 13 // 13 = 00001101
   fmt.Printf("b = %d, %08b\n", b, b)

   c := a & b // 12 = 00001100
   fmt.Printf("a & b = %d, %08b\n", c, c)

   c = a | b // 61 = 00111101
   fmt.Printf("a | b = %d, %08b\n", c, c)

   c = a ^ b // 49 = 00110001
   fmt.Printf("a ^ b = %d, %08b\n", c, c)

   c = a << 2 // 240 = 11110000
   fmt.Printf("a << 2 = %d, %08b\n", c, c)

   c = a >> 2 // 15 = 00001111
   fmt.Printf("a >> 2 = %d, %08b\n", c, c)
}

/* 打印结果
a = 60, 00111100
b = 13, 00001101
a & b = 12, 00001100
a | b = 61, 00111101
a ^ b = 49, 00110001
a << 2 = 240, 11110000
a >> 2 = 15, 00001111
*/

例题

29. 两数相除

题目:输入2个 int 型整数,它们进行除法计算并返回商,要求不得使用乘号'*'、除号'/'及求余符号'%'。当发生溢出时,返回最大的整数值。假设除数不为0。
例如,输入15和2,输出15/2的结果,即7。

分析: 当被除数大于除数时,继续比较判断被除数是否大于除数的2倍,如果是,则继续判断被除数是否大于除数的4倍、8倍等。如果被除数最多大于除数的 2k 倍,那么将被除数减去除数的 2k 倍,然后将剩余的被除数重复前面的步骤。由于每次将除数翻倍,因此时间复杂度是 O(logn)。

32 位有符号整数的取值范围是 [−231, 231 − 1]。将 −231 转换为正数会导致溢出,将任意正数转换为负数都不会溢出,所以可以先将正数转换为负数,计算完后,再调整结果的正负号。这里唯一的溢出情况是 −231 / -1,231 超过了正数范围。

可以用左移运算符替代乘以2,右移运算符替代除以2。

参考代码:

func divide(dividend int, divisor int) int {
   if dividend == math.MinInt32 && divisor == -1 {
      return math.MaxInt32
   }

   negative := 2
   if dividend > 0 {
      negative--
      dividend = -dividend
   }
   if divisor > 0 {
      negative--
      divisor = -divisor
   }

   result := 0
   halfMinInt32 := math.MinInt32 >> 1
   for dividend <= divisor {
      value := divisor
      quotient := 1
      for value >= halfMinInt32 && dividend <= value << 1 {
         quotient = quotient << 1
         value = value << 1
      }
      result += quotient
      dividend -= value
   }

   if negative == 1 {
      return -result
   }
   return result
}

67. 二进制求和

题目:输入两个表示二进制的字符串,请计算它们的和,并以二进制字符串的形式输出。
例如,输入的二进制字符串分别是"11"和"10",则输出"101"。

分析: 如果直接转为整数去运算,可能会溢出。可以从两个字符串的右端开始相加,逢二进一,把每一位的运算结果依次添加到数组中,最后再反转数组。

func addBinary(a string, b string) string {
   arr := make([]byte, 0)

   k := 0
   var carry uint8
   for k < len(a) || k < len(b) || carry > 0 {
      var r = carry
      if k < len(a) {
         r = r + a[len(a)-k-1] - '0'
      }
      if k < len(b) {
         r = r + b[len(b)-k-1] - '0'
      }
      k++

      if r == 2 {
         r = 0
         carry = 1
      } else if r == 3 {
         r = 1
         carry = 1
      } else {
         carry = 0
      }

      arr = append(arr, r+'0')
   }

   // 反转数组
   for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
      arr[i], arr[j] = arr[j], arr[i]
   }
   return string(arr)
}

338. 比特位计数

题目:输入一个非负数n,请计算0到n之间每个数字的二进制形式中1的个数,并输出一个数组。
例如,输入的n为4,由于0、1、2、3、4的二进制形式中1的个数分别为0、1、1、2、1,因此输出数组[0,1,1,2,1]。

分析: 最简单的方式是把整数转为二进制字符串,然后数该字符串中 1 的个数。 Golang 把整数转为二进制字符串的方法是 strconv.FormatInt(int64(i), 2),但是这么做的效率较低,

我们发现 i & (i-1) 的结果会把 i 的二进制形式最右边的 1 置为 0,比如 111 & 110 = 110, 110 & 101 = 100, 100 & 011 = 0

由此可以得到状态转移方程:f(i) = f(i & (i-1)) + 1,参考代码如下:

func countBits(n int) []int {
   arr := make([]int, n+1)
   for i := 1; i <= n; i++ {
      arr[i] = arr[i&(i-1)] + 1
   }
   return arr
}

思路二: 偶数右移一位时,1 的个数不变,奇数右移一位时,1 的个数减 1。比如 110 >> 1 = 11, 111 >> 1 = 11

同时用 i & 1 替代 i % 2 运行效率会更高。

可以得到状态转移方程:f(i) = f(i >> 1) + i & 1,参考代码如下:

func countBits(n int) []int {
   arr := make([]int, n+1)
   for i := 1; i <= n; i++ {
      arr[i] = arr[i>>1] + (i & 1)
   }
   return arr
}

318. 最大单词长度乘积

题目:输入一个字符串数组words,请计算不包含相同字符的两个字符串words[i]和words[j]的长度乘积的最大值。如果所有字符串都包含至少一个相同字符,那么返回0。假设字符串中只包含英文小写字母。
例如,输入的字符串数组words为["abcw","foo","bar","fxyz","abcdef"],数组中的字符串"bar"与"foo"没有相同的字符,它们长度的乘积为9。
"abcw"与"fxyz"也没有相同的字符,它们长度的乘积为16,这是该数组不包含相同字符的一对字符串的长度乘积的最大值。

分析: 首先,遍历比较任意两个字符串是否存在相同字符,不存在相同字符则计算乘积。这个题目假设所有字符都是英文小写字母,只有26个可能的字符,因此最多只需要在每个字符串对应的哈希表中查询26次就能判断两个字符串是否包含相同的字符。

由于这个题目只需要考虑26个英文小写字母,因此可以用一个长度为26的布尔型数组来模拟哈希表。数组下标为0的值表示字符'a'是否出现,下标为1的值表示字符'b'是否出现,其余以此类推。代码参考如下:

func maxProduct(words []string) int {
   wordCnt := make([][26]bool, len(words))
   for i, word := range words {
      for _, ch := range word {
         wordCnt[i][ch - 'a'] = true
      }
   }

   var res int
   for i := 0; i < len(words)-1; i++ {
      for j := i+1; j < len(words); j++ {
         k := 0
         for ; k < 26; k++ {
            if wordCnt[i][k] && wordCnt[j][k] {
               break
            }
         }

         if k == 26 {
            res = max(len(words[i]) * len(words[j]), res)
         }
      }
   }
   return res
}

思路二: 用整数的二进制数位记录字符串中出现的字符。

布尔值只有两种可能,即true或false,这与二进制有些类似,要么是0要么是1。因此,可以将长度为26的布尔型数组用26个二进制的数位代替,二进制的0对应布尔值false,而1对应true。同时 int32 的二进制有 32 位,可以满足我们的需求。代码参考如下:

func maxProduct2(words []string) int {
   wordCnt := make([]int, len(words))
   for i, word := range words {
      for _, ch := range word {
         wordCnt[i] |= 1 << (ch - 'a')
      }
   }

   var res int
   for i := 0; i < len(words)-1; i++ {
      for j := i+1; j < len(words); j++ {
         if wordCnt[i] & wordCnt[j] == 0 {
            res = max(len(words[i]) * len(words[j]), res)
         }
      }
   }
   return res
}

参考