剑指offer:面试题3:数组中重复的数字

1.数组介绍

首先对数组进行基本的介绍,数组的特点就是连续存储。在C++中,我们一般有两种类型的数组:一种是静态数组,我们事先知道其大小。一种是动态数组,也就是vector,事先可以不规定大小。

2.题目介绍

这一类型题目有很多种类型,比如数组是否有序,重复元素的个数等等。

2.1 找出数组中的重复元素

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

根据题意,我们可以知道数组元素的范围是确定的,以及最后的要求是输出任意一个元素。

2.1.1排序,然后遍历

已知数组是无序的(没有说有序就是无序),我们可以先对数组进行排序,比如 { 2 , 3 , 1 , 0 , 2 , 5 , 3 } \{2,3,1,0,2,5,3\} {2,3,1,0,2,5,3},排序之后得到 { 0 , 1 , 2 , 2 , 3 , 3 , 5 } \{0,1,2,2,3,3,5\} {0,1,2,2,3,3,5},这样一来,我们就可以通过判断相邻元素是否相等,得到重复元素。这种时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)。因为排序是在数组上进行的。代码如下:

bool duplicate1(int numbers[], int length, int* duplication) {
        sort(numbers,numbers+length);
        bool flag=false;
        for(int i=0;i<length-1;i++)
        {
            if(numbers[i]==numbers[i+1])
            {
                *duplication= numbers[i];
                flag=true;
                break;
            }
        }
        return flag;
}
2.1.2 使用哈希表

因为数组中的每个元素都在确定的范围里,并且都为正数。所以我们可以直接使用数组当作哈希表。因此我们额外创建一个长度为n的数组,使用输入数组的元素作为下标。以 n u m = { 2 , 3 , 1 , 0 , 2 , 5 , 3 } num=\{2,3,1,0,2,5,3\} num={2,3,1,0,2,5,3}为例,我们初始化一个全是0的哈希数组, { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } \{0,0,0,0,0,0,0,0\} {0,0,0,0,0,0,0,0},我们最终要求的哈希表是 { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 } \{0,1,2,3,4,5,6,7\} {01,2,3,4,5,6,7},即 h a s h [ i ] = i hash[i]=i hash[i]=i;所以当 h a s h [ n u m [ i ] ] ! = n u m [ i ] hash[num[i]]!=num[i] hash[num[i]]!=num[i]的时候,我们就将对应元素放入哈希表。比如, n u m [ 0 ] = 2 num[0]=2 num[0]=2; h a s h [ 2 ] hash[2] hash[2]此时为0,所以将 h a s h [ 2 ] = 2 hash[2]=2 hash[2]=2;当我们遍历到 n u m [ 4 ] num[4] num[4]的时候,我们发现此时 h a s h [ 2 ] hash[2] hash[2]已经等于2了,则说明之前已经遇到过2了,那么2为重复元素。空间复杂度为 O ( n ) O(n) O(n),时间复杂度为 O ( n ) O(n) O(n).

bool duplicate2(int numbers[], int length, int* duplication) {
        bool flag=false;
        //array initialize
        int hash_t[length];
        for(int i=0;i<length;i++)
        {
            hash_t[i]=0;
        }
        for(int i=0;i<length;i++)
        {
            if(hash_t[numbers[i]]!=0)
            {
                flag=true;
                *duplication=numbers[i];
                 break;
            }
            else
                hash_t[numbers[i]]=numbers[i];
        }
        return flag;
}
2.1.3 手动重排数组

使用哈希表,空间复杂度过高;使用排序,时间复杂度过高。因为采取一种折中的办法,通过交换手动将输入数组排序为哈希表的样子,也就是把每个数组元素放在以该元素为下标的位置。

  1. { 2 , 3 , 1 , 0 , 2 , 5 , 3 } \{2,3,1,0,2,5,3\} {2,3,1,0,2,5,3} 对于第0个位置,num[0]!=0,我们不知道0在哪里,但是我们知道num[0]=2 这个2应该放在哪里。所以我们先查看第2个位置的值,发现第二个位置的值为1,不为2,因此我们交换第0个位置的元素和第二个位置的元素,即 { 1 , 3 , 2 , 0 , 2 , 5 , 3 } \{1,3,2,0,2,5,3\} {1,3,2,0,2,5,3}
  2. 交换之后,其实第0个位置的元素依然不符合要求,因此我们进行如上操作,交换第0个位置和第1个位置的元素,即 { 3 , 1 , 2 , 0 , 2 , 5 , 3 } \{3,1,2,0,2,5,3\} {3,1,2,0,2,5,3}
  3. 依然不符合要求,进行操作,即 { 0 , 1 , 2 , 3 , 2 , 5 , 3 } \{0,1,2,3,2,5,3\} {0,1,2,3,2,5,3}
  4. 操作完成之后,发现第0~3位置都符合要求,因此我们对于第4个位置元素进行操作,第4个位置元素为2,我们查看发现此位置的元素已经是2了,也就是说明此时不需要交换了,出现了一个多余的2。依次类推,可以找出所有多余元素。

时间复杂度为 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1).

bool duplicate3(int numbers[], int length, int* duplication) {

    bool flag=false;
    for(int i=0;i<length;i++)
    {
        while(numbers[i]!=i)
        {
            if(numbers[numbers[i]]==numbers[i])//satisfied hash condition
            {
                flag=true;
                *duplication=numbers[i];
                return flag;
            }
            //exchange
            int temp=numbers[i];
            numbers[i]=numbers[temp];
            numbers[temp]=temp;
        }

    }
    return flag;

}

第三种方法效率最高,但是修改了数组本身,如果题目要求不修改数组呢?

2.2 不修改数组找到重复数字(Leetcode.287寻找重复数)

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: [1,3,4,2,2]
输出: 2
示例 2:

输入: [3,1,3,4,2]
输出: 3
说明:

不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
2.2.1 使用hash表

因为给定了数组元素的范围,所以可以直接使用数组做hash表。和上面一样,有 O ( n ) O(n) O(n)的空间开销。

int findDuplicate(vector<int>& nums) {
        int n=nums.size();
        int result;
        vector<int>hash_n(n,0);
        for(int i=0;i<n;i++)
        {
            if(hash_n[nums[i]]!=0)
            {
                result=nums[i];
                break;
            }
            else
            {
                hash_n[nums[i]]=nums[i];
            }
        }
        return result;
    }
2.2.2 使用二分查找

因为不可以修改数组,所以我们应该就是查找重复元素。并且只有一个重复元素,所以目标就是找到那个重复元素。

因为所有元素都在1-n之间,所以我们可以划分前后两个区间,比如n=5,我们分成两份1-2,3-5,我们统计所有元素在这两个区间的个数,以 [ 1 , 3 , 4 , 2 , 2 ] [1,3,4,2,2] [1,3,4,2,2]为例,在[1,2]之间的有3个,但是区间长度为2,说明那个重复元素在[1,2]之间。然后再次进行划分,因为长度为1,也就是统计1,2的次数,可以找到重复元素为2. 时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)

public:
    int CountNumber(vector<int>& nums,int start,int end_)
    {
    int n=nums.size();
    int ans=0;
    for(int i=0;i<n;i++)
    {
        if(nums[i]>=start&&nums[i]<=end_)
            ++ans;
    }
    return ans;
    }
    int findDuplicate(vector<int>& nums) {
        int length=nums.size();
        int start=1;
        int end_=length;
        while(start<=end_)
        {
            int mid=(start+end_)/2;
            int count_s_m=CountNumber(nums,start,mid);
            if(end_==start&&count_s_m>1)
                return start;
            if(count_s_m>mid-start+1)
                end_=mid;
            else
                start=mid+1;

        }
        return -1;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值