基于时间的分组

时间:2011-10-19 12:54:52

标签: java algorithm

我有以下问题:

我有一个对象列表(包含一个日期属性),其大小为S(通常S将达到约200k,但有时可能会达到一百万)。

我需要根据以下条件过滤列表:

  • 只有从日期属性开始,才能将项目保留在最终列表中。可以在时间间隔T中的列表X事件中找到该项目。

考虑到大量列表以及低效解决方案可能产生的性能问题,您能否告诉我这个问题的最佳方法/算法是什么。

感谢。

PS:实施是在JAVA

6 个答案:

答案 0 :(得分:2)

我在这里可能完全错了,但我认为人们错过了这样一个事实,即你的标题使用了“分组”而不是“过滤”这个词,以及“从其日期属性开始”的部分。所以这是我对你的问题的看法:

  • 您有一个源列表L,其中包含类型I的项目,其中包含时间戳属性 t
  • 您希望构建一个包含L项(可能是相同顺序)的目标列表T.
  • 您有一些半径 r ,表示为时间单位(毫秒,秒,分钟......)。
  • 你有一些正整数阈值 l (这里用完了字母)。
  • 当且仅当L中有 l 或更多项目且时间戳落在区间[]内时,L中的项 i 应放在T中它 - r it + r ]。

迭代列表并计算每个条目的间隔内的项目数将非常昂贵,假设列表未排序。这将导致O(n²)性能。

如果T中的项目顺序可能与L中的项目不同,那么以下是有效的解决方案:

  1. 对L中的项进行排序。如果项类型I实现了Comparable接口,并且按时间戳排序,那么它将很简单。否则,您可能需要实现对timestamp属性进行排序的Comparator。排序不能比O(n * log n)更好,所以这就是现在的底线。

  2. 制作三个索引:s(用于开始),e(用于结束),c(用于当前项目)。

  3. 使用索引c迭代排序列表。对于每个条目,请执行以下操作:

    3.1从索引s开始,计算有多少项不再在“半径”范围内。也就是说,有多少项的时间戳低于索引c处项目的时间戳减去时间半径。将该值添加到索引s。这使得它指向半径范围内的“最早”项目。

    3.2从索引e开始,检查已排序列表中的项目以查看它们是否在半径范围内。让e指向最后一个项目。

    3.3取值(e - s)。如果它高于阈值 l ,则表示索引c处的项目通过了过滤器。否则,它没有。

    3.4增量c。如果c现在是> e,也增加e。

  4. 就是这样。步骤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的查询来处理传入事件,您可以扫描时间轴中的现有事件。让它处理数据结构。