剑指Offer--051-数组中重复的数字

链接


牛客OJ:数组中重复的数字

九度OJ:未收录

GitHub代码: 051-数组中重复的数字

CSDN题解:剑指Offer–051-数组中重复的数字

牛客OJ九度OJCSDN题解GitHub代码
051-数组中重复的数字未收录剑指Offer–051-数组中重复的数字051-数组中重复的数字

题意


题目描述

在一个长度为n的数组里的所有数字都在0到n-1的范围内。

数组中某些数字是重复的,但不知道有几个数字是重复的。

也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或者3。

样例输入

2, 3, 1, 0, 2, 5, 3

2, 1, 3, 1, 4

样例输出

2

1

检查数据的合法性


  • 检查输入参数是否合法

  • 数组中的数据是否满足所有数字都在0到n-1的范围内

bool CheckValidity(int *numbers, int length)
{
    //  输入数据不合法
    if(numbers == NULL || length <= 0)
    {
        return false;
    }

    //  元素必须在[0, n-1]的范围
    for(int i = 0; i < length; i++)
    {
        if(numbers[i] < 0 || numbers[i] > length - 1)
        {
            return false;
        }
    }

    return true;
}

排序后判断重复


最简单的思路就是先把输入的数组排序。从排序的数组中找出重复的数字就是个很容易的事情了。只需要从头向尾扫描一遍排序好的数组即可。

对一个数组排序的时间复杂度是 O(nlogn)

扫描一个排序好的数组发现重复的数字的时间复杂度是 O(n)

class Solution
{
public:
    bool duplicate(int numbers[], int length, int* duplication)
    {
        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }

        sort(numbers, numbers + length);    //  对数组进行排序

        int i = 0;
        bool isDup = false;
        *duplication = -1;

        //  扫描一遍数组发现重复的数字
        for(i = 0; i < length - 1; i++)
        {
            if(numbers[i] == numbers[i + 1])    //  重复了
            {
                isDup = true;
                *duplication = numbers[i];
                break;
            }
        }

        return isDup;
    }
};

hashtable哈希法优化查找速率


当然我们用hashtabble也可以实现,

hashtable中可以很方便的查找到某个元素是否存在,时间复杂度 O(n) ,空间复杂度 O(n)

class Solution
{
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    bool duplicate(int numbers[], int length, int* duplication)
    {

        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }

        set<int> s;
        bool isDup = false;
        *duplication = -1;
        for(int i = 0; i < length; i++)
        {
            if(s.count(numbers[i]) == 1)  // 如果当前元素已经存在set中
            {
                isDup = true;
                *duplication = numbers[i];
                break;
            }
            s.insert(numbers[i]);

        }

        return isDup;
    }
};

标识数组法


hashtable的方式是在优化查找的过程,因为hashtable查找的时间复杂度为 O(1) ,

那么我们继而联想到,如果不设hash,直接用一个用一个数组flag当前标识,记录某个元素是否出现,

flag[i] =0标识元素i未出现,

flag[i] =1标识元素i出现

这样也可以在 O(1) 的时间内实现判断

空间 O(n) 的标识数组


class Solution
{
public:
    bool duplicate(int numbers[], int length, int* duplication)
    {
        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }
        int flags[length];
        memset(flags, 0, sizeof(flags));
        for(int i = 0; i < length; i++)
        {
            if(flags[numbers[i]] != 0)
            {
                *duplication = numbers[i];
                return true;
            }
            else
            {
                flags[numbers[i]] = 1;
            }
        }

        return false;
    }

};

我们采用了一个以空间换时间的方式, 但是有没有什么办法能够优化空间上的消耗呢?

因为我们需要的只是一个标识, 0或者1,那么我们完全没必要一个整数来做标识,

因此可做如下优化

用一个n位的单元来存储即可, 每一位都是一个标识

这种方法可以把空间消耗将低, 但是还是需要额外的空间,那么有没有不需要空间消耗的方法呢?

符号位标识法


我们可以看到数组中元素的大小都在[0-n)这个区间内, 都是正数,那么他们的符号位对我们来说就是无关紧要的, 因此我们直接拿符号位当成我们的标识位就行了

numbers[i]符号位为0表示数字i没有重复

numbers[i]符号位为1标识数字i重复

#define SET_SYMBOL_BIT(num)  ((num) |= (1 << 31))       /*  设置符号位为1 */
#define GET_ORIGIN_NUM(num)  ((num) & (~(1 << 31)))        /*  获取到源数据  */
#define GET_SYMBOL_BIT(num)  (((num) >> 31) & 1)        /*  获取符号位(标识)*/

class Solution
{
public:
    /*
        对于每一个数据numbers[i]
        其源数据origin = GET_ORIGIN_NUM(numbers[i])的标识存储在numbers[origin]的符号位
        判断其标识是否为1即可
    */
    bool duplicate(int numbers[], int length, int* duplication)
    {
        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }
        for(int i = 0; i < length; i++)
        {
            //  当前数字numbers[i]的标识即是numbers[numbers[i]]的符号位
            //  检查numbers[i]
            debug <<"origin = " <<GET_ORIGIN_NUM(numbers[i]) <<", flag = " <<GET_SYMBOL_BIT(numbers[GET_ORIGIN_NUM(numbers[i])]) <<endl;
            if(GET_SYMBOL_BIT(numbers[GET_ORIGIN_NUM(numbers[i])]) == 1)
            {
                *duplication = GET_ORIGIN_NUM(numbers[i]);
                return true;
            }
            else
            {
                SET_SYMBOL_BIT(numbers[GET_ORIGIN_NUM(numbers[i])]);
                debug <<"SET origin = " <<GET_ORIGIN_NUM(numbers[i]) <<", flag = " <<GET_SYMBOL_BIT(numbers[GET_ORIGIN_NUM(numbers[i])]) <<endl;
            }
        }

        return false;
    }

};

固定偏移法


跟标识法类似, 如果不借助外部辅助空间,那么我们只能在数组内部下功夫,又能设置标识,又能恢复数据(不破坏数据)的方式,前面我们用符号位作为标识的方法就是通过符号位,即判断了是否存在,又可以通过符号位的反转重新恢复数据,那么有没有其他类似的方法呢?

我们想到我们的数据都是[0, n)这个区间的,那么我们采用类似与移码的方法,让数据加上或者减去一个固定的偏移量,这样就可以即标识数据,又不损坏数据,为了能够区分出数据,这个偏移必须大于N,这样我们的原数据与标识数据存在一一映射关系。

[0, n-1] -=>+偏移n-=> [n, 2n-1]

class Solution
{
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    bool duplicate(int numbers[], int length, int* duplication)
    {
        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }

        for(int i = 0; i < length; i++)
        {
            if(numbers[numbers[i] % length] >= length)
            {
                *duplication = numbers[i] % length;
                return true;
            }
            else
            {
                numbers[numbers[i] % length] += length;
            }
        }

        return false;

    }
};

将元素放在自己该在的位置


剑指offer上提供的方法,这种方法采用交换的方法

我们考虑如果每个数字都置出现一次,那么此时是最完美的,每一个下标i对应元素numbers[i],也就是说我们对于数组中的每个元素numbers[i]都把它放在自己应该在的位置上numbers[numbers[i]]上, 如果我们发现有两个元素想往同一个位置上放的时候,说明此元素必然重复

即如下的过程

  1. 如果numbers[i] == i, 那么我们认为number[i]这个元素是在自己的位置上的

  2. 否则的话, numbers[i]这个元素就应该在numbers[numbers[i]]这个位置上, 于是我们交换numbers[i]和numbers[numbers[i]]

  3. 重复操作1, 直到number[i]== i, 则继续操作下一个位置的元素, 或者numbers[i] == numbers[numbers[i],元素重复

class Solution
{
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    bool duplicate(int numbers[], int length, int* duplication)
    {
        *duplication = -1;

        if(CheckValidity(numbers, length) == false)
        {
            return false;
        }

        for(int i = 0; i < length; i++)
        {
            // 极端情况按顺序排序, 第i个位置上的数numbers[i]应该等于其下标i
            while(numbers[i] != i)
            {
                debug <<"numbers[" <<i <<"] = " <<numbers[i] <<" isn't equal to it's index = " <<i <<endl;
                //  第numbers[i]位置上的数据numbers[numbers[i]应该是numbers[i]
                //  即数numbers[i]应该是数组第numbers[i]个数numbers[numbers[i]
                if(numbers[i] == numbers[numbers[i]])
                {
                    *duplication = numbers[i];
                    return true;
                }
                debug <<"swap numbers[" <<i <<"] and numbers[" <<numbers[i] <<"]" <<endl;
                swap(numbers[i], numbers[numbers[i]]);
            }
            debug <<"numbers[" <<i <<"] = " <<numbers[i] <<" is equal to it's index = " <<i <<endl;

        }

        return false;

    }

};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值