特例:散列和等式
首先,我们需要确定一些关于假设的事情,即是否存在等式和具有函数关系。我这是什么意思?我的意思是,对于源对象集 S,给定作为 S 元素的任意两个对象 x1 和 x2,存在一个(散列)函数 F,使得:
if (x1.equals(x2)) then F(x1) == F(x2)
Java 有这样的关系。这允许您将重复检查作为接近 O(1) 的操作,从而将算法简化为简单的 O(n) 问题。如果订单不重要,那就是一个简单的班轮:
List result = new ArrayList(new HashSet(inputList));
如果顺序很重要:
List outputList = new ArrayList();
Set set = new HashSet();
for (Object item : inputList) {
if (!set.contains(item)) {
outputList.add(item);
set.add(item);
}
}
你会注意到我说的是“接近 O(1)”。这是因为此类数据结构(如 Java HashMap 或 HashSet)依赖于一种方法,其中一部分哈希码用于在后备存储中查找元素(通常称为存储桶)。桶的数量是 2 的幂。这样,该列表中的索引很容易计算。 hashCode() 返回一个整数。如果您有 16 个存储桶,您可以通过将 hashCode 与 15 进行“与”运算来找到要使用的存储桶,得到一个从 0 到 15 的数字。
当你尝试往那个桶里放东西时,它可能已经被占用了。如果是这样,那么将对该存储桶中的所有条目进行线性比较。如果碰撞率变得太高或者你试图在结构中放置太多元素,那么结构将会增长,通常会增加一倍(但始终是 2 的幂)并且所有项目都被放置在它们的新桶中(基于新的面具)。因此,调整此类结构的大小相对昂贵。
查找也可能很昂贵。考虑这个类:
public class A {
private final int a;
A(int a) { this.a == a; }
public boolean equals(Object ob) {
if (ob.getClass() != getClass()) return false;
A other = (A)ob;
return other.a == a;
}
public int hashCode() { return 7; }
}
此代码完全合法,并且符合 equals-hashCode 合约。
假设您的集合只包含 A 实例,您的插入/搜索现在变成 O(n) 操作,将整个插入变成 O(n2)。
显然这是一个极端的例子,但有必要指出这种机制还依赖于映射或集合使用的值空间内相对良好的哈希分布。
最后,必须说这是个特例。如果您使用的语言没有这种“散列快捷方式”,那就另当别论了。
一般情况:无订购
如果列表不存在排序函数,那么您将陷入对每个对象与每个其他对象的 O(n2) 蛮力比较。所以在 Java 中:
List result = new ArrayList();
for (Object item : inputList) {
boolean duplicate = false;
for (Object ob : result) {
if (ob.equals(item)) {
duplicate = true;
break;
}
}
if (!duplicate) {
result.add(item);
}
}
一般情况:订购
如果存在排序函数(例如,整数或字符串列表),则对列表进行排序(O(n log n)),然后将列表中的每个元素与下一个元素进行比较 ( O(n)) 所以总算法是 O(n log n)。在 Java 中:
Collections.sort(inputList);
List result = new ArrayList();
Object prev = null;
for (Object item : inputList) {
if (!item.equals(prev)) {
result.add(item);
}
prev = item;
}
注意:以上示例假定列表中没有空值。