学习目标:
掌握抽象数据类型的表示与实现方法。
学习内容:
描述一个集合的抽象数据类型ASet,其中所有元素为整数且所有元素不相同,集合的基本操作包括:
- 由整数数组a[0..n-1]创建一个集合。
- 输出集合中的所有元素。
- 判断一个元素是否在一个集合中。
- 求两个集合的并集,并输出结果。
- 求两个集合的差集,并输出结果。
- 求两个集合的交集,并输出结果。
实验报告:
引入
引入代码所需要的头文件和宏定义
#include <stdio.h>
#include <stdbool.h>
#define MAX_SIZE 105
#define ffi(a) for (int i = 0; i < a; i++)
结构体(集合)
自定义一个结构体,用于存储我们的集合,以及加上一个size用于记录集合的大小
typedef struct
{
int arr[MAX_SIZE];
int size;
} ASet;
排序函数
应为集合是不含有重复元素的,所以我们决定建立一个函数用来排序,并辅助后面去重。
// 交换两个变量的值
// 后面的qsort函数需要用到
void swap(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
// 快速排序算法
void qsort(ASet *as, int l, int r)
{
if (l >= r)
{
return;
}
int x = as->arr[(l + r) / 2], i = l - 1, j = r + 1;
while (i < j)
{
do
{
i++;
} while (as->arr[i] < x);
do
{
j--;
} while (as->arr[j] > x);
if (i < j)
{
swap(&as->arr[i], &as->arr[j]);
}
}
qsort(as, l, j);
qsort(as, j + 1, r);
}
去重函数
而且因为我们已经编写了一个函数用于排序,所以在集合中我们的元素都是有序的。只要有相同的元素,那么他们物理空间上一定在一起(相同的元素排在一起),所以在遍历数组的时候,如果两个元素相同,我们可以直接如果条件成立,将当前元素复制到新数组的 newSize 位置,并将 newSize 加 1。
最后的as->size = newSize再更新数组的大小,即可完成去重操作。
// 去重算法
void weight_removal(ASet *as)
{
int newSize = 0; // 去重后的数组大小
ffi(as->size)
{
if (i == 0 || as->arr[i] != as->arr[i - 1])
{
as->arr[newSize++] = as->arr[i];
}
}
as->size = newSize; // 更新数组大小
}
输出函数
题目中还对我们有要求,需要一个函数输出,所以我们直接遍历数组用于打印集合的元素。
// 打印函数
void print_array(ASet as)
{
ffi(as.size)
{
printf("%d ", as.arr[i]);
//朴实无华的打印函数
}
printf("\n");
}
求并集操作
初始化并集集合ASet unionSet;unionSet.size = 0;创建一个新的 ASet 类型的变量 unionSet,并将其大小初始化为 0,用于存储并集结果。再创建i和j两个索引变量,分别遍历as1和as2。并合并,最后如果 as1 或 as2 中还有剩余元素未处理,则将这些剩余元素依次添加到 unionSet 中。
// 求并集算法
ASet union_of_sets(ASet as1, ASet as2)
{
ASet unionSet;
unionSet.size = 0;
int i = 0, j = 0;
// 合并两个集合的元素
while (i < as1.size && j < as2.size)
{
if (as1.arr[i] < as2.arr[j])
{
unionSet.arr[unionSet.size++] = as1.arr[i++];
}
else if (as1.arr[i] > as2.arr[j])
{
unionSet.arr[unionSet.size++] = as2.arr[j++];
}
else
{
unionSet.arr[unionSet.size++] = as1.arr[i++];
j++;
}
}
// 处理剩余元素
while (i < as1.size)
{
unionSet.arr[unionSet.size++] = as1.arr[i++];
}
while (j < as2.size)
{
unionSet.arr[unionSet.size++] = as2.arr[j++];
}
return unionSet;
}
求交集操作
初始化交集集合ASet intersectionSet;intersectionSet.size = 0;创建一个新的 ASet 类型的变量 intersectionSet ,并将其大小初始化为 0。用于存储交集结果。intersectionSet 的大小初始化为 0,表示当前交集中没有元素。再和上一个函数一样,创建索引i和j用于遍历。
在while循环中遍历两个数组,其中一个集合的所有元素都被遍历完,通过比较 as1.arr[i] 和 as2.arr[j] 的大小来决定如何移动索引i和j(前提是我们的数组元素是有序的)。
此处,我们设置了三段条件用于决定是否移动索引:
- 如果 as1.arr[i] < as2.arr[j],说明 as1 中的当前元素不在 as2 中,因此将 i 增加 1,继续比较下一个元素。
- 如果 as1.arr[i] > as2.arr[j],说明 as2 中的当前元素不在 as1 中,因此将 j 增加 1,继续比较下一个元素。
- 如果 as1.arr[i] == as2.arr[j],说明当前元素在两个集合中都存在,将其添加到 intersectionSet 中,并将 i 和 j 都增加 1,继续比较下一个元素。
// 求交集算法
ASet intersection_of_sets(ASet as1, ASet as2)
{
ASet intersectionSet;
intersectionSet.size = 0;
int i = 0, j = 0;
while (i < as1.size && j < as2.size)
{
if (as1.arr[i] < as2.arr[j])
{
i++;
}
else if (as1.arr[i] > as2.arr[j])
{
j++;
}
else
{
intersectionSet.arr[intersectionSet.size++] = as1.arr[i++];
j++;
}
}
return intersectionSet;
}
求差集操作
与前面的两段代码基本一样,只是说我们的判定逻辑改变了
- 如果 as1 中的当前元素小于 as2 中的当前元素,则将 as1 中的当前元素添加到 differenceSet 中,并移动 as1 的索引。
- 如果 as1 中的当前元素大于 as2 中的当前元素,则移动 as2 的索引。
- 如果两个元素相等,则移动两个集合的索引。
因为我们是as1减去as2,所以只要说我们的as1有剩余,那么就不管他,直接将其添加至differenceSet中。
// 求差集算法
ASet difference_of_sets(ASet as1, ASet as2)
{
ASet differenceSet;
differenceSet.size = 0;
int i = 0, j = 0;
while (i < as1.size && j < as2.size)
{
if (as1.arr[i] < as2.arr[j])
{
differenceSet.arr[differenceSet.size++] = as1.arr[i++];
}
else if (as1.arr[i] > as2.arr[j])
{
j++;
}
else
{
i++;
j++;
}
}
while (i < as1.size)
{
differenceSet.arr[differenceSet.size++] = as1.arr[i++];
}
return differenceSet;
}
判断数字是否在集合中
我们选择了直接遍历数组,来判断element这个变量是否在数组中
// 判断元素是否在集合中
bool is_element_in_set(ASet as, int element)
{
ffi(as.size)
{
if (as.arr[i] == element)
{
return true; // 元素在集合中
}
}
return false; // 元素不在集合中
}
测试环节
在到main函数中编写一段代码用于测试,看我们的结果是否正确。
ASet as1, as2;
int n, m;
// 输入 as1 的大小
scanf("%d", &n);
if (n)
{
// 输入 as1 的元素
ffi(n)
{
scanf("%d", &as1.arr[i]);
}
}
as1.size = n;
// 输入 as2 的大小
scanf("%d", &m);
if (m)
{
// 输入 as2 的元素
for (int i = 0; i < m; i++)
{
scanf("%d", &as2.arr[i]);
}
}
as2.size = m;
// 打印原始数据
printf("as1: ");
print_array(as1);
printf("as2: ");
print_array(as2);
// 对 as1 和 as2 进行排序和去重
qsort(&as1, 0, as1.size - 1);
weight_removal(&as1);
qsort(&as2, 0, as2.size - 1);
weight_removal(&as2);
// 打印排序和去重后的数据
printf("排序和重复数据 1:");
print_array(as1);
printf("排序和重复数据为2:");
print_array(as2);
// 测试判断元素是否在集合中
int element;
printf("输入一个元素,检查它是否在 as1 中:");
scanf("%d", &element);
if (is_element_in_set(as1, element))
{
printf("%d 在 as1 中。\n", element);
}
else
{
printf("%d 不在 as1 中。\n", element);
}
// 计算并集
ASet unionSet = union_of_sets(as1, as2);
printf("as1 和 as2 的并集:");
print_array(unionSet);
// 计算交集
ASet intersectionSet = intersection_of_sets(as1, as2);
printf("as1 和 as2 的交集:");
print_array(intersectionSet);
// 计算差集
ASet differenceSet = difference_of_sets(as1, as2);
printf("as1 和 as2 的差集:");
print_array(differenceSet);
结果
测试均使用vscode的Competitive Programming Helper插件辅助进行。
学习思考(代码优点)
快速排序:编写了一段代码,用来实现快速排序(时间复杂度平均为 O(n log n)),降低了后面去重的时间(毕竟集合中不能出现重复元素)。
去重:在排序后,利用相邻元素的比较进行去重,确保集合内元素唯一。
集合运算的效率:在对于集合的运算中如并集、交集和差集都使用了双指针(定义了i和j两个变量)的操作用于降低函数的时间复杂度用于提高效率。