90. Subsets II

LeetCode 第 90 题:Subsets II

题目叙述
给定一个可能包含重复元素的整数数组 nums,返回该数组的所有可能的子集(幂集),但解集不能包含重复的子集。

模式识别(考点)

  • 回溯算法:是本题的核心算法,通过递归和回溯的思想生成所有可能的组合。
  • 排序:为了方便处理重复元素,先对数组进行排序,将相同元素相邻排列,便于在回溯过程中跳过重复元素。

解题思路

  1. 回溯算法
    • 首先对输入数组 nums 进行排序,使重复元素相邻。
    • 定义一个回溯函数,该函数接收当前处理的元素索引作为参数。
    • 在回溯函数中,对于每个元素有两种选择:
      • 不选择当前元素,直接递归调用下一个元素。
      • 选择当前元素,但要考虑重复元素的情况,若当前元素与前一个元素相同且前一个元素未被选择,则跳过,否则添加该元素并递归调用下一个元素。

C 语言代码实现

#include <stdio.h>
#include <stdlib.h>


/**
 * Return an array of arrays of size *returnSize.
 * The sizes of the arrays are returned as *returnColumnSizes array.
 * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
 */


// 存储最终结果的二维数组
int **res;
// 存储当前正在生成的子集的数组
int *path;
// 结果数组 res 的索引
int restop;
// 当前正在生成的子集 path 的元素数量
int pathtop;
// 存储每个结果子集的元素数量
int *pathSize;


// 比较函数,用于 qsort 排序,将元素按升序排列
int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}


// 回溯函数,用于生成不重复的子集
// nums: 输入的整数数组
// numsSize: 输入数组的大小
// startindex: 开始处理元素的索引
// used: 标记元素是否已被使用的数组
void backtracking(int *nums, int numsSize, int startindex, int *used) {
    // 为当前正在生成的子集分配内存并复制元素
    int *temp = (int*)malloc(sizeof(int) * pathtop);
    for (int i = 0; i < pathtop; i++) {
        temp[i] = path[i];
    }
    // 存储当前子集的元素数量
    pathSize[restop] = pathtop;
    // 将当前子集添加到结果数组中
    res[restop++] = temp;

    // 遍历从 startindex 开始的元素
    for (int i = startindex; i < numsSize; i++) {
        // 若当前元素是重复元素且前一个元素未被使用,则跳过
        if (i > 0 && used[i - 1] == 0 && nums[i] == nums[i - 1])
            continue;
        // 标记当前元素已使用
        used[i] = 1;
        // 将当前元素添加到当前正在生成的子集中
        path[pathtop++] = nums[i];
        // 递归调用,继续生成子集
        backtracking(nums, numsSize, i + 1, used);
        // 回溯操作,移除刚刚添加的元素
        pathtop--;
        // 标记当前元素未使用
        used[i] = 0;
    }
}


// 生成具有重复元素的数组的所有不重复子集
// nums: 输入的整数数组
// numsSize: 输入数组的大小
// returnSize: 存储结果子集的数量
// returnColumnSizes: 存储每个结果子集的元素数量
int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    // 对输入数组进行升序排序
    qsort(nums, numsSize, sizeof(int), cmp);
    // 为结果数组分配内存,假设最多 10000 个子集
    res = (int**)malloc(sizeof(int*) * 10000);
    // 为存储当前正在生成的子集分配内存,大小为 numsSize
    path = (int*)malloc(sizeof(int) * numsSize);
    // 为存储每个结果子集的元素数量分配内存,假设最多 10000 个子集
    pathSize = (int*)malloc(sizeof(int) * 10000);
    // 初始化结果数组和当前正在生成的子集的索引为 0
    restop = pathtop = 0;
    // 标记元素是否已被使用的数组
    int *used = (int*)malloc(sizeof(int)* numsSize);
    // 初始化 used 数组,标记元素都未使用
    for (int i = 0; i < numsSize; i++)
        used[i] = 0;
    // 调用回溯函数生成子集
    backtracking(nums, numsSize, 0, used);
    // 存储结果子集的数量
    *returnSize = restop;
    // 为存储每个结果子集的元素数量分配内存
    *returnColumnSizes = (int*)malloc(sizeof(int) * restop);
    // 存储每个结果子集的元素数量
    for (int i = 0; i < restop; i++)
        (*returnColumnSizes)[i] = pathSize[i];
    return res;
}

代码解释

  • cmp 函数:
    • int cmp(const void* a, const void* b):是 qsort 的比较函数,将元素转换为 int 并比较大小,实现升序排序。
  • backtrack 函数:
    • void backtrack(int* nums, int numsSize, int index, int* cur, int curSize, int*** res, int* resSize, int** resColSize)
      • nums:输入的整数数组,包含可能重复的元素。
      • numsSize:输入数组的大小,决定了递归的深度。
      • index:当前处理的元素索引,用于决定是否处理该元素。
      • cur:存储当前正在生成的子集的数组。
      • curSize:当前正在生成的子集的元素数量。
      • res:存储最终结果的二维数组,存储所有不重复的子集。
      • resSize:存储最终结果的数量。
      • resColSize:存储每个结果子集的元素数量。
      • if (index == numsSize) {...}:当处理完所有元素时,为当前生成的子集分配内存,将其添加到结果数组并更新结果大小。
      • backtrack(nums, numsSize, index + 1, cur, curSize, res, resSize, resColSize);:不选择当前元素,直接递归调用下一个元素。
      • if (index == 0 || nums[index]!= nums[index - 1]) {...}:选择当前元素,添加到当前子集并递归调用下一个元素,同时处理重复元素。
  • subsetsWithDup 函数:
    • int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColumnSizes)
      • nums:输入的整数数组。
      • numsSize:输入数组的大小。
      • returnSize:存储结果子集的数量。
      • returnColumnSizes:存储每个结果子集的元素数量。
      • qsort(nums, numsSize, sizeof(int), cmp);:对输入数组进行排序,方便处理重复元素。
      • *returnColumnSizes = (int*)malloc(1000 * sizeof(int));:为存储结果子集的元素数量分配内存,假设最多 1000 个子集。
      • int** res = NULL;:存储最终结果的二维数组,初始化为 NULL
      • int cur[100];:存储当前正在生成的子集,假设最大长度为 100。
      • backtrack(nums, numsSize, 0, cur, 0, &res, returnSize, returnColumnSizes);:调用回溯函数生成子集。

设计模式

  • 回溯模式
    • 使用递归函数 backtrack 来生成所有可能的组合,对于每个元素都有选择或不选择的分支,这是回溯算法的核心。
    • 通过 if (index == 0 || nums[index]!= nums[index - 1]) 来处理重复元素,避免生成重复的子集,确保结果的唯一性。

时间复杂度

  • 时间复杂度为 O ( n ∗ 2 n ) O(n * 2^n) O(n2n)。因为对于每个元素都有两种选择(选或不选),并且需要对 n 个元素进行操作,同时还需要对数组进行排序,排序的时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn),但在 O ( n ∗ 2 n ) O(n * 2^n) O(n2n) 的量级下,排序的时间可以忽略。

空间复杂度

  • 空间复杂度为 O ( n + 2 n ) O(n + 2^n) O(n+2n)
    • 存储结果所需的空间为 O ( 2 n ) O(2^n) O(2n),因为结果的数量是 2 n 2^n 2n 的量级。
    • 递归调用的栈空间为 O ( n ) O(n) O(n),因为递归的最大深度是 n

注意事项

  • 在使用 realloc 时,要注意其可能返回 NULL,导致原数据丢失,需要进行适当的错误处理。
  • 代码中使用了固定大小的数组 cur[100] 存储当前正在生成的子集,对于较大的输入数组,可能需要使用动态数组或根据实际情况调整数组大小。
  • 对于存储结果的内存,使用完后要使用 free 函数释放,防止内存泄漏,如 main 函数中的内存释放操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请向我看齐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值