[LeetCode] 只出现一次或者重复数专题总结(位运算、二分查找)

写在前面

只出现一次以及只出现n次的题以及找重复数的题,本质上是在考察数组元素重复特征的题,这类题会出现不同的演变方式,若不做任何限制,采取排序或者哈希统计,解这种题是最简单的,但往往会附加数组只读和空间复杂度为O(1)的限制条件,这意味着排序和哈希统计将不work,此类题的另一个研究方式是,要么限制数组元素范围在[1,n]内,要么不做此限制,若元素在[1,n]范围内的话,那么解题的关键或者题眼大概率会在元素值和元素索引上打开,因为值的范围和索引的范围是相同的。前面可能说的有点饶,本笔记在LT中是两种题型,即出现n次和重复数的问题,但是他们特征相同,因此放在一个比较中做比较。

——————————————————

出现n次题分析题型特征

只出现n次的基准题是题136,题137和题260在其基础上做演进,题136是只有一个元素出现一次,其他元素均出现两次,题137是只有一个元素出现一次,其他元素均出现三次,题260是有两个元素只出现一次,其他元素均出现两次,具体解法看下面各题分析。

136. 只出现一次的数字

原题链接

解题思路: 本题的题意是只有一个元素出现一次,其他元素均出现两次,找出只出现一次的元素,那么两个关键点,两类元素出现的次数(1次和2次),要求取的元素个数为1个,OK,这里面如果我们第一次做题,可能比较难想到,这里需要用到位运算,我们如果想使最终只留下那个只出现一次的元素,势必需要找方法消除其他的出现两次的元素,那么对应到位运算中异或算法可以使相同元素异或被消除,至此,思路应该就出来了,我们对整个数组做异或运算,异或的结果便是那个只出现一次的元素。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (auto &num : nums) {
            res ^= num;
        }
        return res;
    }
};

137. 只出现一次的数字 II

原题链接

解题思路: 此题是在题136基础上演进过来的,题意是,只有一个元素出现一次,其他元素均出现三次,求取只出现一次的元素,我们先分析一个此题与题136有什么区别,看能不能找到可借鉴的解题思路,此题其他元素出现的次数为3,仅此处有差别,但是我们会发现,异或的方法不work了,对出现偶次元素做异或能将元素从结果中的贡献消除,但是奇次元素最后仍然会在最终结果中留下一个贡献,因此异或不work了,但是上题用的是位运算解题,此题依然可以去尝试想,题136是从宏观上去考虑整体数组的位运算结果,那么此题能否从微观上去考虑呢,OK,确实是如此,我们依然采取消除的做法,三次想消除,那么我们考虑和值,然后对和值与3取余,那么三次和值的bit位自然在结果数的bit位中贡献为零了,OK,至此思路应该就出来了,我们对32位的数,取每个数的bit和值对3取余作为结果数的相应位的贡献,那么最终出现3次的数对结果数的贡献一定为0,那么结果数便是只出现一次的数了。

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int res = 0;
        for (int i = 0; i < 32; ++i) {
            int sum = 0;
            for (auto &num : nums) {
                sum += ((num >> i) & 1);
            }
            res |= ((sum % 3) << i);
        }
        return res;
    }
};

260. 只出现一次的数字 III

原题链接

解题链接: 本题题意是,只有两个元素出现一次,其他元素均出现两次,找出只出现一次的两个元素,同样此题是由题136和题137演进过来的,我们先分析一下他们的区别,本题与题136相近,区别是存在两个只出现一次的元素,感觉上此题依然是要采取消除的策略,i.e.,让我们要找的数在结果数中全贡献,而不需要的数在结果数中零贡献,但是如果采取题136相同的异或方法,结果数中其他数确实是被消除了,但是要求取的两个数将混合在一起无法识别,而此步是宏观的,那么我们结合题137想一下,算法需要也考虑一下微观的,既然这两个数不同,那么在bit位上只要有一个位不不同,那么我们以这个位为标准将数组元素分成两组,组内的数采取与题136方法相同的异或得到的结果数一定是只出现一次的元素,另一组同理。

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int resxor = 0;
        for (auto &num : nums) {
            resxor ^= num;
        }
        int bit = 0;
        for (; bit < 32; ++bit) {
            if ((resxor >> bit) & 1) break;
        }
        int resxor1 = 0, resxor2 = 0;
        for (auto &num : nums) {
            if ((num >> bit) & 1) resxor1 ^= num;
            else resxor2 ^= num;
        }
        return {resxor1, resxor2};
    }
};

分支只出现一次题的关联

通过上面分析,我们应该能察觉到3题间的关联,基本都是围绕消除思想想解决办法,核心的目的是将无关的数抵消使其在结果数中贡献为0,而使要求取的数在结果数中全贡献,采取的方法是,从宏观上对数组做异或运算,在微观上对bit位做取余运算,其实由题137可以延伸出,一部分数出现4次,而另一部分数出现2次,等等组合,我们只需要对较大的数取余,较大的数自然被消除留下较小的数。

——————————————————

287. 寻找重复数

原题链接

解题思路: 虽然很快就能分析出此题要考察二分查找,但是如何将二分查找应用到此题卡了很长时间。现在记录一下我的思路,发现考察二分查找不是通过题意分析出来的,哈哈说来滑稽╮(╯▽╰)╭,是通过题面要求时间复杂度不超过 O ( n 2 ) O(n^2) O(n2),推断题面要求的时间复杂度可能是 O ( n l g n ) O(nlgn) O(nlgn),只是一种直觉上的猜测,但是最后发现确实是这样的,而想到lgn时同时也会想到二分,当然,读者会质疑小于 O ( n 2 ) O(n^2) O(n2)时间复杂度的表达式有很多,为什么是上面那个么,这个我也答不上来,因为我已经非常特别且敏感的时间复杂度,如O(lgn)是与具体算法关联的,即二分,这样结合起来考才是最有意义的,而有时候思维确实存在一定跳跃性,大量的信息混在一起,再加上如果做过大量题,此时直觉上就会告诉你,见到什么时间复杂度会考什么算法才是最合理的。OK,解题方向基本推测准了,那现在就是如何应用二分查找,换句话说,对谁二分以及缩小查找范围的标准,这个也是解所有二分查找题的关键,这里推荐一篇总结博客,讲解二分查找的。此题二分的对象和标准并没有顺着题目思路走,具体的是,我们分析数字特征,所有的数均在[1,n]范围内,而这个范围正好是他们数组索引的范围,那么这里就回答对谁二分的问题了,对1…n的索引数二分,因为如果没有重复数,并且数组有序,那么每个数各在其位,而若此时有重复数,则一定会挤占某个数的位置,使某个范围的空间「 拥挤」,OK,那回答缩小查找范围的标准,我们确定初始搜索范围[left,right]且二分mid之后,对[left,mid]区间统计数组中落在这个范围内的元素个数,若个数大于区间宽度,则说明重复数落在[left,mid]内,那么搜索区间向左半区间靠拢,否则向右半区间靠拢。

// 二分查找,二分的标准是,nums数组落在left到mid之间的数的
// 个数,若个数大于区间宽度,则重复数落在这个区间内,否则
// 落在mid到right区间内
// T: O(n), S: O(1)
class Solution {
public:
    int count(vector<int>& nums, int a, int b) {
        int res = 0;
        for (auto &num : nums) {
            if (num >= a && num <= b) ++res;
        }
        return res;
    }
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            int cnt = count(nums, left, mid);
            if (cnt <= mid - left + 1) left = mid + 1;
            else right = mid;
        }
        // cout << left << "," << right << endl;
        return right;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值