【问题标题】:Most efficient way to find index of matching values in two sorted arrays using C++使用 C++ 在两个排序数组中查找匹配值索引的最有效方法
【发布时间】:2016-08-08 17:37:03
【问题描述】:

我目前有一个解决方案,但我觉得它没有解决这个问题的效率,所以我想看看是否有更快的方法来解决这个问题。

我有两个数组(例如 std::vectors)。两个数组都只包含唯一的整数值,这些值已排序但值稀疏,即:1,4,12,13 ...我想问的是有没有快速的方法可以找到其中一个数组的索引值是相同的。例如,array1 的值为 1,4,12,13,array2 的值​​为 2,12,14,16。 array2 中的第一个匹配值索引是 1。数组中的索引很重要,因为我有其他数组包含将使用此“匹配”索引的数据。

我并不局限于使用数组,地图也是可以的。我只比较这两个数组一次。在第一次匹配通过后,它们将不再被重复使用。任何一个数组中都可以有小到大数量的值(300,000+),但并不总是有相同数量的值(这会使事情变得更容易)

更糟糕的情况是线性搜索 O(N^2)。使用 map 会使我的 O(log N) 变得更好,但我仍然需要将数组转换为值、索引对的映射。

我目前不需要做任何容器类型转换的是这个。循环两个数组中较小的一个。将小数组 (array1) 的当前元素与大数组 (array2) 的当前元素进行比较。如果 array1 元素值大于 array2 元素值,则增加 array2 的索引,直到它不再大于 array1 元素值(while 循环)。然后,如果 array1 元素值小于 array2 元素,则进入下一个循环迭代并重新开始。否则它们必须相等,并且我对匹配值的任意一个数组都有我的索引。

所以在这个循环中,如果所有值都匹配,我充其量是 O(N),如果没有匹配,我最好是 O(2N)。所以我想知道那里是否有更快的东西?很难确定这两个数组匹配的频率,但我会更倾向于大多数数组将大部分匹配而不是不匹配。

我希望我已经充分解释了这个问题,并且感谢任何关于改进此问题的反馈或提示。

代码示例:

std::vector<int> array1 = {4,6,12,34};
std::vector<int> array2 = {1,3,6,34,40};

for(unsigned int i=0, z=0; i < array1.size(); i++) 
{
    int value1 = array1[i];
    while(value1 > array2[z] && z < array2.size())
        z++;

    if (z >= array2.size())
        break; // reached end of array2

    if (value1 < array2[z])
        continue;

    // we have a match, i and z indices have same value

}

结果将匹配 array1 = [1,3] 和 array2= [2,3] 的索引

【问题讨论】:

  • 添加了当前解决方案的示例代码
  • 我很好奇,你需要这个算法的上下文是什么?
  • 我正在使用它与两组数据之间的某种关联。简而言之,我有一些包含对象 ID 的数组。然后,我需要从另一个数组中的一组 ID 中收集数据,该数组可能在也可能不在前者中。通过查找数组中存在匹配项的索引,我可以使用该索引来提取基于该索引的各种其他数据。
  • O() 的定义中有一个隐含的常数因子,因此对于任何实数 C,0(x) 等价于 O(C*x)。基本上像 2 这样的常数因子被忽略在 O 表示法中。我实际上并没有建议像那样进行显式边界检查,而是在其中一个数组保持较小元素时查看 head-2、-4、-8 等的自适应算法。在这种情况下,当数组在我的示例中大部分不相交时,您只需要 ~lg(N) 检查。

标签: c++ arrays algorithm loops matching


【解决方案1】:

由于数组已经排序,您可以使用非常类似于mergesort 的合并步骤的东西。这只是查看每个数组的头元素,并丢弃较低的元素(下一个元素成为头)。当您找到匹配项时停止(或者当任一数组耗尽时,表示没有匹配项)。

这是 O(n) 并且对于任意干扰可以做到的最快。对于某些集群分布,可以使用“跳过”方法,而不是总是查看下一个元素。对于某些发行版,这可能会导致优于 O(n) 的运行时间。例如,给定数组1,2,3,4,510,11,12,13,14,算法可以确定在一次比较中找不到匹配项(5

【讨论】:

  • 有趣,我会仔细研究一下归并排序算法。我喜欢您的优化想法,以检查两个数组的尾部和头部以排除没有重叠的范围。根据您对查看每个数组的头元素并在其较低时丢弃的描述,这与我目前正在做的事情不相似吗?
  • 是的,您的算法(在我回答后添加)是一样的。我被甩了,因为你最初提到它不是 O(N^2)。顺便说一句 O(2N) 没有多大意义。它在数学上等价于 O(N)。
  • 对不起,我提到对另一个数组中每个元素的线性搜索可能是 N^2,我不擅长大 O 概念,但我认为从头到尾循环两个数组会是2N,如果粗略估计。但我想这样的事情不存在?是的,有人在您发布后要求提供代码示例,所以现在一切都说得通了。感谢您确认您写的内容符合我的理解。
【解决方案2】:

存储数字的范围是多少?

我的意思是,你说数字是整数、排序的和稀疏的(即非连续的),并且可能超过 300,000 个,但它们的实际范围是多少?

我问的原因是,如果有一个相当小的上限,u,(比如说,u=500,000),最快和最方便的解决方案可能只是将这些值用作索引。是的,你可能在浪费内存,但是 4*u 真的是很多内存吗?这取决于您的应用程序和您的目标平台(即,如果这是针对内存受限的嵌入式系统,那么与拥有 32GiB RAM 的笔记本电脑相比,它不太可能是一个好主意)。

当然,如果值或多或少均匀分布在 0-2^31-1 上,这个粗略的想法就没有吸引力,但也许输入值的某些属性可以利用,而不是简单地范围。你也许可以手写一个相当简单的哈希函数。

另一件值得考虑的事情是您是否真的需要能够快速检索索引,或者它是否有助于快速判断索引是否存在于另一个数组中。一个值是否存在于特定索引只需要一个位,因此您可以使用 32 倍更少的内存来获得输入值范围的位图(即屏蔽 5 个 LSB 并将其用作位位置,然后移动剩余的27 位 5 位,并将其用作数组索引)。

最后,一种混合​​方法可能值得考虑,您决定准备使用多少内存(假设您决定 256KiB,对应于 64Ki 4 字节整数)然后将其用作查找表小得多的子问题。假设您有 300,000 个值,它们的 LSB 分布相当均匀。然后,您可以使用 16 个 LSB 作为索引到列表的查找表中,这些列表(平均)只有 4 或 5 个元素长,然后您可以通过其他方式进行搜索。几年前,我开发了一些模拟软件,它有大约 200,000,000 个细胞,每个细胞都有一个细胞 ID。一些实用程序功能使用二进制搜索来通过 id 识别单元格。通过这种策略,我们能够显着且非侵入地加速它。不是一个完美的解决方案,而是一个很大的改进。 (如果 LSB 分布不均匀,也许这是您可以利用的属性,或者您可以选择一定范围的位,或者进行一些散列。)

我猜结果是“考虑某种散列”,甚至是“身份散列”或简单的掩码/取模,旁边有一点“你的解决方案不必完全通用”和一些“你的解决方案”不必是完全节省空间的”酱汁。

【讨论】:

  • 感谢您的想法!我将无法强制执行数组中存在的范围或上限值。里面的大小和值是在运行时由用户交互决定的。我唯一能确定的是排序和唯一性。我可以将其中一个数组转换为非稀疏版本,几乎就像颠​​倒索引/值关系一样,但我仍然必须遍历整个数组才能转换它,但查找 yes 会更快。如果我再次重用数组,我可以看到效果更好,但我不是。我也会更多地研究散列。谢谢!
【解决方案3】:

我使用一种算法编写了这个函数的实现,该算法在稀疏分布上的表现优于普通的线性合并。

对于相似的分布,它具有 O(n) 复杂度,但分布差异很大的范围内,它的性能应该低于线性,在最佳情况下接近 O(log n)。但是,我无法证明最坏的情况并不比 O(n log n) 好。另一方面,我也找不到最坏的情况。

我对其进行了模板化,以便可以使用任何类型的范围,例如子范围或原始数组。从技术上讲,它也适用于非随机访问迭代器,但复杂性要大得多,因此不推荐。我认为在这种情况下应该可以修改算法以回退到线性搜索,但我没有打扰。

相似分布,我的意思是这对数组有很多交叉交叉,我的意思是如果你要按排序顺序将两个数组合并在一起,你会从一个数组切换到另一个数组。

#include <algorithm>
#include <iterator>
#include <utility>

// helper structure for the search
template<class Range, class Out>
struct search_data {
    // is any there clearer way to get iterator that might be either
    // a Range::const_iterator or const T*?
    using iterator = decltype(std::cbegin(std::declval<Range&>()));
    iterator curr;
    const iterator begin, end;
    Out out;
};

template<class Range, class Out>
auto init_search_data(const Range& range, Out out) {
    return search_data<Range, Out>{
        std::begin(range),
        std::begin(range),
        std::end(range),
        out,
    };
}

template<class Range, class Out1, class Out2>
void match_indices(const Range& in1, const Range& in2, Out1 out1, Out2 out2) {
    auto search_data1 = init_search_data(in1, out1);
    auto search_data2 = init_search_data(in2, out2);

    // initial order is arbitrary
    auto lesser = &search_data1;
    auto greater = &search_data2;

    // if either range is exhausted, we are finished
    while(lesser->curr != lesser->end
            && greater->curr != greater->end) {
        // difference of first values in each range
        auto delta = *greater->curr - *lesser->curr;

        if(!delta) { // matching value was found
            // store both results and increment the iterators
            *lesser->out++ = std::distance(lesser->begin, lesser->curr++);
            *greater->out++ = std::distance(greater->begin, greater->curr++);
            continue; // then start a new iteraton
        }

        if(delta < 0) { // set the order of ranges by their first value
            std::swap(lesser, greater);
            delta = -delta; // delta is always positive after this
        }

        // next crossing cannot be farther than the delta
        // this assumption has following pre-requisites: 
        // range is sorted, values are integers, values in the range are unique
        auto range_left = std::distance(lesser->curr, lesser->end);
        auto upper_limit =
            std::min(range_left, static_cast<decltype(range_left)>(delta));

        // exponential search for a sub range where the value at upper bound
        // is greater than target, and value at lower bound is lesser
        auto target = *greater->curr;
        auto lower = lesser->curr;
        auto upper = std::next(lower, upper_limit);
        for(int i = 1; i < upper_limit; i *= 2) {
            auto guess = std::next(lower, i);
            if(*guess >= target) {
                upper = guess;
                break;
            }
            lower = guess;
        }

        // skip all values in lesser,
        // that are less than the least value in greater
        lesser->curr = std::lower_bound(lower, upper, target);
    }
}

#include <iostream>
#include <vector>

int main() {
    std::vector<int> array1 = {4,6,12,34};
    std::vector<int> array2 = {1,3,6,34};

    std::vector<std::size_t> indices1;
    std::vector<std::size_t> indices2;

    match_indices(array1, array2,
                  std::back_inserter(indices1),
                  std::back_inserter(indices2));

    std::cout << "indices in array1: ";
    for(std::vector<int>::size_type i : indices1)
        std::cout << i << ' ';

    std::cout << "\nindices in array2: ";
    for(std::vector<int>::size_type i : indices2)
        std::cout << i << ' ';
    std::cout << std::endl;
}

【讨论】:

  • 感谢您的详细示例,我理解这将如何帮助跳过更多的数字而不是每个数字。这给了我一些新的想法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-08-25
  • 2021-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-23
  • 2016-12-02
相关资源
最近更新 更多