求缺失的最小正整数

       转自:https://blog.csdn.net/stpeace/article/details/109544261

       今天,我们来看一个leetcode问题,也是当年B公司的面试题,有难度。问题如下:

 

       给定一个整数数组,找出其中缺失的最小的正整数,要求时间复杂度为O(n), 空间复杂度为O(1). 输入输出示例如下:

输入数组a输出
[1, 2, 0]3
[3, 4, 1, -1]2
[6, 7, 8, 12]1

 

      我们先来分析一下:

      A. 假设a中的n个元素占满了1~n, 那么缺失的最小正整数就是n+1.

      B. 假设a中的n个元素没有完全占满1~n, 那么缺失的最小正整数必然是1~n之间的某个数。

      综合A和B可知:缺失的最小正整数必然在区间[1, n+1]中。这是一个非常重要的结论。

 

算法1:暴力算法

      暴力算法很简单,直接遍历区间[1, n+1], 然后判断元素是否在a中。此时,时间复杂度是O(n*n), 空间复杂度为O(1).  

       显然,无法通过面试。        

 

算法2:哈希算法

      从暴力算法可知,这无非就是一个查找的问题,那么可以考虑用哈希表。也就是把a数组用哈希表来记录,然后直接遍历区间[1, n+1], 就可以判断元素是否在哈希表中。此时,时间复杂度和空间复杂度都是O(n).

       显然,无法通过面试。

 

算法3:巧妙标记法

      我们参考算法2,并在标记元素是否存在时做巧妙优化。

      以a = [3, 4, 1, -1]为例,n = 4,过程如下:

原始数组[3, 4, 1, -1]
非正数改为n+1[3, 4, 1, 5]
3存在,用a[3-1]的负号来标识[3, 4, -1, 5]
4存在,用a[4-1]的负号来标识[3, 4, -1, -5]
1存在,用a[1-1]的负号来标识[-3, 4, -1, -5]
a[x-1]=4,是正数,故x必然不存在x=2便是缺失的最小正整数


      可以看到,在记录元素x是否存在时,使用的是a[x - 1]的符号,其中,x的区间是[1, n].  该算法的时间复杂度是O(n), 空间复杂度是O(1), 完全符合题目要求。 这种标记方式是非常巧妙的,而且确实不太容易想到。

      既然算法的步骤确定了,那么对应的程序就相对简单了,来看一下:

 
package main
import "fmt"
 
func abs(x int) int {
    if x < 0 {
        return -x
    }
  
    return x
}
 
func solution(a []int) int {
    n := len(a)
    for i := 0; i < n; i++ {
        if a[i] <= 0 {
            a[i] = n + 1
        }
    }
  
    for i := 0; i < n; i++ {
        num := abs(a[i])
        if num <= n {
            a[num - 1] = -abs(a[num - 1])
        }
    }
  
    for i := 0; i < n; i++ {
        if a[i] > 0 {
            return i + 1
        }
    }
  
    return n + 1
}
 
func main() {
  a := []int{3, 4, 1, -1}
  fmt.Println(solution(a))
}

      结果:2

 

算法4:置换归位法

      算法3是比较难想到的,那么我们再看看更加直观的想法。对于数组a的元素i, 如果i在区间[1, n]中,可通过置换归位,把i放到a[i-1]处,如下:

 

输入数组a置换目标
[1, 2, 0][1, 2, 0]
[3, 4, 1, -1][1, -1, 3, 4]
[6, 7, 8, 12][6, 7, 8, 12]

 

     置换后,再次遍历a, 如果a[x-1]和x不相等,那么,x肯定就是缺失的最小正整数,如下:

置换目标x(缺失的最小正整数)
[1, 2, 0]3
[1, -1, 3, 4]2
[6, 7, 8, 12]1

 

     该算法的时间复杂度是O(n), 空间复杂度是O(1), 完全符合题目要求。既然算法确定了,那么对应的程序就相对简单了,如下:

package main
import "fmt"
 
func solution(a []int) int {
    n := len(a)
    for i := 0; i < n; i++ {
        // 注意:下面这里要用for, 而不是if.
        for a[i] > 0 && a[i] <= n && a[a[i]-1] != a[i] {
            a[a[i]-1], a[i] = a[i], a[a[i]-1]
        }
    }
  
    for i := 0; i < n; i++ {
        if a[i] != i + 1 {
            return i + 1
        }
    }
  
    return n + 1
}
 
func main() {
  a := []int{3, 4, 1, -1}
  fmt.Println(solution(a))
}

      结果:2

 

      综上所述:算法1和算法2无法通过面试,算法3和算法4可以通过面试。其中,算法3是比较绕的,在面试现场不太容易想到。相比较而言,算法4直观易懂,在程序实现时要注意内层要用for, 而不是if.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值