learn-tech/专栏/程序员的数学基础课/数学专栏课外加餐(二)位操作的三个应用实例.md
2024-10-16 09:22:22 +08:00

180 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
数学专栏课外加餐(二) 位操作的三个应用实例
你好,我是黄申。欢迎来到第二次课外加餐时间。
位操作的应用实例
留言里很多同学对位操作比较感兴趣,我这里通过计算机中的位操作的几个应用,来帮你理解位操作。
1.验证奇偶数
在第2节里我提到了奇偶数其实也是余数的应用。编程中我们也可以用位运算来判断奇偶数。
仔细观察你会发现偶数的二进制最后一位总是0而奇数的二进制最后一位总是1因此对于给定的某个数字我们可以把它的二进制和数字1的二进制进行按位“与”的操作取得这个数字的二进制最后一位然后再进行判断。
我这里写了一段代码比较了使用位运算和模运算的效率我统计了进行1亿次奇偶数判断使用这两种方法各花了多少毫秒。如果在你的机器上两者花费的时间差不多你可以尝试增加统计的次数。在我的机器上测试下来同样次数的奇偶判断使用位运算的方法耗时明显更低。
public class Lesson1_append1 {
public static void main(String[] args) {
int even_cnt = 0, odd_cnt = 0;
long start = 0, end = 0;
start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
if((i & 1) == 0){
even_cnt ++;
}else{
odd_cnt ++;
}
}
end = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(even_cnt + " " + odd_cnt);
even_cnt = 0;
odd_cnt = 0;
start = 0;
end = 0;
start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
if((i % 2) == 0){
even_cnt ++;
}else{
odd_cnt ++;
}
}
end = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(even_cnt + " " + odd_cnt);
}
}
2.交换两个数字
你应该知道要想在计算机中交换两个变量的值通常都需要一个中间变量来临时存放被交换的值不过利用异或的特性我们就可以避免这个中间变量具体的代码如下
x = (x ^ y);
y = x ^ y;
x = x ^ y;
把第一步代入第二步中可以得到
y = (x ^ y) ^ y = x ^ (y ^ y) = x ^ 0 = x
把第一步和第二步的结果代入第三步中可以得到
x = (x ^ y) ^ x = (x ^ x) ^ y = 0 ^ y = y
这里用到异或的两个特性第一个是两个相等的数的异或为0比如x^x= 0第二个是任何一个数和0异或之后还是这个数不变比如0^y=y。
3.集合操作
集合和逻辑的概念是紧密相连的因此集合的操作也可以通过位的逻辑操作来实现
假设我们有两个集合{1, 3, 8}{4, 8}我们先把这两个集合转为两个8位的二进制数从右往左以1到8依次来编号
如果某个数字在集合中相应的位置1否则置0那么第一个集合就可以转换为10000101第二个集合可以转换为10001000那么这两个二进制数的按位与就是10000000只有第8位是1代表了两个集合的交为{8}而这两个二进制数的按位或就是10001101第8位第4位第3位和第1位是1代表了两个集合的并为{1, 3, 4, 8}
说到这里不禁让我想起Elasticsearch的BitSet我曾经使用Elasticsearch这个开源的搜索引擎来实现电商平台的搜索
当时为了提升查询的效率我使用了Elasticsearch的Filter查询我研究了一下这个Filter查询的原理发现它并没有考虑各种文档的相关性得分因此它可以把文档匹配关键字的情况转换成了一个BitSet
你可以把BitSet想成一个巨大的位数组每一位对应了某篇文档是否和给定的关键词匹配如果匹配这一位就置1否则就置0每个关键词都可以拥有一个BitSet用于表示哪些文档和这个关键词匹配那么要查看同时命中多个关键词的文档有哪些就是对多个BitSet求交集利用上面介绍的按位与这点是很容易实现的而且效率相当之高
二分查找时的两个细节
第3节我介绍了迭代法并讲解了相关的代码实现其中有两个细节我在这里补充说明一下
第一个是关于中间值的计算我优化了两处代码分别是Lesson3_2的第16行和Lesson3_3的第22行
其中Lesson3_2的第16行由原来的
double middle = (min + max) / 2;
改为
double middle = min + (max - min) / 2;
Lesson3_3的第22行由原来的
int middle = (left + right) / 2;
改为
int middle = left + (right - left) / 2;
这两处改动的初衷都是一样的是为了避免溢出在第一篇加餐中介绍负数的加法时我已经解释了什么是溢出那这里为什么会发生溢出呢我以第二处代码为例来讲解下
从理论上来说(left+right)/2=left+(right-left)/2。可是我们之前说过计算机系统有自身的局限性无论是何种数据类型都有一个上限或者下限。一旦某个数字超过了这些限定就会发生溢出。
对于变量left和right而言在定义的时候都指定了数据类型因此不会超出范围可是left+right的和就不一定了从下图可以看出当left和right都已经很接近某个数据类型的最大值时两者的和就会超过这个最大值发生上溢出这也是为什么最好不用通过(left+right)/2来求两者的中间值
那么为什么left + (right -left)/2就不会溢出呢首先right是没有超过最大值的那么(right -left)/2自然也就没有超过范围即使left加上了(right -left)/2也不会超过right的值所以运算的整个过程都不会产生溢出
第二个是关于误差百分比和绝对误差在Lesson3_2中有这么一行
double delta = Math.abs((square / n) - 1);
这里我使用了误差的百分比也就是误差值占输入值n的比例其实绝对误差也是可以的不过我在这里考虑了n的大小比如如果n是一个很小的正整数比如个位数那么误差可能要精确到0.00001但是如果n是一个很大的数呢比如几个亿那么精确到0.00001可能没有多大必要也许精确到0.1也就可以了所以使用误差的百分比可以避免由于不同的n导致的迭代次数有过大差异
由于这里n是大于1的正整数所以可以直接拿平方值square去除以n否则我们要单独判断n为0的情况并使用绝对误差
关于迭代法数学归纳法和递归
从第3节到第6节我连续介绍了迭代法数学归纳法递归这些概念之间存在相互联系又不完全一样很多同学对此也有一些疑惑所以这里我来帮你梳理一下
迭代法和递归都是通过不断反复的步骤计算数值或进行操作的方法迭代一般适合正向思维而递归一般适合逆向思维而递归回溯的时候也体现了正向递推的思维它们本身都是抽象的流程可以有不同的编程实现
对于某些重复性的计算数学归纳法可以从理论上证明某个结论是否成立如果成立它可以大大节约迭代法中数值计算部分的时间不过在使用数学归纳法之前我们需要通过一些数学知识假设命题并证明该命题成立
对于那些无法使用数学归纳法来证明的迭代问题我们可以通过编程实现这里需要注意的是广义上来说递归也是迭代法的一种不过在计算机编程中我们所提到的迭代是一种具体的编程实现是指使用循环来实现的正向递推而递归是指使用函数的嵌套调用来实现的逆向递推当然两种实现通常是可以相互转换的
循环的实现很容易理解对硬件资源的开销比较小不过循环更适合单线剧情例如计算2^nn!1+2+3++n等等而对于存在很多分支剧情的复杂案例而言使用递归调用更加合适
利用函数的嵌套调用递归编程可以存储很多中间变量我们可以很轻松地跟踪不同的分支而所有这些对程序员基本是透明的如果这时使用循环我们不得不自己创建并保存很多中间变量当然正是由于这个特性递归比较消耗硬件资源
递归编程本身就体现了分治的思想这个思想还可以延伸到集群的分布式架构中最近几年比较主流的MapReduce框架也体现了这种思想
综合上面说的几点你可以大致遵循这样的原则
如果一个问题可以被迭代法解决而且是有关数值计算的那你就看看是否可以假设命题并优先考虑使用数学归纳法来证明
如果需要借助计算机那么优先考虑是否可以使用循环来实现如果问题本身过于复杂再考虑函数的嵌套调用是否可以通过递归将问题逐级简化
如果数据量过大可以考虑采用分治思想的分布式系统来处理
最后给你留一道思考题吧
在1到n的数字中有且只有唯一的一个数字m重复出现了其它的数字都只出现一次请把这个数字找出来提示可以充分利用异或的两个特性
好了前面6讲的补充内容就到这里了欢迎你留言给我你也可以点击请朋友读把今天的内容分享给你的好友和他一起精进