列表中的高效查找

时间:2009-08-05 12:07:17

标签: java algorithm data-structures

我有一种情况,即我用“ArrayList”填充TransactionEventTransactionEvent有一个属性“事务ID”。在大多数情况下,每个新事件的事务ID都大于先前事件的ID - 但是,这不能保证;即数据几乎已分类

我的问题是:如何根据交易ID执行快速查询?我目前的想法是调用Collections.binarySearch(...),如果失败则执行线性搜索。但是,我注意到Javadoc声明binarySearch的结果是未定义的,因为数据是无序的,所以我可能不得不滚动自己的实现。

其他:

  • 我尝试过使用索引地图 - >事务ID,但这种方法是有缺陷的,因为每当更新/删除列表元素时,我必须重建整个地图;即任何收益都会被删除。
  • 这不是过早优化的情况:当List包含大量行(100,000)时,TableModel是当前执行速度非常慢的基础。

任何帮助表示感谢。

11 个答案:

答案 0 :(得分:3)

使用LinkedHashMap,它结合了散列访问的双链表,您应该能够像使用ArrayList一样与TableModel连接,但也可以通过TransactionID上的散列查找来访问这些条目。

您甚至可以根据密钥替换(例如更新),而不会影响迭代顺序。

答案 1 :(得分:3)

您可以在添加每个TransactionEvent时通过搜索插入点来对ArrayList进行排序。 Collections.binarySearch返回

  

搜索关键字的索引,如果它包含在列表中;否则,( - (插入点) - 1)。插入点定义为键将插入列表的点:第一个元素的索引大于键,或list.size(),如果列表中的所有元素都小于指定的键。请注意,当且仅当找到密钥时,这可以保证返回值>> =。

搜索插入点后,您可以使用ArrayList add(int index, Object element)方法,而不是像平常一样添加到列表末尾。这会使每个插入速度减慢一小部分,但它可以让您使用二进制搜索来快速查找。

答案 2 :(得分:1)

ArrayList适用于玩具大小的问题。 100.000行从玩具空间中获得了一点点。这意味着您必须更准确地了解需要支持的访问模式。排序的ArrayList可能已足够,如果处理速度增长的速度超过问题大小,您可能不想打扰,但BTree在100K元素上的速度会更快。

ArrayList在问题规模较大时存在以下问题:

  • 当集合必须增长(复制所有元素)时,添加到结尾很慢
  • 在随机位置插入很慢,因为平均一半的集合必须移动一个位置

具有固定页面大小(例如BTree)的两级集合可以提供帮助,因为增长意味着添加(理想情况下)大约sqrt(大小)页面,随机插入将最多将一页分成两页。

有两个必需的排序顺序,你可以简单地使用两个(排序的)BTree

[编辑] 早期问题的答案是问题的关键。对于1000个元素的ArrayList,插入成本为7微秒,1000000个元素为7毫秒。 BTree保持在微秒范围内(但对于1000个元素的页面大小,可能是两倍慢)。

您可以通过保留每个页面中元素数量的索引来创建索引访问。 如果在每个页面上设置脏标志,则可以使用后台线程更新每个页面的起始索引,或者可以使用延迟索引构建添加批量操作。

索引可能无效,但只是sqrt(size)大。对于100K元素,它平均只增加150个索引。这需要微秒,而不是毫秒

答案 3 :(得分:0)

从你所说的,看起来快速查看是最重要的事情。

所以也许您应该使用HashMap而不是ArrayList。在HashMap中,使用TransactionID作为Key存储TransactionEvents。 HashMap中的查找是O(1)。

请注意,如果超过其初始容量,添加到HashMap会变得非常慢 - 因为它必须进行重新哈希。如果可以的话,尝试使用最佳猜测(高位错误)初始化它,如果它将保留的数字。

对于100k行,您可能必须增加Java堆大小以防止OutOfMemoryErrors。

java -Xms<initial heap size> -Xmx<maximum heap size>

默认值为:

java -Xms32m -Xmx128m

修改

如果订购真的很重要,您可以使用SortedMap

答案 4 :(得分:0)

您可以对列表进行排序。如果您在添加项目时插入排序,并且要添加的项目几乎已排序,则插入仍将有效地运行恒定时间。这将允许您以对数时间进行二进制搜索。

答案 5 :(得分:0)

我会使用二进制搜索来获取id的大致位置,然后线性向外搜索。不利的一面是,如果你要搜索的id不在列表中,那么它将需要O(n + log n)。

二进制搜索非常容易实现,我建议您阅读维基百科article

答案 6 :(得分:0)

我遇到了同样的问题。我想出的解决方案是基于ArrayList的自定义集合,它也包含了所有元素的Map。 这并不难。如果您希望我发布源代码 - 请告诉我

答案 7 :(得分:0)

我的投票是您按顺序插入列表。然后你可以进行二进制搜索。几点说明:

  1. 这将比正常插入更快,因为插入到结尾附近的ArrayList比插入开头附近更快(需要移动的元素更少),并且大多数插入将在结束时或附近(因为它们几乎-ordered)。
  2. 通常,您会找到使用二进制搜索算法插入ArrayList的插入点。在这种情况下,从最后开始线性搜索会更快,因为大多数插入都会在结束时或接近结束时发生。

答案 8 :(得分:0)

为什么不使用已排序的集合作为表模型而不是列表。 TreeMap似乎合乎逻辑,因为您的条目都是有序的。如果还需要按行或任何其他列快速访问,则只需添加辅助映射即可。基本上你正在做数据库索引的工作。

我认为由于某种原因你可以使用map.headSet(key)并找到第k个条目 - 这不起作用。你需要能够从表格行 - &gt; EventID(或接近它)。

如果你使用这样的模型

Map<EventID, Event> model = new TreeSet<EventID, Event>();

从概念上讲,你的getValueAt()看起来像这样:

getValueAt(int row, column) {
 eventID = getSortPosition(row);
 Event e = model.headSet(eventID).next();
 return getColumn(e, column);
}

关键是能够有效地维护排序索引中的地图 - &gt;键(反向映射)。这是非繁琐的,因为在最顶层插入一个新事件会影响它下面所有人的绝对顺序。似乎这里应该有一个CS答案,但它逃脱了我。

这是最基本的实现:   - 在每个插页上,您更新地图,然后实现您的有序地图。

ArrayList<Event> orderedEvents = new ArrayList<Event>();
public void insert(Event event) {
 model.put(event.getID(), event);

 // update the 
 model.headSet().addAll(orderedEvents);
}

你的getValueAt()非常简单。

getValueAt(int row, column) {w);
 Event e = orderedEvents.get(row);
 return getColumn(e, column);
}
  • 这使得插入O(n)而不是O(n log n)(仍然不是很好)

我认为你应该重新考虑你的UI设计 如果您让用户浏览100K行表,添加搜索过滤器将解决您的性能问题:

  • 没有用户会阅读100k行
  • 如果您的用户通过eventID进行搜索是有意义的,那么这很有效,当用户选择eventID时,您可以:sortedMap.headSet(searchFilterID)//先取200将它们放入您的表中
  • 如果用户按时间搜索是有意义的,那么从中制作地图并执行相同操作。

答案 9 :(得分:0)

我的第一个答案并不是你真正想要的。现在我更好地理解了这个问题,试一试。我只实现了关键部分。这将占用更多内存,但由于我非常确定ArrayList存储引用,而不是对象本身,因此与实际对象存储相比,内存差异不应太大。

class TransactionEventStore
{
    private ArrayList<TransactionEvent> byOrder, byId;

    private void insertByOrder(TransactionEvent e) { this.byOrder.add(e); }

    private void insertById(TransactionEvent e)
    {
        for(int i = this.byId.length() - 1; i > 0; i--)
            if(e.getId() > this.byId.get(i).getId())
            {
                this.byId.add(i,e);
                break;
            }
    }

    public void insert(TransactionEvent e)
    {
        this.insertByOrder(e);
        this.insertById(e);
    }
}

现在,当您需要按广告订单查询时,请查看this.byOrder,当您需要按ID查询时,请查看this.byId

答案 10 :(得分:0)

我从之前的帖子中清理了一些东西。 @Lizzard,您的解决方案最好是新条目通常在最后。如果以随机地图为内存的随机到达,下面的解决方案应该会更好。它还允许您推迟数组插入(可能是O(n)最坏的情况),直到您确实需要在最早的插入点下方绘制一行的单元格。

// sorted events (using natural ordering on eventID)
SortedSet<Event> model = new TreeSet<Event>();
ArrayList<Event> sortedList = new ArrayList<Event>();
Event lowestAddition, additionPrevEntry; // low water mark for insertions

public void insert(Event x) {
 if (x < lowestAddition) {
  Set<Event> headSet = model.headSet(x); // find the insertion point
  additionPrevEntry = headSet.isEmpty()?model.last():headSet.first();  
  lowestAddition = x;
 }

 model.add(x);  // add
}

public void materialize() {
 SortedSet<Event> tailSet = model.tailSet(additionPrevEntry);

 Event firstValue = tailSet.first();    // this element does not change its order
 Integer order = firstValue.getOrder(); // keep order on Event
 for (Event x : tailSet) {
  x.setOrder(order);
  sortedList.set(order, x);
  order++;
 }

 lowestAddition = null; additionPrevEntry = null;
}

以下是您的swing代码的样子,我假设您使用的是Swing,因为您需要一个表模型:

// now your model code uses the array
public Object getValueAt(int row, int col) {
 return getColumn(sortedList.elementAt(row), col);
}

// you can gain significant performance by deferring
// materialization until you acutally need it
public class DeferredJTable extends JTable {
 public void paintComponent(Graphics G, ...) {
  // if you knew what rows in the table were being drawn
  // ahead of time, you could further defer
  materialize();

  super.paintComponent();
 }
}