目录
[编辑 算法分析](#编辑 算法分析)
[只出现一次的数字 II](#只出现一次的数字 II)
[只出现一次的数字 III](#只出现一次的数字 III)
[面试题 17.19. 消失的两个数字](#面试题 17.19. 消失的两个数字)
在前面,已经讲解了位运算的基础知识,那么这一篇就来讲算法题。
判定字符是否唯一
算法分析
本道题就是要判断字符串中是否存在重复的字符,如果存在,那么就返回false,反之,返回true。我们可以使用hash表,可以让时间复杂度达到O(n),空间复杂度为O(1).但本道题有限制,就是不能使用额外的数据结构,所以我们可以使用位运算来进行。利用位图的思想。
算法步骤
- 预判断 :我们都知道字符最多只有26个不重复的,如果超过了26,说明其中有字符重复,可以直接返回false。(根据鸽巢原理)
- 初始化:我们可以定义一个bitmap并初始化为0。
- 遍历字符串 :字符最多不超过26个,所以我们可以根据位图的思想,定义一个x,用来表示bitmap上的x处的比特位。如果有某个比特位上的二进制数按位与(&)1后不为0,说明字符串中存在着重复的字符,直接返回false。若不为1,说明不存在,并将此处的比特位赋值为1**(bitmap|=(1<<x)**)。
- 返回结果:当遍历完字符串,若没有出现重复的字符,则直接返回true。
算法代码
hash表
java
/**
* 判断字符串中的字符是否都是唯一的
*
* @param astr 输入的字符串,由小写字母组成
* @return 如果字符串中的字符都是唯一的,返回true;否则返回false
*/
public boolean isUnique(String astr) {
// 创建一个长度为26的数组,用于记录每个字母出现的次数
int hash[]=new int[26];
// 遍历输入的字符串
for(int i=0;i<astr.length();i++){
// 将字符转换为对应的数组索引
int ch=astr.charAt(i)-'a';
// 将对应索引的值加1,表示该字符出现了一次
hash[ch]++;
// 如果该字符出现的次数大于1,则字符串中存在重复字符,返回false
if(hash[ch]>1) return false;
}
// 遍历完字符串后,没有发现重复字符,返回true
return true;
}
时间复杂度为O(n),空间复杂度为O(1)
位运算
java
/**
* 检查字符串是否所有字符唯一
*
* @param astr 待检查的字符串
* @return 如果字符串中的所有字符都是唯一的,则返回true;否则返回false
*/
public boolean isUnique1(String astr) {
// 如果字符串长度超过26,则不可能所有字符唯一(因为只有一个字母表)
if(astr.length()>26) return false;
// 使用位图来记录每个字符是否已经出现过,初始为0
int bitmap=0;
// 遍历字符串中的每个字符
for(int i=0;i<astr.length();i++){
// 将字符转换为a-z的索引(0-25)
int x=astr.charAt(i)-'a';
// 如果该位置在位图中已经被设置为1,表示字符重复,返回false
if(((bitmap>>x)&1)==1) return false;
// 将位图中对应位置设置为1,表示字符已经出现过
else bitmap|=(1<<x);
}
// 字符串遍历完成后,没有发现重复字符,返回true
return true;
}
时间复杂度为O(n),空间复杂度为O(1)
两整数之和
算法分析
本道题最简答的方法,我不用说都知道,但是这样不符合题意了hh。这道题我们可以使用位运算来进行。需要用到按位异或(^)、按位与(&)等操作。
我们拿第一个案例分析一下。
a=1,b=3
转为二进制就是
0000 0001
0000 0011
我们知道按位异或是无进位相加,那么我们按位异或就会得到
所以我们可以使用按位与(&),按位与的特点就是相同为1。所以当两个数按位与,得到二进制位为1的数,说明此处需要向左移一位。
将a^b的值给a,(a&b)<<1的值给b,重复以上操作,直到b为0,就可以得到结果。
依次类推。
算法步骤
- **初始化:**定义变量digit1和digit2并初始化为0,digit1用来存储a^b的结果,digit2用来存储(a^b)<<1的结果。
- 循环:先让a^b并将结果存到digit1中,再(a&b)<<1存储到digit2中,将digit1的值给a,digit2的值给到b,重复上述操作,直到为0.
- 返回结果:此时我们返回a,就是我们a+b的和。
算法代码
java
/**
* 计算两个整数的和
* 该方法通过位操作实现加法,避免使用算术加法运算符
*
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的和
*/
public int getSum(int a, int b) {
// 初始化两个变量,用于存放每一步的无进位加法结果和进位
int digit1=0,digit2=0;
// 当第二个数不为零时,继续进行加法操作
while(b!=0){
// 通过异或操作计算无进位加法结果
digit1=a^b;
// 通过按位与操作和左移操作计算进位
digit2=(a&b)<<1;
// 将无进位加法结果赋值给a,将进位赋值给b,进行下一轮计算
a=digit1;
b=digit2;
}
// 当进位为零时,a中存放的就是最终的和
return a;
}
时间复杂度为O(1),空间复杂度为O(1).
只出现一次的数字 II
算法分析
在上一篇中,我们讲过只出现一次的数字I,而本道题与这道题的不同点就在于是要在一个其他元素都出现3次的数组中找出现一次的数字。这道题我们需要用到位图的思想。
先来想下,对于每一个比特位来说,它可能出现的情况有4种:
- 有3n个0和一个0
- 有3n个0和一个1
- 有3n个1和一个0
- 有3n个1和一个1
对于这4种情况,其实只有一个0或者1的数,就是a的比特位。我们可以看到,当把比特位上的数加起来之后,%3得到的依旧是只出现一次的比特位,所以我们可以判断每个数的第x个比特位上是否为1,若为1则加到一个计数器k中,当遍历完数组,如果k%3不为0,说明此处的比特位不为0,改为1即可.遍历32个比特位,重复上述操作即可。
算法步骤
- 初始化:定义变量ans初始化为0,用来存储只出现一次的数。
- 遍历:我们需要从第一个比特位开始遍历,定义k用来存储第x个比特位上1的个数,内循环遍历数组,同时判断第x位上是否为1.若为1则k+1。当遍历完数组后,判断k%3是否不为0,若不为0,则将x位上设置为1.重复上述操作。
- 返回结果:当遍历完之后,此时返回ans就是只出现一次的值。
算法代码
java
/**
* 寻找数组中只出现一次的数字
* 给定一个整数数组 nums,其中除一个数字之外,其他数字都出现三次
* 找出那个只出现了一次的数字
*
* @param nums 一个整数数组,其中除一个数字之外,其他数字都出现三次
* @return 那个只出现了一次的数字
*/
public int singleNumber(int[] nums) {
// 初始化答案变量
int ans=0;
// 从最高位开始,逐位检查数字中1的个数
for(int x=31;x>=0;x--){
// 初始化当前位1的个数变量
int k=0;
// 遍历数组中的每个数字
for(int num:nums) {
// 累加当前位上的1的个数
k+=(num>>x)&1;
}
// 如果当前位1的个数不是3的倍数,则该位上的数字为1
if(k%3!=0) ans|=1<<x;
}
// 返回最终结果
return ans;
}
时间复杂度为O(n),空间复杂度为O(1)
只出现一次的数字 III
算法分析
本道题要求我们在数组中找到两个只出现1次的数字,我们可以使用hashset,但是hashset的时空复杂度为O(n),但本道题我们可以使用位运算来解决。
算法步骤
- 初始化 :定义一个sum,用来消除两两出现的数字,存储出现1次的两个数。
- 遍历数组:遍历数组,将值按位异或到sum中。
- 定义变量 :定义变量lsb同时初始化为sum(这里需要判断sum是否溢出,如果溢出要进行**sum&(-sum)**相当于取反),同时定义type1和type2用来存储出现1次的数字。
- 遍历数组:遍历数组,此时同时判断(lsb&x)的值,若为1,则让type1^=x,反之,则让type2^=x.
- 返回结果:当遍历完数组,此时new一个数组存储两个值即可。
算法代码
java
/**
* 寻找数组中出现一次的两个数字
* 该方法通过位操作实现,主要利用异或运算的性质来找到出现一次的两个数字
*
* @param nums 输入的整数数组,其中除了两个数字出现一次外,其他数字都出现两次
* @return 返回一个包含两个出现一次的数字的数组
*/
public int[] singleNumber(int[] nums) {
// 对数组中的所有数字进行异或运算,得到的结果是两个只出现一次的数字的异或结果
int sum=0;
for(int x:nums) sum^=x;
// 找到sum中最低位的1,用于将数组分为两组,一组包含只出现一次的第一个数字,另一组包含只出现一次的第二个数字
// 这样做是为了防止溢出,并确保能够正确分组
int lsb=(sum==Integer.MIN_VALUE?sum:sum&(-sum));
// 初始化两个数字,分别用来存放只出现一次的两个数字
int type1=0; int type2=0;
// 再次遍历数组,根据lsb是否为1,将数组分为两组,并分别对两组进行异或运算
for(int x:nums){
if((lsb&x)!=0){
// 如果lsb位为1,异或结果放入type1
type1^=x;
}else{
// 如果lsb位为0,异或结果放入type2
type2^=x;
}
}
// 返回两个只出现一次的数字
return new int[]{type1,type2};
}
面试题 17.19. 消失的两个数字
算法分析
这道题其实就是消失的数字+只出现一次的数组III的结合。
算法步骤
- 初始化 :定义变量sum,并初始化为0
- 预处理:让sum按位异或(^)遍历数组元素,同时这里是缺了两个数,所以我们也让sum按位异或从1遍历到nums.length+2的数。从而可以找出缺失的两个数字的按位异或。
- 定义diff:这里我们定义一个diff,用来表示sum的比特位。
- 循环遍历:我们遍历sum的比特位,如果(((sum>>diff)&1)==1),说明找到了两数不同的位置,停止循环;反之,让diff++,
- 定义数组ret:这里我们定义ret数组来存储两个消失的值。
- 遍历:这里我们需要遍历nums数组和从1到nums.length+2的数字,根据异或的数不同来分成两类(全是0或者1),从而来找出不同的数.
算法代码
java
/**
* 找到数组中缺失的两个数字
* 使用异或运算来确定缺失的数字
*
* @param nums 输入的数组,其中包含了N个数字,范围从1到N+2,有两个数字缺失
* @return 返回一个包含两个缺失数字的数组
*/
public int[] missingTwo(int[] nums) {
// 初始化sum为0,用于异或运算
int sum = 0;
// 对数组中的每个数字进行异或运算,目的是找出缺失的两个数字的异或结果
for (int num : nums)
sum ^= num;
// 对1到N+2的每个数字进行异或运算,其中N为数组长度
// 目的是取消掉数组中出现的数字,最终得到的异或结果是两个缺失数字的异或结果
for (int i = 1; i <= nums.length + 2; i++)
sum ^= i;
// 找到两个缺失数字的异或结果中的第一个为1的位,作为分组依据
int diff = 0;
while (true) {
// 检查sum的第diff位是否为1
if (((sum >> diff) & 1) == 1)
break;
else
diff++;
}
// 初始化结果数组
int[] ret = new int[2];
// 根据diff位是否为1,将数组中的数字分为两组,并分别对两组数字进行异或运算
for (int x : nums) {
// 根据diff位是否为1来决定是哪一组
if (((x >> diff) & 1) == 1)
ret[0] ^= x;
else
ret[1] ^= x;
}
// 对1到N+2的每个数字进行同样的分组和异或运算
for (int x = 1; x <= nums.length + 2; x++) {
if (((x >> diff) & 1) == 1)
ret[0] ^= x;
else
ret[1] ^= x;
}
// 返回结果数组,即两个缺失的数字
return ret;
}
时间复杂度为O(n),空间复杂度为O(1).
以上就是本篇的所有内容,位运算就先到这里了~
若有不足,欢迎指正~