剑指offer03题C++版集合/原地置换法多解&&反思&&总结

看总结直接跳到末尾

代码(集合方法:利用set集合的性质)

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        set<int> arr;
        int repeat=-1;
        for(int i=0;i<nums.size();++i){
            if(arr.find(nums[i])==arr.end()) {
                arr.insert(nums[i]);
            }
            else {
                repeat=nums[i];
                break;
            }
        }
        return repeat;
    }
};

代码(原地置换法)

如果没有重复数字,那么正常排序后,数字i应该在下标为i的位置,所以思路是重头扫描数组,遇到下标为i的数字如果不是i的话,(假设为m),那么我们就拿与下标m的数字交换。在交换过程中,如果有重复的数字发生,那么终止返回nums[i]

 注意下面这个代码是错误的,虽然能过,但不是准确的原地置换,见后面的解释,问题出在i位置是否和m一样归位

我这个优化不如正牌的原地置换,保留下来以助理解真正的原地置换(也是正确能过的代码)可以优先阅读

经过又一段时间调试发现这段代码也是错的,虽然力扣通过了,但是跑例子是错的,代码是错的,力扣判题系统给我判通过了,但实际上是错的,见后面,也可以现在先读完思考一下为什么是错的

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int m;
        for(int i=0;i<nums.size();++i){
            m=nums[i];
            if(nums[i]!=i){
                if(nums[m]!=m){
                    int tmp=nums[i];//这里的nums[i]是不可以无脑换成m的
                    nums[i]=nums[m];//因为这个交换的意义在于对于数组的改变
                    nums[m]=tmp;//必须要用数组本身空间上的值进行交换
                }else{
                    return m;
                }
            }
        }
        return 0;
    }
};

我最开始读完原地置换的代码在想

交换m位置和i位置值为m的,如果位置m上的值正好是重复的,被换到前面了,怎么办?或者说m位置上的值比i还要小怎么办?而且m归位了,i上的值也没有归位

真正的原地置换是把i也归位了的,我把for-while-if写成了for-if-if,写上面这段代码的时候理解有一点偏差

不能保证i及之前每个位置都有nums[i]==i啊,感觉很难直观想象出来

但你换一个方向思考,假设数组从0到n-1一个也不重复一个也不缺少,经过这一趟for循环的原地置换后,是什么?

答案是有序数组!❌ ❌ ❌(这是对于真正的原地置换)

上面一句说错了,我上面写的这段不是原地置换的代码并不能做到排序,但反正他给重复的找出来了,解题过了,目前还没想明白怎么过的,总之就是过了,容我慢慢想想,上面的推导对于真正的原地置换是成立的...

而有重复有缺失的,是必不可能最终变成有序数组的,一定会有障碍,这个障碍就是nums[m]==m了!

而且没有数被覆盖,只是交换,无论如何重复的数也run🏃‍♀️不掉的,必然被查出来

然后我突然发现我的原地置换写错了,上面并不是真正的原地置换,而是个优化不如原地置换的四不像,原地置换应该在把m位置的值归位后,在while中继续把i位置的值也同样归位,再进行++i

真正的原地置换代码👇

等等...还没有,我现在在力扣上跑对了自己认为的原地置换的代码,但写出来调试的例子却明显不是正确答案

这个输入例子我用了我俩已经通过代码跑到的结果都是错误的...

刚才我担心的事情发生了,用最初写的for-if-if代码跑例子得到错误答案的原因就是我想的,重复的元素被交换到前面去,后面不能再和它判断是否相等重复,所以漏判

真正的原地置换代码👇

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int m;
        for(int i=0;i<nums.size();++i){
            while(nums[i]!=i){
                m=nums[i];
                if(nums[m]!=m){
                    int tmp=nums[i];//这里的nums[i]是不可以无脑换成m的
                    nums[i]=nums[m];//因为这个交换的意义在于对于数组的改变
                    nums[m]=tmp;//必须要用数组本身空间上的值进行交换
                }else{
                    return m;
                }
            }
        }
        return 0;
    }
};

终于啊这次例子跑对了

上个我自认为是原地置换的代码错在把m=nums[i]这个借由while循环把i归位的最关键的代码放到了while外面!太愚了!

不过写了这么多遍调试了这么多次现在终于都知道了什么是原地置换的2个关键点

1.把m归位之后把i也要归位,保证i之前的全部必定有序,且键值一一对应相等

2.实现1的秘诀就是用while循环判断i是否归位,每一次while循环中重置m,归位新的m

如果你真的一路看下来我的每一步试错和分析,现在应该也对原地置换有了更深一层的认识了

还要注意能用原地置换的题必要有一个条件

n长度数组值的范围都在0~n-1才能搞原地置换而不出现数组越界

更普遍但没有这样优化的方法就是拿空间换时间,使用哈希表或者集合                                                                                                                                 //暴力遍历O(n^2)🐶都不用

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        //哈希表解法  
        unordered_map<int,int> mp;
        for(auto& num:nums)
        {
            mp[num]++;
            if(mp[num]>1)
                return num;
        }
        return 0;
    }
};

当然这可比原地置换好想和简单一万倍

然后这是一种原地置换的while-if-if方法

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
    //原地置换法
        int n=nums.size();
        int i=0;
        while(i<n){
            //如果nums[i]==i,就说明已经下标和数字匹配,就向下面寻找不匹配的值
            if(nums[i]==i){
                i++;
                continue;
            }   
            else
            {
                //检测是否发生碰撞,碰撞就说明两者有重复的数字,直接返回重复数字,如果没有碰撞,就把该数字放到其应该放置的位置
                if(nums[nums[i]]==nums[i])
                    return nums[i];
                else
                    swap(nums[i],nums[nums[i]]);
            }
        }
        return 0;
    }
};

同样是for-while-if的思想,最外层的for用while的小于判断实现,那你问while用什么实现?用else中不进行++i从而让(新方法)while同时也发挥(老方法)while的作用,感受下真的非常巧妙,这就是魅力。

一路说了这么多,做个总结吧

原地置换法使用的条件:在一个具有n个元素大小的数组中,所有元素在0~n-1范围内(以防数组越界)

原地置换法的两个关键思路:

    1.把m归位之后把i也要归位,保证i之前的全部必定有序,且键值一一对应相等

    2.实现1的秘诀就是用while循环判断i是否归位,每一次while循环中重置m,归位新的m

原地置换法的代码两种:

原地置换for-while-if
class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int m;
        for(int i=0;i<nums.size();++i){
            while(nums[i]!=i){
                m=nums[i];
                if(nums[m]!=m){
                    int tmp=nums[i];//这里的nums[i]是不可以无脑换成m的
                    nums[i]=nums[m];//因为这个交换的意义在于对于数组的改变
                    nums[m]=tmp;//必须要用数组本身空间上的值进行交换
                }else{
                    return m;
                }
            }
        }
        return 0;
    }
};
原地置换法while-if-if
class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
    //原地置换法
        int n=nums.size();
        int i=0;
        while(i<n){
            //如果nums[i]==i,就说明已经下标和数字匹配,就向下面寻找不匹配的值
            if(nums[i]==i){
                i++;
                continue;
            }   
            else
            {
                //检测是否发生碰撞,碰撞就说明两者有重复的数字,直接返回重复数字,如果没有碰撞,就把该数字放到其应该放置的位置
                if(nums[nums[i]]==nums[i])
                    return nums[i];
                else
                    swap(nums[i],nums[nums[i]]);
            }
        }
        return 0;
    }
};

 

                                                                                                                                  感谢阅读🙏

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值