LeetCode 第 90 题:Subsets II
题目叙述:
给定一个可能包含重复元素的整数数组 nums
,返回该数组的所有可能的子集(幂集),但解集不能包含重复的子集。
模式识别(考点):
- 回溯算法:是本题的核心算法,通过递归和回溯的思想生成所有可能的组合。
- 排序:为了方便处理重复元素,先对数组进行排序,将相同元素相邻排列,便于在回溯过程中跳过重复元素。
解题思路:
- 回溯算法:
- 首先对输入数组
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(n∗2n)。因为对于每个元素都有两种选择(选或不选),并且需要对
n
个元素进行操作,同时还需要对数组进行排序,排序的时间复杂度为 O ( n l o g n ) O(n log n) O(nlogn),但在 O ( n ∗ 2 n ) O(n * 2^n) O(n∗2n) 的量级下,排序的时间可以忽略。
空间复杂度:
- 空间复杂度为
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
函数中的内存释放操作。