优秀程序员不得不知道的20个位运算技巧 优化
一提起位运算,人们往往想到它的高效性,无论是嵌入式编程还是优化系统的核心代码,适当的运用位运算总是一种迷人的手段,或者当您求职的时候,在代码中写入适当的位运算也会让您的程序增加一丝亮点,最初当我读《编程之美》求“1的数目”时,我才开始觉得位运算是如此之美,后来读到 《Hacker's Delight》,感慨到Henry S.Warren把位运算运用的如此神出鬼没,很多程序都十分精妙,我觉得在一个普通的程序中大量运用这样的代码的人简直是疯了!但掌握简单的位运算技巧还是必要的,所以今天写这篇博文把我积累的一些位运算技巧分享给大家,这些技巧不会是如求“1的数目”的技巧,是最基本的一行位运算技巧!
Welcome To My BitTricks
1.获得int型最大值
-
int getMaxInt(){
-
return (1 << 31) - 1;//2147483647, 由于优先级关系,括号不可省略
-
}
另一种写法
-
int getMaxInt(){
-
return ~(1 << 31);//2147483647
-
}
另一种写法
-
int getMaxInt(){//有些编译器不适用
-
return (1 << -1) - 1;//2147483647
-
}
C语言中不知道int占几个字节时候
-
int getMaxInt(){
-
return ((unsigned int) - 1) >> 1;//2147483647
-
}
2.获得int型最小值
-
int getMinInt(){
-
return 1 << 31;//-2147483648
-
}
另一种写法
C语言版
-
long getMaxLong(){
-
return ((long)1 << 127) - 1;//9223372036854775807
-
}
4.乘以2运算
-
int mulTwo(int n){//计算n*2
-
return n << 1;
-
}
5.除以2运算
-
int divTwo(int n){//负奇数的运算不可用
-
return n >> 1;//除以2
-
}
6.乘以2的m次方
-
int divTwoPower(int n,int m){//计算n/(2^m)
-
return n >> m;
-
}
8.判断一个数的奇偶性
-
boolean isOddNumber(int n){
-
return (n & 1) == 1;
-
}
9.不用临时变量交换两个数(面试常考)
C语言版
-
void swap(int *a,int *b){
-
(*a) ^= (*b) ^= (*a) ^= (*b);
-
}
通用版(一些语言中得分开写)
-
a ^= b;
-
b ^= a;
-
a ^= b;
10.取绝对值(某些机器上,效率比n>0 ? n:-n 高)
-
int abs(int n){
-
return (n ^ (n >> 31)) - (n >> 31);
-
/* n>>31 取得n的符号,若n为正数,n>>31等于0,若n为负数,n>>31等于-1
-
若n为正数 n^0=0,数不变,若n为负数有n^-1 需要计算n和-1的补码,然后进行异或运算,
-
结果n变号并且为n的绝对值减1,再减去-1就是绝对值 */
-
}
11.取两个数的最大值(某些机器上,效率比a>b ? a:b高)
通用版
-
int max(int a,int b){
-
return b & ((a-b) >> 31) | a & (~(a-b) >> 31);
-
/*如果a>=b,(a-b)>>31为0,否则为-1*/
-
}
C语言版
-
int max(int x,int y){
-
return x ^ ((x ^ y) & -(x < y));
-
/*如果x<y x<y返回1,否则返回0,
-
、 与0做与运算结果为0,与-1做与运算结果不变*/
-
}
12.取两个数的最小值(某些机器上,效率比a>b ? b:a高)
通用版
-
int min(int a,int b){
-
return a & ((a-b) >> 31) | b & (~(a-b) >> 31);
-
/*如果a>=b,(a-b)>>31为0,否则为-1*/
-
}
C语言版
-
boolean isSameSign(int x, int y){ //有0的情况例外
-
return (x ^ y) >= 0; // true 表示 x和y有相同的符号, false表示x,y有相反的符号。
-
}
14.计算2的n次方
-
int getFactorialofTwo(int n){//n > 0
-
return 2 << (n-1);//2的n次方
-
}
15.判断一个数是不是2的幂
-
boolean isFactorialofTwo(int n){
-
return n > 0 ? (n & (n - 1)) == 0 : false;
-
/*如果是2的幂,n一定是100... n-1就是1111....
-
所以做与运算结果为0*/
-
}
16.对2的n次方取余
-
int quyu(int m,int n){//n为2的次方
-
return m & (n - 1);
-
/*如果是2的幂,n一定是100... n-1就是1111....
-
所以做与运算结果保留m在n范围的非0的位*/
-
}
17.求两个整数的平均值
-
int getAverage(int x, int y){
-
return (x + y) >> 1;
-
}
另一种写法
-
int getAverage(int x, int y){
-
return ((x ^ y) >> 1) + (x & y);
-
/*(x^y) >> 1得到x,y其中一个为1的位并除以2,
-
x&y得到x,y都为1的部分,加一起就是平均数了*/
-
-
}
下面是三个最基本对二进制位的操作
18.从低位到高位,取n的第m位
-
int setBitToOne(int n, int m){
-
return n | (1 << (m-1));
-
/*将1左移m-1位找到第m位,得到000...1...000
-
n在和这个数做或运算*/
-
}
20.从低位到高位,将n的第m位置0
-
int setBitToZero(int n, int m){
-
return n & ~(1 << (m-1));
-
/* 将1左移m-1位找到第m位,取反后变成111...0...1111
-
n再和这个数做与运算*/
-
}
另附一些对程序效率上没有实质提高的位运算技巧,一些也是位运算的常识(面试也许会遇到)
计算n+1
==================================================================================================
作者:nash_ 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:http://blog.csdn.net/nash_/article/details/8262185
--------------------- 作者:nash_ 来源:CSDN 原文:https://blog.csdn.net/zmazon/article/details/8262185?utm_source=copy 版权声明:本文为博主原创文章,转载请附上博文链接!
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication20 { class Program { static void Main(string[] args) { //1 for 循环优化 string str = "asdfghjkl"; for (int i = 0; i < str.Length; i++) { Console.WriteLine(str[i]); } Console.WriteLine("----"); for (int i = 0, len = str.Length; i < len; i++) { Console.WriteLine(str[i]); } //二者唯一的不同在于后者用len变量将字符串s的长度保存了,在条件判断时直接将i与len比较。第二种方法用一个额外变量len避免了每次条件判断都要重复执行函数strlen(s),而执行该函数是非常耗时的(假设字符串的长度为n,函数执行的复杂度为O(n)),尤其是当for循环体的语句比较少,字符串比较长的时候。在很多leetcode题目中,两种不同的写法需要的运行时间相差巨大。 //同样在C++、Java中,这种写法for (int i = 0; i < s.length(); i++),也是不值得推荐的,尽管C++编译时期有的编译器会将length()函数用内联或者一个确定的变量来替代,Java也会将其用“属性”来替代,但我仍然倾向于使用后者。 //在Python的语法中,for循环用这种方式来表示: for i in range(len(s)) //这就避免了重复去求字符串s的长度,这种方法既有语义感,又获得了高性能。 //2 变量定义位置(for循环内部还是外部) for (int i = 0; i < str.Length; i++) { string s = str[i].ToString(); //... } //外部 string ss = ""; for (int i = 0; i < str.Length; i++) { ss = str[i].ToString(); // ... } //如果定义在内部,每次循环都要重新定义string变量s,意味着每次循环都要调用构造和析构函数;而定义在外部每次循环只需要调用复制构造函数。一般建议将大的对象定义到外部,提高运行效率,把小的对象定义在里面,提高程序可读性。 // 1.在乘以2(或2的整数次幂)或除以2(或2的整数次幂)的时候尽量用位运算来替代。 n >> 1 n << 1 //2.尽量减少使用除法运算(可以适当转换为乘法,如条件判断时将if (a == b / c)替换为if (a * c == b)。除法运算需要更多的移位和转换操作,往往需要的时间是相应乘法的两倍) //3.多使用+=、-=、*=、/=等复合运算符,以加一为例,效率由高到低是(i++ 、 i += 1 、 i = i + 1) //4.多掌握一些小巧的库函数,例如:swap, max, min, sort, qsort, ati, stoi...它们用起来方便,效率更是比一般人写的代码高。 Console.WriteLine(4 >> 1); Console.WriteLine(4 << 1); //inline让函数内联,建议编译器将函数体代码“复制粘贴”到函数调用处,在函数体短小,函数调用又比较频繁的时候能有效避免因函数调用带来的内存开销(因为每一次调用函数系统都会生成许多额外的变量)。 //const不仅仅可以保证其修饰的变量不被修改,提高程序的稳定性,同时也让编译器更好地为我们优化代码。举个例子:我们如果用const修饰某一个常量,那么程序中所有用到该常量的地方都会用其值来代替,这样就避免了读取其地址而浪费时间。 //&修饰返回值类型和参数类型表示采取引用的方式传递,避免了对象赋值构造所需的时间和内存。 //缓存(cache)和寄存器(register) //除了CPU,就是寄存器和缓存的访问速度最高了,一般不建议我们自己定义寄存器变量和控制数据缓存,编译器会自动帮我们把经常用到的一些数据放到缓存和寄存器中。但是,了解一些编译器控制数据的依据对编程也有极大帮助。一般来说,放到寄存器/缓存的数据优先级为:用register修饰的变量,循环控制变量,auto局部变量,静态变量,用户自己分配的内存数据。 //访问容器中元素的时候尽量使用迭代器而不是下标或者指针。在刚从C语言转到C++的那段时间,我非常不适应迭代器的用法,总觉得下标访问多好,与for循环搭配在一起简直是无敌的存在。后来我才慢慢发掘出迭代器的众多优势。首先,迭代器访问元素类似与指针,相对于下标访问不用根据下标值计算地址,这在循环中能够节省不少时间。其次,迭代器作为指针一种延拓,能更好的代表并操作其所指的对象,而在下标访问中我们往往用一个int值pos来表示pos下标下的元素,没有面向对象编程的直观。再次,迭代器为我们访问各种容器(数组,vector,list,map,queue,deque,set …)中的元素提供了统一的方法,其作用类似于“语法糖”,让编程更加简单、方便。 //2.另外在使用迭代器的自增和自减运算符需要注意,iterator++,和++iterator的效率有天壤之别。两种自增方式的运算符重载如下: //iterator & operator++() //{ // 前增 // ++*this; // return (*this); //} //iterator operator++(int) //{ // 后增 // iterator temp = *this; // ++*this; // return (temp); //} //后增(iterator++)相对于前增(++iterator)创建了一个临时迭代器temp,并将其返回,而前增直接返回原来迭代器的引用。在for循环中的频繁自增操作中,创建临时迭代器temp,以及返回temp时调用的复制构造函数所需的时间不容忽视。 // vector容器毫无疑问是C++STL使用最为频繁的容器了,当然这个强大容器的使用也包含了很多的小技巧。 //1.在适当时候使用emplace和emplace_back函数来替代insert和push_back函数。它们之间的区别很明显,insert和push_back函数参数是vector容器里面的模板对象,而带emplace的函数参数是模板对象的构造函数的参数,这意味着后者将模板对象插入到vector容器的过程中不用先生成好对象,而是可以直接利用参数构造。当然如果模板对象已经是生成好的,那就没有必要用emplace函数了。在很多循环递归迭代中,往往需要反复向vector容器中添加对象,这时候额外构造一个对象所需要的时间和空间就不容忽视了,因此这是一个vector进阶用法的好trick。 //2.vector容器的底层实现是数组,并且在当元素大于最大容量的时候会重新生成一个更大的数组,将原来数组中的对象复制构造到新数组中。由于要重新分配大量内存以及反复调用复制构造函数,这对时间和空间的开销是巨大的。为了减少内存的重新分配,我们可以适当的估计我们需要保存的元素数量,并在vector初始化的时候指定其capacity。这种方法很直接但也有其缺点,就是我们往往很难在开始的时候就估计准确我们要保存的元素数量(如果能,我们就直接用数组得了)。一个很好的解决办法是:将vector中保存的元素改为指针,指针指向我们真正想要保存的对象。由于指针相对于其所指向的对象来说占用内存很小,而且在复制的时候不用调用复制构造函数,因此以上提到的一些缺点都能很好的克服。事实上,对于能够熟练控制内存分配的老码农来说,这种vector + 指针的方式是十分完美的。 // 在进入讨论之前,我们先思考下面这个例子: //一个班的数学成绩如下:74、76、78、94、97、68、77、65、54、89…,总共有50个数据。要求用程序将分数为优秀(>=80)、良好(>=70)、及格(>=60)、不及格(>=0)四个分数段。 //for 所有学生分数 // if 分数 < 60 // 归为不及格段 // else if 分数 < 70 // 归为及格段 // else if 分数 < 80 // 归为良好段 // else // 归为优秀段 //这个伪代码逻辑没有问题,但是就这个数据来看这段代码运行效率糟透了。由于这个班的数学成绩绝大多数是良好和优秀,而这个程序需要三次if判断才能将分数归为良好,三次if判断加上一个else才能将分数归为优秀,所以绝大多数前两个if判断是不必要的。我们将if判断语句的顺序变换下: //for 所有学生分数 // if 分数 >= 80 // 归为优秀段 // else if 分数 >= 70 // 归为良好段 // else if 分数 >= 60 // 归为及格段 // else // 归为不及格段 //在这个伪代码中绝大多数分数都在前两个if语句中完成了分段。两者的时间效率相差巨大,实际运行也发现,前者是后者运行时间的两倍多。 //switch语句的底层实现主要有三种方式:转换为if else 语句,跳转表,树形结构。当分支比较小时,编译器倾向于转换为if else语句,当分支比较多,分支范围很广时,用树形结构,当分支数量不算多,分支范围紧凑时,用跳转表。跳转表的底层实现是数组映射,对条件转换的效率为O(1),相比于另外两种方式优势明显,因此我们应该尽量控制分支的数量,以及让各个分支的int型数据紧凑。 //Console.WriteLine(""); //for (int i = 0; !str[i]; i++) //{ // Console.WriteLine(str[i]); //} int[] arr = { 1, 3, 5, 9, 7, 8, 10 }; for (int i = 0, item = 0; item == arr[i++]; ) { Console.WriteLine(item); } } } }