原地哈希用来解决这样一种问题:需要一个使得数组尽量有序的方式,并且要求时间复杂度达到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=
给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。
示例 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;
}
};