剑指Offer03.数组中重复的数字

  • 题目1:03.数组中重复的数字
    一个长为n的数组,其中所有元素都位于[ 0,n-1 ]内;
    其中有数字是重复的,且重复数字不确定有几个,且重复了几次也不确定;
    返回数组中任意一个重复数字;

  • 思路:
    相当于n个位置放n种数,若每种数放一个则刚好没重复元素,题目说有若干种数重复;

1.遍历一次:O(n),O(1)
把遍历过的数字都放在它们应在的坑里,每次++i或swap都将一个数放在了它应在的坑里,直到发现我应在的坑里已经有一个我了;
缺点:修改了原数组

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int len = nums.size();
        for (int i = 0; i < len; ++i) {
            while (i != nums[i] && nums[i] != nums[nums[i]]) { //若nums[i] == i就往后遍历
                swap(nums[i], nums[nums[i]]); //每交换一次都使下标Nums[i]处匹配
            }
            if (nums[i] != i && nums[nums[i]] == nums[i]) { //我不在对应位置,我应该在的位置已经有一个我了
                return nums[i];
            }
        }
        return -1;
    }
};

2.把应在的坑内的数取反,若下次又发现坑内的数已经为负,则说明该坑的索引重复O(n),O(1)
缺点:①数组内的数必须>0:若有负数,很好理解,不再解释 ; 若有0,则重复数字应在的坑内放了一个0,则无法确定重复数字是几,例如3,3,2,0 ② 同样修改了原数组
对于本题,长为n的数组,取值均为[ 0,n - 1 ],因此无法直接使用,若想使用此方法,可以把所有数提前+1,就不会出现0了,但要注意需要-1才是应放的坑;

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        for (int& x : nums) x += 1;//提前把所有数+1,防止0的出现
        for (int i = 0; i < nums.size(); ++i) {
            //在没发现重复数字之前,遍历到的坑内可能放的是已经取反的数
            //需要减1是因为提前把所有数加了1,因此需要减1才是我应在的坑
            int index = abs(nums[i]) - 1;
            if (nums[index] < 0) {
                return index;//说明有两个重复数index+1(这两个数原本是index)都放到了index这个坑内
            }
            nums[index] *= (-1);
        }
        return 
    }
};

3.unordered_set边遍历边存,发现集合中已经有了说明重复:O(n):遍历一次,O(n):额外集合空间,最坏情况重复数字位于数组的末尾两个,此时需要O(n)
优点:无需修改原数组,但缺点是花费O(n)额外空间

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        unordered_set<int> us;
        for (int i = 0; i < nums.size(); ++i) {
            if (us.find(nums[i]) != us.end()) return nums[i];
            us.insert(nums[i]);
        }
        return -1;
    }
};
  • 题目2:不修改数组找出重复的数字
    一个长度为n的数组,数组值均为[1,n-1 ],找出任一重复数字;
    1:抽屉原理 + 二分:O(nlogn):每次二分都需要遍历一次数组,来统计值位于[ l,mid ]的数目,O(1)
    n个坑放n-1个数,因此一定有重复数字;
class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        int n = nums.size() - 1;
        int l = 1, r = n;
        while (l < r) {
            int mid = l + ((r - l) >> 1);
            //[l, mid], [mid + 1, r]
            int count = 0;
            for (int x : nums) 
                if (x >= l && x <= mid) ++count;
            if (count > mid - l + 1) r = mid;//[l, mid]长度为mid-l+1,但位于此区间的值的个数超过此长度,说明重复数字位于此区间
            else l = mid + 1;
        }
        
        return l;//出循环的时候l==r
    }
};

2.(最优解)等价于链表找环:O(n):慢指针每次走一格,刚好遍历到链表尾部(即环起点)处结束,O(1)
①我们可以将数组视为一个(或多个链表),每个元素都是一个节点,元素的下标代表节点地址,元素的值代表next指针,因此,重复的元素意味着两个节点的next指针一样,即指向同一个节点,因此存在环,且环的起点即重复的元素。
②为了找到任意一个环的起点(重复元素),我们只需要拿到一个链表的首部,然后利用前置知识即可解决问题。显然,0一定是一个链表的首部,因为所有元素值的范围在1 - n-1之间,即没有节点指向0节点。
题解流程即为:从0开始,快慢指针分别以2、1的速度向前遍历,当它们相遇时,将快指针置为0,继续分别以1、1的速度向前遍历,当它们再次相遇时,此时它们的下标就是题解。

//链表找环很好理解,但本题怎么等价过去的没理解好,为何下标0就是链表首节点有点晕
class Solution {
public:
    int duplicateInArray(vector<int>& nums) {
        int f = 0, s = 0;
        while (f == 0 || f != s) {//从0开始,快慢指针分别以2、1的速度向前遍历,直到相遇
            f = nums[nums[f]];
            s = nums[s];
        }

        f = 0;//将快指针置为0,继续分别以1、1的速度向前遍历,当它们再次相遇时,此时它们的下标就是题解。
        while (f != s) {
            f = nums[f];
            s = nums[s];
        }
        return s;
    }
};
  • 总结:
    这道题长度为n的数组,数组值取[0,n - 1],[1,n-1] 采用的方法都不同,后者因为n个坑放n-1种数因此想到抽屉原理,进而对值二分;前者n个坑放n种树,通常想到用下标映射值的方式,一个萝卜一个坑;
    根据不同的条件要有条件反射性的想到对应思路;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值