嵌套交叉引用列表重复项移除的高性能算法实现问题
嵌套交叉引用列表去重方案
核心思路
你的哈希预计算思路是可行的,结合无环引用链、子项最多1级的特性,可通过「自底向上归并+确定性指纹计算」实现线性时间复杂度,支持多线程并行,10万量级对象的处理耗时可控制在秒级。
方案实现步骤
1. 预处理规则简化
首先利用给定的约束条件降低计算复杂度:
- 对象不能同时持有引用和子项,因此每个对象只会属于「引用型」「子项持有型」「基础型(无引用无子项)」三类,无需处理两种属性同时存在的分支
- 引用链无环且最终指向
ClassType为Type_Base的基础对象,可按引用深度倒序处理对象,避免重复递归计算
2. 确定性等价指纹计算
为每个对象生成全局唯一的等价指纹,指纹相同的对象直接判定为等价,无需重复逐字段比对:
- 指纹计算规则(按依赖顺序):
- 基础对象:指纹 = 哈希组合(
ClassType+Name的不变文化哈希) - 引用型对象:指纹 = 哈希组合(
ClassType+Name的不变文化哈希 + 被引用对象的指纹) - 子项持有型对象:指纹 = 哈希组合(
ClassType+Name的不变文化哈希 + 所有子项按顺序拼接的指纹)
- 基础对象:指纹 = 哈希组合(
- 为避免哈希冲突,采用双层哈希校验:第一层用64位FNV-1a哈希做粗筛,第二层用128位XXHash做精筛,两层哈希同时相等才判定为指纹等价,冲突概率可忽略。
3. 多线程并行处理流程
步骤1:并行计算所有对象的引用深度
遍历AllObjects,对每个对象递归计算到基础对象的引用链长度作为处理优先级,深度越大越先处理。该步骤可多线程并行,用线程安全字典存储深度结果。
步骤2:按深度分组并行计算指纹
把所有对象按深度降序分组,同深度的对象互相没有依赖,可完全并行计算指纹:
- 用线程安全字典
FingerprintMap<(ulong Hash1, ulong Hash2), uint>存储指纹对应的保留对象ID - 每个对象计算完指纹后查询
FingerprintMap:- 不存在该指纹:将当前对象ID存入
FingerprintMap,标记为保留 - 已存在该指纹:标记当前对象为重复,记录映射关系
DuplicateMap<uint, uint>(重复对象ID -> 保留对象ID)
- 不存在该指纹:将当前对象ID存入
步骤3:批量更新引用和子项
遍历所有保留对象,将对象的ReferencedID、子项的ReferencedID按DuplicateMap替换为保留对象ID。
步骤4:清理死链和重复对象
遍历AllObjects,删除所有标记为重复的对象,同时删除未被任何保留对象引用的死链对象。
核心代码示例
// 指纹结构,值类型避免装箱开销 public readonly struct ObjectFingerprint : IEquatable<ObjectFingerprint> { public ulong Hash1 { get; } public ulong Hash2 { get; } public ObjectFingerprint(ulong hash1, ulong hash2) { Hash1 = hash1; Hash2 = hash2; } public bool Equals(ObjectFingerprint other) { return Hash1 == other.Hash1 && Hash2 == other.Hash2; } public override bool Equals(object obj) { return obj is ObjectFingerprint other && Equals(other); } public override int GetHashCode() { return HashCode.Combine(Hash1, Hash2); } } // 并行处理入口 public static void DeduplicateObjects(Dictionary<uint, MyObject> allObjects, Dictionary<uint, MyObject> preserved) { // 1. 并行计算所有对象的引用深度 var depthMap = new ConcurrentDictionary<uint, int>(); Parallel.ForEach(allObjects.Values, obj => { depthMap[obj.ID] = CalculateDepth(obj, allObjects); }); // 2. 按深度降序分组 var depthGroups = allObjects.Values .GroupBy(obj => depthMap[obj.ID]) .OrderByDescending(g => g.Key) .ToList(); var fingerprintMap = new ConcurrentDictionary<ObjectFingerprint, uint>(); var duplicateMap = new ConcurrentDictionary<uint, uint>(); var fingerprintCache = new ConcurrentDictionary<uint, ObjectFingerprint>(); // 3. 按深度分组并行计算指纹 foreach (var group in depthGroups) { Parallel.ForEach(group, obj => { var fingerprint = CalculateFingerprint(obj, allObjects, fingerprintCache); if (fingerprintMap.TryAdd(fingerprint, obj.ID)) { fingerprintCache[obj.ID] = fingerprint; return; } // 标记为重复 var preservedId = fingerprintMap[fingerprint]; duplicateMap[obj.ID] = preservedId; }); } // 4. 批量更新所有保留对象的引用 // 注意:若MyObject为不可变类,需重新构造实例替换allObjects中的原对象 Parallel.ForEach(allObjects.Values.Where(obj => !duplicateMap.ContainsKey(obj.ID)), obj => { // 更新自身引用 if (obj.ReferencedID != 0 && duplicateMap.TryGetValue(obj.ReferencedID, out var newRefId)) { obj.ReferencedID = newRefId; } // 更新子项引用 foreach (var child in obj.Children) { if (child.ReferencedID != 0 && duplicateMap.TryGetValue(child.ReferencedID, out var childRefId)) { child.ReferencedID = childRefId; } } }); // 5. 清理重复对象和死链 var referencedIds = new HashSet<uint>(preserved.Keys); foreach (var obj in allObjects.Values.Where(o => !duplicateMap.ContainsKey(o.ID))) { if (obj.ReferencedID != 0) referencedIds.Add(obj.ReferencedID); foreach (var child in obj.Children) { if (child.ReferencedID !=0) referencedIds.Add(child.ReferencedID); } } // 移除重复+未被引用的对象 foreach (var id in allObjects.Keys.ToList()) { if (duplicateMap.ContainsKey(id) || !referencedIds.Contains(id)) { allObjects.Remove(id); } } } // 深度计算辅助方法 private static int CalculateDepth(MyObject obj, Dictionary<uint, MyObject> allObjects) { if (obj.ReferencedID == 0) return 0; var refObj = allObjects[obj.ReferencedID]; return CalculateDepth(refObj, allObjects) + 1; } // 指纹计算辅助方法,FNV-1a和XXHash的实现可替换为项目中已有的实现 private static ObjectFingerprint CalculateFingerprint(MyObject obj, Dictionary<uint, MyObject> allObjects, ConcurrentDictionary<uint, ObjectFingerprint> cache) { if (cache.TryGetValue(obj.ID, out var cached)) return cached; ulong hash1 = 14695981039346656037UL; // FNV-1a初始值 ulong hash2 = 0; // XXHash初始值 // 合并ClassType hash1 ^= (ulong)obj.ClassType; hash1 *= 1099511628211UL; hash2 = HashCode.Combine(hash2, obj.ClassType); // 合并Name if (obj.Name != null) { foreach (var c in obj.Name) { hash1 ^= c; hash1 *= 1099511628211UL; } hash2 = HashCode.Combine(hash2, obj.Name.GetHashCode(StringComparison.InvariantCulture)); } // 合并引用 if (obj.ReferencedID != 0) { var refFingerprint = CalculateFingerprint(allObjects[obj.ReferencedID], allObjects, cache); hash1 ^= refFingerprint.Hash1; hash1 *= 1099511628211UL; hash2 = HashCode.Combine(hash2, refFingerprint.Hash2); } // 合并子项 else { foreach (var child in obj.Children) { var childFingerprint = CalculateFingerprint(child, allObjects, cache); hash1 ^= childFingerprint.Hash1; hash1 *= 1099511628211UL; hash2 = HashCode.Combine(hash2, childFingerprint.Hash2); } } var result = new ObjectFingerprint(hash1, hash2); cache[obj.ID] = result; return result; }
性能说明
- 时间复杂度为O(N*L),其中N是总对象数,L是引用链平均长度(5-15),10万量级下总运算量不到200万次,单线程即可在1秒内完成,多线程下耗时可进一步降低
- 所有分组处理阶段完全并行,没有全局锁竞争,可充分利用多核CPU
- 确定性指纹计算保证了结果和单线程处理完全一致,没有多线程并发导致的结果差异
内容的提问来源于stack exchange,提问作者Simmy
相关产品推荐
相关产品推荐

