
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) 【追问清单】:
7) 【常见坑/雷区】: