[二分查找 双指针 位运算] 287.寻找重复数(值域二分、位运算、快慢指针找环入口)

287.寻找重复数

题目链接:https://leetcode-cn.com/problems/find-the-duplicate-number/


分类:

  • 二分查找(对数组元素的值域做二分寻找重复数值)
  • 位运算(统计数组所有元素二进制下每一列1的个数a[i],[1~n]所有元素二进制下每一列1的个数b[i],a[i] > b[i]则重复数的第i位值为1)
  • 快慢指针法(将数组转换成带环的链表、快慢指针寻找环入口 = 重复数)

在这里插入图片描述

题目分析

本题给定的nums数组内的元素的取值范围是固定的,大小=n+1的数组,元素值范围在[1,n]上,根据抽屉原理数组内至少存在一个重复值,假设只有一个这样的重复值,需要我们找出来。

同时题目给出了三个限制条件

1、不能修改原数组;

2、只能使用O(1)的空间;

3、时间复杂度小于O(N^2)

这些限制条件实际上就表示下列“在值域有限的数组里寻找一个重复数”的方法被禁用了:

  1. 排序法:将数组排序,然后遍历寻找重复数。违反了条件1。
  2. 哈希表:统计每个数字的出现次数,当一个数字出现次数>1时即找到重复数。违反了条件2。
  3. 计数排序(原地哈希):元素值范围固定在[1,n],可以直接利用原数组统计每个数字的出现次数(下标=元素值,元素值=出现次数)。违反了条件1;
  4. 暴力解法:枚举所有元素,对于每个元素都遍历一次数组寻找是否有和它相同的数字。违反条件3。

根据题目提示,时间复杂度小于O(N^2),我们可以猜想是不是可以使用二分查找来寻找重复数,但这里的二分查找和一般二分查找有所不同:

一般的二分查找是索引二分,不断缩小查找的索引范围最终定位到目标元素,前提是待查找数组是有序的;

这里的二分查找是值域二分,不断缩小查找的值域范围最终定位到目标数值,前提是待查找数组的元素值范围是有限的,固定的,但不要求数组本身有序。值域二分的理论依据是抽屉原理。

  • 思路1值域二分是比较通用的解法,而快慢指针(思路3)更有技巧性,但并不通用。
  • 思路2是从位运算的角度解题,隐含的规律比较简单,但这个角度比较难想到。

思路1:值域二分(非常规二分,时间换空间,O(NlogN))

参考题解:使用二分法查找一个有范围的整数(结合抽屉原理)

题目给定的数组nums内的元素值范围在[1,n]上,一趟二分流程为:

获取值域的中位点mid=(1+n)/2,然后遍历整个数组,统计数组中元素值 <= mid的个数cnt,然后判断cnt的大小:

  • 如果cnt > mid,根据抽屉原理,元素值<=mid的个数>mid,说明重复数的数值在[left,mid]范围内(对值域做了二分);
  • 如果cnt <= mid,说明重复数的数值在[mid+1,right]范围内。

重复上述步骤,直到left==right,说明找到这个重复数的数值。

例如:[1,3,4,2,2],值域范围[1,4]
left=1,right=4,mid=2,遍历数组,统计<=mid的元素个数cnt=3>mid,所以重复数的数值在[1,2]之间;
left=1,right=2,mid=1,遍历数组,统计<=mid的元素个数cnt=1==mid,所以重复数的数值在[2,2]之间。
因为left==right,所以退出二分查找过程,返回left.

实现代码:

class Solution {
    public int findDuplicate(int[] nums) {
        int len = nums.length;
        int left = 1, right = len - 1;//nums元素值的值域上下界
        while(left < right){
            //获取值域的中位点
            int mid = left + (right - left) / 2;
            //遍历数组,元素值统计<=mid的个数
            int cnt = 0;
            for(int num : nums){
                if(num <= mid) cnt++;
            }
            //如果cnt>mid,则重复数的数值在[left,mid]上
            if(cnt > mid) right = mid;
            //如果cnt<=mid,则重复数的数值在[mid+1,right]上
            else left = mid + 1;
        }
        return left;
    }
}
  • 时间复杂度:对值域[1,n]不断二分,总共需要O(logN)趟二分,每一趟二分查找都需要遍历整个数组,需要O(N),所以整体时间复杂度为O(NlogN)
  • 空间复杂度:O(1).

思路2:位运算(O(N))

参考题解:详细通俗的思路分析,多解法

int型共32位,我们统计数组nums所有元素的二进制形式下每一列出现1的个数a,再统计[1~n]的二进制形式下每一列出现1的个数b,如果数组中存在一个重复数字k,那么如果k的二进制形式中第i位的值为1,则都有a[i]>b[i],我们找出32位中a[i]>b[i]的位并置1,就能得到重复数字k。

例如:[1,3,4,2,2]
统计数组元素二进制形式下每一列1的个数:
1 0001
3 0011
4 0100
2 0010
2 0010
a 0132
再统计[1~n]二进制形式下每一列1的个数:
1 0001
2 0010
3 0011
4 0100
b 0122
可以发现:a[2]>b[2],所以将一个全0数的第2位置1就是重复数字0010=2.

这个结论没有严格的证明,但可以这样归纳:

  • 如果重复数字k只重复一次,则数组的元素包含[1~n]上的每一个数值,在此基础上还包含一个k,在统计数组所有元素的二进制每一列出现1的个数a和[1~n]所有元素每一列出现1的个数b时,数组的统计结果就是在[1~n]的统计结果的基础上增加一个k,所以在k的二进制形式第i位的值为1时,a[i]很明显>b[i]。
  • 如果重复数字k重复不止1次,也就是在k只重复一次的基础上,选择数组中几个元素转换成k,被转换的元素在k值为1的位上无非有两种转换情况:原来值为0,转换成了1;原来值为1,则保持不变,在k只重复一次的基础上就存在a[i]>b[i],两种转换情况下都不会使a统计的1数量减少,最差也是持平的情况,所以a[i]>b[i]始终成立。
实现遇到的问题:

1、如何判断一个int型数字的第i位是否等于1?

设置一个掩码mask,例如要查看第2位,就只保留该数字的第2位,其他位都置0:

	mask=(1 << 2);//0100

和数字做与操作,相当于只保留这个数字的第2位,其他位全部置0,如果这个数字的第2位是0,则相与的结果=0,如果第2位是1,则相与结果>0.

所以,根据数字和mask的相与结果是否>0可以判断这一位是否等于1.

2、如何在一趟遍历里同时统计数组所有元素和[1~n]所有元素第i位1的个数?

for-j循环遍历nums数组,遍历的起点是0,终点是n,拿nums[j]&mask就能统计数组所有n+1个元素;

同样直接拿j & mask就能统计1~n所有元素,因为j=0时,j&mask必定==0,b的值不会改变,所以相当于只统计1~n。

实现代码:

class Solution {
    public int findDuplicate(int[] nums) {
        int res = 0;//存放重复数字
        //遍历所有元素的32位
        for(int i = 0; i < 32; i++){
            int a = 0, b = 0;//记录数组和[1~n]所有元素在第i位上出现1的个数
            int mask = (1 << i);//掩码
            //一趟遍历统计数组所有元素和[1~n]所有元素在第i位值为1的个数a,b
            for(int j = 0; j < nums.length; j++){
                if((nums[j] & mask) > 0) a++;
                //
                if((j & mask) > 0) b++;
            }
            //如果a>b,则将res的第i位置1
            if(a > b){
                res |= mask;
            }
        }
        return res;
    }
}

思路3:快慢指针法(数组 → 链表,环入口 = 重复数,O(N))

将数组转换成链表,其中元素下标作为节点的val,元素值作为节点的next,例如:

在这里插入图片描述

可以发现,重复数字就是链表中环的入口节点,问题转化成在链表里寻找环的入口节点。
我们可以使用快慢指针来寻找环入口:

  1. 快指针每次移动两个节点,慢指针每次移动一个节点,不断循环遍历数组,直到两个指针相遇,记下相遇节点的位置。
  2. 再使用两个同步移动的指针,一个从链表头结点出发,一个从相遇节点出发,两个节点相遇的节点就是环的入口节点,该节点的val就是重复值 == 元素下标。

如何将数组转换成链表?

只是逻辑上的转换,设指针为p,从一个节点移动到下一个节点就是:p = nums[p],所以快慢指针就按下面的代码工作:

    slow=nums[slow];//一次移动一个节点
    fast=nums[nums[fast]];//一次移动两个节点

实现代码:

class Solution {
    public int findDuplicate(int[] nums) {
        int slow = 0, fast = 0;
        //快慢指针寻找相遇点
        while(true){
            slow = nums[slow];
            fast = nums[nums[fast]];
            if(slow == fast) break;
        }
        //指针复用,一个从起点开始,一个从相遇点开始同步移动,寻找环入口
        slow = 0;
        while(slow != fast){
            slow = nums[slow];
            fast = nums[fast];
        }

        return slow;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值