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

栏目: 编程工具 · 发布时间: 5年前

内容简介:分享一道leetcode上的题,当然,居然不是放在刷题贴里来讲,意味着分享的这道题不仅仅是教你怎么来解决,更重要的是这道题引发出来的一些解题技巧或许可以用在其他地方,下面我们来看看这道题的描述。给定一个未排序的整数数组,找出其中没有出现的最小的正整数。说明:你的算法的时间复杂度应为O(n),并且只能使用常数级别的空间。

分享一道leetcode上的题,当然,居然不是放在刷题贴里来讲,意味着分享的这道题不仅仅是教你怎么来解决,更重要的是这道题引发出来的一些解题技巧或许可以用在其他地方,下面我们来看看这道题的描述。

问题描述

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

示例 1:

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

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

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

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

解答

这道题在 leetcode 的定位是 困难 级别,或许你可以尝试自己做一下,然后再来看我的解答,下面面我一步步来分析,秒杀的大佬请忽略.....

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

方法一:采用集合

我们知道,如果数组的长度为 n,那么我们要找的目标数一定是出于 1~n+1 之间的,我们可以先把我们数组里的所有数映射到集合里,然后我们从 1~n 开始遍历判断,看看哪个数是没有在集合的,如果不存在的话,那么这个数便是我们要找的数了。如果 1~n 都存在,那我们要找的数就是 n+1 了。

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

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),不知道大家还记不记得位算法,其实我们是可以利用位算法来继续优化我们的空间的,如果不知道位算法的可以看我直接写的一篇文章:

1、 什么是bitmap算法

2、 自己用代码实现bitmap算法 ;

通过采用位算法,我们我们把空间复杂度减少8倍,即从 O(n) -> O(n/8),但其实 O(n/8) 任然还算 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 开始向右遍历

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

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

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

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

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

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

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

5、此时的 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多篇原创文件,也分享了很多实用工具,海量视频资源、电子书资源,关注自提。点击扫码关注哦。戳我即可关注,


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

灵活Web设计

灵活Web设计

Zoe Mickley Gillenwater / 李静 / 2009-9 / 45.00元

《灵活Web设计》讲述如何应用可变或不固定布局及弹性布局来实现灵活设计,以满足用户的根据自己需求而调整浏览站点的窗口大小的要求。全书共分为9章,内容包括:理解灵活布局、可变布局和弹性布局存在的挑战、设计灵活布局的方法、准备网页设计、创建可变布局结构、创建弹性布局结构、规范灵活性、设置文字间距、添加背景图像和颜色、创建灵活的图像。 《灵活Web设计》适用于网页设计人员、网页设计爱好者。一起来看看 《灵活Web设计》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具