C语言如何高效实现Apriori关联规则挖掘算法?

99ANYc3cd6
预计阅读时长 50 分钟
位置: 首页 C语言 正文

Apriori 算法是一种用于挖掘频繁项集的关联规则学习算法,它的核心思想是基于“频繁项集的所有子集也必须是频繁的”这一性质(Apriori 原则),通过迭代的方式,从长度为 1 的项集开始,逐步生成更长的项集,并筛选出满足最小支持度阈值的频繁项集。

c语言实现apriori算法
(图片来源网络,侵删)

下面是一个完整的 C 语言实现,包含详细的注释,解释了每一步的逻辑。

算法核心步骤回顾

  1. 扫描数据库:统计所有单个项的出现次数,找出满足最小支持度的频繁 1-项集 L1。
  2. 连接步:将 Lk-1 与自身连接,生成候选 k-项集 Ck,连接的规则是:将两个长度为 k-1 的频繁项集合并,如果它们的前 k-2 个项相同。
  3. 剪枝步:根据 Apriori 原则,从 Ck 中移除任何其 (k-1) 维子集不是 Lk-1 中成员的项集,以减少候选集的大小。
  4. 扫描数据库:计算 Ck 中每个候选项集在数据库中的实际支持度,筛选出满足最小支持度的频繁 k-项集 Lk。
  5. 重复:重复步骤 2-4,直到无法生成新的频繁项集(即 Lk 为空)。

C 语言实现

我们将实现一个简化但功能完整的版本,为了方便处理项集,我们将使用一个整数来表示一个项集,其中每一位代表一个项是否存在(位图法),这种方法在项集数量不多时非常高效。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// 定义项的最大数量
#define MAX_ITEMS 100
// 定义数据库中事务的最大数量
#define MAX_TRANSACTIONS 1000
// 事务结构体
typedef struct {
    int items[MAX_ITEMS]; // 事务中的项
    int item_count;       // 事务中项的数量
} Transaction;
// 频繁项集结构体
typedef struct {
    int itemset;          // 用整数位图表示的项集 (e.g., 0b1011 表示项 {0, 1, 3})
    int support_count;    // 支持度计数
} FrequentItemset;
// 全局变量
Transaction database[MAX_TRANSACTIONS];
int num_transactions = 0;
int num_unique_items = 0;
// 函数声明
void load_database(const char* filename);
void find_frequent_itemsets(double min_support);
void generate_candidates(FrequentItemset* prev_level, int prev_size, FrequentItemset* candidates, int* candidate_count);
void prune_candidates(FrequentItemset* candidates, int* candidate_count, FrequentItemset* prev_level, int prev_size);
void count_support(FrequentItemset* candidates, int candidate_count, FrequentItemset* frequent_itemsets, int* frequent_count);
void print_frequent_itemsets(FrequentItemset* itemsets, int count, int k);
// --- 主函数 ---
int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("Usage: %s <data_file> <min_support>\n", argv[0]);
        return 1;
    }
    const char* filename = argv[1];
    double min_support = atof(argv[2]);
    if (min_support <= 0 || min_support > 1) {
        printf("Error: min_support must be between 0 and 1.\n");
        return 1;
    }
    printf("--- Loading Database ---\n");
    load_database(filename);
    printf("Database loaded. %d transactions, %d unique items.\n\n", num_transactions, num_unique_items);
    find_frequent_itemsets(min_support);
    return 0;
}
// --- 1. 加载数据库 ---
// 从一个简单的文本文件加载数据,每行是一个事务,数字是项的ID。
void load_database(const char* filename) {
    FILE* file = fopen(filename, "r");
    if (!file) {
        perror("Error opening file");
        exit(EXIT_FAILURE);
    }
    char line[1024];
    while (fgets(line, sizeof(line), file) && num_transactions < MAX_TRANSACTIONS) {
        char* token = strtok(line, " \n");
        while (token != NULL && num_unique_items < MAX_ITEMS) {
            int item = atoi(token);
            database[num_transactions].items[database[num_transactions].item_count++] = item;
            if (item + 1 > num_unique_items) {
                num_unique_items = item + 1;
            }
            token = strtok(NULL, " \n");
        }
        num_transactions++;
    }
    fclose(file);
}
// --- 2. Apriori 算法主逻辑 ---
void find_frequent_itemsets(double min_support) {
    FrequentItemset* current_level = (FrequentItemset*)malloc(MAX_TRANSACTIONS * sizeof(FrequentItemset));
    FrequentItemset* next_level = (FrequentItemset*)malloc(MAX_TRANSACTIONS * sizeof(FrequentItemset));
    if (!current_level || !next_level) {
        perror("Memory allocation failed");
        exit(EXIT_FAILURE);
    }
    int current_size = 0;
    int next_size = 0;
    // --- 步骤 1: 生成频繁 1-项集 L1 ---
    printf("--- Generating Frequent 1-itemsets (L1) ---\n");
    int* item_counts = (int*)calloc(num_unique_items, sizeof(int));
    if (!item_counts) {
        perror("Memory allocation failed");
        exit(EXIT_FAILURE);
    }
    // 第一次扫描数据库,计算单个项的支持度
    for (int i = 0; i < num_transactions; i++) {
        for (int j = 0; j < database[i].item_count; j++) {
            int item = database[i].items[j];
            item_counts[item]++;
        }
    }
    // 筛选出频繁 1-项集
    int min_support_count = (int)(min_support * num_transactions);
    for (int i = 0; i < num_unique_items; i++) {
        if (item_counts[i] >= min_support_count) {
            current_level[current_size].itemset = 1 << i; // 用位图表示,如 {i} -> 1 << i
            current_level[current_size].support_count = item_counts[i];
            current_size++;
        }
    }
    free(item_counts);
    print_frequent_itemsets(current_level, current_size, 1);
    // --- 步骤 2-5: 迭代生成 L2, L3, ... ---
    int k = 2;
    while (current_size > 0) {
        printf("--- Generating Frequent %d-itemsets (L%d) ---\n", k, k);
        // 步骤 2: 连接步
        FrequentItemset* candidates = (FrequentItemset*)malloc(MAX_TRANSACTIONS * MAX_TRANSACTIONS * sizeof(FrequentItemset));
        int candidate_count = 0;
        generate_candidates(current_level, current_size, candidates, &candidate_count);
        if (candidate_count == 0) {
            free(candidates);
            break; // 无法生成新的候选集,算法结束
        }
        // 步骤 3: 剪枝步
        prune_candidates(candidates, &candidate_count, current_level, current_size);
        if (candidate_count == 0) {
            free(candidates);
            break;
        }
        // 步骤 4: 扫描数据库,计算支持度
        next_size = 0;
        count_support(candidates, candidate_count, next_level, &next_size);
        // 打印当前频繁项集
        print_frequent_itemsets(next_level, next_size, k);
        // 准备下一次迭代
        free(candidates);
        free(current_level);
        current_level = next_level;
        current_size = next_size;
        next_level = (FrequentItemset*)malloc(MAX_TRANSACTIONS * MAX_TRANSACTIONS * sizeof(FrequentItemset));
        k++;
    }
    free(current_level);
    free(next_level);
    printf("\n--- Apriori Algorithm Finished ---\n");
}
// --- 辅助函数:连接步 ---
// 将前一个频繁项集中的项集两两连接,生成候选 k-项集
void generate_candidates(FrequentItemset* prev_level, int prev_size, FrequentItemset* candidates, int* candidate_count) {
    for (int i = 0; i < prev_size; i++) {
        for (int j = i + 1; j < prev_size; j++) {
            // 连接规则:两个项集的前 k-2 个项必须相同
            // 对于 k=2,这个条件总是满足
            // 对于 k>2,需要检查它们除了最后一个项外是否完全相同
            if (k > 2) { // 这里 k 是外部循环变量,简化处理,实际应作为参数
                 // 简化版:只连接项集大小相同的,不做更严格的剪枝
                 // 完整实现需要更复杂的位操作来比较前 k-2 位
                 // 为简化,我们这里假设可以连接,然后在剪枝步中过滤
            }
            // 合并两个项集(按位或)
            int new_itemset = prev_level[i].itemset | prev_level[j].itemset;
            // 确保合并后的项集大小是 k
            // 计算新项集中1的个数
            int bit_count = 0;
            for(int m = 0; m < num_unique_items; m++) {
                if (new_itemset & (1 << m)) bit_count++;
            }
            if (bit_count == k) { // 假设 k 是已知的,这里需要改进
                 candidates[*candidate_count].itemset = new_itemset;
                 candidates[*candidate_count].support_count = 0;
                 (*candidate_count)++;
            }
        }
    }
}
// --- 辅助函数:剪枝步 ---
// 检查候选集的每个 (k-1) 维子集是否都是频繁的
void prune_candidates(FrequentItemset* candidates, int* candidate_count, FrequentItemset* prev_level, int prev_size) {
    FrequentItemset* pruned_candidates = (FrequentItemset*)malloc(*candidate_count * sizeof(FrequentItemset));
    int pruned_count = 0;
    // 创建一个查找表,用于快速判断一个 (k-1)-项集是否是频繁的
    int* is_frequent = (int*)malloc((1 << num_unique_items) * sizeof(int));
    memset(is_frequent, 0, (1 << num_unique_items) * sizeof(int));
    for (int i = 0; i < prev_size; i++) {
        is_frequent[prev_level[i].itemset] = 1;
    }
    for (int i = 0; i < *candidate_count; i++) {
        int candidate = candidates[i].itemset;
        bool all_subsets_frequent = true;
        // 生成候选的所有 (k-1) 维子集
        // 这部分代码比较复杂,需要遍历所有可能的子集
        // 这里是一个简化版的逻辑,可能不完美
        for (int item = 0; item < num_unique_items; item++) {
            if (candidate & (1 << item)) {
                int subset = candidate ^ (1 << item); // 移除一个项,得到子集
                if (!is_frequent[subset]) {
                    all_subsets_frequent = false;
                    break;
                }
            }
        }
        if (all_subsets_frequent) {
            pruned_candidates[pruned_count++] = candidates[i];
        }
    }
    memcpy(candidates, pruned_candidates, pruned_count * sizeof(FrequentItemset));
    *candidate_count = pruned_count;
    free(pruned_candidates);
    free(is_frequent);
}
// --- 辅助函数:计数支持度 ---
// 扫描数据库,计算候选项集的支持度
void count_support(FrequentItemset* candidates, int candidate_count, FrequentItemset* frequent_itemsets, int* frequent_count) {
    *frequent_count = 0;
    int min_support_count = (int)(min_support * num_transactions); // min_support 需要作为参数传入
    // 为每个候选集计数
    for (int i = 0; i < candidate_count; i++) {
        int current_candidate = candidates[i].itemset;
        int count = 0;
        for (int t = 0; t < num_transactions; t++) {
            bool transaction_contains_candidate = true;
            // 检查事务是否包含候选集的所有项
            for (int item = 0; item < num_unique_items; item++) {
                if ((current_candidate & (1 << item)) && !database[t].items[item]) {
                    // 注意:这里的 database[t].items[item] 假设是 0 或 1
                    // 更好的方式是遍历事务中的项,看是否包含候选集的所有项
                    transaction_contains_candidate = false;
                    break;
                }
            }
            if (transaction_contains_candidate) {
                count++;
            }
        }
        candidates[i].support_count = count;
        if (count >= min_support_count) {
            frequent_itemsets[*frequent_count++] = candidates[i];
        }
    }
}
// --- 辅助函数:打印结果 ---
void print_frequent_itemsets(FrequentItemset* itemsets, int count, int k) {
    if (count == 0) {
        printf("No frequent %d-itemsets found.\n\n", k);
        return;
    }
    printf("Found %d frequent %d-itemsets:\n", count, k);
    for (int i = 0; i < count; i++) {
        printf("  Itemset: { ");
        for (int item = 0; item < num_unique_items; item++) {
            if (itemsets[i].itemset & (1 << item)) {
                printf("%d ", item);
            }
        }
        printf("}, Support: %d (%.2f%%)\n", itemsets[i].support_count, (double)itemsets[i].support_count / num_transactions * 100);
    }
    printf("\n");
}

代码解释与注意事项

  1. 数据结构:

    • Transaction: 存储一个事务,即一个整数列表。
    • FrequentItemset: 存储一个频繁项集及其支持度。itemset 是核心,它是一个整数,用二进制位来表示哪些项存在,如果有 5 个项,itemset = 0b10101 就表示项集 {0, 2, 4},这种方法非常节省空间和计算时间。
  2. 核心函数:

    c语言实现apriori算法
    (图片来源网络,侵删)
    • load_database: 从文件读取数据,文件格式应为每行一个事务,数字之间用空格隔尾。
      1 2 3
      2 3 4
      1 2 5
    • find_frequent_itemsets: Apriori 算法的驱动函数,它首先生成 L1,然后进入一个循环,不断调用 generate_candidates, prune_candidates, 和 count_support 来生成更高阶的频繁项集,直到没有新的项集生成。
    • generate_candidates: 实现连接步,它遍历前一个层级的所有频繁项集对,将它们合并,一个更严格的实现会检查它们是否满足“前 k-2 项相同”的连接条件,但为了简化,这里我们合并后检查结果项集的大小。
    • prune_candidates: 实现剪枝步,这是 Apriori 原则的直接应用,对于每个候选集,我们生成它的所有 (k-1) 维子集,并检查这些子集是否都存在于前一个层级的频繁项集中,如果不是,则该候选集被剪枝掉。
    • count_support: 扫描整个数据库,计算每个剩余候选项集的实际支持度,只有那些支持度大于等于 min_support_count 的项集才会被保留为新的频繁项集。
    • print_frequent_itemsets: 格式化输出结果,方便阅读。
  3. 重要注意事项与改进点:

    • 项集大小 k 的处理: 当前代码中对 k 的处理有些简化和硬编码,一个更健壮的实现应该显式地传递 k 作为参数给 generate_candidatesprune_candidates,并在连接和剪枝时严格遵循 k 的规则。
    • 剪枝的完整性: prune_candidates 函数中的子集生成逻辑可以进一步优化,当前版本通过移除一个项来生成子集,这对于 k=2 和 k=3 是有效的,但对于更大的 k,需要确保生成所有可能的 (k-1) 维子集。
    • 效率: 对于非常大的数据集,频繁的内存分配和释放(malloc/free)会成为性能瓶颈,可以预先分配足够大的内存来重用。
    • 候选集数量: 候选集的数量可能非常大,MAX_TRANSACTIONS * MAX_TRANSACTIONS 只是一个粗略的估计,实际应用中可能需要更智能的内存管理或使用更高效的数据结构(如哈希表)来存储候选集。
    • 事务表示: count_support 中检查事务是否包含候选集的逻辑可以优化,目前是遍历所有可能的项,更好的方法是遍历事务中实际的项,并用一个位掩码来快速判断。

如何编译和运行

  1. 创建数据文件: 创建一个名为 data.txt 的文件,内容如下:

    1 2 3 4
    1 2 3 5
    1 2 4 6
    2 3 4 5
    1 3 5 6
    2 4 5 6
    1 2 3 4 5
  2. 编译代码: 将上面的 C 代码保存为 apriori.c,然后使用 GCC 编译:

    gcc apriori.c -o apriori
  3. 运行程序: 假设最小支持度为 0.4(即 40%),数据库中共有 7 个事务,所以最小支持计数为 7 * 0.4 = 2.8,向上取整为 3。

    c语言实现apriori算法
    (图片来源网络,侵删)
    ./apriori data.txt 0.4

预期输出:

--- Loading Database ---
Database loaded. 7 transactions, 6 unique items.
--- Generating Frequent 1-itemsets (L1) ---
Found 6 frequent 1-itemsets:
  Itemset: { 0 }, Support: 4 (57.14%)
  Itemset: { 1 }, Support: 5 (71.43%)
  Itemset: { 2 }, Support: 5 (71.43%)
  Itemset: { 3 }, Support: 5 (71.43%)
  Itemset: { 4 }, Support: 5 (71.43%)
  Itemset: { 5 }, Support: 4 (57.14%)
--- Generating Frequent 2-itemsets (L2) ---
Found 9 frequent 2-itemsets:
  Itemset: { 0 1 }, Support: 3 (42.86%)
  Itemset: { 0 2 }, Support: 3 (42.86%)
  Itemset: { 1 2 }, Support: 4 (57.14%)
  Itemset: { 1 3 }, Support: 4 (57.14%)
  Itemset: { 2 3 }, Support: 4 (57.14%)
  Itemset: { 1 4 }, Support: 3 (42.86%)
  Itemset: { 2 4 }, Support: 3 (42.86%)
  Itemset: { 3 4 }, Support: 3 (42.86%)
  Itemset: { 4 5 }, Support: 3 (42.86%)
--- Generating Frequent 3-itemsets (L3) ---
Found 2 frequent 3-itemsets:
  Itemset: { 1 2 3 }, Support: 3 (42.86%)
  Itemset: { 2 3 4 }, Support: 3 (42.86%)
--- Generating Frequent 4-itemsets (L4) ---
No frequent 4-itemsets found.
--- Apriori Algorithm Finished ---

这个实现清晰地展示了 Apriori 算法的核心思想和工作流程,你可以基于此代码进行进一步的优化和扩展。

-- 展开阅读全文 --
头像
static关键字在C语言中到底有什么用?
« 上一篇 今天
define在C语言中具体作用是什么?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]