目录
判定字符是否唯一
编辑 算法分析
算法步骤
算法代码
hash表
位运算
两整数之和
算法分析
算法步骤
算法代码
只出现一次的数字 II
算法分析
算法步骤
算法代码
只出现一次的数字 III
算法分析
算法步骤
算法代码
面试题 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表
/**
* 判断字符串中的字符是否都是唯一的
*
* @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)
位运算
/**
* 检查字符串是否所有字符唯一
*
* @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的和。
算法代码
/**
* 计算两个整数的和
* 该方法通过位操作实现加法,避免使用算术加法运算符
*
* @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就是只出现一次的值。
算法代码
/**
* 寻找数组中只出现一次的数字
* 给定一个整数数组 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一个数组存储两个值即可。
算法代码
/**
* 寻找数组中出现一次的两个数字
* 该方法通过位操作实现,主要利用异或运算的性质来找到出现一次的两个数字
*
* @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),从而来找出不同的数.
算法代码
/**
* 找到数组中缺失的两个数字
* 使用异或运算来确定缺失的数字
*
* @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).
以上就是本篇的所有内容,位运算就先到这里了~
若有不足,欢迎指正~