C++单链表实现集合运算实战项目

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C++语言,利用单链表数据结构实现集合的交集、并集和差集运算,支持从文件读取集合数据及将运算结果保存回文件。通过自定义Node结构体构建链表,项目涵盖链表的基本操作、文件I/O处理、内存管理与异常控制等核心内容。该实现不仅强化了对线性结构的操作理解,也提升了对数据持久化和程序健壮性的掌握,是数据结构学习中典型的综合实践案例。

单链表与集合运算的系统性实现:从基础结构到工程落地

你有没有想过,为什么我们每天用的微信好友列表、淘宝推荐商品,甚至权限管理系统背后都离不开一个看似简单的数据结构——链表?🤔 其实啊,这些复杂系统的底层逻辑,往往就藏在像单链表这样的“小工具”里。今天咱们不整虚的,直接上硬核干货,带你从零开始,用 C++ 手搓一个 完整的集合类系统 ,支持去重、交集、并集、差集,还能读写文件,最后再加点安全防护,让它真正能跑在生产环境里!

别担心代码多,我会像讲故事一样,一步步拆解每个环节的设计思路和坑点。准备好了吗?Let’s go!🚀


1. 单链表:不只是教科书里的玩具

先来打地基。说到链表,大家第一反应可能就是教科书里的 Node 结构体:

struct Node {
    int data;
    Node* next;
    Node(int val) : data(val), next(nullptr) {}
};

这玩意儿看起来简单,但真要把它变成一个可用的数据结构,光有节点可不够。我们得有个“管家”——也就是链表类,来管理头指针、内存释放、插入删除这些操作。

class LinkedList {
private:
    Node* head;

public:
    LinkedList() : head(nullptr) {}

    ~LinkedList() {
        while (head) {
            Node* temp = head;
            head = head->next;
            delete temp;
        }
    }

    void push_front(int val) {
        Node* newNode = new Node(val);
        newNode->next = head;
        head = newNode;
    }

    void print() const {
        Node* curr = head;
        while (curr) {
            std::cout << curr->data << " -> ";
            curr = curr->next;
        }
        std::cout << "nullptr\n";
    }
};

看到析构函数没?🔥 这就是新手最容易翻车的地方 。你不手动 delete ,程序跑一次就内存泄漏了。我以前实习时就因为忘了写析构,被导师骂得狗血淋头 😂。

不过现在问题来了:如果我们拿这个链表当集合用,怎么防止重复元素呢?比如连续 push_front(5) 两次,结果就有两个 5 了。这显然不符合集合的“唯一性”原则。那怎么办?


2. 集合去重:暴力法 vs 哈希加速

💥 暴力查重:简单但慢得让人心碎

最直接的办法就是在每次插入前遍历一遍链表,看看有没有相同的值:

bool contains(int value) const {
    Node* curr = head;
    while (curr) {
        if (curr->data == value) return true;
        curr = curr->next;
    }
    return false;
}

void add(int value) {
    if (!contains(value)) {
        push_front(value);
    }
}

这叫“暴力比较法”,优点是 不用额外空间,代码清晰 ;缺点嘛……时间复杂度是 O(n),如果我要插 n 个元素,总时间就是 O(n²)。
想象一下你要处理 10000 个用户 ID,光查重就得做快一亿次比较……CPU 表示:我裂开了 💀。

📊 小贴士:O(n²) 是什么概念?n=1000 时大约要 50 万次操作;n=5000 就要 1250 万次。现代 CPU 每秒能算几亿次,听起来好像还行?但别忘了还有缓存命中、分支预测失败等问题,实际性能远不如理论值。

所以,对于稍大一点的数据,这种办法基本没法用。

🚀 加速方案:借力标准库容器

既然链表自己找得慢,那就请个“助理”帮忙记一下哪些数已经存在过呗!C++ 标准库里的 std::unordered_set 就是个好帮手,平均查找时间 O(1):

#include <unordered_set>

class Set {
private:
    Node* head;
    std::unordered_set<int> lookup;  // 助理小本本 📒

public:
    Set() : head(nullptr) {}

    bool contains(int value) const {
        return lookup.find(value) != lookup.end();
    }

    void add(int value) {
        if (lookup.find(value) == lookup.end()) {
            Node* newNode = new Node(value);
            newNode->next = head;
            head = newNode;
            lookup.insert(value);  // 同步更新小本本
        }
    }

    ~Set() { /* 正常释放链表 */ }
};

这样插入 n 个元素的总时间就降到了 O(n),提升了好几个数量级!当然代价是多用了点内存(哈希表本身),而且你得保证链表和哈希表同步更新,否则状态就乱了。

⚠️ 注意:这里有个隐藏雷区——异常安全!如果 new Node() 抛出 std::bad_alloc (虽然少见),而你已经把 value 写进 lookup 了,那就会出现“哈希表里有记录,但链表没节点”的不一致状态。工业级代码必须考虑这种极端情况,可以用 RAII 或者“先建节点再写表”的顺序规避。

✅ 安全版本长这样:

void addSafe(int value) {
    if (lookup.count(value)) return;

    Node* newNode = nullptr;
    try {
        newNode = new Node(value);
    } catch (...) {
        throw std::runtime_error("Memory allocation failed");
    }

    newNode->next = head;
    head = newNode;
    lookup.insert(value);
}

你看,是不是瞬间感觉代码稳了很多?😎

flowchart TB
    Start([开始 add(value)]) --> Check{已存在?}
    Check -- 是 --> End1([结束])
    Check -- 否 --> Alloc[尝试 new Node]
    Alloc --> Fail{分配失败?}
    Fail -- 是 --> Throw[抛异常]
    Fail -- 否 --> Link[链接到链首]
    Link --> Update[更新head]
    Update --> Sync[同步哈希表]
    Sync --> End2([完成])

    style Start fill:#4CAF50,color:white
    style End1 fill:#FF9800,color:black
    style Throw fill:#F44336,color:white
    style Update fill:#2196F3,color:white

这张流程图清楚展示了工业级插入的完整路径——连内存不足都照顾到了,这才是靠谱的做法!


3. 集合交集:不只是数学公式

有了带去重的链表集合,接下来玩点更酷的: 求交集

数学上很简单:A ∩ B = {x | x ∈ A 且 x ∈ B}。但在代码里怎么做效率最高?

🔁 双重循环法:适合教学,不适合实战

最朴素的想法是遍历 A 的每个元素,在 B 中查是否存在:

LinkedList intersect(const LinkedList& other) const {
    LinkedList result;

    if (isEmpty() || other.isEmpty()) return result;

    Node* curr = head;
    while (curr) {
        if (other.contains(curr->data)) {
            result.add(curr->data);  // 利用自带去重
        }
        curr = curr->next;
    }
    return result;
}

逻辑没错,但时间复杂度是 O(m×n),m 和 n 分别是两个链表长度。当两者都是 1000,就要比较 100 万次!太慢了。

⏩ 性能优化两大杀招

✅ 方案一:预排序 + 双指针扫描

如果你允许对原链表排序(不影响集合语义),就可以先排序,然后像归并排序那样双指针前进:

LinkedList intersectSorted(const LinkedList& other) const {
    auto a = *this; a.sort();   // 假设有 sort()
    auto b = other; b.sort();

    LinkedList res;
    Node *p = a.head, *q = b.head;

    while (p && q) {
        if (p->data == q->data) {
            res.add(p->data);
            p = p->next; q = q->next;
        } else if (p->data < q->data) {
            p = p->next;
        } else {
            q = q->next;
        }
    }
    return res;
}

总时间 O(m log m + n log n),比 O(mn) 快多了。但问题是:你得改结构或者复制副本,空间开销大。

✅ 方案二:哈希辅助查找(推荐)

把较小的那个集合扔进 unordered_set ,然后遍历另一个集合去查:

LinkedList intersectHash(const LinkedList& other) const {
    LinkedList result;
    std::unordered_set<int> smallSet;

    // 选小的放哈希表,省空间
    const LinkedList& small = (size() <= other.size()) ? *this : other;
    const LinkedList& large = (size() > other.size()) ? *this : other;

    // 构建哈希索引
    Node* curr = small.head;
    while (curr) {
        smallSet.insert(curr->data);
        curr = curr->next;
    }

    // 遍历大的,在哈希表中查
    curr = large.head;
    while (curr) {
        if (smallSet.find(curr->data) != smallSet.end()) {
            result.add(curr->data);
        }
        curr = curr->next;
    }

    return result;
}

时间复杂度直接干到 O(m + n),是目前最快的通用做法!

方法 时间复杂度 空间复杂度 是否修改原数据 推荐场景
双重循环 O(mn) O(1) 数据极小(<100)
排序+双指针 O(m log m + n log n) O(1) 或 O(m+n) 是/否 支持排序且追求速度
哈希辅助 O(m + n) O(min(m,n)) 大数据量、频繁查询

选择哪个?看你的业务需求咯~ 🤔

graph LR
    A[选策略] --> B{能排序吗?}
    B -- 是 --> C[双指针法]
    B -- 否 --> D{内存够吗?}
    D -- 是 --> E[哈希法]
    D -- 否 --> F[忍着用双重循环]

4. 并集 & 差集:组合拳出击

🧩 并集(Union):合并去重一条龙

并集的目标是把两个集合的所有元素合起来,还要自动去重。

最简单的做法就是复用 add() 函数:

LinkedList unionWith(const LinkedList& other) const {
    LinkedList result;

    // 插入自己的所有元素
    Node* curr = head;
    while (curr) {
        result.add(curr->data);
        curr = curr->next;
    }

    // 插入对方的所有元素
    curr = other.head;
    while (curr) {
        result.add(curr->data);
        curr = curr->next;
    }

    return result;
}

但如果 result.add() 内部是暴力查重,那整体就是 O((m+n)^2),太慢了。升级版加个哈希表:

LinkedList unionOptimized(const LinkedList& other) const {
    std::unordered_set<int> seen;
    LinkedList result;

    auto safeInsert = [&](int val) {
        if (seen.find(val) == seen.end()) {
            seen.insert(val);
            result.push_back(val);  // 假设尾插 O(1)
        }
    };

    Node* curr = head;
    while (curr) { safeInsert(curr->data); curr = curr->next; }
    curr = other.head;
    while (curr) { safeInsert(curr->data); curr = curr->next; }

    return result;
}

时间复杂度 O(m + n),完美!

🔻 差集(Difference):筛掉共有的

差集 A - B 表示在 A 中但不在 B 中的元素。注意方向敏感哦,A-B ≠ B-A!

LinkedList difference(const LinkedList& other) const {
    std::unordered_set<int> otherSet;
    Node* curr = other.head;
    while (curr) {
        otherSet.insert(curr->data);
        curr = curr->next;
    }

    LinkedList result;
    curr = head;
    while (curr) {
        if (otherSet.find(curr->data) == otherSet.end()) {
            result.push_back(curr->data);
        }
        curr = curr->next;
    }

    return result;
}

一样的套路,先建哈希索引,再筛选。时间 O(m + n),稳得很。

flowchart LR
    Start --> LoadB["加载B到哈希表"]
    LoadB --> TraverseA["遍历A"]
    TraverseA --> Check{"在B中?"}
    Check -- 否 --> AddToResult
    AddToResult --> Next
    Check -- 是 --> Next
    Next --> TraverseA
    TraverseA -.-> EndNull
    EndNull --> ReturnResult

5. 文件持久化:让数据活下来

上面都在内存里玩,关机就没了。要想实用,还得支持读写文件!

📁 文本文件格式设计

我们定义一种简洁又人性化的格式:

  • 每行一个集合
  • 元素用空格或逗号分隔
  • # 开头为注释
  • 忽略空白行

例子:

# 用户A标签
1 3 5 7 9
# 用户B标签
2,3,6,7,8

📥 读取实现

bool loadSetsFromFile(const std::string& filename,
                      std::vector<std::list<int>>& sets) {
    std::ifstream file(filename);
    if (!file.is_open()) return false;

    std::string line;
    while (std::getline(file, line)) {
        // 清理前后空白
        auto start = line.find_first_not_of(" \t");
        auto end = line.find_last_not_of(" \t");
        if (start == std::string::npos) continue; // 空行
        line = line.substr(start, end - start + 1);

        if (line.empty() || line[0] == '#') continue;

        // 统一分隔符为空格
        std::replace(line.begin(), line.end(), ',', ' ');

        std::list<int> current;
        std::stringstream ss(line);
        int val;
        while (ss >> val) {
            if (std::find(current.begin(), current.end(), val) == current.end()) {
                current.push_back(val);
            }
        }
        sets.push_back(current);
    }
    return true;
}

📤 写回文件

void writeResultToFile(const std::string& file, 
                       const std::string& label,
                       const std::list<int>& result) {
    std::ofstream out(file, std::ios::app); // 追加模式
    if (!out.is_open()) return;

    out << "=== " << label << " ===\n";
    for (int x : result) out << x << " ";
    out << "\n\n";
    out.close();
}

加上时间戳更专业:

std::string getTimestamp() {
    auto now = std::chrono::system_clock::now();
    auto t = std::chrono::system_clock::to_time_t(now);
    std::ostringstream oss;
    oss << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S");
    return oss.str();
}

输出长这样:

=== Intersection of A and B === [2025-04-05 14:23:10]
3 7 

=== Union of A and C === [2025-04-05 14:23:10]
1 3 5 7 9 10 20 30 

审计日志的感觉立马就有了吧?😉

graph TD
    A[开始主程序] --> B{读取输入文件}
    B -- 成功 --> C[解析多组集合]
    B -- 失败 --> D[报错并退出]
    C --> E[执行各种集合运算]
    E --> F[构建结果]
    F --> G[打开输出流]
    G --> H[写入带标签结果]
    H --> I[关闭文件]
    I --> J[释放资源]
    J --> K[结束]

6. 最后一步:健壮性与测试

再牛的功能,没有测试也是白搭。写几个关键断言:

LinkedListSet s;
assert(s.add(1));
assert(!s.add(1));   // 第二次应失败
assert(s.size() == 1);

// 测试空集
LinkedList empty;
assert(empty.unionWith(s).size() == 1);

// 测试差集
auto diff = s.difference(empty);
assert(diff.size() == 1);

边界情况全覆盖:

类型 示例输入 预期行为
正常输入 A={1,2}, B={2,3} A∪B={1,2,3}, A−B={1}
空集参与 A={}, B={1,2} A∪B=B, A∩B={}
全重复 A={1,2}, B={1,2} A−B={}, A∩B=A
单元素 A={5}, B={} A∪B={5}, A−B={5}

结语:从玩具到武器

你看,一个简简单单的单链表,经过层层包装——
✅ 加上去重机制 → 变成集合
✅ 实现交并差 → 支持复杂运算
✅ 接入文件IO → 实现持久化
✅ 引入哈希优化 → 提升性能
✅ 添加异常处理 → 保障安全

它就不再是实验室里的玩具,而是可以嵌入真实系统的 重型武器 了!

下次面试官问你“链表有什么用”,你就把这套流程甩他脸上 😎。不是为了炫技,而是告诉你: 每一个底层结构,只要深入打磨,都能撑起一片天

所以,别再说“学链表没用了”——是你还没把它用对地方而已 😉。

Keep coding, keep hacking! 💻✨

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目基于C++语言,利用单链表数据结构实现集合的交集、并集和差集运算,支持从文件读取集合数据及将运算结果保存回文件。通过自定义Node结构体构建链表,项目涵盖链表的基本操作、文件I/O处理、内存管理与异常控制等核心内容。该实现不仅强化了对线性结构的操作理解,也提升了对数据持久化和程序健壮性的掌握,是数据结构学习中典型的综合实践案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值