跳转至

AC107-寻找缺失的最小正整数

给定一个含 n 个整数的数组,设计一个时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如:数组 {-5, 3, 2, 3} 中未出现的最小正整数是 1;数组 {1, 2, 3} 中未出现的最小正整数是 4。(2018 年统考题)

标准代码以及原地哈希代码[[#2. 原地哈希优化版本(空间O(1))]]

代码实现

C
/**
 * 找出数组中未出现的最小正整数
 * @param A[] 输入数组
 * @param n   数组长度
 * @return    未出现的最小正整数
 */
int func(int A[], int n) {
    // 创建辅助数组B,用于标记1到n的正整数是否出现
    // B[i]表示正整数i是否在数组A中出现
    int *B = (int *)malloc(sizeof(int) * (n + 1));

    // 初始化辅助数组,所有位置都标记为未出现(0)
    for (int i = 0; i < n + 1; ++i) {
        B[i] = 0;
    }

    // 遍历原数组,标记出现的正整数
    for (int i = 0; i < n; ++i) {
        // 只处理范围在[1, n]内的正整数
        // 因为缺失的最小正整数必定在[1, n+1]范围内
        if (A[i] > 0 && A[i] <= n) {
            B[A[i]] = 1;  // 标记该正整数已出现
        }
    }

    // 从1开始查找第一个未出现的正整数
    for (int i = 1; i < n + 1; ++i) {
        if (B[i] == 0) {  // 如果正整数i未出现
            return i;     // 返回结果
        }
    }

    // 如果1到n都出现了,则缺失的最小正整数是n+1
    return n + 1;
}

算法执行过程图解

示例1:数组{-5, 3, 2, 3}

Text Only
输入数组A: [-5, 3, 2, 3]  n=4

步骤1: 创建辅助数组B[5]并初始化为0
B: [0, 0, 0, 0, 0]
    ↑  ↑  ↑  ↑  ↑
    0  1  2  3  4

步骤2: 遍历A数组标记出现的正整数
i=0: A[0]=-5 → 不在[1,4]范围内,跳过
i=1: A[1]=3  → 在范围内,B[3]=1
i=2: A[2]=2  → 在范围内,B[2]=1  
i=3: A[3]=3  → 在范围内,B[3]已为1,不变

B: [0, 0, 1, 1, 0]
    ↑  ↑  ↑  ↑  ↑
    0  1  2  3  4

步骤3: 查找第一个未出现的正整数
i=1: B[1]=0 → 找到未出现的正整数1
返回结果: 1

示例2:数组{1, 2, 3}

Text Only
输入数组A: [1, 2, 3]  n=3

步骤1: 创建辅助数组B[4]并初始化为0
B: [0, 0, 0, 0]
    ↑  ↑  ↑  ↑
    0  1  2  3

步骤2: 遍历A数组标记出现的正整数
i=0: A[0]=1 → B[1]=1
i=1: A[1]=2 → B[2]=1
i=2: A[2]=3 → B[3]=1

B: [0, 1, 1, 1]
    ↑  ↑  ↑  ↑
    0  1  2  3

步骤3: 查找第一个未出现的正整数
i=1: B[1]=1 → 已出现
i=2: B[2]=1 → 已出现
i=3: B[3]=1 → 已出现

所有1到3都出现了,返回n+1=4
返回结果: 4

复杂度分析

时间复杂度:O(n)

详细分析:

  1. 初始化辅助数组
  2. 循环 n+1 次
  3. 时间复杂度 O(n)

  4. 标记出现的正整数

  5. 遍历原数组 n 次
  6. 每次操作为常数时间
  7. 时间复杂度 O(n)

  8. 查找缺失的正整数

  9. 最多循环 n 次
  10. 时间复杂度 O(n)

  11. 总体时间复杂度O(n)

空间复杂度:O(n)

详细分析:

  1. 辅助数组B
  2. 大小为 n+1
  3. 空间复杂度 O(n)

  4. 其他变量

  5. 只使用常数个额外变量
  6. 空间复杂度 O(1)

  7. 总体空间复杂度O(n)

关键知识点延伸

1. 算法核心思想

数学原理

  • 对于长度为n的数组,缺失的最小正整数必定在[1, n+1]范围内
  • 如果数组包含1到n的所有正整数,则答案是n+1
  • 否则答案是1到n中第一个缺失的正整数

哈希表思想

  • 使用辅助数组作为哈希表,下标表示数值,值表示是否出现
  • 实现O(1)时间的查找和标记

2. 原地哈希优化版本(空间O(1))

C
/**
 * 原地哈希版本 - 空间复杂度O(1)
 * 利用数组下标作为哈希表
 */
int firstMissingPositive(int A[], int n) {
    // 第一步:将所有非正数和大于n的数替换为n+1
    for (int i = 0; i < n; i++) {
        if (A[i] <= 0 || A[i] > n) {
            A[i] = n + 1;
        }
    }

    // 第二步:使用数组本身作为哈希表
    for (int i = 0; i < n; i++) {
        int num = abs(A[i]);
        if (num <= n) {
            // 将对应位置的数标记为负数(表示已出现)
            A[num - 1] = -abs(A[num - 1]);
        }
    }

    // 第三步:找到第一个正数的位置
    for (int i = 0; i < n; i++) {
        if (A[i] > 0) {
            return i + 1;
        }
    }

    // 如果都标记了,则返回n+1
    return n + 1;
}

3. 置换排序版本

C
/**
 * 置换排序版本 - 将每个正整数放到正确位置
 */
int firstMissingPositiveSwap(int A[], int n) {
    // 将每个正整数i放到A[i-1]的位置
    for (int i = 0; i < n; i++) {
        while (A[i] > 0 && A[i] <= n && A[A[i] - 1] != A[i]) {
            // 交换A[i]和A[A[i]-1]
            int temp = A[A[i] - 1];
            A[A[i] - 1] = A[i];
            A[i] = temp;
        }
    }

    // 查找第一个不在正确位置的数
    for (int i = 0; i < n; i++) {
        if (A[i] != i + 1) {
            return i + 1;
        }
    }

    return n + 1;
}

4. 算法正确性证明

定理:缺失的最小正整数在[1, n+1]范围内

证明: - 数组最多包含n个元素 - 最好的情况是包含1,2,3,...,n,此时答案为n+1 - 其他情况必然缺少某个[1,n]范围内的正整数 - 因此答案必定在[1, n+1]范围内

5. 边界情况处理

C
/**
 * 完善的边界处理版本
 */
int funcComplete(int A[], int n) {
    // 空数组情况
    if (n <= 0) return 1;

    // 分配内存并检查
    int *B = (int *)malloc(sizeof(int) * (n + 1));
    if (B == NULL) return -1;  // 内存分配失败

    // 初始化
    memset(B, 0, sizeof(int) * (n + 1));

    // 标记过程
    for (int i = 0; i < n; i++) {
        if (A[i] > 0 && A[i] <= n) {
            B[A[i]] = 1;
        }
    }

    // 查找过程
    for (int i = 1; i <= n; i++) {
        if (B[i] == 0) {
            free(B);
            return i;
        }
    }

    free(B);
    return n + 1;
}

6. 与其他解法的比较

方法 时间复杂度 空间复杂度 优缺点
哈希表法 O(n) O(n) 简单直观,易理解
原地哈希 O(n) O(1) 空间效率高,但实现复杂
置换排序 O(n) O(1) 巧妙利用数组结构,但难理解
排序+遍历 O(n log n) O(1) 简单但时间效率低

算法特点总结

  1. 高效性:时间复杂度O(n),接近理论下界
  2. 正确性:基于严格的数学证明
  3. 实用性:适用于各种输入情况
  4. 可扩展性:可以扩展到其他相关问题
  5. 教育价值:体现了哈希表思想的经典应用

记忆要点

  • 范围限定:答案必定在[1, n+1]范围内
  • 哈希思想:用数组下标表示数值,值表示状态
  • 三步法
  • 初始化标记数组
  • 遍历标记出现的数
  • 查找第一个未标记的数
  • 边界处理:考虑空数组、全出现等情况
  • 优化方向:可以进一步优化到空间O(1)