算法精讲:分享一道值得分享的算法题

分享一道leetcode上的题,固然,竟然不是放在刷题贴里来说,意味着分享的这道题不只仅是教你怎么来解决,更重要的是这道题引起出来的一些解题技巧或许能够用在其余地方,下面咱们来看看这道题的描述。算法

问题描述

给定一个未排序的整数数组,找出其中没有出现的最小的正整数。数组

示例 1:

输入: [1,2,0]
输出: 3
示例 2:

输入: [3,4,-1,1]
输出: 2
示例 3:

输入: [7,8,9,11,12]
输出: 1
复制代码

说明: 你的算法的时间复杂度应为O(n),而且只能使用常数级别的空间。bash

解答

这道题在 leetcode 的定位是困难级别,或许你能够尝试本身作一下,而后再来看个人解答,下面面我一步步来分析,秒杀的大佬请忽略.....工具

对于一道题,若是不能第一时间想到最优方案时,我以为能够先不用那么严格,能够先采用暴力的方法求解出来,而后再来一步步优化。像这道题,我想,若是能够你要先用快速排序先把他们排序,而后在再来求解的话,那是至关容易的,不过 O(nlogn) 的时间复杂度过高,其实咱们能够先牺牲下咱们的空间复杂度,让保证咱们的时间复杂度为 O(n),以后再来慢慢优化咱们的空间复杂度。学习

方法一:采用集合

咱们知道,若是数组的长度为 n,那么咱们要找的目标数必定是出于 1~n+1 之间的,咱们能够先把咱们数组里的全部数映射到集合里,而后咱们从 1~n 开始遍历判断,看看哪一个数是没有在集合的,若是不存在的话,那么这个数即是咱们要找的数了。若是 1~n 都存在,那咱们要找的数就是 n+1 了。优化

不过这里须要注意的是,在把数组里面的数存进集合的时候,对于 小于 1 或者大于 n 的数,咱们是不须要存进集合里的,由于他们不会对结果形成影响,这也算是一种优化吧。光说还不行,还得会写代码,代码以下:ui

public int firstMissingPositive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] >= 1 && nums[i] <= n) {
                set.add(nums[i]);
            }
        }
        for (int i = 1; i <= n; i++) {
            if (!set.contains(i)) {
                return  i;
            }
        }
        return n + 1;
    }
复制代码

采用 bitmap

方法一的空间复杂度在最块的状况下是 O(n),不知道你们还记不记得位算法,其实咱们是能够利用位算法来继续优化咱们的空间的,若是不知道位算法的能够看我直接写的一篇文章:spa

一、什么是bitmap算法.net

二、本身用代码实现bitmap算法;3d

经过采用位算法,咱们咱们把空间复杂度减小8倍,即从 O(n) -> O(n/32),但其实 O(n/32) 任然还算 O(n),不过,在实际运行的时候,它是确实可以让咱们运行的更快的,在 Java 中,已经有自带的支持位算法的类了,即 bitSet,若是你没学过这个类,我相信你也是能一眼看懂的,代码以下:

public int firstMissingPositive2(int[] nums) {
        BitSet bitSet = new BitSet();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] >= 1 && nums[i] <= n) {
                bitSet.set(nums[i]);
            }
        }
        for (int i = 1; i <= n; i++) {
            if (!bitSet.get(i)) {
                return  i;
            }
        }
        return n + 1;
    }
复制代码

方法3:最终版本

若是这个数组是有序的,那就好办了,可是若是咱们要把它进行排序的话,又得须要 O(nlogn) 的时间复杂度,那咱们有没有啥办法把它进行排序,而后时间复杂度又不须要那么高呢?

答是能够,刚才咱们说过,对于那些小于 1 或者大于 n 的数,咱们是实际上是能够不理的,竟然咱们,咱们须要处理的这些数,他们都是处于 1~n 之间的,那要你给这些处于 1~n 之间的数排序,而且重复的元素咱们也是能够忽略掉的,记录一个就能够了,那么你能不能在 O(n) 时间复杂度排序好呢?

不知道你们是否还记得我之间写过的下标法

一些经常使用的算法技巧总结

或者是否还记得计数排序?(计数排序其实就是下标法的一个应用了)

不过学过计数排序的朋友可能都知道,计数排序是须要咱们开一个新的数组的,不过咱们这里不须要,这道题咱们能够这样作:例如对于 nums[i],咱们能够把它放到数组下标位 nums[i] - 1 的位置上,这样子一处理的话,全部 1<=nums[i]<=n 的数,就都是是处于相对有序的位置了。注意,我指的是相对,也就是说对于 1-n 这些数而言,其余 小于 1 或者大于 n 的咱们不理的。例如对于这个数组 nums[] = {4, 1, -1, 3, 7}。

让 nums[i] 放到数组下标为 nums[i-1]的位置,而且对于那些 nums[i]<=0 或 nums > n的数,咱们是能够不用理的,因此过程以下:从下标为 0 开始向右遍历

一、把 4 放在下标为 3 的位置,为了避免让下标为 3 的数丢失,把下标为 3 的数与 4进行交换。

二、此时咱们还不能向右移动来处下标为1的数,由于咱们当前位置的3还不是处于有序的位置,还得继续处理,因此把 3 与下标为 2 的数交换

三、当前位置额数为 -1,不理它,前进到下标为 1 的位置,把 1 与下标为 0的数交换

四、当前位置额数为 -1,不理它,前进到下标为 2 的位置,此时的 3 处于有序的位置,不理它继续前进,4也是处于有序的位置,继续前进。

五、此时的 7 > n,不理它,继续前进。

遍历完成,此时,那些处于 1~n的数都是处于有序的位置了,对于那些小于1或者大于n的,咱们忽略它伪装没看到就是了

这里还有个要注意的地方,就是 nums[i] 与下标为 nums[i]-1的数若是相等的话也是不须要交换的。

接下来,咱们再次从下标 0 到下标 n-1 遍历这个数组,若是遇到 nums[i] != i + 1 的数,,那么这个时候咱们就找到目标数了,即 i + 1。

好吧,我以为我讲的有点啰嗦了,还一步步话题展示过程给大家看,连我本身都感受有点啰嗦了,大佬勿喷哈。最后代码以下:

public int firstMissingPositive(int[] nums) {
        if(nums == null || nums.length < 1)
            return 1;
        int n = nums.length;
        for(int i = 0; i < n; i++){
            // 这里还有个要注意的地方,就是 nums[i] 与下标为 nums[i]-1的数若是相等的话
            // 也是不须要交换的。
            while(nums[i] >= 1 && nums[i] <= n && nums[i] != i + 1 && nums[i] != nums[nums[i]-1] ){
                // 和下标为 nums[i] - 1的数进行交换
                int tmp = nums[i];
                nums[i] = nums[tmp - 1];
                nums[tmp - 1] = tmp;
            }
        }
        for(int i = 0; i < n; i++){
            if(nums[i] != i + 1){
                return i + 1;
            }
        }
        return n + 1;
    }
复制代码

这道题我以为仍是由挺多值得学习的地方的,例如它经过这道原地交换的方法,使指定范围内的数组有序了。

还有就是这种经过数组下标来解决问题的方法也是一种经常使用的技巧,例如给你一副牌,让你打乱顺序,以后分发给4我的,也是能够采用这种方法的,详情能够看这道题:什么是洗牌算法

最后推广下个人公众号:苦逼的码农:公众号里面已经有100多篇原创文件,也分享了不少实用工具,海量视频资源、电子书资源,关注自提。点击扫码关注哦。 戳我便可关注

相关文章
相关标签/搜索