我有以下问题:
我有一个对象列表(包含一个日期属性),其大小为S(通常S将达到约200k,但有时可能会达到一百万)。
我需要根据以下条件过滤列表:
考虑到大量列表以及低效解决方案可能产生的性能问题,您能否告诉我这个问题的最佳方法/算法是什么。
感谢。
PS:实施是在JAVA
答案 0 :(得分:2)
我在这里可能完全错了,但我认为人们错过了这样一个事实,即你的标题使用了“分组”而不是“过滤”这个词,以及“从其日期属性开始”的部分。所以这是我对你的问题的看法:
迭代列表并计算每个条目的间隔内的项目数将非常昂贵,假设列表未排序。这将导致O(n²)性能。
如果T中的项目顺序可能与L中的项目不同,那么以下是有效的解决方案:
对L中的项进行排序。如果项类型I实现了Comparable
接口,并且按时间戳排序,那么它将很简单。否则,您可能需要实现对timestamp属性进行排序的Comparator
。排序不能比O(n * log n)更好,所以这就是现在的底线。
制作三个索引:s(用于开始),e(用于结束),c(用于当前项目)。
使用索引c迭代排序列表。对于每个条目,请执行以下操作:
3.1从索引s开始,计算有多少项不再在“半径”范围内。也就是说,有多少项的时间戳低于索引c处项目的时间戳减去时间半径。将该值添加到索引s。这使得它指向半径范围内的“最早”项目。
3.2从索引e开始,检查已排序列表中的项目以查看它们是否在半径范围内。让e指向最后一个项目。
3.3取值(e - s)。如果它高于阈值 l ,则表示索引c处的项目通过了过滤器。否则,它没有。
3.4增量c。如果c现在是> e,也增加e。
就是这样。步骤3检查排序列表中的每个项目,以便获得O(n)性能。最坏的情况取决于时间戳和半径。您不是每次都从列表的开头搜索下限,而是从尾随索引s开始搜索。您不是每次都从c开始搜索上限,而是从前导索引e开始。所以我认为最糟糕的表现仍然是O(n),因为s和e也只能遍历列表一次。假设您有100个条目,并且从条目3中您发现所有剩余条目都在半径范围内(即e变为99),您将永远不必进一步检查。
通过一个O(n * log n)步骤和一个O(n)步骤,分摊的性能变为O(n * log n)。似乎完全可以接受。
请注意,如果已过滤的列表必须维护原始项目顺序,则需要执行其他步骤。在这种情况下,可以使用某种索引列表。
编辑:我刚刚意识到你的字面意思可能是“从”开始,所以如果是这样的话,只需忽略尾随索引,只能使用前导索引e。除了采用(e - c)而不是(e - s)之外,算法保持不变。我在预编辑版本中也有一些“框架列表”,这显然是无稽之谈,因为索引足以计算所需的数量。
答案 1 :(得分:1)
我认为我不完全理解您的问题,但如果列表按日期排序,您应该能够确定以下X项是否全部在区间T中,否则删除它们。
如果X变高,您可以考虑使用不同的排序结构,例如一个TreeMap
,其中键是日期。然后你可以做这样的事情:
SortedMap<Date, YourObject> map = ...;//left for you
int numEventsInInterval = map.subMap( intervalStart, intervalEnd ).size();
答案 2 :(得分:1)
你需要一个:
class Foo { Date date; }
class Event { Period period; }
class Period { Date start, end; }
List<Foo> foos = ...
List<Event> events = ...
Period p = new Period(...)
[pseudo-code]
foreach foo in foos:
eventsAfterFoo = findEventsAfter(foo.date, events);
c = 0;
foreach event in eventsAfterFoo:
if(event.isInPeriod(p)) c++
if(c >= X)
finalList.add(foo)
对于大量元素,您肯定会使用数据库简化您的解决方案,即使是像HSQL这样的非主流数据库。
您当然可以将列表拆分到不同计算机上的不同VM中,唯一的共享/只读列表将是“事件”。
答案 3 :(得分:1)
将长度为X的子列表放在完整列表中。
每次迭代:
如果sublist.tail.date - sublist.head.date&gt;然后标记sublist.head以丢弃。
不幸的是,您最终可能会放弃一些符合条件的活动,但可以修复。
答案 4 :(得分:1)
您可以通过监视器“流式传输”对象(我称之为事件),该监视器使用所需属性标记事件。
例如:
public class EventMonitor {
private int minimumGroupSize;
private long window;
private LinkedList<Event> events = new LinkedList<Event>();
public EventMonitor(int minimumGroupSize, long window) {
this.minimumGroupSize = minimumGroupSize;
this.window = window;
}
public void handle(Event newest) {
System.out.println(newest);
events.addLast(newest);
if (events.size() == minimumGroupSize) {
Event oldest = events.peekFirst();
if (newest.getTimestamp() - oldest.getTimestamp() < window) {
System.out.println("Group starter: " + oldest);
}
events.removeFirst();
}
}
public static class Event {
private final long timestamp;
Event(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public String toString() {
return String.valueOf(timestamp);
}
}
public static void main(String[] args) {
EventMonitor monitor = new EventMonitor(5, 15);
feedEventData(monitor);
}
private static void feedEventData(EventMonitor monitor) {
long timestamp = 0;
for (int i = 0; i < 20; i++) {
long interval = 1 + (long) (Math.random() * 10);
timestamp = timestamp + interval;
monitor.handle(new Event(timestamp));
}
}
}
这会将事件间隔1-10传递给EventMonitor。监视器会跟踪最近的minimumGroupSize事件数,并在最新事件属于时间窗口时打印出最旧的事件。
注意:此示例实现不是线程安全
答案 5 :(得分:1)
我建议使用Esper,这是一个复杂的事件处理器。它使用类似SQL的查询来处理传入事件,您可以扫描时间轴中的现有事件。让它处理数据结构。