算法1:开辟一个新的数组记录当前数字是否已经被输入
for(int i=1;i<=n+1;i++) { cin>>nums[i]; if(!st[nums[i]])//st数组表示当前数字是否已经被输入,未输入则标记为1,已经输入则为答案直接输出 { st[nums[i]]=1; } else { cout<<nums[i]; return 0; } }
[题解主要讨论思考题部分,暴力写法不做过多解析]
思考题*:不使用额外数组
算法2:两层循环,暴力枚举
时间复杂度O(n^2)
for(int i=1;i<=n+1;i++) { for(int j=i+1;j<=n+1;j++) { if((nums[i] ^ nums[j]) == 0)//异或和为0说明相同 return nums[j]; } }
算法3:(分治,抽屉原理) O(nlogn) 这道题目主要应用了抽屉原理和分治的思想。
抽屉原理:n+1 个苹果放在 n 个抽屉里,那么至少有一个抽屉中会放两个苹果。
用在这个题目中就是,一共有 n+1 个数,每个数的取值范围是1到n,所以至少会有一个数出现两次。
然后我们采用分治的思想,将每个数的取值的区间[1, n]划分成[1, n/2]和[n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。 注意这里的区间是指数的取值范围,而不是数组下标。
划分之后,左右两个区间里一定至少存在一个区间,区间中数的个数大于区间长度。 这个可以用反证法来说明:如果两个区间中数的个数都小于等于区间长度,那么整个区间中数的个数就小于等于n,和有n+1个数矛盾。
因此我们可以把问题划归到左右两个子区间中的一个,而且由于区间中数的个数大于区间长度,根据抽屉原理,在这个子区间中一定存在某个数出现了两次。
依次类推,每次我们可以把区间长度缩小一半,直到区间长度为1时,我们就找到了答案。
复杂度分析 时间复杂度:每次会将区间长度缩小一半,一共会缩小 O(logn) 次。每次统计两个子区间中的数时需要遍历整个数组,时间复杂度是 O(n)。所以总时间复杂度是 O(nlogn)。 空间复杂度:代码中没有用到额外的数组,所以额外的空间复杂度是O(1)。
#include<bits/stdc++.h> using namespace std; const int N=1e5; int a[N]; int main() { int n; cin>>n; for(int i=1;i<=n+1;i++) cin>>a[i]; int l=1;int r=n; while(l<r) { int mid=l+r>>1;// 划分的区间:[l, mid], [mid + 1, r] int cnt=0; for(auto x: a) cnt+=x>=l&&x<=mid; if(cnt>mid-l+1) r=mid; else l=mid+1; } cout<<l; return 0; }
注意:若数组中存在多个重复的数字,该方法可以求出所有重复数字中其中的一个
算法4:利用抽屉原理,用等差数列求和
#include<stdio.h> int main(){ int n; int sum=0; scanf("%d",&n); for(int i=0;i<=n;i++){ int temp; scanf("%d",&temp); sum+=temp; } printf("%d",(sum-(1+n)*n/2));//所有输入数的总和减去1~n的总和即为重复的数字 return 0; }
注意:若数组中存在多个重复的数字,该方法无法使用
算法5:把位运算玩出花
思想:1001个数异或结果与1-1000异或的结果再做异或,得出的值即位所求。 原理: 设重复数为A,其余999个数异或结果为B。1001个数异或结果为A^A^B。1-1000异或结果为A^B。由于异或满足交换律和结合律, 则有 (A^B)^(A^A^B)=A^B^B=A
-
异或操作,任意数字与自身相异或,结果都为0。
-
任何元素与0相异或,结果都为元素自身。
#include<bits/stdc++.h> using namespace std; int main() { int n; cin>>n; int ans=0; for(int i=1;i<=n+1;i++) { int t; cin>>t; ans^=t; if(i!=n+1) ans^=i; } cout<<ans; }
注意:若数组中存在多个重复的数字,该方法无法使用
算法6:模拟带环链表,快慢指针证明
第一步:首先了解,什么是链表有环
第二步:判断方法(快慢指针)
双指针,一快(每次跑两格)一慢(每次跑一格),从链表首部开始遍历,两个指针最终都会进入环内,由于快指针每次比慢指针多走一格,因此快指针一定能在环内追上慢指针。而如果链表没环,那么快慢指针不会相遇。
从网上找到了图解过程,感受一下:
第三步:将数组看做模拟链表
我们可以将数组视为一个(或多个链表),每个元素都是一个节点,元素的下标代表节点地址,元素的值代表next指针,因此,重复的元素意味着两个节点的next指针一样,即指向同一个节点,因此存在环,且环的起点即重复的元素。
为了找到任意一个环的起点(重复元素),我们只需要拿到一个链表的首部,然后利用前置知识即可解决问题。显然,0一定是一个链表的首部,因为所有元素值的范围在1 - n-1之间,即没有节点指向0节点。
题解流程即为:从0开始,快慢指针分别以2、1的速度向前遍历,当它们相遇时,将快指针置为0,继续分别以1、1的速度向前遍历,当它们再次相遇时,此时它们的下标就是题解。
时间复杂度分析:慢指针每次走一格,刚好遍历到链表尾部(即环起点)处结束,因此复杂度为O(n) 空间复杂度分析:O(1)
#include<bits/stdc++.h> using namespace std; const int N=1e5; int nums[N]; int main() { int n; cin>>n; for(int i=0;i<=n;i++) { cin>>nums[i]; } int f = 0,s = 0; while (f == 0 || f != s) { f = nums[nums[f]];//跑两格 s = nums[s];//跑一格 } f = 0; while (f != s) { f = nums[f]; s = nums[s]; } cout<<f; }
链表有环部分的示意图来自:(6条消息) 链表有环是什么意思_链表算法看我就够了_vnam的博客-CSDN博客