Find the Duplicate Number寻找重复数(剑指offer,寻找重复的数)

本文介绍了一种在数组中寻找重复数字的有效算法。利用二分查找和循环链表的概念,可以在O(1)的空间复杂度下解决问题。文章还对比了不同方法的优缺点,并提供了详细的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 之间,包括 1 和 n ,可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

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

示例 2:

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

说明:

  1. 不能更改原数组(假设数组是只读的)。
  2. 只能使用额外的 O(1) 的空间。
  3. 时间复杂度小于 O(n2) 。
  4. 数组中只有一个重复的数字,但它可能不止重复出现一次。

这道题如果没有说明中的限制条件,允许申请O(n)的辅助空间的话,可以用数组代替哈希表来做,但是题目只能使用O(1)的辅助空间,这道题才变得有意思起来。基于暴力法和数组哈希表法这里不再赘述,提供两种新的解题思路,二分法循环链表法

二分法:

很多博客直接上来就是代码,让人云里雾里,解释的不也到位,需要阅读者花费很长时间来理解和吸收。这里直接以举例的方式来讲解。比如原数组a为(重复数字为2)

[1,2,2,3,4,5]

一共n+1个数字(n=5),数字范围为1-n及1-5,根据抽屉原理,在数字1-5中有6个数字,则至少有一个数字出现了两次以上。我们初始化low=1(数字的范围下限),high=5(数字的范围上限),mid=(high+low)/2=3,则mid把原数组分为1-3(包含3)和大于3的部分,如果原数组的数字在1-3的出现的次数cnt大于mid(简单理解为数字1-3最多只能包含3个数字,如果包含的数字个数大于3个,那么一定会出现重复的数字,及重复的数字一定在1-3的范围内),所以每次二分,在每个二分循环内都遍历一次数组,时间复杂度为O(nlogn),空间复杂度为O(1)。

代码如下:

int findDuplicate(vector<int>& nums) {
	//数字下限
	int low = 1;
	//数字上限
	int high = nums.size() - 1;
	while (low < high) {
		int cnt = 0;
		int mid = (low + high) / 2;
		for (int i = 0; i < nums.size(); i++) {
			//统计小于等于mid的个数
			if (nums[i] <= mid) {
				cnt++;
			}
		}
		//如果大于mid则在数字在小的那部分,及low-mid
		if (cnt > mid) {
			high = mid;
		}
		//否则,在大的那部分mid+1-high
		else {
			low = mid + 1;
		}
	}
	return low;
}

循环链表法:

这种方法非常巧妙,中心思想是如果数组中存在重复数字,那对数组a而言:

i->a[i]->a[a[i]]->a[a[a[i]]]

的访问方式最后一定会陷入循环,且循环的数就是重复的数,以下举例说明:

对于不重复的数组:

[1,2,6,3,4,5,8]

我们以下方式生成链表:下标(0)->下标对应的值(1)->下标对应的值[下标对应的值](2)。。。的方式来遍历数组,可以得到如下链表:

0->1->2->6->8->nullptr

不会出现循环,如果对于重复数组:

[1,2,2,3,4,5]

构建的链表为:

0->1->2->2->2->2->......(为什么不是第一个2,因为循环是从第二个2才开始循环,第一个2只是刚好下标为1的数为2)

节点2开始循环,重复的数字也是2,所以思路转化为寻找带环链表的第一个环节点,通过快慢指针来做,之前leetcode已经分析过,点击这里,这里不再赘述。

int findDuplicate(vector<int>& nums) {
	int n = nums.size();
	if (n > 0) {
		//访问下标0的值
		int slow = nums[0];
		//“下标0的值”作为下标访问
		int fast = nums[nums[0]];
		while (slow != fast) {
			slow = nums[slow];
			fast = nums[nums[fast]];
		}
		fast = 0;
		while (slow != fast) {
			slow = nums[slow];
			fast = nums[fast];
		}
		return slow;
	}
	return -1;
}

同时,剑指offer也有同样的题,但是题目有些许出入:

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

注意:数字的范围变成了0~n-1,且数组可修改,之前讨论的二分法可以,循环链表法不行。因为循环链表从第0个元素开始且数组中的值从1开始,可以保证从第一个元素开始不会再回到第一个元素,不像这道题(比如[1,0],该数组会形成:第一个元素->第二个元素->第一个元素......的死循环,但是其实并没有重复元素)。

所以这道题的做法是每次把固定当前元素i,把当前元素的值array[i]交换到对应的下标index是array[i]的地方,相当于归位操作:0对应a[0],1对应a[1],如果a[2]=7,那么就把7和a[7]交换 (swap(a[7],a[2])),让a[7]对应正确的下标,直到找到a[i]==a[j],那么a[i]和a[j]就是重复的数据。

参考代码:

class Solution {
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    bool duplicate(int numbers[], int length, int* duplication) {
        for(int i=0;i<length;i++){
            while(i!=numbers[i]){
                if(numbers[i]==numbers[numbers[i]]) {
                    *duplication=numbers[i];
                    return true;
                }
                swap(numbers[i],numbers[numbers[i]]);
            }
        }
        return false;
    }
};

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值