找到所有数组中消失的数字思路

题目:

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。

前置点播:

1. 桶计数的基本概念
  • :一个辅助数组,通常用于记录数据的某种状态(如是否出现、出现次数等)。

  • 桶的大小:桶的大小通常与数据的范围相关。例如,如果数据范围是 1 到 n,则桶的大小为 n + 1

  • 桶的初始化:桶数组通常需要初始化为某个默认值(如 0),表示初始状态。


2. 桶计数的应用场景

桶计数常用于以下场景:

  1. 统计频率

    • 统计数组中每个数字出现的次数。

    • 例如:统计一个字符串中每个字符的出现次数。

  2. 查找缺失数字

    • 在一个范围内查找缺失的数字。

    • 例如:LeetCode 448. 找到所有数组中消失的数字。

  3. 去重

    • 判断数组中是否有重复元素。

    • 例如:LeetCode 217. 存在重复元素。

  4. 排序

    • 桶排序(Bucket Sort)是一种基于桶计数的排序算法。


3. 桶计数的实现步骤

以下是桶计数的通用实现步骤:

(1)确定桶的大小

  • 根据数据的范围确定桶的大小。

  • 例如:如果数据范围是 1 到 n,则桶的大小为 n + 1

(2)初始化桶

  • 将桶数组的所有元素初始化为默认值(如 0)。

  • 例如:使用 calloc 或 memset 初始化。

(3)遍历数据,更新桶状态

  • 遍历原始数据,根据数据的值更新桶的状态。

  • 例如:将出现的数字对应的桶位置标记为 1,或者统计每个数字的出现次数。

(4)根据桶状态解决问题

  • 遍历桶数组,根据桶的状态解决问题。

  • 例如:查找值为 0 的位置(缺失的数字),或者统计某个值的出现次数。


4. 桶计数的时间复杂度
  • 时间复杂度O(n),其中 n 是数据的大小。

    • 需要遍历原始数据一次,遍历桶数组一次。

  • 空间复杂度O(m),其中 m 是桶的大小。

    • 需要额外的空间存储桶数组。


5. 桶计数的代码模板

以下是桶计数的通用代码模板:

#include 
#include 

void bucketCounting(int* nums, int numsSize) {
    // 确定桶的大小
    int bucketSize = numsSize + 1;  // 假设数据范围是 1 到 numsSize
    
    // 初始化桶
    int* bucket = (int*)calloc(bucketSize, sizeof(int));
    
    // 遍历数据,更新桶状态
    for (int i = 0; i < numsSize; i++) {
        bucket[nums[i]]++;  // 统计每个数字的出现次数
    }
    
    // 根据桶状态解决问题
    for (int i = 1; i < bucketSize; i++) {
        if (bucket[i] == 0) {
            printf("%d 缺失\n", i);
        } else if (bucket[i] > 1) {
            printf("%d 重复\n", i);
        }
    }
    
    // 释放桶数组的内存
    free(bucket);
}
6. 桶计数的经典问题
(1)统计频率
  • 问题描述:统计数组中每个数字的出现次数。

  • 代码实现

    void countFrequency(int* nums, int numsSize) {
        int bucket[100] = {0};  // 假设数据范围是 0 到 99
        for (int i = 0; i < numsSize; i++) {
            bucket[nums[i]]++;
        }
        for (int i = 0; i < 100; i++) {
            if (bucket[i] > 0) {
                printf("%d 出现了 %d 次\n", i, bucket[i]);
            }
        }
    }
(2)查找缺失数字
  • 问题描述:在一个范围内查找缺失的数字。

  • 代码实现

    int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize) {
        int* bucket = (int*)calloc(numsSize + 1, sizeof(int));
        for (int i = 0; i < numsSize; i++) {
            bucket[nums[i]] = 1;
        }
        int* result = (int*)malloc(numsSize * sizeof(int));
        int count = 0;
        for (int i = 1; i <= numsSize; i++) {
            if (bucket[i] == 0) {
                result[count++] = i;
            }
        }
        *returnSize = count;
        free(bucket);
        return result;
    }
(3)判断重复元素
  • 问题描述:判断数组中是否有重复元素。

  • 代码实现

    bool containsDuplicate(int* nums, int numsSize) {
        int bucket[100000] = {0};  // 假设数据范围是 0 到 99999
        for (int i = 0; i < numsSize; i++) {
            if (bucket[nums[i]] > 0) {
                return true;
            }
            bucket[nums[i]]++;
        }
        return false;
    }

7. 桶计数的优缺点

优点:

  • 简单直观:逻辑清晰,易于实现。

  • 高效:时间复杂度为 O(n),适合处理大规模数据。

缺点:

  • 空间开销:需要额外的空间存储桶数组。

  • 数据范围限制:如果数据范围很大,桶数组可能会占用过多内存。

思路:

利用桶计数来实现,创建一个桶数组,遍历原数组,标记出现的数字,查找缺失的数字,返回缺失的数字。

代码:

int* findDisappearedNumbers(int* nums, int numsSize, int* returnSize)
{
    // 创建桶数组,初始化为 0
   int* bucket = (int*) calloc(numsSize+1,sizeof(int));
    //将出现过的元素都置为1
    for(int i = 0; i

代码分析:

  1. 创建桶数组

    • 使用 calloc 分配一个大小为 numsSize + 1 的数组,并初始化为 0

  2. 标记出现的数字

    • 遍历原数组 nums,将每个数字对应的桶位置标记为 1

  3. 统计缺失数字

    • 遍历桶数组,统计值为 0 的位置,这些位置对应的数字就是缺失的数字。

  4. 分配结果数组

    • 根据缺失数字的数量 count,分配内存存储结果。

  5. 填充结果数组

    • 将缺失的数字存入结果数组。

  6. 设置返回值大小

    • 将 returnSize 设置为缺失数字的数量,方便调用者知道结果数组的大小。

  7. 释放桶数组

    • 释放桶数组的内存,避免内存泄漏。

补充:

calloc 和 malloc 都是 C 语言中用于动态分配内存的函数,但它们有一些关键区别如下:


1. 初始化内存

  • calloc

    • calloc 在分配内存后,会自动将内存块中的所有字节初始化为 0

    • 例如:int* arr = (int*)calloc(n, sizeof(int)); 会将分配的内存块中的所有值初始化为 0

  • malloc

    • malloc 只分配内存,但不会初始化内存块中的内容。

    • 例如:int* arr = (int*)malloc(n * sizeof(int)); 分配的内存块中的值是未定义的(可能是随机值)。

在你的代码中,桶数组需要初始化为 0,以表示所有数字最初都未出现过。如果使用 malloc,你需要额外调用 memset 或手动遍历数组来初始化,而 calloc 直接完成了这一步骤,代码更简洁。


2. 参数形式

  • calloc

    • 参数形式为 calloc(num_elements, element_size)

    • 例如:calloc(10, sizeof(int)) 分配 10 个 int 大小的内存块,并初始化为 0

  • malloc

    • 参数形式为 malloc(total_size)

    • 例如:malloc(10 * sizeof(int)) 分配 10 个 int 大小的内存块,但不会初始化。

calloc 的参数形式更直观,尤其是在分配数组时,直接指定元素个数和元素大小即可。


3. 安全性

  • calloc

    • 由于 calloc 会自动初始化内存为 0,可以避免未初始化内存导致的潜在问题(如随机值干扰逻辑)。

  • malloc

    • 如果忘记初始化内存,可能会导致程序行为不可预测。

在你的代码中,桶数组需要初始化为 0,使用 calloc 可以确保这一点,避免手动初始化的麻烦和潜在错误。


4. 性能

  • calloc

    • 由于 calloc 会初始化内存,它的性能可能略低于 malloc

    • 但对于大多数应用场景,这种性能差异可以忽略不计。

  • malloc

    • malloc 只分配内存,性能略高。

 

你可能感兴趣的:(算法,leetcode,c语言)