引言
有这样一类题型,他需要你在给定的数组中找到缺失的值或者重复的值,当然你可以利用HashSet来做,遍历数组,存储每个元素,然后根据题目要求,即可以得到正确的解,但这样做也有一个问题,导致其空间复杂度为 O ( n ) O(n) O(n),能否有一种方法,使得其既能完成题目要求,同时只用常数级别的空间呢,确实有,就是原地哈希
思想
原地哈希的思想可以理解为:把原始的数组当做哈希表来使用,我们自己编写哈希函数来存储值,根据题目的不同,所设置的哈希函数也不同。
下面我们直接以例题分析,从例题中明白其中思想。
实例
剑指 Offer 03. 数组中重复的数字
有题目可知,数组长度为n,数组里的数都集中在0~n-1的范围,由鸽巢原理可知,必定存在两个数值相同,即数组元素的 索引 和 值 是 一对多 的关系,而值是0 ~ n-1,而索引也是0 ~ n-1;
可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = i
)
如果nums[i]!=i
,则进行交换,否则说明该索引已和值正确对应;
如果nums[nums[i]]=nums[i]
,代表索引 nums[i]
处和索引 i
处的元素值都为 nums[i]
,即找到一组重复值,返回此值 nums[i]
;
代码实现:
class Solution {
public int findRepeatNumber(int[] nums) {
int n=nums.length;
if(n<1||nums==null) return 0;
for(int i=0;i<n;i++){
while(i!=nums[i]){//这里用while因为,不一定交换一次,值和索引就正确对应,可能需要多次交换直到正确对应为止,所以用while
if(nums[nums[i]]==nums[i]) return nums[i];
swap(nums,i,nums[i]);
}
}
return -1;
}
public void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
当然也可以利用HashSet来解决
class Solution {
public int findRepeatNumber(int[] nums) {
int n=nums.length;
Set<Integer> hashset=new HashSet();
for(int i=0;i<n;i++){
if(!hashset.add(nums[i])) return nums[i];
}
return -1;
}
}
442. 数组中重复的数据
这道题同上道题类似,都是去找重复的数字,但这道题因为数组中的元素的值对应在1~n之间,所以对应的哈希函数的写法也略有不同,不能直接下标和元素对应了,因为整体相对下标+1了,我们可以这么构建:如果当前元素出现过,则让这个值-1作为下标,下标对应的元素取负数,如果已经为负数,说明这个值已经重复出现过,这个值是我们要找的值
此外,这个做法还可以用于寻找没出现的值,在执行完一轮后,如果有下标对应的元素值为正,则说明这个下标+1的值根本没有出现过。
这个方法具有普适性,在数组元素取值为1~n时,都适用
具体代码:
class Solution {
public List<Integer> findDuplicates(int[] nums) {
List<Integer> ans=new ArrayList();
int n=nums.length;
if(n==0||nums==null) return ans;
for(int i=0;i<n;i++){//用绝对值,因为可能数值在前面的遍历中被变为负数
if(nums[Math.abs(nums[i])-1]>0) nums[Math.abs(nums[i])-1]*=-1;
else ans.add(Math.abs(nums[i]));
}
return ans;
}
}
448. 找到所有数组中消失的数字
此题是找缺失的数字,可以利用上述的取负的方法来做:
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> ans=new ArrayList();
int n=nums.length;
if(n==0||nums==null) return ans;
for(int i=0;i<n;i++){//用绝对值,因为可能数值在前面的遍历中被变为负数
if(nums[Math.abs(nums[i])-1]>0) nums[Math.abs(nums[i])-1]*=-1;
}
for(int i=0;i<n;i++){
if(nums[i]>0) ans.add(i+1);
}
return ans;
}
}
可以利用hash函数:f(nums[i]) = nums[i] - 1
去完成,如果出现元素值不等于下标+1的情况的话,则为缺失值
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> ans=new ArrayList();
int n=nums.length;
if(n==0||nums==null) return ans;
for(int i=0;i<n;i++){
while(nums[i]!=nums[nums[i]-1]) swap(nums,i,nums[i]-1);
}
for(int i=0;i<n;i++){
if(nums[i]!=i+1) ans.add(i+1);
}
return ans;
}
public void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
利用额外空间,使用hash表(手动创建,or利用HashSet)也可以
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n=nums.length;
int[] hash=new int[n+1];
for(int num:nums){
hash[num]++;
}
List<Integer> ans=new ArrayList<>();
for (int i = 1; i <=n; i++) {
if(hash[i]==0) ans.add(i);
}
return ans;
}
}
41. 缺失的第一个正数
-
按照刚才我们读例子的思路,其实我们只需从最小的正整数 1 开始,依次判断 2、 3 、4 直到数组的长度 N 是否在数组中;
-
如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;
-
我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1
-
我们可以采取这样的思路:就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 11个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
-
利用hash函数:
f(nums[i]) = nums[i] - 1
去完成
public class Solution {
public int firstMissingPositive(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
// 满足在指定范围内、并且没有放在正确的位置上,才交换
// 例如:数值 3 应该放在索引 2 的位置上
swap(nums, nums[i] - 1, i);
}
}
// [1, -1, 3, 4]
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 都正确则返回数组长度 + 1
return len + 1;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
}
没有测试用变负的方法,但应该没有问题,思路类似,不过要注意这个的取值范围是可能大于数组长度,所以变负需要注意边界条件
也可以利用hashset完成
class Solution {
public int firstMissingPositive(int[] nums) {
Set<Integer> set=new HashSet();
for(int num:nums){
set.add(num);
}
int n=nums.length;
for(int i=1;i<=n;i++){
if(!set.contains(i)) return i;
}
return n+1;
}
}
也可以利用二分查找,这个问题其实就是要我们查找一个元素,而查找一个元素,如果是在有序数组中查找,会快一些;我们可以将数组先排序,再使用二分查找法从最小的正整数 11 开始查找,找不到就返回这个正整数;
class Solution {
public int firstMissingPositive(int[] nums) {
int n=nums.length;
Arrays.sort(nums);
for(int i=1;i<=n;i++){
if(find(nums,i)==-1) return i;
}
return n+1;
}
public int find(int[] nums,int target){
int n=nums.length;
int l=0,r=n-1;
while(l<=r){
int m=l+(r-l)/2;
if(nums[m]>=target) r=m-1;
else l=m+1;
}
if(l>=nums.length||nums[l]!=target) return -1;
return l;
}
}