【问题标题】:Find index of Nth occurrence of a number using Binary Search使用二分搜索查找数字第 N 次出现的索引
【发布时间】:2018-06-20 06:36:55
【问题描述】:

我有一个有限数组,它的元素只有 -1,0 或 1。我想找到第 N 次出现的数字(比如 0)的索引。

我可以遍历整个数组,但我正在寻找一种更快的方法。我可以考虑使用二分搜索,但在对算法建模时遇到了麻烦。在这种情况下如何进行二分搜索?

【问题讨论】:

  • 如果你有一个数组,你就不能进行二分查找。如果将数组变成二叉树,当然可以进行搜索,但树的顺序不一定与数组的顺序匹配。
  • 我们是否假设数组已排序?
  • 数组未排序
  • 那么二分查找对你没有好处。二分查找假定数组已排序。
  • 您能否对数据进行排序,还是必须保持其有序结构?

标签: c++ algorithm binary-search


【解决方案1】:

如果不经过至少一次 O(N) 预处理,您将无法做到这一点。仅从信息论的角度来看,您必须了解元素 [0:k-1] 才能知道元素 [k] 是否是您想要的。

如果您要多次进行此搜索,则可以对数组进行简单的线性传递,同时计算每个元素。将索引存储在二维数组中,这样您就可以直接索引您想要的任何事件。

例如,给定 [-1 0 1 1 -1 -1 0 0 0 -1 1],您可以将其转换为 3xN 数组,idx

[[0 4 5 9]]
[[1 6 7 8]]
[[2 3 10]]

第 N 次出现的元素 I 是 idx[I+1][N-1]

在最初的 O(N) 次通过之后,您的查找是 O(1) 时间,使用 O(N) 空间。

【讨论】:

    【解决方案2】:

    由于您正在寻找通过arrayvector 或一些container 进行搜索,其中所讨论的搜索与某个元素T 的索引位置有关,基于其Nth 出现在它的容器中,这篇文章可能对你有所帮助:


    根据您的问题以及一些与此相关的 cmets,您在考虑使用 binary search 并且在建模算法的过程中遇到问题时明确指出您的容器是 Unsorted

    • 此处的这篇博文作为算法设计开发过程的示例,它可以帮助您实现所需的目标:

    • 这里的搜索算法是线性的,二分搜索不适合你当前的需求:

    • 构建算法的相同过程可以应用于其他类型的算法,包括二进制搜索、哈希表等。


    - 第一次构建
    struct Index {
        static unsigned counter; // Static Counter
    
        unsigned location; // index location of Nth element
        unsigned count; // How many of this element up to this point
    
        Index() : location( 0 ), count( 0 ) {}
    };    
    unsigned Index::counter = 0;
    
    // These typedefs are not necessarily needed; 
    // just used to make reading of code easier.
    typedef Index IndexZero; 
    typedef Index IndexPos1;
    typedef Index IndexNeg1;
    
    template<class T>
    class RepititionSearch {
    public:
        // Some Constants to compare against: don't like "magic numbers"
        const T NEG { -1 }; 
        const T ZERO { 0 };
        const T POS { 1 };
    
    private:
        std::vector<T> data_;   // The actual array or vector of data to be searched
        std::vector<Index> indices_; // A vector of Indexes - record keeping to prevent multiple full searches.
    
    public:
        // Instantiating a search object requires an already populated container
        explicit RepititionSearch ( const std::vector<T>& data ) : data_( data )  {
            // make sure indices_ is empty upon construction.
            indices_.clear();
        }
    
        // method to find the Nth occurrence of object A
        unsigned getNthOccurrence( unsigned NthOccurrence, T element ) {
            // Simple bounds checking
            if ( NthOccurrence < 0 || NthOccurrence >= data.size() ) {
                // Can throw error or print message...;
                return -1;
            }           
    
            IndexZero zeros;
            IndexPos1 ones;
            IndexNeg1 negOnes;
    
            // Clear out the indices_ so that each consecutive call is correct
            indices_.clear();
            unsigned idx = 0;
            for ( auto e : data_ ) {
                if (  element == e && element == NEG ) {
                    ++negOnes.counter;
                    negOnes.location = idx;
                    negOnes.count = negOnes.counter;
                    indices_.push_back( negOnes );
                }
    
                if ( element == e && element == ZERO ) {
                    ++zeros.counter;
                    zeros.location = idx;
                    zeros.count = zeros.counter;
                    indices_.push_back( zeros );
                }
    
                if ( element == e && element == POS ) {
                    ++ones.counter;
                    ones.location = idx;
                    ones.count = ones.counter;
                    indices_.push_back( ones );
                }
                idx++;
            } // for each T in data_
    
            // Reset static counters
            negOnes.counter = 0;
            zeros.counter = 0;
            ones.counter = 0;
    
            // Now that we saved a record: find the nth occurance
            // This will not search the full vector unless it is last element
            // This has early termination. Also this vector should only be
            // a percentage of the original data vector's size in elements.
            for ( auto index : indices_ ) {
                if ( index.count == NthOccurrence) {
                    // We found a match
                    return index.location;
                } 
            }
    
            // Not Found
            return -1;
        }
    };    
    

    int main() {
    
        // using the sample array or vector from User: Prune's answer!
        std::vector<char> vec{ -1, 0, 1, 1, -1, -1, 0, 0, 0, -1, 1 };
    
    
        RepititionSearch <char> search( vec );
        unsigned idx = search.getNthOccurrence( 3, 1 );
    
        std::cout << idx << std::endl;
    
        std::cout << "\nPress any key and enter to quit." << std::endl;
        char q;
        std::cin >> q;
        return 0;
    }
    

    // output:
    10
    

    值 10 是正确答案,因为值 1 的第 3rd 次出现在原始向量中的位置 10,因为向量是基于 0 的。索引向量仅用作book keeping 以加快搜索速度。

    如果您注意到我什至将此作为类模板来接受任何基本类型T,只要T 具有可比性,或者为它定义了运算符,就可以将其存储在std::vector&lt;T&gt; 中。

    AFAIK 对于您正在努力争取的搜索类型,我认为没有任何其他搜索方法比这更快,但不要引用我的话。不过我想我可以再优化一下这段代码……只是需要一些时间仔细看看。


    这可能看起来有点疯狂,但这确实有效:只是玩代码有点乐趣

    int main() {
    
        std::cout << 
        RepititionSearch<char>( std::vector<char>( { -1, 0, 1, 1, -1, -1, 0, 0, 0, -1, 1 } ) ).getNthOccurrence( 3, 1 ) 
                  << std::endl;  
    }
    

    它可以在一行上完成并打印到控制台,而无需创建类的实例。


    - 第二次构建

    现在这可能不一定会使算法更快,但这会清理代码以提高可读性。在这里,我删除了 typedef,只需在 3 个 if 语句中使用 Index 结构的单个版本,您就会看到 duplicate 代码,所以我决定为此创建一个私有辅助函数,这就是算法看起来多么简单为了清晰易读。


    struct Index {
        unsigned location;
        unsigned count;
        static unsigned counter;
    
        Index() : location(0), count(0) {}
    };
    unsigned Index::counter = 0;
    
    template<class T>
    class RepitiionSearch {
    public:
        const T NEG  { -1 };
        const T ZERO {  0 };
        const T POS  {  1 };
    
    private:
        std::vector<T> data_;
        std::vector<Index> indices_;
    
    public:
        explicit RepititionSearch( const std::vector<T>& data ) : data_( data ) 
            indices_.clear();            
        }
    
        unsigned getNthOccurrence( unsigned NthOccurrence, T element ) {
            if ( NthOccurrence < 0 || NthOccurrence >= data.size() ) {
                return -1;
            }
    
            indices_.clear(); 
    
            Index index;     
            unsigned i = 0;
    
            for ( auto e : data_ ) {
                if ( element == e && element == NEG ) {
                    addIndex( index, i );
                }
                if ( element == e && element == ZERO ) {
                    addIndex( index, i );
                }
                if ( element == e && element == POS ) {
                    addIndex( index, i );
                }
                i++;
            }
            index.counter = 0;
    
            for ( auto idx : indices_ ) {
                if ( idx.count == NthOccurrence ) {
                    return idx.location;
                }
            }
    
            return -1; 
        }
    
    private:
        void addIndex( Index& index, unsigned inc ) {
            ++index.counter;
            index.location = inc;
            index.count = index.counter;
            indices_.push_back( index );
        }
    };
    


    - 第三次构建

    为了使这完全通用以查找任何元素 T 的任何 Nth occurrence,上述可以简化并简化为:我还从 Index 中删除了静态计数器并将其移至RepititionSearch,把它放在那里更有意义。

    struct Index {
        unsigned location;
        unsigned count;
        Index() : location(0), count(0) {}
    };
    
    template<class T>
    class RepititionSearch {    
    private:
        static unsigned counter_;
        std::vector<T> data_;
        std::vector<Index> indices_;
    
    public:
        explicit RepititionSearch( const std::vector<T>& data ) : data_( data ) {
            indices_.clear();
        }
    
        unsigned getNthOccurrence( unsigned NthOccurrence, T element ) {
            if ( NthOccurrence < 0 || NthOccurrence >= data_.size() ) {
                return -1;
            }
            indices_.clear();
    
            Index index;
            unsigned i = 0;
    
            for ( auto e : data_ ) {
                if ( element == e ) {
                    addIndex( index, i );
                }
                i++;
            }
            counter_ = 0;
    
            for ( auto idx : indices_ ) {
                if ( idx.count == NthOccurrence ) {
                    return idx.location;
                }
            }
            return -1;
        }
    
    private:
        void addIndex( Index& index, unsigned inc ) {
            ++counter_;
            index.location = inc;
            index.count = counter_;
            indices_.push_back( index );
        }    
    };
    
    template<class T>
    unsigned RepititionSearch<T>::counter_ = 0;
    


    - 第四次构建

    我在上面也做了同样的算法,不需要或依赖一个向量来保存索引信息。这个版本根本不需要Index 结构,也不需要辅助函数。它看起来像这样:

    template<class T>
    class RepititionSearch {
    private:
        static unsigned counter_;
        std::vector<T> data_;
    public:
        explicit RepititionSearch( const std::vector<T>& data ) : data_( data ) {}
    
        unsigned getNthOcc( unsigned N, T element ) {
            if ( N < 0 || N >= data_.size() ) {
                return -1;
            }
    
            unsigned i = 0;
            for ( auto e : data_ ) {
                if ( element == e ) {
                    ++counter_;
                    i++;
                } else {
                    i++;
                }
    
                if ( counter_ == N ) {
                    counter_ = 0;
                    return i-1;
                }
            }
    
            counter_ = 0;
    
            return -1;
        }
    };
    
    template<class T>
    unsigned RepititionSearch<T>::counter_ = 0;
    

    因为我们能够移除辅助向量的依赖关系并移除对辅助函数的需求;我们甚至根本不需要一个类来保存容器;我们可以只写一个函数模板,它接受一个向量并应用相同的算法。此版本也不需要静态计数器。


    - 第 5 次构建
    template<class T>
    unsigned RepititionSearch( const std::vector<T>& data, unsigned N, T element ) {    
    
        if ( data.empty() || N < 0 || N >= data.size() ) {
            return -1;
        }
    
        unsigned counter = 0;
        unsigned i = 0;
    
        for ( auto e : data ) {
            if ( element == e ) {
                ++counter;
                i++;
            } else {
                i++;
            }
    
            if ( counter == N ) {
                return i - 1;
            }
        }
        return -1;
    }
    

    是的,这需要考虑很多;但这些是编写和设计算法并将其细化为更简单代码的过程中涉及的步骤。正如你所看到的,我已经改进了这段代码大约 5 次。我从使用带有多个存储容器的 structclasstypedefsstatic member,到删除 typedefs 并将可重复代码放入辅助函数中,再到删除对辅助容器和辅助函数,甚至根本不需要一个类,只需创建一个函数来完成它应该做的事情。

    您可以对这些步骤应用类似的方法来构建执行您想要或需要它执行的功能的函数。您可以使用相同的过程编写一个执行二分查找、哈希表等的函数。

    【讨论】:

    • 为什么说二叉搜索需要二叉树? Std 库包含内置函数,用于在具有前向迭代器的任何已排序容器上进行二进制搜索:cplusplus.com/reference/algorithm/binary_search
    • @MarekWawrzos 我得回去看看我的帖子;我仍在编辑中。
    • @MarekWawrzos 感谢您的提醒;在写这个答案时,我在不同的时间有多种想法。我已经编辑了几十次。我没有回去阅读实际的句子,而不仅仅是“代码精炼”。竖起大拇指!如果您发现任何其他看起来不准确的内容,请随时给我提示或线索。
    • @MarekWawrzos 我现在记得binary search 声明;我之前从未完成过我的思路,因为我多次被从键盘上叫走……是的,向量有二进制搜索,但他们希望标准已经排序。在OP的情况下;他的容器没有分类,我相信他们需要维持当前的秩序......或者至少这是我从他们的问题和其他人的问题中假设或理解的。
    • @FrancisCugler:我预计这会因为相关想法太长而受到影响。经过最终编辑后,这将成为软件开发教程中的一篇可爱的博客文章。然而,为了使它成为一个好的 SO 答案,它应该更直接地解决这个问题:更多的是关于解决方案而不是关于过程。
    【解决方案3】:

    OP 声明有序结构很重要,vectorarrayunsorted。据我所知,对于未排序的数据,没有比线性更快的搜索算法了。以下是一些参考链接:

    以上链接供参考;这应该足以证明如果arrayvector中的数据是未排序的并且必须保持其结构,那么没有选择使用线性迭代,可能可以使用散列技术,但是这仍然很棘手,在大多数情况下,使用二分搜索仅适用于 sorted 数据。


    - 这是一个很好的线性算法,可以在 data 中找到 Nth 出现的 T

    要解决您在给定的unsorted arrayvectorvectorcontainer 中查找元素T 出现的Nth 问题,您可以使用这个简单的函数模板:

    • 它需要 3 个参数:
      • 对填充了数据的容器的 const 引用
      • 一个常量无符号值N,其中NNth 的出现。
      • 和您正在搜索的 const 模板类型 T
    • 它为容器内的索引位置返回一个无符号值 Nth 出现的元素 T

    template<class T>
    unsigned RepititionSearch( const std::vector<T>& data, const unsigned N, const T element ) {    
    
        if ( data.empty() || N < 0 || N >= data.size() ) {
            return -1;
        }
    
        unsigned counter = 0;
        unsigned i = 0;
    
        for ( auto e : data ) {
            if ( element == e ) {
                ++counter;
                i++;
            } else {
                i++;
            }
    
            if ( counter == N ) {
                return i - 1;
            }
        }
        return -1;
    }
    

    算法分解

    • 它首先进行一些健全性检查:
      • 检查容器是否为空
      • 它检查值N 看它是否在[0,container.size()) 的范围内
      • 如果其中任何一个失败,则返回-1;在生产代码中,这可能会抛出 异常或错误
    • 然后我们需要 2 个递增计数器:
      • 1 表示当前索引位置
      • 1 表示元素T 的出现次数
    • 然后我们使用 c++11 或更高版本的简化 for 循环
      • 我们遍历e中的每个data
      • 我们检查传入函数的element是否为equal to data 中的当前 e
      • 如果检查通过或为真,我们将 pre-increment counterpost-increment i 否则我们只想post-increment i
      • 增加计数器后,我们检查当前是否 counter 等于传递给函数的 Nth
      • 如果检查通过,我们将返回 i-1 的值,因为容器是基于 0
      • 如果此处检查失败,我们将继续循环的下一次迭代并重复该过程
    • 如果毕竟data 中的e 已经检查并且没有出现 T == eN != counter 然后我们离开 for 循环和函数 返回一个-1;在生产代码中,这可能会引发异常或返回错误。

    这里最坏的情况是要么没有发现,要么Nth 出现T 恰好是data 中的最后一个e,这将产生O(N),这是线性的,并且对于基本容器,这应该足够有效。如果容器具有array indexing 功能,并且您知道所需的索引位置,则它们的项目访问权限应该是O(1) 常量。


    注意:这将是我认为应该解决问题的答案,如果您对设计或建模此类算法的设计过程的分解感兴趣,可以参考我的reference answerhere

    AFAIK 我不认为有better 的方法可以用unsorted array data 做到这一点,但不要引用我的话。

    【讨论】:

    • 问题是寻求更好的方法。你只是重写了迭代。
    • @KennyOstrom 用于未排序的数组数据;我认为没有更好的方法。
    猜你喜欢
    • 1970-01-01
    • 2016-11-21
    • 1970-01-01
    • 2019-04-13
    • 1970-01-01
    • 2017-12-20
    • 2010-09-16
    • 1970-01-01
    相关资源
    最近更新 更多