完美解决数组重排问题

有时候一个算法可以很简单,但是如果细究起来可以非常复杂,每解决一个这样的问题,我们的算法就提升一大截,看一下这个题:

找出数组中重复的数字。在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
例如:
输入:
[2, 3, 1, 0, 2, 5, 3]
因为2和3有重复,所以可以输出:2 或 3 

对于重复元素,Hash和集合是一个常用策略,我们首先想到可以用集合来记录数组的各个数字,当查找到重复数字则直接返回就行了,具体说来, (1)初始化: 新建 HashSet ,记为 set; (2)然后遍历数组 nums 中的每个数字 num :

  • 当 num在 set 中,说明重复,直接返回 num ;

  • 将 num 添加至 set 中;

如果不存在则返回−1 。本题中说一定有重复数字,因此遇到了直接返回就行了。

所以代码就这样子:

 public int findRepeatNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for(int num : nums) {
            if(set.contains(num))
                 return num;
            set.add(num);
        }
        return -1;
    }

如果是在工程里遇到这个问题,就足够了,但是在算法里还不行,因为你需要开辟一个O(n)的空间。到这里你只能得个及格分。面试官可能会问你还有其他方法吗?然后貌似排序也可以是吧,从排序的数组中找出重复的数字只需要从头到尾扫描即可,所以先排序再查找的时间复杂度主要就取决于排序算法,一般为O(nlogn)。但是为了一个搜索就执行排序这样的重量级操作,还将原始数组给改了,还不好,继续找。

我们希望得到一种不消耗额外空间的算法,也就是本题的第三种解法:数组重排。由于题目中告诉我们所有的数字都在0到n-1的范围内,因此如果没有重复,那么所存储的值也正好是0到n-1这n个数字,我们把原数组重新排列为一个元素和对应下标值相同的数组。具体思路如下:

从头到尾扫描整个数组中的数字,当扫描到下标为i的数字时,首先比较这个数字(用arr[i]表示)是不是等于下标i,如果是,接着比较下一个数字;如果不是,则将其与索引位置为arr[i]的数字比较,若与其相同,则说明它就是一个重复数字,如果不同,就将其与第m个数字进行交换,也就是把它放到自己应在的位置去。重复这个过程,直到该位置上的数与下标相同为止。这种思路就像收拾房间一样,物品如果不在自己的位置上就将其拿过去,这样假如你买了两包一样的零食,骗来一个妹子几张一样的照片都可以清楚地知道了。

该算法看起来是两层循环,但是每个数字最多进行两次交换就会找到属于自己的位置,因为总的时间复杂度还是O(n),不需要额外内存也就是这样子:

{2,3,1,0,2,5,3}具体调整过程是:

  • 0(索引值)和2(索引值位置的元素)不相等,并且2(索引值位置的元素)和1(以该索引值位置的元素2为索引值的位置的元素)不相等,则交换位置,数组变为:{1,3,2,0,2,5,3};

  • 0(索引值)和1(索引值位置的元素)仍然不相等,并且1(索引值位置的元素)和3(以该索引值位置的元素1为索引值的位置的元素)不相等,则交换位置,数组变为:{3,1,2,0,2,5,3};

  • 0(索引值)和3(索引值位置的元素)仍然不相等,并且3(索引值位置的元素)和0(以该索引值位置的元素3为索引值的位置的元素)不相等,则交换位置,数组变为:{0,1,2,3,2,5,3};

  • 0(索引值)和0(索引值位置的元素)相等,遍历下一个元素;

  • 1(索引值)和1(索引值位置的元素)相等,遍历下一个元素;

  • 2(索引值)和2(索引值位置的元素)相等,遍历下一个元素;

  • 3(索引值)和3(索引值位置的元素)相等,遍历下一个元素;

  • 4(索引值)和2(索引值位置的元素)不相等,但是2(索引值位置的元素)和2(以该索引值位置的元素2为索引值的位置的元素)相等,则找到了第一个重复的元素。

    实现代码如下:

 

public boolean duplicate(int numbers[],int length,int [] duplication) {
        if(numbers==null || length<1)
            return false;
         for(int i=0;i<length;i++){
            while(numbers[i]!=i){ //每个元素最多被交换两次就可以找到自己的位置,依次复杂度是O(n)
                if(numbers[numbers[i]]==numbers[i]){
                    duplication[0]=numbers[i];
                    return true;
                }else{
                    int temp=numbers[numbers[i]]; //交换
                    numbers[numbers[i]]=numbers[i]; //将numbers[i]放到属于他的位置上
                    numbers[i]=temp;
                }
            }
        }
        return false;
    }

这里虽然效率更高了,但是仍然改变了原始数组的内容,有没有不改变的方法呢?还有一种思路简单,但是实现比较麻烦的方式,这种思路类似折半查找,这里说一下思路。

 以长度为8的数组{2,3,5,4,3,2,6,7}为例,根据题目要求,这个长度为8的数组,所有元素都在1到7的范围内,中间的数字4将1—7分成两部分,分别为1—4和5—7,接下来统计1—4在数组中出现的次数,发现是5次,则说明这4个数字中一定有重复数字。接下来再把1—4分成1、2和3、4两部分,1和2一共出现了两次,3和4一共出现了3次,说明3和4中有一个重复,再分别统计即可得到是3重复了。这并不保证找出所有的重复数字,比如2就没有找到。

  实际上,这种二分查找时间复杂度也达到了O(nlogn),不如用哈希表空间换时间来的直观

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值