原地哈希

原地哈希用来解决这样一种问题:需要一个使得数组尽量有序的方式,并且要求时间复杂度达到O(n)。

我们先来看这样一个问题,一个长度为n的数组,所有的数都不相同,且数据的范围为[1,n],如何在O(n)的时间复杂度内完成排序。

实际上,我们在做一般排序的时候,是基于数字具体值的大小来决定顺序的,也就是说,数字具体值决定了数字应该去的位置。而这题中的条件:长度为n个数组,所有的数均不相同,不妨我们就让num[i]去到索引为num[i]的位置。实际上,在本题的条件下,num[i]就应该去索引为num[i]的位置上。

复杂度分析:上述思路每一个位置上的置换都可以至少让一个数成功归位,因此复杂度为O(n)。

测试样例:

8

2 7 6 4 3 5 8 1

输出:

1 2 3 4 5 6 7 8

当我们以上述思路看待这个数组的时候,整个数组可以被置换环分割。

如图所示,2 1 6 4 3 5 7 8被三个置换环分割,分别为2->7->8->1->2,6->5->3->6,4->4。按照置换环指定的方式进行轮换,就可以完成排序。

我们还有一种方式进行置换,如下图所示:

此处的A、B、C、D只是一种抽象化表示,表示置换环的顺序。我们知道,num[i]该去什么地方,是由num[i]值本身的大小决定的,与索引i无关,因此我们可以先把A、B置换,此时A已经归位,但是B没有,相当于把A从环中断开,将D和B连接。代码中采用了这种方式:

#include<iostream>
using namespace std;

int num[10010];

int main(){
	int n,i;
	cin>>n;
	for(i=1;i<=n;++i){
		cin>>num[i];
	}
	for(i=1;i<=n;++i){
		while(num[i]!=i){
			swap(num[i],num[num[i]]);
		} 
	}
	for(i=1;i<=n;++i)cout<<num[i]<<" ";
	return 0;
} 

来看看原地哈希的妙用吧=w=

41. 缺失的第一个正数

给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。

示例 1:

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

示例 2:

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

示例 3:

输入: [7,8,9,11,12]
输出: 1

提示:

你的算法的时间复杂度应为O(n),并且只能使用常数级别的额外空间。

思路:

A.最直观的想法无非就是直接从1开始枚举。

B.为了有效利用遍历过的信息,我们可以将数据进行排序,预设答案为ans=1(ans为没有枚举到的答案的最小可能值),开始遍历整个数组,如果发生了ans==num[i],则++ans,如果发生了num[i]>ans的情况,由ans自增的逻辑我们可以知道,在数据保持相邻不变或者递增1的情况下,ans>=num[i]是必定成立的,如果num[i]>ans,则一定是发生了跳跃,此时ans必为答案。如果能够循环到数组结束,那么答案就是ans。

#include<bits/stdc++.h>
using namespace std;

int num[10010];

int main(){
	int n,i,ans=1;
	cin>>n;
	for(i=1;i<=n;++i){
		cin>>num[i];
	}
	sort(num+1,num+1+n);
	for(i=1;i<=n;++i){
		if(num[i]==ans)++ans;
		else if(num[i]>ans)break;
	}
	cout<<ans;
	return 0;
} 

C.本题中要求的是O(n)的复杂度,而想要快速得出答案就必须要排序(或者说数组有一定的顺序),如果将原地哈希引入会发生什么呢?我们发现这题的约束要宽松很多,乍一看是不适合应用原地哈希的,主要问题是我们需要给那些数值范围不处于[1,n]的数据提供相应的正确处理法则。

1.首先我们看数值小于1的数。

这些数显然对答案没有任何贡献,所以可以直接忽略(它只起到了类似于占位符的作用)。

2.数值大于n的数。一个长度为n的数组,他所能够形成的答案的最大值为ans=n+1,而数组中一旦有数大于n,就会导致ans<=n,(换言之,每有一个数>n,则会导致答案的最大值减1)。我们先假设只有一个数大于n,暂时不考虑其他数<=0的情况,那么n-1个位置能够安排下这些数,而且通过B我们知道,一个长度为n的有序数组,最大可以跑出的答案为n+1,而此处需要用n-1长度的数组跑出最大答案为n的情况显然是没问题的。一句话总结就是,如果一个数大于n,一定不会由它推出答案。

3.重复的数解决。如果数组种有重复的数,他们可能会形成闭合的死循环,此时需要处理。

当我们遇到序列1(A)   1(B),[括号后的序列只是用于表示区分相同的数,没有任何意义],当我们遇到1(A)时,它已经归位,不再管他;而我们遇到1(B)时,将它与1(A)交换,而交换过来的1(A)又要与1位置上的1(B)交换[禁止套娃!]。而到底有几个1实际上对我们没有任何影响,因此,我们既使用尾部缓存的方式,将它与数组尾部进行交换(即让数组尾部负责保存这些无用数据),也可以把它按照小于1的数做忽略处理,也就是说,当我们发现当前数需要交换到的位置上的数是已经归位的数时,将当前数当作无效数字做忽略处理。

#include<bits/stdc++.h>
using namespace std;

int num[10010];

int main(){
	int n,i,ans=1;
	cin>>n;
	for(i=1;i<=n;++i){
		cin>>num[i];
	}
	for(i=1;i<=n;++i){
		while(num[i]!=i&&num[i]>0&&num[i]<=n&&num[num[i]]!=num[i]){
			swap(num[i],num[num[i]]);
		}
	}
	for(i=1;i<=n;++i){
		if(num[i]!=i){
			cout<<i;
			return 0;
		}
	}
	cout<<n+1;
	return 0;
} 

回到lintcode,其实我们可以在题目给的vector数组前加一个数,然后套用相同的逻辑,或者就是做一个偏移量为1的映射。

增加数字,不做偏移:

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n=nums.size(),i;
        reverse(nums.begin(),nums.end());
        nums.push_back(0);
        reverse(nums.begin(),nums.end());
        for(i=1;i<=n;++i){
            while(nums[i]!=i&&nums[i]>0&&nums[i]<=n&&nums[nums[i]]!=nums[i]){
                swap(nums[i],nums[nums[i]]);
            }
        }
        for(i=1;i<=n;++i){
            if(nums[i]!=i)return i;
        }
        return n+1;
    }
};

做偏移映射:(即0位置放1,1位置放2,nums[i]要放置到nums[i]-1的位置)

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n=nums.size(),i;
        for(i=0;i<n;++i){
            while(nums[i]!=i+1&&nums[i]>0&&nums[i]<=n&&nums[nums[i]-1]!=nums[i]){
                swap(nums[i],nums[nums[i]-1]);
            }
        }
        for(i=0;i<n;++i){
            if(nums[i]!=i+1)return i+1;
        }
        return n+1;
    }
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值