在O(1)的额外空间下寻找数组中唯一出现两次的数六种方法(分治、位运算、模拟带环链表......)

算法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

  1. 异或操作,任意数字与自身相异或,结果都为0。

  2. 任何元素与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博客

算法6的方法来自:AcWing 14. 不修改数组找出重复的数字 - O(n)解法 - AcWing

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值