51mee - AI智能招聘平台Logo
模拟面试题目大全招聘中心会员专区

在推荐系统中,计算用户与内容的相似度(如余弦相似度)时,如何高效处理大规模数据?结合C++实现,说明如何使用倒排索引(inverted index)或哈希表(如unordered_map)加速相似度计算,并讨论时间复杂度和空间复杂度的优化。

快手C++开发工程师 📦 工程类难度:中等

答案

1) 【一句话结论】:在推荐系统计算大规模用户与内容的余弦相似度时,通过构建倒排索引(或哈希表)预处理特征,快速定位共同特征,将全量计算点积的时间复杂度从O(m*n)优化为O(m + n),需权衡空间复杂度及动态更新成本,适用于稀疏离散特征场景。

2) 【原理/概念讲解】:余弦相似度衡量向量方向相似性,公式为( \cos\theta = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}| |\mathbf{B}|} )。当特征维度高(如用户行为序列、内容标签),全量计算点积需遍历所有维度,效率低。倒排索引是一种索引结构,将每个特征(如标签)映射到包含该特征的实体(用户/内容)ID列表。例如,标签“电影”对应用户ID列表[1,3,5],内容ID列表[2,4]。计算时,先通过倒排索引快速找到用户u和内容c共同的特征,再计算这些特征上的相似度,避免全量遍历。类比:查字典时,查“电影”一词,快速找到包含该词的页面(用户/内容列表),而不是逐页检查所有页面。

3) 【对比与适用场景】:

方法定义特性使用场景注意点
倒排索引将特征(如标签、关键词)映射到包含该特征的实体ID列表适合稀疏离散特征(标签、关键词),支持多值查询,能高效定位共同特征用户/内容特征为稀疏标签(如内容标签、用户兴趣标签)需额外空间存储索引,动态更新时需增量维护
哈希表(unordered_map)将键(如连续向量哈希值)映射到值(向量或哈希值)适合高维连续向量(如用户行为嵌入、内容文本嵌入),或需快速判断特征是否存在特征为高维连续向量,或需近似匹配(如LSH)哈希冲突可能影响性能,需选择合适的哈希函数,适用于连续特征或单值查询

4) 【示例】:伪代码展示增量更新(带并发控制)和加权计算,考虑缓存。
构建倒排索引(增量更新,带并发控制):

// 假设特征为标签,倒排索引结构:map<标签, vector<实体ID>>
unordered_map<string, vector<int>> inverted_index;
mutex index_mutex; // 并发控制

// 增量更新:仅处理新增/删除的特征,使用互斥锁保证一致性
void update_inverted_index(const User& user, const Content& content, bool is_add = true) {
    lock_guard<mutex> lock(index_mutex);
    if (is_add) {
        for (auto& tag : user.tags) {
            inverted_index[tag].push_back(user.id);
        }
        for (auto& tag : content.tags) {
            inverted_index[tag].push_back(content.id);
        }
    } else {
        for (auto& tag : user.tags) {
            inverted_index[tag].erase(remove(inverted_index[tag].begin(), inverted_index[tag].end(), user.id), inverted_index[tag].end());
        }
        for (auto& tag : content.tags) {
            inverted_index[tag].erase(remove(inverted_index[tag].begin(), inverted_index[tag].end(), content.id), inverted_index[tag].end());
        }
    }
}

// LRU缓存热门标签的实体列表(减少磁盘I/O)
class LruCache {
    // 简化实现,实际用std::unordered_map<string, vector<int>> + 双向链表维护顺序
    // 这里省略具体实现,核心是缓存热门标签的倒排列表
};

// 计算余弦相似度(加权)
double cosine_similarity(int u_id, int c_id, const vector<double>& weights) {
    double common_sum = 0.0;
    double u_sum = 0.0;
    double c_sum = 0.0;
    
    // 遍历用户u的标签(带权重)
    for (size_t i = 0; i < user.tags.size(); ++i) {
        string tag = user.tags[i];
        vector<int> entities;
        // 尝试从LRU缓存获取,否则从倒排索引加载
        if (lru_cache.find(tag) != lru_cache.end()) {
            entities = lru_cache[tag];
        } else {
            entities = inverted_index[tag];
            // 将热门标签加入缓存
            if (lru_cache.size() > max_size) {
                auto it = lru_cache.begin();
                lru_cache.erase(it->first);
                lru_cache[tag] = entities;
            }
        }
        
        auto it = find(entities.begin(), entities.end(), c_id);
        if (it != entities.end()) {
            common_sum += weights[i]; // 加权
            u_sum += weights[i];
        }
    }
    
    // 遍历内容c的标签(带权重)
    for (size_t i = 0; i < content.tags.size(); ++i) {
        string tag = content.tags[i];
        vector<int> entities;
        if (lru_cache.find(tag) != lru_cache.end()) {
            entities = lru_cache[tag];
        } else {
            entities = inverted_index[tag];
            lru_cache[tag] = entities;
        }
        
        auto it = find(entities.begin(), entities.end(), u_id);
        if (it != entities.end()) {
            common_sum += weights[i];
            c_sum += weights[i];
        }
    }
    
    if (u_sum == 0 || c_sum == 0) return 0.0;
    return common_sum / (sqrt(u_sum) * sqrt(c_sum));
}

时间复杂度:用户u标签数m,内容c标签数n,共同特征数k,总时间O(m + n),比全量O(m*n)高效。空间复杂度:存储倒排索引(O(m + n)),LRU缓存热门标签(额外空间,减少磁盘I/O)。

5) 【面试口播版答案】:在推荐系统中计算用户与内容的余弦相似度时,当用户数百万、内容数千万,全量计算特征向量的点积会非常慢。我们可以用倒排索引预处理,把每个特征(比如内容标签)映射到包含该特征的用户或内容ID列表。计算时,先通过倒排索引快速找到用户和内容共同的特征,再计算这些共同特征上的相似度,避免全量遍历所有特征。比如,用户u的标签是{电影, 音乐},内容c的标签是{电影, 游戏},倒排索引中标签“电影”对应用户u和内容c,那么共同特征是“电影”,计算这个特征的权重(假设权重为1),再除以用户u和内容c的标签向量模长,这样时间复杂度从O(m*n)降到O(m + n),提升效率。具体来说,构建倒排索引后,计算用户u和内容c的相似度时,遍历用户u的标签,检查每个标签是否在内容c的标签倒排列表中,如果是,累加共同特征的数量(带权重),最后计算余弦值,既高效又准确。同时,为了应对动态更新,我们采用增量更新机制,只维护新增或删除的特征对应的索引条目,避免全量重建;对于热门标签,还通过LRU缓存减少磁盘I/O,进一步优化性能。

6) 【追问清单】:

  • 追问1:如果用户或内容特征动态更新(比如用户新增行为,内容更新标签),如何维护倒排索引?
    回答:采用增量更新机制,仅更新新增/删除特征对应的索引条目,避免全量重建,减少时间开销。例如,用互斥锁保证多线程下的索引一致性。
  • 追问2:如果特征是高维连续向量(如用户行为嵌入、内容文本嵌入),倒排索引是否适用?
    回答:不适用,此时应使用哈希表或近似方法(如局部敏感哈希LSH),因为倒排索引针对离散特征,连续向量需通过哈希函数映射到哈希表。
  • 追问3:当数据量极大时,倒排索引的空间复杂度如何优化?
    回答:通过压缩倒排索引(如稀疏表示、前缀编码、字典序压缩),减少存储空间,同时保持查询效率,例如用前缀编码存储ID列表,减少内存占用。
  • 追问4:如何处理不同特征的重要性(如标签权重不同)?
    回答:在倒排索引中存储特征权重,计算时乘以权重,调整相似度计算,提升推荐准确性,例如标签“电影”权重为1.5,“音乐”权重为1,共同特征时按权重累加。
  • 追问5:如果共同特征数量很多,倒排索引的查询效率是否会下降?
    回答:倒排索引的查询效率与共同特征数量正相关,若共同特征多,时间复杂度接近O(m),但仍比全量计算高效,可通过特征选择(如Top-K标签)优化,只保留最相关的特征参与计算。

7) 【常见坑/雷区】:

  • 坑1:忽略动态更新导致数据过时,未提及增量维护机制,导致推荐结果不准确。
  • 坑2:认为所有特征都适合倒排索引,未区分离散与连续特征,高维连续向量用倒排索引会效率低下。
  • 坑3:空间复杂度分析错误,未说明额外存储空间对系统的影响,例如倒排索引存储量可能过大,导致内存不足。
  • 坑4:计算时未考虑特征权重,导致结果不准确,例如所有标签权重相同,忽略实际重要性。
  • 坑5:未讨论近似方法(如LSH),当数据量极大时,倒排索引可能无法满足实时性需求,需要结合近似算法。
51mee.com致力于为招聘者提供最新、最全的招聘信息。AI智能解析岗位要求,聚合全网优质机会。
产品招聘中心面经会员专区简历解析Resume API
联系我们南京浅度求索科技有限公司admin@51mee.com
联系客服
51mee客服微信二维码 - 扫码添加客服获取帮助
© 2025 南京浅度求索科技有限公司. All rights reserved.
公安备案图标苏公网安备32010602012192号苏ICP备2025178433号-1