【问题标题】:Data Structure for Quickly Checking Numbers against Overlapping Ranges用于根据重叠范围快速检查数字的数据结构
【发布时间】:2014-04-12 05:02:58
【问题描述】:

假设我有以下数字范围:

0-500 0-100 75-127 125-157 130-198 198-200

现在,假设我需要能够检查任何给定的数字,并查看它在哪个范围内。哪种数据结构最有效地用于判断数字 100 属于范围 0- 500、0-100 和 75-127?我只想要一个保存起始值的二叉树吗?在这种情况下,树中的每个节点是否可以在该起点保存多个包含每个范围的对象?

请注意,我只需要检索这个特定的应用程序,我并不认为自己需要在中间进行修改,因此检索速度是我的首要任务。

谢谢!

【问题讨论】:

  • 你希望它只对这个输入起作用?
  • 否,对于任何输入和范围。
  • 可以自己创建数据结构吗?
  • 理想情况下,我需要能够在 100 毫秒内从多达 1000 个不同的范围内检索。
  • 您是否真的存储了这些数字。这意味着如果您只是想检查一个数字是否适合特定范围,或者您想检查一个数字是否实际存储在一个范围内?

标签: algorithm data-structures


【解决方案1】:

您需要的是interval tree。区间树是一个非常笼统的概念,每个问题在节点中保留的数据略有不同。在您的情况下,每个节点都将保留一个输入区间列表,该列表涵盖了所代表的区间节点。

【讨论】:

  • 您可能想要添加这个链接,它比维基百科更好地解释了interval treedgp.toronto.edu/people/JamesStewart/378notes/22intervals
  • 这看起来很有趣,我会仔细研究一下。感谢您的建议,并感谢您提供链接!
  • 链接中的这一行看起来正是您所需要的。 In the abstract, the query algorithm must do the following: Given an integer k and an interval tree T, list all the intervals stored in T that contain k. An interval [a,b] contains k if a <= k <= b.
  • 将是正常的选择 except 如果检查不是数据结构的构造需要速度优化,那么您可以预先计算范围内所有点的答案并执行一个简单的表格查找作为检查 - 请参阅我的 Python 解决方案。
【解决方案2】:

R 表示可能的范围数。 (例如R=6

创建R 哈希表,使每个哈希表只能包含一个范围。对于您的示例,您需要创建 6 个哈希表。第一个哈希表R1将只包含0-500之间的值。

填充哈希表。

每个数字都会进入适当的哈希表。对于您的示例编号100 将进入R1R2R3。如果R 很大,那么您需要创建很多哈希表。但是总空间受限于存储在所有哈希表中的实际数据。

检索:

对于任何给定的数字,检查它是否存在于每个 R 哈希表中。您可以通过选择要查看的哈希表来进一步优化。例如,对于100,您只需查看 6 个哈希表中的 3 个。

时间复杂度:

在哈希表中搜索单个值需要constant time(平均)。所以 摊销O(1) 以查看哈希表中是否存在数字。

摊销O(R) 以产生输出,因为我们需要查看所有哈希表以产生输出

【讨论】:

  • 感谢您再次发帖!这可能很好用,不过我还没有研究过区间树。
  • 您能解释一下“时间复杂度”部分的含义吗?如果这不是一个简短的解释,你当然不需要花费更多的时间,我相信我可以查一下。 :)
  • @JS:你的意思是问Time complexity 的一般含义是什么,或者Time complexity 是什么意思?
  • 我不知道“Amortized O(R/1)”术语是什么意思。
  • 经典示例是快速排序与合并排序。合并排序的最坏情况时间复杂度为O(nlogn),快速排序的最坏情况复杂度为O(n^2)。但在practice 中,quicksort 的性能比mergesort 好。为什么?因为我们需要查看平均案例时间复杂度。合并排序平均需要O(nlogn),但快速排序平均需要O(n)。如果看不懂amortized这个词,可以认为是average
【解决方案3】:

假设你有足够的内存,我会使用一个列表数组,其中列表包含包含索引的所有范围。

这是一个更详细地展示算法的 Python 解决方案:

# (Inclusive) ranges
ranges = [(0,500), (0,100), (75,127), (125,157), (130,198), (198,200)]
smallest = min(r[0] for r in ranges)
largest  = max(r[1] for r in ranges)

# Ceate table
table = [[] for i in range(smallest, largest+1)] # List of lists
for r in ranges: # pre-compute results
    mn, mx = r
    for index in range(mn, mx+1):
        table[index - smallest].append(r)

def check(n):
    'Return list of ranges containing n'
    if smallest <= n <= largest:
        return table[n - smallest]
    else:
        return []   # Out of range

for n in [-10, 10, 75, 127, 129, 130, 158, 197, 198, 199, 500, 501]:
    print('%3i is in groups: %r' % (n, check(n)))

输出是:

-10 is in groups: []
 10 is in groups: [(0, 500), (0, 100)]
 75 is in groups: [(0, 500), (0, 100), (75, 127)]
127 is in groups: [(0, 500), (75, 127), (125, 157)]
129 is in groups: [(0, 500), (125, 157)]
130 is in groups: [(0, 500), (125, 157), (130, 198)]
158 is in groups: [(0, 500), (130, 198)]
197 is in groups: [(0, 500), (130, 198)]
198 is in groups: [(0, 500), (130, 198), (198, 200)]
199 is in groups: [(0, 500), (198, 200)]
500 is in groups: [(0, 500)]
501 is in groups: []

可以进行进一步优化,例如使用位集来存储每个索引处的范围而不是列表。

从下面的 cmets 中,我决定修改上面的方法,在上面的表查找方法和基于直接比较的较慢但内存效率更高的解决方案之间进行选择。 (理想情况下也会包含一个基于区间树的解决方案):

# (Inclusive) ranges
ranges = [(0,500), (0,100), (75,127), (125,157), (130,198), (198,200)]
limit = 1000000 # Or whatever

smallest = min(r[0] for r in ranges)
largest  = max(r[1] for r in ranges)

if (largest - smallest) * len(ranges) < limit:
    # Ceate table
    table = [[] for i in range(smallest, largest+1)] # List of lists
    for r in ranges:
        mn, mx = r
        for index in range(mn, mx+1):
            table[index - smallest].append(r)

    def check(n):
        'Return list of ranges containing n'
        if smallest <= n <= largest:
            return table[n - smallest]
        else:
            return []   # Out of range
else:
    # mpre emory efficient method, for example
    def check(n):
        return [(mn, mx) for mn, mx in ranges if mn <= n <= mx]

for n in [-10, 10, 75, 127, 129, 130, 158, 197, 198, 199, 500, 501]:
    print('%3i is in groups: %r' % (n, check(n)))

【讨论】:

  • 查表总是很快的。您添加了警告Assuming you have enough memory。您的方法的空间复杂度是多少?
  • extent*range_count 其中extent是所有范围的最大值-最小值,range_count是范围数(最大值)。
  • 如果我理解正确,extent 这个例子是 500,range_count 是 6
  • 是的。时间是一些常数,在最坏的情况下,范围有很大的重叠。当然,如果一个新的确切范围,那么您可以修改一个简单的表查找:例如,如果表是稀疏的,那么您可以通过使用某种形式的稀疏表结构来交换内存以换取速度,但是所有替代解决方案都需要比较要使用的实际范围集的访问速度和内存使用情况。 (P.S. Wot!没有赞成票?:-)
  • 如果一个特定的范围是10k-15k 和其他在100s 范围内,如本例所示。你最终会创建一个大表吗?
【解决方案4】:

已编辑:

  • 更快
  • 内存消耗效率更高
  • 对于 1000 个输入,保证在最坏的情况下它可以在不到 100 毫秒的时间内工作
  • 对于 10 000 个输入范围,比所需输入多 10x,对于 50 个 from 重复项,它仍可在不到 100 毫秒内工作,例如:{0 - #}x50 , {1 - #}x50 等...适用于所有 from 范围。

目标

给定 10,000 个输入范围间隔(从到 - 为了使情况更糟,每个“从”重复 ​​50 次)。程序采用一个目标值并显示目标所属的所有范围。

4 个范围的工作原理示例:

给定以下范围:

0 - 100
0 - 500
50 - 500
20 - 300

目标:40

输出

20 - 300
0 - 500
0 - 100

算法说明(使用前面的例子):

每个from 值都映射到从index = 0 开始的index++。因此,在我们的示例中:

From => index
0 => 0
50 => 1
20 => 2

现在有一个名为 pointers 的 Tree Set 数组,该数组的每个索引 i 都引用 Tree Set 中值 ikey。所以pointers[0] 指的是'from':0,pointers[1] 指的是'from':50,pointers[2] 指的是'from':20。

现在我们通过查看树形图'key' =&gt; 'value'to 值分别添加到每个from 值,其中valuekeypointers 数组中的索引。

此外,我们希望在每个索引处添加到 pointers 数组中的 to 值按降序排序(稍后我会解释原因)。

所以现在指针变成了这样:

index => TreeSet[values..]
0 => 500 | 100
1 => 500
2 => 300

现在我们已经准备好获取目标所属的范围了。

对于target = 40

1 - 在树形图中搜索最近的楼层键 40。程序发现 20 是最接近的。

2 - 它转到指针数组中对应于 20 的索引。要获得 20 的索引:查看键 20 处的树图。20 的索引是 2。

3 - 所以现在去pointers[2],它发现有数字300。

4 - 现在应用程序检查 300 是否小于目标,它这样做的原因是因为我之前提到过创建的每个树集都按降序排序。因此,如果 300 小于目标值,则无需继续检查 pointers[2] 中的下一个值,因为它保证它们更小。

5 - 在这种情况下,300 大于目标,然后将密钥打印为 from 并将 pointer[2] {current element} 打印为 to

6 - 由于在pointers[2]中只找到了一个元素,for循环退出,并且key从tree map中移除,所以下次程序要查找下一个最近的楼层key时,会找到下一个一个,如果存在的话。

7 - (while循环的下一次迭代)删除键20后找到的下一个键为0,根据树形图索引为0。

8 - 转到pointers[0]。发现pointers[0]处Tree Set的元素个数为2。

9 - 从第一个元素 pointers[0] {first element} 开始。 500 小于 40 吗?不,打印“范围”。下一个元素,是 100 小于 40 吗?不,打印“范围”。没有更多元素退出 for 循环。

10 - 从树形图中删除键 0。

11 - 现在,While 循环条件检查目标是否存在最近的楼层键。不,因为 0 和 20 已被删除。所以条件不满足,退出while循环。

12 - 消耗的打印时间和结束程序:)

希望你的解释对你有用。

代码

import java.util.Collections;
import java.util.TreeMap;
import java.util.TreeSet;

public class Interval {
    public static void main(String[] args) {
        int MAX_SIZE = 10000;
        int[] from = new int[MAX_SIZE];
        int[] to = new int[MAX_SIZE];

        //Generate 10,000 (from - to), with 50 redundant (from range) for every value of from. (to make the application heavy)
        int c = 0;
        for(int i=0; i<MAX_SIZE;i++){
            from[i] = 0+c;
            to[i] = from[i] + (int)(Math.random()*100);
            if(i%50 == 0) 
                c++;
        }

        //Start time counting
        long time = System.currentTimeMillis();

        int target = 97;
        TreeMap<Integer, Integer> treePointer = new TreeMap<Integer, Integer>(); // will sotre <From, index++>

        int index = 0;
        int size = from.length;
        TreeSet<Integer>[] pointers = new TreeSet[size]; //Array of tree set to store values of every "from" range

        //insert into tree
        for(int i=0; i<from.length;i++){
            if(!treePointer.containsKey(from[i])){ //if the "from" does not exist in the tree yet, insert it
                treePointer.put(from[i], index);
                pointers[index++] = new TreeSet<Integer>(Collections.reverseOrder()); //sort descending order
            }

            //index of "from" in the pointers array
            int virtualIndex = treePointer.get(from[i]);
            //add the 'to' element to the corresponding index of "from" in Tree Set at pointers[index of "from"]
            pointers[virtualIndex].add(to[i]);
        }

        // Display part of the pointers array to understand how elements are stored
//      for(int i=0; i<10; i++){
//          for(int current : pointers[i]){
//              System.out.print(current + " ");
//          }
//          System.out.println();
//      }

        //Start checking for the ranges
        Integer currentKey = -1; //dummy number at first
        while((currentKey = treePointer.floorKey(target)) != null){ // while there is a closest floor key
          //get index assigned to the closest floor key
            int virtualIndex = treePointer.get(currentKey);
            //loop on the elements found at pointers[index of closest floor number]
            for(int toValue : pointers[virtualIndex]){
                if(toValue < target) //remember the values are sorted in a descending order, so whenever the value becomes smaller than the target don't continue the for loop
                    break;
                System.out.println(currentKey + " - " + toValue); // else target less or equal to toValue, so print range
            }
            treePointer.remove(currentKey); //remove key from tree to fetch the next floor key in the next while iteration
        }
        //Display time consumed in ms
        System.out.println("\nTotal time: " + (System.currentTimeMillis() - time) + " ms");
    }
}

【讨论】:

  • 我几乎没有使用 Java 的经验,我不确定某些导入的对象/方法到底是做什么的,但是查找时间看起来很棒,并且可能与我进行的单独查找的数量大致相同会需要。这绝对看起来很有希望。
  • @Amir Bawab:感谢您为编写代码所做的努力。但是很难理解代码背后的算法。可以加个伪代码吗?
  • 书籍告诉我们先写逻辑再写代码,但有时我们最终会做相反的事情:)
  • 好吧,我在实现之前把它画在了白板上:)
  • @arunmoezhi 现在好点了吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-23
  • 1970-01-01
  • 2020-08-14
  • 1970-01-01
相关资源
最近更新 更多