我需要在填充了文件读取循环的大整数集中找到空白,我想知道是否存在已经为此目的而做的事情,以避免出现堆溢出风险的简单Set对象。
为了更好地解释我的问题,我必须告诉你我的票证java软件是如何工作的。 每张票都有一个全局渐进号,存储在包含其他信息的每日日志文件中。我必须编写一个检查程序来验证每日日志文件中是否存在数字空白。
第一个想法是创建一个包含所有日志文件的读取循环,读取每一行,获取票号并将其存储在Integer TreeSet对象中,然后在此Set中查找间隙。 问题是票号可能非常高并且可能使内存堆空间饱和,如果我必须切换到Long对象,我也想要一个好的解决方案。 Set解决方案浪费了大量内存,因为如果我发现前100个数字中没有间隙就没有意义将它们存储在Set中。
我该如何解决?我可以使用为此目的已经完成的一些数据结构吗?
答案 0 :(得分:4)
我假设(A)您正在寻找的空白是例外,而不是规则和(B)您正在处理的日志文件大多按票号排序(尽管有些不按顺序排序)条目没问题)。
如果是这样,那么我就考虑为此推出自己的数据结构。这是我的意思的快速示例(向读者留下 lot )。
它的基本功能是实现Set
,但实际上将其存储为Map
,每个条目代表集合中的一系列连续值。
重写add
方法以适当地维护支持Map
。例如,如果你向集合添加5并且已经有一个包含4的范围,那么它只是扩展了该范围而不是添加新条目。
请注意"主要排序的原因"假设对于完全未排序的数据,这种方法仍将使用大量内存:支持映射将变大(因为未分类的条目在整个地方都被添加),然后逐渐变小(因为额外的条目填补了空白,允许连续要合并的条目。)
以下是代码:
package com.matt.tester;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
public class SE {
public class RangeSet<T extends Long> implements SortedSet<T> {
private final TreeMap<T, T> backingMap = new TreeMap<T,T>();
@Override
public int size() {
// TODO Auto-generated method stub
return 0;
}
@Override
public boolean isEmpty() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean contains(Object o) {
if ( ! ( o instanceof Number ) ) {
throw new IllegalArgumentException();
}
T n = (T) o;
// Find the greatest backingSet entry less than n
Map.Entry<T,T> floorEntry = backingMap.floorEntry(n);
if ( floorEntry == null ) {
return false;
}
final Long endOfRange = floorEntry.getValue();
if ( endOfRange >= n) {
return true;
}
return false;
}
@Override
public Iterator<T> iterator() {
throw new IllegalAccessError("Method not implemented. Left for the reader. (You'd need a custom Iterator class, I think)");
}
@Override
public Object[] toArray() {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public <T> T[] toArray(T[] a) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public boolean add(T e) {
if ( (Long) e < 1L ) {
throw new IllegalArgumentException("This example only supports counting numbers, mainly because it simplifies printGaps() later on");
}
if ( this.contains(e) ) {
// Do nothing. Already in set.
}
final Long previousEntryKey;
final T eMinusOne = (T) (Long) (e-1L);
final T nextEntryKey = (T) (Long) (e+1L);
if ( this.contains(eMinusOne ) ) {
// Find the greatest backingSet entry less than e
Map.Entry<T,T> floorEntry = backingMap.floorEntry(e);
final T startOfPrecedingRange;
startOfPrecedingRange = floorEntry.getKey();
if ( this.contains(nextEntryKey) ) {
// This addition will join two previously separated ranges
T endOfRange = backingMap.get(nextEntryKey);
backingMap.remove(nextEntryKey);
// Extend the prior entry to include the whole range
backingMap.put(startOfPrecedingRange, endOfRange);
return true;
} else {
// This addition will extend the range immediately preceding
backingMap.put(startOfPrecedingRange, e);
return true;
}
} else if ( this.backingMap.containsKey(nextEntryKey) ) {
// This addition will extend the range immediately following
T endOfRange = backingMap.get(nextEntryKey);
backingMap.remove(nextEntryKey);
// Extend the prior entry to include the whole range
backingMap.put(e, endOfRange);
return true;
} else {
// This addition is a new range, it doesn't touch any others
backingMap.put(e,e);
return true;
}
}
@Override
public boolean remove(Object o) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public boolean containsAll(Collection<?> c) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public boolean addAll(Collection<? extends T> c) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public boolean retainAll(Collection<?> c) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public boolean removeAll(Collection<?> c) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public void clear() {
this.backingMap.clear();
}
@Override
public Comparator<? super T> comparator() {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public SortedSet<T> subSet(T fromElement, T toElement) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public SortedSet<T> headSet(T toElement) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public SortedSet<T> tailSet(T fromElement) {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public T first() {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
@Override
public T last() {
throw new IllegalAccessError("Method not implemented. Left for the reader.");
}
public void printGaps() {
Long lastContiguousNumber = 0L;
for ( Map.Entry<T, T> entry : backingMap.entrySet() ) {
Long startOfNextRange = (Long) entry.getKey();
Long endOfNextRange = (Long) entry.getValue();
if ( startOfNextRange > lastContiguousNumber + 1 ) {
System.out.println( String.valueOf(lastContiguousNumber+1) + ".." + String.valueOf(startOfNextRange - 1) );
}
lastContiguousNumber = endOfNextRange;
}
System.out.println( String.valueOf(lastContiguousNumber+1) + "..infinity");
System.out.println("Backing map size is " + this.backingMap.size());
System.out.println(backingMap.toString());
}
}
public static void main(String[] args) {
SE se = new SE();
RangeSet<Long> testRangeSet = se.new RangeSet<Long>();
// Start by putting 1,000,000 entries into the map with a few, pre-determined, hardcoded gaps
for ( long i = 1; i <= 1000000; i++ ) {
// Our pre-defined gaps...
if ( i == 58349 || ( i >= 87333 && i <= 87777 ) || i == 303998 ) {
// Do not put these numbers in the set
} else {
testRangeSet.add(i);
}
}
testRangeSet.printGaps();
}
}
输出是:
58349..58349
87333..87777
303998..303998
1000001..infinity
Backing map size is 4
{1=58348, 58350=87332, 87778=303997, 303999=1000000}
答案 1 :(得分:2)
你要么把所有内容都存储在内存中,要么存在溢出堆的风险,或者你没有将它存储在内存中,你需要进行大量的计算。
我建议介于两者之间 - 存储处理过程中所需的最少信息。您可以将已知非间隙序列的端点存储在具有两个Long字段的类中。并且所有这些序列数据类型都可以存储在排序列表中。找到新数字后,遍历列表以查看它是否与其中一个端点相邻。如果是这样,请将端点更改为新的整数,并检查是否可以合并相邻的序列对象(从而删除其中一个对象)。如果没有,请在正确排序的位置创建一个新的序列对象。
这将最终在内存使用量O(n)
和cpu使用率O(n)
。但是使用任何存储有关所有数字的信息的数据结构在内存使用中只会是n
,如果查找时间不是在常量时间内完成的,那么在cpu中O(n*lookuptime)
。
答案 2 :(得分:1)
我相信这是熟悉bloom-filter
的完美时刻。它是一个很棒的概率数据结构,可用于立即证明元素不在集合中。
它是如何工作的?这个想法很简单,提升更复杂,可以在Guava中找到实现。
想法
初始化一个过滤器,该过滤器将是一个长度位数组,允许您存储使用的hash function
的最大值。向集合中添加元素时,请计算它的哈希值。确定哪些位是1
并确保它们都在过滤器(数组)中切换到1
。如果要检查元素是否在集合中,只需计算它的散列,然后检查散列中1
s的所有位是否为过滤器中的1
。如果过滤器中的任何一个位是0
,则该元素肯定不在该集合中。如果所有这些都设置为1
,则元素可能位于过滤器中,因此您必须循环遍历所有元素。
提升
简单概率模型提供了滤波器(以及散列函数的范围)应该为false positive
提供最佳机会的答案,即1
所有位都是bloom-filter
但是元素不在集合中。
<强>实施强>
Guava实现为create(Funnel funnel, int expectedInsertions, double falsePositiveProbability)
:expectedInsertions
提供了以下构造函数。您可以自行配置过滤器,具体取决于falsePositiveProbability
和bloom-filters
。
误报
有些人因为假阳性的可能性而意识到mightBeInFilter
。 Bloom过滤器可以以不依赖filter#mightBe
标志的方式使用。如果可能的话,你应该循环遍历所有元素,如果元素在集合中,则逐个检查。
可能的用法
在你的情况下,我为集合创建过滤器,然后在添加所有票证之后简单地循环遍历所有数字(因为你必须循环)并检查它们是否falsePositiveProbability
在集合中。如果您将O(n^2-0.03m*n)
设置为3%,则会在m
附近实现复杂性,其中ListView
代表间隙数。如果我对复杂性估计有误,请纠正我。
答案 3 :(得分:1)
尽可能多地读取可用内存中的票号。
对它们进行排序,并将排序后的列表写入临时文件。根据预期的间隙数量,在编写已排序的数字时,可能会节省使用行程编码方案的时间和空间。
在将所有故障单编号分类到临时文件后,您可以将它们合并为一个有序的故障单编号流,以查找间隙。
如果这会导致一次打开太多临时文件以进行合并,则可以将文件组合并到中间文件中,依此类推,将总数保持在可操作限制之下。但是,这种额外的复制会显着减慢这一过程。
旧的磁带机算法仍然相关。
答案 4 :(得分:1)
这是一个想法:如果您事先知道数字的范围,那么
预先计算您期望在那里的所有数字的总和。 2.然后继续阅读你的数字并产生所有读数的总和以及你的数字。 3.如果您提出的总和与预先计算的总和相同,则没有间隙。 4.如果总和不同,并且您的号码数量只是预期数量之一,则预先计算的总和 - 实际金额将为您提供缺失的数字。 5.如果您的号码数量超过一个,那么您将知道丢失了多少号码以及它们的总和。
最好的部分是您不需要将数字集合存储在内存中。