判断int数组/string数据相等的方法及位操作讨论
判断int数组相等(数组元素可能重复)
go中我们时常会遇到判断两个int数组是否相同的需求, 自己撸一个方法也很简单, 搞起来:
方法1
思路1: 把两个数组排序以后,对比每个位置上的数据是否相同.
1 | func isSliceEqualSlow(a, b []int) bool { |
但是这样每个数组都需要排序, 如果是快排, 也是o(2nlgn+n) ~ o(nlgn)时间复杂度, 有点慢啊
方法2
思路2: 观察发现, 如果两个数组相同 , 那么其中任一个int数据其出现的次数一定是偶数个的, 那么用hash记录一下出现次数, 空间换时间?
1 | func isSliceEqualHash(a, b []int) bool { |
方法3 (只针对数组内存在重复的情况)
思路3: 如果两个数组相同 , 那么其中任一个int数据其出现的次数一定是偶数个的, 用hash空间消耗也不少. 我们知道一个数与或其自己一定是0,偶数个相同的数与或也一定是0,那么用位操作岂不是很快?
1 | func isSliceEqual(a, b []int) bool { |
以上实现 针对 [2 2 2] [2 3 3]
和[-1 -1 2 2] [0 0 1 1]
这样的数组就失效了.
对比测试
- 测试正确性:
1 | var ( |
注:TestIsSliceEqual3
无法通过测试,但是如果是数组元素不重复的测试集则可以通过测试.
- benchmarks测试
1 | func BenchmarkIsSliceEqual(b *testing.B) { |
测试结果:
1 | goos: linux |
由测试结果可见: 位操作的威力之大! 写代码时常还是需要考虑性能的, 代码=数据结构+算法的确是哲理.同时附上个人理解 业务>代码,哈哈:-).
针对不同的数据场景可以选择不同的比较方式, 如果输入的数组中元素不重复, 那么就使用位操作的方法, 如果重复, 则使用第一种方式, 排序后依次对比.
发散思考1
既然位操作很niubility, 那么不由得想到leetcode上各种位操作解题. 其中有个问题正好来研究研究:位移操作 比 & 操作 慢吗?
问题由来: counting-bits 以及Number of 1 Bits
对于计算1的个数的实现:
方法1 使用位移操作
1 | func hammingWeightBitMove(num uint32) int { |
方法2 使用&和减1操作
1 | func hammingWeightMinusOne(num uint32) int { |
测试:
1 | var ( |
结果如下, 可见使用减1取与 n = n & (n-1)
确实比位移操作 n = n >> 1
快, 原因是计算机中数据都是使用补码存储的, 操作补码执行位移操作就比执行&操作耗时一些了.
1 | goos: linux |
发散思考2
那么比较两个字符串数组是否相等, 又该如何比较呢?
暂时只想到用hash/sort. 如果你有好的方法, 请不吝赐教.
1 | func isStrSliceEqualHash(a []string, b []string) bool { |
测试数据:
1 | type D struct { |
测试结果:
1 | goos: linux |
两者相差无几.
速记: 位操作的各种技巧
- 实现乘除法:
>> 1
右移1位 等于 除以2,<<1
左移1位等于 乘以2, 左右移相比乘除法快一点 - 位操作进行两数值交换:
a^=b; b^=a; a^=b;
, 适合各种比较后交换的排序场景 - 判断奇偶:
n & (n -1) == 0
或者n & 1 == 0
是偶数 否则 奇数, 比使用n % 2
效率高, 适合交替执行的条件判断场景 - 交换符号将正数变成负数,负数变成正数:
~a + 1
, 整数取反加1,正好变成其对应的负数(补码表示);负数取反加一,则变为其原码,即正数 - 求绝对值: 对于32位数而言, 符号位:
flag = a >> 31
; 绝对值则是((a ^ flag) - flag)
, 求绝对值公式:(a^(a>>31))-(a>>31)
, 适合高频求绝对值进行数据判断的场景 - 二进制逆序: 对于32位数, 依次以 2 , 4, 6, 8 位作为一组,再进行组内高低位交换
1
2
3
4a = (( a & 0xAAAA) >> 1 | (( a & 0x5555) << 1)
a = (( a & 0xCCCC) >> 2 | (( a & 0x3333) << 2)
a = (( a & 0xF0F0) >> 4 | (( a & 0x0F0F) << 4)
a = (( a & 0xFF00) >> 8 | (( a & 0x00FF) << 8) - 取两个数的平均数: 使用
( x&y ) + ((x ^ y) >> 1)
比使用(x+y)/2
效率高. 适合热点代码并且无需考虑x+y
溢出的问题. 前半部分求得相同的bit,其除2后保持不变,后半部分求不同的bit,右移1位即除以2.加和后即是平均数. 适合高频求平均数场景.