立下flag)每日10道前端面试题-19 关于【位运算】算法题

1.多数元素

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [3,2,3]
输出: 3
示例 2:

输入: [2,2,1,1,1,2,2]
输出: 2

解法一:

因为大于一半, 所以排序后的 中间那个数必是

代码

/**
 * @param {number[]} nums
 * @return {number}
 */
var majorityElement = function(nums) {
  nums.sort((a,b) => a - b)
  return nums[Math.floor(nums.length / 2) ]
};

时间复杂度 O(nlogn) 使用了快速排序 空间复杂度 O(n)

方法2: 搞对象

用对象记录数出现的次数, 大于一半的就是了

var majorityElement = function(nums) {
   let len = nums.length
   let obj = {}
   let num = 0
   nums.forEach(item => {
     obj[item] = obj[item] ? obj[item] + 1 : 1
     if(obj[item] > len / 2) num = item
   })
   return num
};

时间复杂度: O(n) 遍历一次 空间复杂度: O(n) obj中属性最多为 n/2 个

解法三:投票算法

该算法在其局部变量中维护一个序列元素和一个计数器,计数器最初为零。然后,它一次一个地处理序列的元素。处理元素x时,如果计数器为零,则算法将x存储为其维护的序列元素,并将计数器设置为1。否则,它将x与存储的元素进行比较,并使计数器递增(如果相等)或递减计数器(不相等)。

这里有两个先觉条件要明确:

出现超过一半以上(n/2)的元素有且只有一个;

这个元素一定存在

原理解析

为了解析算法的原理,我们只要考虑存在多数元素的情况即可,因为第二趟扫描可以检测出不存在多数元素的情况。

举个例子,我们的输入数组为[1,1,0,0,0,1,0],那么0就是多数元素。首先,candidate被设置为第一个元素1,count也变成1,由于1不是多数元素,所以当扫描到数组某个位置时,count一定会减为0。在我们的例子中,当扫描到第四个位置时,count变成0.

count 值变化过程:[1,2,1,0……

当count变成0时,对于每一个出现的1,我们都用一个0与其进行抵消,所以我们消耗掉了与其一样多的0,而0是多数元素,这意味着当扫描到第四个位置时,我们已经最大程度的消耗掉了多数元素。

var majorityElement = function(nums) {
    let count = 1;
    let majority = nums[0];
    for(let i = 1; i < nums.length; i++) {
        if (count === 0) {
            majority = nums[i];
        }
        if (nums[i] === majority) {
            count ++;
        } else {
            count --;
        }
    }
    return majority;
};

2.只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,1]
输出: 1
示例 2:

输入: [4,1,2,1,2]
输出: 4

解法一:位运算

n ^ n === 0 且 n ^ 0 === n

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    let ans = 0;
    for(const num of nums) {
        ans ^= num;
    }
    return ans;
};

解法二:排序

因为只存在一个不重复的数字其他都两两出现,所以进行排序后,相同的数字必定是连续的,所以只要判断是否连续

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    nums = nums.sort((a, b) => {
        return a-b
    })
    for (let i = 0; i<nums.length;i+=2){
        if (nums[i] != nums[i+1]) {
            return nums[i]
        }
    }
};

解法三:暴力法,但是慢

暴力法,根据索引来判断。indexOf从左往右找,lastIndexOf从右往左找。当元素只出现一次时,无论从哪里找起,索引都是相同的。结果返回元素即可

/**
 * @param {number[]} nums
 * @return {number}
 */
var singleNumber = function(nums) {
    for (let item of nums) {
        if (nums.indexOf(item) === nums.lastIndexOf(item))
            return item
    }
};

3.只出现一次的数字 II

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。你可以不使用额外空间来实现吗?

示例 1:

输入: [2,2,3,2]
输出: 3
示例 2:

输入: [0,1,0,1,0,1,99]
输出: 99

解法一:位运算

1.位运算求解此题; 2.如果把所有出现三次的数字的二进制表示的每一位加起来,那么每一位都能被3整除; 3.如果某一位的和能被3整除,那么只出现一次数字的二进制位中对应的那一位就是0,否则就是1; 4.完结。

得到的启发,把每个32位的位置都记录一遍

public int singleNumber(int[] nums) {
 if (nums == null || nums.length <= 0)
  throw new RuntimeException("数组不合法");

 int[] bits = new int[32];

 for (int j = 0; j < bits.length; j++) {
  for (int i = 0; i < nums.length; i++) { // 每一位数字对应的0或1加起来
   int num = nums[i] >> j;
   bits[j] += (num & 1);
  }
 }

 int result = 0;
 for (int i = bits.length - 1; i >= 0; i--) {
  result <<= 1;
  result += bits[i] % 3;
 }
 return result;
  }

解法二:排序

解题思路:

  • 排序,升序或降序均可

  • 循环,每三组进行相加,并判断是否等于第一项的三倍

  • 相同的话说明三个数字相同

  • 不同的话,说明肯定存在不同的数字

  • 进行异或

  • 循环完后还没有,则最后一个是

var singleNumber = function(nums) {
    nums = nums.sort((a, b) => a - b);
    for(var i = 2; i < nums.length; i += 3){
        if((nums[i] + nums[i - 1] + nums[i - 2]) !== nums[i]*3){
            return nums[i] ^ nums[i - 1] ^ nums[i - 2]
        }
    }
    
    return nums[nums.length - 1];
};


解法三:暴力法,但是慢

跟上面一题一样。

4.只出现一次的数字 III

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。

示例 :

输入: [1,2,1,3,2,5]
输出: [3,5]
注意:

结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?

解法一:位运算

异或 + &  如果异或还不太懂的可以先看下相关的几个题目先把异或搞懂先

  • 第一步:所有数异或,所有数异或之后的值就是两个只出现一次的数a,b异或后的值s。

  • 第二步:那我们用s & (-s) 可以得出s最低位(也就是最右边)为1的bit位(这个操作不太会事先知道)对应的数k,

这里有一个很关键点就是:这个为1的bit位肯定只来之a.b其中的一个数字中的对应二进制位的1,这里有点卡主理解,比如我们把数字转成二进制运算看下:

a:1 -> 0001
b:2 -> 0010  => 0011. 不全为1的bit为进行异或操作就为1,这是异或的基本流程,然后我们操作s & (-s)之后得到的是0001,可以看到这个1是来之数字a的
  • 第三步:我们得到s&(-s)之后在对所有数进行&操作的话,就意味着可以将a和b区分在0和1的两个组,至于其他的数字如果相同的数字自然会分到一个组,可以用纸笔写下整个过程

  • 第四步:经过第三步之后就变成了只有一个数字存在一个其他都存在两个的数组(也就变成题目:136. 只出现一次的数字), 然后我们分别对两个组的所有数字再进行异或之后不就得到了要求的那两个数了嘛。

class Solution {
    public int[] singleNumber(int[] nums) {
 
        int s = 0;
        for (int num : nums){ // 第一步
            s ^= num;
        }

        int k = s & (-s); // 第二步

        int[] result = new int[2];
        for (int num : nums){
            if ((num & k) == 0){ // 第三第四步
                result[0] ^= num;
            }else{
                result[1] ^= num;
            }
        }

        return result;
        // 方式一:利用hash记录个数,但是空间复制度不满足,或者排序但是时间复杂度度不满足
    }
}

解法二:

最直接的方法,统计每个数出现的次数。使用 HashMap 或者 HashSet,由于每个数字最多出现两次,我们可以使用 HashSet。

遍历数组,遇到的数如果 HashSet 中存在,就把这个数删除。如果不存在,就把它加入到 HashSet 中。最后 HashSet 中剩下的两个数就是我们要找的了。

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var singleNumber = function(nums) {
    const { length } = nums;
    let res = {};
    for(let i = 0; i < length; i++){
        if (res[nums[i]] !== undefined) {
            delete res[nums[i]];
        } else {
            res[nums[i]] = nums[i];
        }
    }
    return Object.values(res);
};

5.两整数之和

不使用运算符 + 和 - ,计算两整数 a 、b 之和。

示例 1:

输入: a = 1, b = 2
输出: 3
示例 2:

输入: a = -2, b = 3
输出: 1

解法一:位运算

利用位操作实现加法

  • 首先看十进制是如何做的:5+7=12,三步走

    • 第一步:相加各位的值,不算进位,得到2。

    • 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。

    • 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。

  • 同样我们可以用三步走的方式计算二进制值相加:5---101,7---111

    • 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。

    • 第二步:计算进位值,得到1010,相当于各位进行与操作得到101,再向左移一位得到1010,(101&111)<<1。

    • 第三步重复上述两步,各位相加 010^1010=1000,进位值为100=(010 & 1010)<<1。

    • 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。

    • 结束条件:进位为0,即a为最终的求和结果。

class Solution {
    public int getSum(int a, int b) {
        while(b != 0){
            int temp = a ^ b;
            b = (a & b) << 1;
            a = temp;
        }
        return a;
    }
}

6.子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]

=

解法一:迭代

var subsets = function(nums) {
    let ws = [[]];
    for(let i=0; i < nums.length; ++i) {
        for(let j=0, len = ws.length; j < len; ++j) {
            ws.push(ws[j].concat([nums[i]]));
        }
    }
    return ws;
};

解法二:位运算

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    let res = [];
    let len = nums.length;
    // 2^n 获取所有状态
    let resAll = 1 << len;
    for(let i = 0;i < resAll;i++){
        let subset = [];
        // 当前数组的索引位置
        let j = 0;
        // 移位
        let iCopy = i;
        while(iCopy != 0){
            //判断当前位置是否是1
            if( (iCopy & 1) == 1){
                subset.push(nums[j]);
            }
            j++;
            // 右移一位
            iCopy >>= 1;
        }
        res.push(subset);
    }
    return res;
};

解法三:递归回溯

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    let n = nums.length;
    let tmpPath = [];
    let res = [];
    let backtrack = (tmpPath,start) => {
        res.push(tmpPath);
       for(let i = start;i < n;i++){
           tmpPath.push(nums[i]);
           backtrack(tmpPath.slice(),i+1);
           tmpPath.pop();
       }
    }
    backtrack(tmpPath,0);
    return res;
};

本文使用 mdnice 排版

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值