在一个排序的列表中正好找到N个连续项

Finding exactly N consecutives in a sorted list

本文关键字:连续 列表 一个 排序      更新时间:2023-10-16

我最近偶然发现了一个小问题。

我正在研究的算法的一部分需要在一个排序的数字列表中找到n连续的数字。

因此,例如,列表看起来像这样:

1 2 2 3 4 5 5 5 6 7 8 9 9 9 9

给定该列表和N,即连续重复的数量,算法需要在恰好为N个连续数字的最小组中找到第一个数字。例如,对于N=2和给定的列表,算法应该找到"2"。当N=3时,它应该通过2的组,而不是5的组,因为它是该列表中最小的3个连续重复的组。它不应该返回9,因为实际上有4个连续的9,并且在N=3的情况下,我们正在寻找恰好连续3个的最小组。

最后,我确实拼凑出了一些做这项工作的代码,但我想知道一些有经验的程序员会怎么做。利用Stroustroup自己宣传的C++11风格的代码,并使用C++11 STL来实现推理的正确性、可移植性和紧凑性。

如果速度无关紧要:

template <class T >
T firstOfN( std::vector<T> list, unsigned N ){
  std::multiset<T> mset( list.begin(), list.end() );
  for( typename std::multiset<T>::iterator it = mset.begin(); it != mset.end(); ++it ){
    if( mset.count( *it ) == N ) return *it;
  }
  throw std::exception();
}

在算法方面,有一个有趣的优化;伪代码:

size_t N;
RaIterator cur = myvector.begin(), end = myvector.end();
while(cur < end-(N-1))
{
    if(*cur == *(cur+N))
    {
        if(cur+N == end || *cur != *(cur+N+1))
            return {cur, cur+N};
        else
            cur = upper_bound(cur+N+1, end, *cur);
    }else
    {
        cur = lower_bound(cur, cur+N, *(cur+N));
    }
}
return {end, end};

如果我们有随机访问迭代器,一旦我们有了初始元素(前面的元素较小,后面的元素较大或相等),我们就可以很快跳过范围:

  • 如果是*cur == *(cur+N),则具有值*cur的范围足够大。如果是*cur != *(cur+N+1)cur+N == end,那么它确实是我们要寻找的范围。否则,它太大了,我们可以搜索下一个范围(线性搜索或[cur+N+1, end)中的二进制搜索)。

  • 否则,*cur != *(cur+N),则电流范围太小。完全在[cur, cur+N]内部的每个范围也太小,所以下一个要检查的范围是从[cur, cur+N]内部开始并延伸到cur+N之外的范围。这个范围的值为*(cur+N),所以我们只需要找到它的初始元素(二进制搜索)。

注意:与线性搜索(常数因子)相比,二进制搜索的"复杂性"增加了,并且由于相当不可预测的内存访问,对于小范围列表,这可能比严格的线性方法慢。

这里很大程度上取决于插入和删除的频率与搜索、您正在查看的列表的大小等。

目前,我要做两个假设:

  1. 你要处理的列表足够大,渐近更好的算法可能会战胜明显的线性搜索
  2. 您正在使用基本上是静态的数据进行大量查询

如果这是真的,那么首先对输入数据进行游程编码,这样就可以得到值/计数对。

然后主要根据计数对这些对进行排序,其次根据值对它们进行排序。最后,使用std::lower_bound查找一个值,比较仅基于计数。

这需要O(N log N)进行预处理。作为交换,每个查询都需要O(log N)而不是O(N)。因此,您需要对经过预处理的数据进行O(N)查询,以证明预处理的合理性。

当N较大时,对N个相同数字的检测可能会"优化"一点。

for (int i = 0; i < n - N + 1; ) {
    int ai = a[i]; // New value
    if (ai == a[i + N - 1]) { // Last element same
        if (i + N >= n || ai != a[i + N]) { // Thereafter not
            return i;
        }
        i += N; // Move to last known same element (or past end)
    }
    // Go to next new value:
    ++i;
    while (i < n - N + 1 && a[i] == ai) {
        ++i;
    }
}

它依赖于在for循环的开头有一个新值。

这是我的解决方案。它不使用任何stl标准算法,但它具有尽可能好的复杂性-O(n),我相信它是可读和可理解的:

  unsigned cur_value_index = 0;
  unsigned range_size = 1;
  for (unsigned i = 1; i < a.size(); ++i) {
    if (a[i] == a[cur_value_index]) {
      range_size++;
    } else {
      if (range_size == N) {
        cout << cur_value_index << endl;
        break;
      }
      cur_value_index = i;
      range_size = 1;
    }
  }

if(range_size==N){cout<lt;cur_value_index<lt;endl;}

我假设序列是在数组a中提供的,而N是您在问题中谈到的极限。

我用矢量来举例说明,但如果我们没有随机访问,例如列表,同样的算法也可以应用。在这种情况下,我们将保留序列元素的迭代器,而不是索引,但其余部分将保持不变。

#include <algorithm>
#include <array>
#include <iostream>
using namespace std;
template<class T>
class Sequence
{
public:
    Sequence(const uint32_t num_items);
    ~Sequence(){}
    bool operator()(const T data);
private:
    T m_value;
    uint32_t m_counter;
    uint32_t m_max;
};
template<class T>
Sequence<T>::Sequence(const uint32_t num_items)
  : m_value(0),
    m_counter(0),
    m_max(num_items)
{
}
template<class T>
bool Sequence<T>::operator()(const T data)
{
    if(m_value == data) {
        m_counter++;
    } else if(m_counter == m_max{
        m_value = data;
        m_counter = 0;
        return true;
    } else{
        m_value = data;
        m_counter = 0;
    }
    return false;
}
int main()
{
    int data[] = {1,2,2,3,4,5,5,5,6,7,8,9,9,9,9};
    array<int,15> ar;
    for(uint32_t i = 0; i < 15; i++)
        ar[i] = data[i];
    //find three consecutive numbers
    Sequence<int> seq(3);
    //getting the first occurence of the sequence
    array<int,15>::iterator it = find_if(ar.begin(),ar.end(),seq);
    //printing the iterator position from begin
    cout << distance(ar.begin(),it) << endl;
    return 0;
}