算法心经——分治,抽屉原理

分治,抽屉原理

引例1:给定一个长度为 n 的整数数组 nums,数组中所有的数字都在 0∼n−1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

引例2:给定一个长度为 n+1 的数组nums,数组中所有的数均在 0∼n 的范围内,其中 n≥0。请找出数组中任意一个重复的数,但不能修改输入的数组。

我们先看第一题,最简单的做法自然是遍历一遍挨个找。但这种做法不是很高明。最好的做法是这样的:
给出n个数字,而且范围在0~n-1。我们可以这么做,把值为0~n-1的n个数放到与他们值对应的位置上去。
比如数字0,应该放到索引为0的位置中;数字1应该放到索引为1的位置中。当发现要放置的位置已经存在这个数了,自然就找到了重复的数字。
所以我们要开辟一个大小为n的空位数组home,将题目中给的nums数组中的元素在home中为它找一个家。
class Solution {
public:
	int duplicateInArray(vector<int>& nums) {
		vector<int> home(nums.size(), -1);
		for (int i = 0; i < nums.size(); i++) {
			if (home[nums[i]] == -1) {//房子是空的时候给数字安家
				home[nums[i]] = nums[i];
			}
			else {
				return nums[i];
			}
		}
	}
};
仔细分析一下,上面的home数组是多余的。这是因为我们每一次都会将一个数送回家,所以我们关心的只是将数字x送到x索引处,而不关心这个数字存在于哪个数组。所以完全可以在nums数组上进行操作,那么就要思考,什么时候我们才需要把这个数字送回家呢?仅当家里存在另一个数的时候才能送它回家。

形象一点的说,上面给的代码是“分房子”,而这次给的代码是“换房子”,只有两家户主都不对的才能互换,而不能把正确的户主赶走。这样我们每一次可以将一个数字送回家,最多做n次。
class Solution {
public:
	int duplicateInArray(vector<int>& nums) {
		for (int i = 0; i < nums.size(); i++) {//i既指向家,又指向原数组的元素
			while (nums[nums[i]] != nums[i]) swap(nums[nums[i]], nums[i]);//两个位置都不对的才能互换
			if (nums[i] != i)return nums[i];//检查是否为nums[i]安好了家
		}
	}
};
现在看到引例2,现在不允许交换位置了,那么需要怎么做呢?
同样利用安家的思想,现在有1~n个房间,但是却有n+1个数字,所以必然会多出来一个,多出来的这个数字显然就是重复的。这就是抽屉原理。要找出那个重复的数字,就要用到分治的思想。

我们首先将所有的房间号一分为二,分为[0,mid], [mid+1,n-1],然后遍历nums数组,对于任意一个数字x,只要0 <= x <= mid,则它的家一定在左边,sumLeft++,一直遍历完成,如果sumLeft > mid + 1,说明多出来的这个数想分配到左边,于是再将[0,mid]一分为二,分为[0,mid/2], [mid/2+1,mid],在利用这个思想检查,最终剩下的那个房子对应的编号就是多余的数字。
class Solution {
public:
	int duplicateInArray(vector<int>& nums) {
		int left = 0, right = nums.size() - 1;
		while (left < right) {
			int mid = (left + right) / 2;
			int sumleft = 0;
			for (int i = 0; i < nums.size(); i++) {
				if (nums[i] >= left && nums[i] <= mid) {
					sumleft++;
				}
			}
			if (sumleft > mid - left + 1) {
				right = mid;
			}
			else {
				left = mid + 1;
			}
		}
		return right;
	}
};
THE END
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章