本文整理了LeetCode中 程序员面试金典(第 6 版)的练习题的第五章的题目。
给定两个整型数字 N 与 M,以及表示比特位置的 i 与 j(i <= j,且从 0 位开始计算)。
编写一种方法,使 M 对应的二进制数字插入 N 对应的二进制数字的第 i ~ j 位区域,不足之处用 0 补齐。具体插入过程如图所示。
二进制 运算
class Solution {
public:
int insertBits(int N, int M, int i, int j) {
for(int k = i; k < j + 1; ++k){
N &= ~(1 << k);
}
M <<= i;
return N + M;
}
};
二进制数转字符串。给定一个介于 0 和 1 之间的实数(如 0.72),类型为 double,打印它的二进制表达式。如果该数字无法精确地用 32 位以内的二进制表示,则打印“ERROR”。
本题对应另一个很常见的问题:
现有 n 种面额的金币各一个,想买一个价格为 m 的物品,能否恰好凑齐(不找零钱)?
思路:只需按照从大到小的顺序,依次凑即可。(当然这也可以对应到背包问题中)
举个例子:
金币面额 1 2 4 8 16
物品价格 25
面额从大到小遍历, 16 8 1 这三个面额正好可以凑够 25。10011(二进制表示取或不取)。
思考:为什么需要从大到小遍历?从小到大遍历可以吗?
答案:只能够从大到小遍历。从大到小遍历才可以避免借位的问题。同样拿上述例子,如果从小到大遍历,25 比 1 大,故要,25-1=24(最结果上来看也确实需要)。24 比 2 大,故要,24-2=22(已经发生问题,从结果上来看,并未需要 2,但是由于可以从更高位借值,同样也是减去)。而从大到小遍历则不会发生该问题。
class Solution {
public:
string printBin(double num) {
string ans("0.");
double tmp = 1;
//从大到小枚举 30位 浮点数二进制:
// 0. 1 1 1 1 ...
// 0.5 0.25 0.125 0.0625 ...
for(int i = 0; i < 30; ++i){
tmp /= 2;
if(num >= tmp){
// 拥有当前二进制位
ans += '1';
num -= tmp;
}else{
//
ans += '0';
}
}
// 如果可以表示,最后num一定是为 0
if(num != 0) return string("ERROR");
// 去除后导零
while(ans.back() == '0') ans.pop_back();
return ans;
}// end printBin
}; // end class
给定一个 32 位整数 num
,你可以将一个数位从 0 变为 1。请编写一个程序,找出你能够获得的最长的一串 1 的长度。
暴力修改每个位置,将其变为 1。针对每一种可能计算每个数的最长连续 1 的个数。
class Solution {
public:
int reverseBits(int num) {
short mx = 0;
// 利用机会将位置 i 变成 1 (不管是否本来就是1)
for(int i = 0; i < 32; ++i){
int tmp = num | 1 << i;
// 计算 tmp 的最长1的长度
short cnt = 0; short c[32] = {0};
c[0] = tmp & 1; cnt = max(cnt , c[0]);
for(int j = 1; j < 32; ++j){
// 以j为最后一个字符 最长连续1的长度
c[j] += tmp >> j & 1 ? 1 + c[j - 1] : 0;
// tmp中最长的连续1的长度
cnt = max(cnt , c[j]);
}
//所有尝试修改后,最长连续1的长度
mx = max(mx, cnt);
}
return mx;
}// end reverseBits
}; // end class
下一个数。给定一个正整数,找出与其二进制表达式中 1 的个数相同且大小最接近的那两个数(一个略大,一个略小)。
本题还是挺复杂的。
为了更方便处理,可以先将整数 x 转化为二进制字符串,(该字符串的低位表示该整数的低位),在字符串上进行处理。
思路
为了找到略大的数字:不难想到,在二进制串中,从低到高中找到第一个 10,把这两个值交换后,显然数字变大了。这就足够了吗?数字确实是变大了,但却不是第一个大于 x 的值。此时,可以发现如果将上述 1 位置处左侧的 0 都提前,可以发现数字变小了,但还是大于 x 的。
同理,为了找到略小的数字:在二进制串中,从低到高中找到第一个 01,把这两个值交换后,此时显然数字变小。但还不够,继续将上述 0 位置处左侧的 1 都提前,此时数字变大,但还是小于 x。
下面的代码中有详细的注释。
class Solution {
public:
// 逆序的二进制字符串转为int
int binstr2int(string s){
int ans = 0;
for_each(s.rbegin(), s.rend(), [&](char ch){
ans <<= 1;ans += ch - '0';
});
return ans;
}
vector<int> findClosedNumbers(int num) {
vector<int>ve{-1,-1};
if(num == INT32_MAX) return ve;
// 获取31位的二进制表示
string binary("");
do{
binary += num % 2 + '0';
num /= 2;
}while(num > 0);
while(binary.size() != 31) binary += '0';
// find the bigger one
int cnt_0 = 0;// 统计在找到10之前有多少0
string tmp(binary);
for(int i = 0; i + 1 < tmp.size(); ++i){
//发现可以提高的位置
if(tmp[i] == '1' && tmp[i + 1] == '0') {
//将i处1提高一位(一定是增大num)
swap(tmp[i + 1], tmp[i]);
//将i处左侧所有的0提前 (在增大的num的前提下,尽可能减少num )
for(int j = i - 1; j >= 0; --j){
if(cnt_0 > 0) tmp[j] = '0';
else tmp[j] = '1';
cnt_0 -= !(tmp[i] - '0');
}
//赋值
ve[0] = binstr2int(tmp);
break;
}
cnt_0 += !(tmp[i] - '0');
}
//find the smaller one
int cnt_1 = 0; // 统计在找到01之前有多少1
for(int i = 0; i + 1 < binary.size(); ++i){
//发现可以减少的位置
if(binary[i] == '0' && binary[i + 1] == '1'){
//将i+1处1向左移动一位(一定是减少num)
swap(binary[i], binary[ i + 1]);
// 将i位置之前的1都提到前面(在减少num的情况下,尽可能增大num)
for(int j = i - 1; j >= 0; --j){
if(cnt_1 > 0) binary[j] = '1';
else binary[j] = '0';
cnt_1 -= binary[j] - '0';
}
ve[1] = binstr2int(binary);
break;
}
cnt_1 += binary[i] - '0';
} // end find smaller one
return ve;
}// end findClosedNumbers
}; // end class
整数转换。编写一个函数,确定需要改变几个位才能将整数 A 转成整数 B。
A 和 B 不相等的位置都需要改变。通过 C= A^B 可以得到不相同的位置。求 C 中包括几个 1 即可。
class Solution {
public:
int convertInteger(int A, int B) {
// __builtin_popcount(x) 为内置函数,返回x的二进制中1的个数。
return (int)__builtin_popcount(A ^ B);
}
};
配对交换。编写程序,交换某个整数的奇数位和偶数位,尽量使用较少的指令(也就是说,位 0 与位 1 交换,位 2 与位 3 交换,以此类推)。
num
的范围在[0, 2^30 - 1]之间,不会发生整数溢出。该题需要用到 magic number
二进制数的奇数位和偶数位可以通过十六进制表示。如此一个数的所有奇数位可以直接获得,同理所有偶数位也可获得。
随后通过移位可以获得最后答案。
下面代码对整个过程进行详细描述。
class Solution {
public:
int exchangeBits(int num) {
// magic number 的构造过程
// even 偶数的构造
// 0101 0101 0101
// 5 5 5
// odd 奇数的构造
// 1010 1010 1010
// a a a
int ans = 0;
// 交换并赋值给ans
// odd
// cout << "odd" << (num & 0x55555555) << "\n";
ans |= (num & 0x55555555) << 1;
// even
// cout << "even" << (num & 0xaaaaaaaa) << "\n";
ans |= (num & 0xaaaaaaaa) >> 1;
return ans;
}
};
绘制直线。有个单色屏幕存储在一个一维数组中,使得 32 个连续像素可以存放在一个 int 里。屏幕宽度为 w,且 w 可被 32 整除(即一个 int 不会分布在两行上),屏幕高度可由数组长度及屏幕宽度推算得出。请实现一个函数,绘制从点(x1, y)到点(x2, y)的水平线。
给出数组的长度 length,宽度 w(以比特为单位)、直线开始位置 x1(比特为单位)、直线结束位置 x2(比特为单位)、直线所在行数 y。返回绘制过后的数组。
示例 1:
输入:length = 1, w = 32, x1 = 30, x2 = 31, y = 0
输出:[3]
说明:在第 0 行的第 30 位到第 31 为画一条直线,屏幕表示为[0b000000000000000000000000000000011]
首先题目一定要能够看懂。题意描述的还不够清晰。
思路:
最直观的想法:将属于第 y 行的第一个数字作为起点开始遍历,如果当前数字的某个二进制位在 x1 和 x2 的范围内,说明该二进制位需要修改为 1,否则修改为 0。如此遍历到第 y 行的最后一个数字。
class Solution {
public:
vector<int> drawLine(int length, int w, int x1, int x2, int y) {
vector<int>ans(length, 0);
// cur 遍历第y行的 各个数字,每个数字32位。
// curInter 记录在第y行中,总共访问过多少二进制位。
for(int cur = y * w / 32, curInter = 0; cur < (y + 1) * w / 32; ++cur){
int val = 0;
//遍历当前数字的32个二进制位
for (int j = 0; j < 32; ++j){
// 当前数字的第j个二进制位 在起点和终点范围内,需要修改
if(curInter + j >= x1 && curInter + j <= x2) {
val |= 1 << (31 - j);
}
}
curInter += 32; // 准备迭代下一个数字
ans[cur] = val; // 赋值,cur指向的是数组中的第cur个数字
}
return ans;
}// end drawLine
};// end class
第二种解法:
只需遍历第 y 行的在 x1 和 x2 范围内的二进制,将其赋值给对应数组中的值。
class Solution {
public:
vector<int> drawLine(int length, int w, int x1, int x2, int y) {
vector<int>ans(length, 0);
// 遍历 直线起点和终点之间的二进制位置
for (int p = x1; p <= x2; ++p){
// y * w / 32: 获取第y行第一个数字的在数组中的位置
// p / 32 : 获取当前二进制位 属于第y行的第几个数字
// p % 32 : 表示当前二进制位 位于当前数字的第几位二进制
ans[y * w / 32 + p / 32] |= 1 << (31 - p % 32);
}
return ans;
}
};