【数据结构与算法(十二)】——位运算

新的一周

位运算

位运算是把数字用二进制表示之后,对每一位上0或1的运算。对于右移运算,如果数字是一个无符号数字,则用0填补最左边的n位;如果数字是一个有符号数字,则如果数字原先是一个正数,则右移之后用0填补左边的n位,如果数字原先是一个负数,则右移之后用1填补左边的n位

题目

二进制中1的个数

请实现一个函数,输入一个整数,输出该二进制表示中“1”的个数。例如,9=1001(2),有2位是1。因此,如果输入9,则函数输出的是2

思路

可能引起死循环的解法

1、判断整数二进制表示中最右边一位是不是1
2、把输入的整数右移以为,此时原来处于从右边数起的第二位被移到了最右边,再判断是不是1
3、这样每次移动一位,直到整个整数变成0为止。
4、所以怎么判断最右边一位是不是1?把移位后的数与1作与运算,如果结果是1,表示最右边一位是1;如果结果是0,表示最右边的一位是0
5、分析这种做法为什么会引起死循环?如果函数的输入是一个负数,那么当右移的时候,每次在左边补位的都是1,到最后得到0xFFFFFFFF,这样就会陷入死循环了,所以要在以上做法的基础上进行改动

常规的做法

1、为了避免死循环,就不要右移输入的数字了。
2、把n和1做与运算,判断最低位是不是1;之后把1左移一位,再和n做与运算判断次低位是不是1……

int NumberOf1(int n)
{
    int count = 0;
    unsigned int flag = 1;
    //循环的次数等于整数二进制的位数
    while (flag) {
        if (n&flag)
            count++;
        flag <<= 1;
    }
    return count;
}

更好的做法

1、把整数减去1,就是把最右边的1变为0.如果它右边还有0,则所有的0都变成1,而它左边的所有位都保持不变。
2、把一个整数和它减去1的结果做与&运算,相当于把它最右边的1变成0.
3、以“1100”为例,它减去1的结果是1011,之后再与原来的1100做与运算得到1000。也就是把1100最右边的1变成了0
4、所以我们要计算有多少个1,就是看能有多少个这样的操作

int Count1(int n)
{
    int count = 0;
    while (n) {
        ++count;
        n = (n - 1)&n;
    }
    return count;
}
//测试的时候,要用正数、负数和0测试

后记

1、用一条语句判断一个整数是不是2的整数次方。首先要知道一个整数如果是2的整数次方,那么它的二进制只有一个1,其他位都是0。就可以使用上面的思路了,进行一次运算,把该整数减去1后得到的数与原整数相与,如果等于0,那么就是只有一个1
2、输入整数m和n,计算需要改变m的二进制中的多少位才能得到n。只需要两步:第一步就是求这两个整数的异或(看二进制哪几个位不一样);第二步就是计算异或得到的结果中有几个1
3、常用的思路:把一个整数减去1之后再和原来的整数做位与运算,得到的结果相当于把整数的二进制表示中最右边的1变成0

数组中数字出现的次数【难,但有趣】

一个整数数组里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)
1、从头到尾依次以后数组中的每个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果,因为其他数字都出现了两次,在异或中全部抵消了,如a&b&c&a&c&d=a&a&c&c&b&d=0&0&b&d=b&d a&b&c&a&c&d=a&a&c&c&b&d=0&0&b&d=b&d
2、由于两个数字不一样,所以异或得到的结果中至少有一个1,接下来我们需要找到结果数字中第一个1的位置,记为第i位
3、以第i位为标准把数组分为两个子数组,第一个数组中每个数字的第i位都是1,第二个数组中每个数字的第i位都是0,这样两个只出现一次的数字分别被分配到了两个子数组中,也就是说每个子数组中只有一个出现一次的数字
4、之后就是对两个子数组,从头到尾一次进行异或了,最后得到的结果就是子数组中只出现一次的数字

//找到第一个1的位置,用传统的做法
unsigned int FindFirstBitIs1(int n)
{
    int indexBit = 0;
    while (((n & 1) == 0) && (indexBit < 8 * sizeof(int))) {
        n = n >> 1;
        indexBit++;
    }
}
//判断第indexBit位是不是1
bool isBit(int num, unsigned int indexBit)
{
    num = num >> indexBit;//将第一个1的位置移到最右边
    return (num & 1);
}
//num1,num2是返回的两个只出现一次的数字
void FindAppearOnce(int data[], int length, int* num1, int* num2)
{
    if (data == nullptr || length == 0)//判断数组是否存在以及存在的话是否是空数组
        return;

    int resultExclusiveOR = 0;
    for (int i = 0; i < length; i++)
        resultExclusiveOR ^= data[i];   //^异或符号
    unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR);
    *num1 = 0;
    *num2 = 0;
    //没有真正分为两个子数组,只是进行判断,之后选择处理方式而已
    for (int i = 0; i < length; i++) {
        if (isBit(data[i], indexOf1))
            *num1 ^= data[i];
        else
            *num2 ^= data[i];
    }
}

数组中唯一只出现一次的数字

在一个数组中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字

思路

1、如果一个数字出现3次,那么它的二进制表示的每一位(0或者1)也出现3次。如果把所有出现3次的数字的二进制表示的每一位都分别加起来,那么每一位的和都能被3整除
2、把数组中所有数字的二进制表示的每一个都加起来。如果某一位的和能被3(10进制的3)整除,那么那个只出现一次的数字二进制表示中对应的那一位是0;否则就是1

#include<exception>
int FindAppearOnceOf3(int data[], int length)
{
    if (data == nullptr || length == 0)
        throw new std::exception("Invalid input!");
    int bitSum[32] = { 0 };//数组中的每一位都是0
    for (int i = 0; i < length; i++)
    {
        int bitMask = 1;
        for (int j = 31; j >= 0; j--) {//从最右边那一位开始,计算所有数每一位的和
            int bit = data[j] & bitMask;
            if (bit != 0)
                bitSum[j]++;
            bitMask = bitMask << 1;
        }
    }
    int result = 0;//result是用来记录每次计算并新加、移位后得到的结果
    for (int i = 0; i < 32; i++) {//从最左边那一位最高位开始计算最终结果
        result = result << 1;   
        //到这一步result的最后一位总是0,所以不可以先加再移位
        //因为最后一步的时候会使得结果比原来结果多移了一位
        result += bitSum[i] % 3;
    }
    return result;
}

不用加减乘除做加法

写一个函数,求两个整数之和,要求在函数体内不得使用“+、-、*、/”四则运算符号

思路

1、从小学的加法计算入手,“三步走”策略(相加—进位—再相加)
2、把二进制的加法用位计算来代替
3、第一步:不考虑进位对每一位进行相加,这与异或运算是一样的:00:0;11:0;10:1;01:1
4、第二步:考虑进位,只有1+1时才有进位,进位可以想象为两个数先做位与运算,之后左移一位。
5、第三步:把前面两个步骤的结果相加,其实还是重复前面两步

int AddWithBit(int num1, int num2)
{
    int sum, carry;
    //为什么要使用循环,因为要将进位加上
    //这个“加”和两个数相加用的是一样的方法,直到没有进位为止
    //想象一下刚学加法时画的图做的运算
    do {
        sum = num1 ^ num2;
        carry = (num1&num2) << 1;//进位,只有11才需要进位,所以这个使用与&运算
        num1 = sum;
        num2 = carry;
    } while (num2 != 0);
    return sum;
}

附:不使用新的变量,交换两个变量的值

基于加减法基于异或运算
a=a+b;a=a^b;
b=a-b;b=a^b;
a=a-b;a=a^b;
点赞