剑指offer (03):数组中重复的数字 (C++ & Python 实现)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Cowry5/article/details/82655933

1 题目一 找出数组中重复的数字

1.1 描述

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

1.2 题解

方法一

将输入的数组排序。从排序的数组中找出重0的数字,只需从头到尾扫描排序后的数组。时间复杂度O(nlogn)。

方法二

利用哈希表,从头到尾按顺序扫描数组的每个数组,每扫描到一个数字的时候,都可以用 O(1)的时间来判断哈希表里是否已经包含了这个数字。如果没有,则加入哈希表。若已存在,就找到重复数字。时间复杂度 O(n),提高时间效率的代价是一个空间复杂度为 O(n)的哈希表。

方法三

有没有时间复杂度是O(n),空间复杂度为O(1)的方法呢?

我们注意到数组长度为 n,且数字范围为 0~n-1,若没有重复的数字,则数组排序后数字i将出现在下标为i的位置,值和下标刚好一一对应。若重复,则有些下标对应的位置存在多个一样的数字。

让我们重新在排列这个数组,从头到尾依次扫描每个数字。当扫到下标为 i 的数字,首先判断这个数字(m)是否等于 i。如果是,则扫描下一个数字。若不是,则再拿它和下标为 m 的数字比较,相等则找到一个重复的数字 (该数字在下标为 i 和 m 的位置都出现了),若不等则交换两者位置。使得数字 m 对应下标 m。接着继续重复这个过程,直到找到重复数字为止。

例如有个数组:{2, 3, 1, 0, 2, 4}

—> {13,2,0,2,4}

—> {3,1,2,0,2,4}

—> {0,1,2,3,2,4} 发现重复数字。

1.3 代码

C++

/*
参数:
    numbers:     一个整数数组
    length:      数组的长度
    duplication: (输出) 数组中的一个重复的数字
返回值:
    true  - 输入有效,并且数组中存在重复的数字
    false - 输入无效,或者数组中没有重复的数字
*/
bool duplicate(int numbers[], int length, int* duplication)
{
    // 数组不能为空,长度必须大于0
    if (numbers == nullptr || length <= 0)
        return false;

    // 确保每个数字范围在 0~n-1
    for (int i = 0; i < length; i++)
    {
        if (numbers[i] < 0 || numbers[i] > length - 1)
            return false;
    }
    for (int i = 0; i < length; ++i)
    {
        while (numbers[i] != i)
        {
            // 若相等则发现重复数字,返回 ture
            if (numbers[i] == numbers[numbers[i]])
            {
                *duplication = numbers[i];
                printf("%d", *duplication);
                return true;
            }
            // 不等则交换 numbers[i] 和 numbers[numbers[i]]
            int temp = numbers[i];
            numbers[i] = numbers[temp];
            numbers[temp] = temp;
        }
    }
    return false;
}

Python

class Solution:
    def duplicate(self, numbers, duplication):
        if numbers == None or len(numbers) <=0:
            return False
        for i in numbers:
            if i > len(numbers) - 1 or i < 0:
                return False
        for i in range(len(numbers)):
            while numbers[i] != i:
                if numbers[i] == numbers[numbers[i]]:
                    duplication.append(numbers[i])              
                    print(duplication[0])
                    return True
                else:
                    index = numbers[i]
                    numbers[i], numbers[index] = numbers[index], numbers[i]
        return False

s = Solution()
array = [1, 2, 3, 3, 4, 4]
duplication = []
s.duplicate(array, duplication)

代码中虽然有个两重循环,但每个数字最多只要交换两次就能找属于它自己的位置。因此中的时间复杂度为 O(n),且空间复杂度为 O(1)。

2 题目二 不修改数组找出重复的数字

2.1 描述

在一个长度为 n+1 的数组里的所有数字都在 1 到 n 的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字2或者3。

2.2 题解

方法一

由于不能修改输入数组,我们可以创建一个长度为 n+1 的辅助数组,逐一把原数组的数字复制到辅助数组。如果原数组中的数字是 m,则把它复制到数组中下标为 m 的位置。持续执行则会发现重复数字,但这个需要 O(n) 的辅助空间。

方法二

接下来我们避免使用 O(n) 的辅助空间。假设没有重复数字,那 1~n 的范围内最多只有 n 个数字,而题目却有 n+1 个数字,所以一定存在重复数字。所以某范围内数字的个数对解决这个问题很重要

原数组的数字范围为 1~n,我们用中间大小 m 把原数组按大小范围分为两部分,前一半的大小范围为 1~m,后一半的范围为 m+1~n。如果范围 1~m 的数字个数超过 m 个,则当中一定存在重复数字,否则后一半存在重复数字。我们继续把包含重复数字的范围一分为二,直到找到一个重复的数字。

2.3 代码

C++

// 参数:
//        numbers:     一个整数数组
//        length:      数组的长度
// 返回值:             
//        正数  - 输入有效,并且数组中存在重复的数字,返回值为重复的数字
//        负数  - 输入无效,或者数组中没有重复的数字
int getDuplication(const int* numbers, int length)
{
    if (numbers == nullptr || length <= 0)
        return -1;

    // 数组长度 n + 1, 数字范围 1 ~ n
    int start = 1;
    int end = length - 1;
    while (end >= start)
    {
        int middle = ((end - start) >> 1) + start; // 不易越界,(end + start)/2 容易越界
        int count = countRange(numbers, length, start, middle);
        if (end == start) // 当只剩一个数字
        {
            if (count > 1) // 且这个数字在数组中出现的次数大于1
                return start; // 返回此重复的数字
            else
                break; // 否则跳出循环,数组内无重复数字
        }

        if (count > (middle - start + 1)) // 如果前半部分范围内数字出现的次数大于范围
            end = middle;  // 说明此部分有重复数字,接着在前半范围搜索
        else 
            start = middle + 1; // 否则后半部分有重复数字,接着在后半范围搜索
    }
    return -1;
}


// 统计给定数组给定范围内数字的个数
int countRange(const int* numbers, int length, int start, int end)
{
    if (numbers == nullptr)
        return 0;

    int count = 0;
    for (int i = 0; i < length; i++)
        if (numbers[i] >= start && numbers[i] <= end)
            ++count;
    return count;
}

Python

class Solution():
    def getDuplicaton(self, numbers):
        if numbers == None or len(numbers) <= 0:
            return 'valid numbers'
        start = 1
        end = len(numbers) - 1
        while(end >= start):
            middle = ((end - start) >> 1) + start
            count = self.countRange(numbers, start, middle)
            if end == start:
                if count > 1:
                    return start
                else:
                    break
            if count > (middle - start + 1):
                end = middle;
            else:
                start = middle + 1
        return 'no duplication'

    def countRange(self, numbers, start, end):
        if numbers == None:
            return 0
        count = 0
        for i in range(len(numbers)):
            if numbers[i] >= start and numbers[i] <= end:
                count += 1
        return count

s = Solution()
a = s.getDuplicaton([])
print(a)

上述代码按照二分查找的思路,长度为 n 的数组,countRange 将被调用 O(logn) 次,每次需要 O(n) 的时间,总的时间复杂度为 O(nlogn),空间复杂度为 O(1)。和方法一相比,相当于以时间换空间。

此方法并不能保证找出所有重复的数字。例如,数组{2,2}在范围 1~2 中的数字出现的次数也是2,并不会认为有重复数字。所以要根据题目的特性来写代码。

猜你喜欢

转载自blog.csdn.net/Cowry5/article/details/82655933