使用索引向量重新排序向量

时间:2009-05-08 05:35:51

标签: c++ stl vector

我想重新排序向量中的项目,使用另一个向量来指定顺序:

char   A[]     = { 'a', 'b', 'c' };
size_t ORDER[] = { 1, 0, 2 };

vector<char>   vA(A, A + sizeof(A) / sizeof(*A));
vector<size_t> vOrder(ORDER, ORDER + sizeof(ORDER) / sizeof(*ORDER));

reorder_naive(vA, vOrder);
// A is now { 'b', 'a', 'c' }

以下是一个低效的实现,需要复制vector:

void reorder_naive(vector<char>& vA, const vector<size_t>& vOrder)  
{   
    assert(vA.size() == vOrder.size());  
    vector vCopy = vA; // Can we avoid this?  
    for(int i = 0; i < vOrder.size(); ++i)  
        vA[i] = vCopy[ vOrder[i] ];  
}  

是否有更有效的方法,例如,使用swap()?

14 个答案:

答案 0 :(得分:26)

我改进了chmike的算法。这个功能与他的所有11个一致! (0..10)的排列作为重排序向量传递。它也不会修改重新排序的向量。

template< class T >
void reorder(vector<T> &v, vector<size_t> const &order )  {   
    for ( int s = 1, d; s < order.size(); ++ s ) {
        for ( d = order[s]; d < s; d = order[d] ) ;
        if ( d == s ) while ( d = order[d], d != s ) swap( v[s], v[d] );
    }
}

这是一个STL风格版本,我付出了更多努力。它快了大约47%(也就是说,几乎快了两倍(0..10)!)因为它尽可能早地完成所有交换,然后返回。重新排序向量由许多轨道组成,每个轨道在到达其第一个成员时被重新排序。当最后几个元素不包含轨道时,它会更快。

template< typename order_iterator, typename value_iterator >
void reorder( order_iterator order_begin, order_iterator order_end, value_iterator v )  {   
    typedef typename iterator_traits< value_iterator >::value_type value_t;
    typedef typename iterator_traits< order_iterator >::value_type index_t;
    typedef typename iterator_traits< order_iterator >::difference_type diff_t;

    diff_t remaining = order_end - 1 - order_begin;
    for ( index_t s = index_t(), d; remaining > 0; ++ s ) {
        for ( d = order_begin[s]; d > s; d = order_begin[d] ) ;
        if ( d == s ) {
            -- remaining;
            value_t temp = v[s];
            while ( d = order_begin[d], d != s ) {
                swap( temp, v[d] );
                -- remaining;
            }
            v[s] = temp;
        }
    }
}

最后,只是为了一劳永逸地回答这个问题,一个确实破坏了重新排序向量的变体。 (它用-1填充它。)它比前一版本快16%。这个使用了一个丑陋的类型转换,但处理它。这涵盖了11个!在我的2.2 GHz笔记本电脑上,在4.25秒内完成了11个字符的40密耳排列,不计算开销。

template< typename order_iterator, typename value_iterator >
void reorder_destructive( order_iterator order_begin, order_iterator order_end, value_iterator v )  {
    typedef typename iterator_traits< value_iterator >::value_type value_t;
    typedef typename iterator_traits< order_iterator >::value_type index_t;
    typedef typename iterator_traits< order_iterator >::difference_type diff_t;

    diff_t remaining = order_end - 1 - order_begin;
    for ( index_t s = index_t(); remaining > 0; ++ s ) {
        index_t d = order_begin[s];
        if ( d == (diff_t) -1 ) continue;
        -- remaining;
        value_t temp = v[s];
        for ( index_t d2; d != s; d = d2 ) {
            swap( temp, v[d] );
            swap( order_begin[d], d2 = (diff_t) -1 );
            -- remaining;
        }
        v[s] = temp;
    }
}

答案 1 :(得分:4)

这是正确的代码

void REORDER(vector<char>& vA, vector<size_t>& vOrder)  
{   
    assert(vA.size() == vOrder.size());

    // for all elements to put in place
    for( int i = 0; i < va.size() - 1; ++i )
    { 
        // while the element i is not yet in place 
        while( i != vOrder[i] )
        {
            // swap it with the element at its final place
            int alt = vOrder[i];
            swap( vA[i], vA[alt] );
            swap( vOrder[i], vOrder[alt] );
        }
    }
}

请注意,您可以保存一个测试,因为如果n-1个元素到位,则最后一个第n个元素肯定就位。

退出时,vA和vOrder已正确订购。

此算法最多执行n-1次交换,因为每次交换都会将元素移动到其最终位置。而且我们必须在vOrder上进行大多数2N测试。

答案 2 :(得分:3)

如果可以修改ORDER数组,那么对ORDER向量进行排序的实现以及在每个排序操作中也会交换相应的值,我认为向量元素可以解决问题。

答案 3 :(得分:3)

在我看来,vOrder包含一组所需顺序的索引(例如,按索引排序的输出)。这里的代码示例遵循vOrder中的“周期”,其中跟随索引的子集(可能是所有vOrder)将循环遍历子集,结束于子集的第一个索引。

关于“周期”的维基文章

https://en.wikipedia.org/wiki/Cyclic_permutation

在以下示例中,每个交换都会在其中放置至少一个元素。此代码示例根据vOrder有效地重新排序vA,同时“unordering”或“unpermuting”vOrder返回其原始状态(0 :: n-1)。如果vA按顺序包含值0到n-1,那么在重新排序之后,vA将在vOrder启动的地方结束。

template <class T>
void reorder(vector<T>& vA, vector<size_t>& vOrder)  
{   
    assert(vA.size() == vOrder.size());

    // for all elements to put in place
    for( size_t i = 0; i < vA.size(); ++i )
    { 
        // while vOrder[i] is not yet in place 
        // every swap places at least one element in it's proper place
        while(       vOrder[i] !=   vOrder[vOrder[i]] )
        {
            swap( vA[vOrder[i]], vA[vOrder[vOrder[i]]] );
            swap(    vOrder[i],     vOrder[vOrder[i]] );
        }
    }
}

使用移动代替交换也可以更有效地实现。在移动期间需要临时对象来保持元素。示例C代码,根据I []中的索引重新排序A [],也对I []:

进行排序
void reorder(int *A, int *I)
{    
int i, j, k;
int tA;
    /* reorder A according to I */
    /* every move puts an element into place */
    /* time complexity is O(n) */
    for(i = 0; i < sizeof(A)/sizeof(A[0]); i++){
        if(i != I[i]){
            tA = A[i];
            j = i;
            while(i != (k = I[j])){
                A[j] = A[k];
                I[j] = j;
                j = k;
            }
            A[j] = tA;
            I[j] = j;
        }
    }
}

答案 4 :(得分:1)

永远不要过早优化。 Meassure然后确定您需要优化的位置和内容。在性能不成问题的许多地方,您可以使用难以维护且容易出错的复杂代码。

话虽如此,不要早期悲观。在不更改代码的情况下,您可以删除一半副本:

    template <typename T>
    void reorder( std::vector<T> & data, std::vector<std::size_t> const & order )
    {
       std::vector<T> tmp;         // create an empty vector
       tmp.reserve( data.size() ); // ensure memory and avoid moves in the vector
       for ( std::size_t i = 0; i < order.size(); ++i ) {
          tmp.push_back( data[order[i]] );
       }
       data.swap( tmp );          // swap vector contents
    }

此代码创建并清空(足够大)向量,其中按顺序执行单个副本。最后,交换了有序和原始向量。这将减少副本,但仍需要额外的内存。

如果您想要就地执行移动,可以使用以下简单算法:

template <typename T>
void reorder( std::vector<T> & data, std::vector<std::size_t> const & order )
{
   for ( std::size_t i = 0; i < order.size(); ++i ) {
      std::size_t original = order[i];
      while ( i < original )  {
         original = order[original];
      }
      std::swap( data[i], data[original] );
   }
}

应检查并调试此代码。简而言之,每个步骤中的算法将元素定位在第i个位置。首先,我们确定该位置的原始元素现在放在数据向量中的何处。如果算法已经触摸了原始位置(它位于第i个位置之前),则原始元素被交换到命令[原始]位置。然后,该元素已经被移动......

该算法在整数运算次数中大致为O(N ^ 2),因此与初始O(N)算法相比,理论上在性能时间上更差。但它可以补偿N ^ 2交换操作(最坏情况)的成本是否低于N次复制操作,或者是否真的受到内存占用的限制。

答案 5 :(得分:1)

对现有答案的调查

您问是否有“更有效的方法”。但是,您所说的高效意味着什么?您有什么要求?

Potatoswatter的answer可以在 O(N²)时间内工作,并带有 O(1)额外的空间,并且不会突变重新排序向量。

chmikercgldr给出了使用 O(N)时间和 O(1)额外空间的答案,但是他们可以通过以下方式实现改变重新排序向量。

您的原始答案会分配新的空间,然后将数据复制到其中,而Tim MB建议使用移动语义。但是,移动仍然需要将事物移动到的位置,并且像std::string这样的对象既具有长度变量又具有指针。换句话说,基于移动的解决方案要求为任何对象分配O(N),并为新矢量本身分配O(1)。我在下面解释了为什么这很重要。

保留重新排序向量

我们可能想要该重新排序向量!排序成本 O(N log N)。但是,如果您知道将以相同的方式对多个向量进行排序,例如在Structure of Arrays (SoA)上下文中,则可以进行一次排序,然后重用结果。这样可以节省很多时间。

您可能还想对数据进行排序,然后再不对其进行排序。有了重新排序向量,您可以执行此操作。这里的用例是在GPU上执行基因组测序,其中通过批量处理相似长度的序列来获得最大的速度效率。我们不能依靠用户按此顺序提供序列,因此我们先进行排序然后再不进行排序。

因此,如果我们想要获得世界上最好的结果,该怎么办: O(N)处理而没有额外分配的 costs 但也没有改变我们的排序向量(毕竟可能要重用)?为了找到那个世界,我们需要问:

为什么多余的空间不好?

您可能不想分配额外空间的原因有两个。

首先是您没有足够的空间来使用。这可能在两种情况下发生:您在内存有限的嵌入式设备上。通常,这意味着您正在使用小型数据集,因此 O(N²)解决方案在这里可能很好。但是,当您使用真正大型数据集时,也会发生这种情况。在这种情况下, O(N²)是不可接受的,您必须使用 O(N)突变解决方案之一。

额外空间不足的另一个原因是分配昂贵。对于较小的数据集,其成本可能高于实际计算。因此,提高效率的一种方法是消除分配。

概述

当我们改变顺序向量时,我们这样做是为了指示元素是否在其排列的位置。与其执行此操作,不如使用位向量来指示相同的信息。但是,如果我们每次都分配位向量,那将很昂贵。

相反,我们可以通过将其重置为零来每次清除位向量。但是,每次使用函数会产生额外的 O(N)费用。

相反,我们可以在向量中存储“版本”值,并在每次使用函数时将其递增。这使我们可以 O(1)访问, O(1)清除,并分配了分配的成本。这与persistent data structure类似。缺点是,如果我们经常使用排序功能,则需要重置版本计数器,尽管这样做会分摊 O(N)的费用。

这引发了一个问题:版本向量的最佳数据类型是什么?位向量可以最大程度地提高缓存利用率,但是每次使用后都需要完全重置 O(N)。 64位数据类型可能永远不需要重置,但是缓存利用率很低。做实验是解决这个问题的最佳方法。

两种排列方式

我们可以将排序向量视为具有两种意义:向前和向后。在向前的意义上,向量告诉我们元素要去哪里。从向后的意义上讲,向量告诉我们元素从何而来。由于排序向量隐式是一个链表,因此后向含义需要O(N)额外的空间,但是同样,我们可以摊销分配成本。依次应用这两种感觉会使我们回到原始的顺序。

性能

在我的“ Intel Xeon(R)E-2176M CPU @ 2.70GHz”上运行单线程时,以下代码每次重新排序耗时约0.81毫秒,长度为32,767个元素。

代码

在测试中完全注释了两种感觉的代码:

#include <algorithm>
#include <cassert>
#include <random>
#include <stack>
#include <stdexcept>
#include <vector>

///@brief Reorder a vector by moving its elements to indices indicted by another 
///       vector. Takes O(N) time and O(N) space. Allocations are amoritzed.
///
///@param[in,out] values   Vector to be reordered
///@param[in]     ordering A permutation of the vector
///@param[in,out] visited  A black-box vector to be reused between calls and
///                        shared with with `backward_reorder()`
template<class ValueType, class OrderingType, class ProgressType>
void forward_reorder(
  std::vector<ValueType>          &values,
  const std::vector<OrderingType> &ordering,
  std::vector<ProgressType>       &visited
){
  if(ordering.size()!=values.size()){
    throw std::runtime_error("ordering and values must be the same size!");
  }

  //Size the visited vector appropriately. Since vectors don't shrink, this will
  //shortly become large enough to handle most of the inputs. The vector is 1
  //larger than necessary because the first element is special.
  if(visited.empty() || visited.size()-1<values.size());
    visited.resize(values.size()+1);

  //If the visitation indicator becomes too large, we reset everything. This is
  //O(N) expensive, but unlikely to occur in most use cases if an appropriate
  //data type is chosen for the visited vector. For instance, an unsigned 32-bit
  //integer provides ~4B uses before it needs to be reset. We subtract one below
  //to avoid having to think too much about off-by-one errors. Note that
  //choosing the biggest data type possible is not necessarily a good idea!
  //Smaller data types will have better cache utilization.
  if(visited.at(0)==std::numeric_limits<ProgressType>::max()-1)
    std::fill(visited.begin(), visited.end(), 0);

  //We increment the stored visited indicator and make a note of the result. Any
  //value in the visited vector less than `visited_indicator` has not been
  //visited.
  const auto visited_indicator = ++visited.at(0);

  //For doing an early exit if we get everything in place
  auto remaining = values.size();

  //For all elements that need to be placed
  for(size_t s=0;s<ordering.size() && remaining>0;s++){
    assert(visited[s+1]<=visited_indicator);

    //Ignore already-visited elements
    if(visited[s+1]==visited_indicator)
      continue;

    //Don't rearrange if we don't have to
    if(s==visited[s])
      continue;

    //Follow this cycle, putting elements in their places until we get back
    //around. Use move semantics for speed.
    auto temp = std::move(values[s]);
    auto i = s;
    for(;s!=(size_t)ordering[i];i=ordering[i],--remaining){
      std::swap(temp, values[ordering[i]]);
      visited[i+1] = visited_indicator;
    }
    std::swap(temp, values[s]);
    visited[i+1] = visited_indicator;
  }
}



///@brief Reorder a vector by moving its elements to indices indicted by another 
///       vector. Takes O(2N) time and O(2N) space. Allocations are amoritzed.
///
///@param[in,out] values   Vector to be reordered
///@param[in]     ordering A permutation of the vector
///@param[in,out] visited  A black-box vector to be reused between calls and
///                        shared with with `forward_reorder()`
template<class ValueType, class OrderingType, class ProgressType>
void backward_reorder(
  std::vector<ValueType>          &values,
  const std::vector<OrderingType> &ordering,
  std::vector<ProgressType>       &visited
){
  //The orderings form a linked list. We need O(N) memory to reverse a linked
  //list. We use `thread_local` so that the function is reentrant.
  thread_local std::stack<OrderingType> stack;

  if(ordering.size()!=values.size()){
    throw std::runtime_error("ordering and values must be the same size!");
  }

  //Size the visited vector appropriately. Since vectors don't shrink, this will
  //shortly become large enough to handle most of the inputs. The vector is 1
  //larger than necessary because the first element is special.
  if(visited.empty() || visited.size()-1<values.size());
    visited.resize(values.size()+1);

  //If the visitation indicator becomes too large, we reset everything. This is
  //O(N) expensive, but unlikely to occur in most use cases if an appropriate
  //data type is chosen for the visited vector. For instance, an unsigned 32-bit
  //integer provides ~4B uses before it needs to be reset. We subtract one below
  //to avoid having to think too much about off-by-one errors. Note that
  //choosing the biggest data type possible is not necessarily a good idea!
  //Smaller data types will have better cache utilization.
  if(visited.at(0)==std::numeric_limits<ProgressType>::max()-1)
    std::fill(visited.begin(), visited.end(), 0);

  //We increment the stored visited indicator and make a note of the result. Any
  //value in the visited vector less than `visited_indicator` has not been
  //visited.  
  const auto visited_indicator = ++visited.at(0);

  //For doing an early exit if we get everything in place
  auto remaining = values.size();

  //For all elements that need to be placed
  for(size_t s=0;s<ordering.size() && remaining>0;s++){
    assert(visited[s+1]<=visited_indicator);

    //Ignore already-visited elements
    if(visited[s+1]==visited_indicator)
      continue;

    //Don't rearrange if we don't have to
    if(s==visited[s])
      continue;

    //The orderings form a linked list. We need to follow that list to its end
    //in order to reverse it.
    stack.emplace(s);
    for(auto i=s;s!=(size_t)ordering[i];i=ordering[i]){
      stack.emplace(ordering[i]);
    }

    //Now we follow the linked list in reverse to its beginning, putting
    //elements in their places. Use move semantics for speed.
    auto temp = std::move(values[s]);
    while(!stack.empty()){
      std::swap(temp, values[stack.top()]);
      visited[stack.top()+1] = visited_indicator;
      stack.pop();
      --remaining;
    }
    visited[s+1] = visited_indicator;
  }
}



int main(){
  std::mt19937 gen;
  std::uniform_int_distribution<short> value_dist(0,std::numeric_limits<short>::max());
  std::uniform_int_distribution<short> len_dist  (0,std::numeric_limits<short>::max());
  std::vector<short> data;
  std::vector<short> ordering;
  std::vector<short> original;

  std::vector<size_t> progress;

  for(int i=0;i<1000;i++){
    const int len = len_dist(gen);
    data.clear();
    ordering.clear();
    for(int i=0;i<len;i++){
      data.push_back(value_dist(gen));
      ordering.push_back(i);
    }

    original = data;

    std::shuffle(ordering.begin(), ordering.end(), gen);

    forward_reorder(data, ordering, progress);

    assert(original!=data);

    backward_reorder(data, ordering, progress);

    assert(original==data);
  }  
}

答案 6 :(得分:0)

你可以递归地做到这一点,我想 - 这样的事情(未经检查,但它提出了这个想法):

// Recursive function
template<typename T>
void REORDER(int oldPosition, vector<T>& vA, 
             const vector<int>& vecNewOrder, vector<bool>& vecVisited)
{
    // Keep a record of the value currently in that position,
    // as well as the position we're moving it to.
    // But don't move it yet, or we'll overwrite whatever's at the next
    // position. Instead, we first move what's at the next position.
    // To guard against loops, we look at vecVisited, and set it to true
    // once we've visited a position.
    T oldVal = vA[oldPosition];
    int newPos = vecNewOrder[oldPosition];
    if (vecVisited[oldPosition])
    {
        // We've hit a loop. Set it and return.
        vA[newPosition] = oldVal;
        return;
    }
    // Guard against loops:
    vecVisited[oldPosition] = true;

    // Recursively re-order the next item in the sequence.
    REORDER(newPos, vA, vecNewOrder, vecVisited);

    // And, after we've set this new value, 
    vA[newPosition] = oldVal;
}

// The "main" function
template<typename T>
void REORDER(vector<T>& vA, const vector<int>& newOrder)
{
    // Initialise vecVisited with false values
    vector<bool> vecVisited(vA.size(), false);

    for (int x = 0; x < vA.size(); x++)
    {
        REORDER(x, vA, newOrder, vecVisited);
    }
}

当然,你确实有vecVisited的开销。关于这种方法的想法,有谁?

答案 7 :(得分:0)

您的代码已损坏。您无法分配到vA,您需要使用模板参数。

vector<char> REORDER(const vector<char>& vA, const vector<size_t>& vOrder)  
{   
    assert(vA.size() == vOrder.size());  
    vector<char> vCopy(vA.size()); 
    for(int i = 0; i < vOrder.size(); ++i)  
        vCopy[i] = vA[ vOrder[i] ];  
    return vA;
} 

以上效率稍高。

答案 8 :(得分:0)

迭代向量是O(n)操作。它有点难以击败。

答案 9 :(得分:0)

标题和问题不清楚是否应按照订购vOrder所采用的相同步骤进行排序,或者vOrder是否已包含所需订单的索引。 第一种解释已经有了令人满意的答案(见chmike和Potatoswatter),我对后者加了一些想法。 如果对象T的创建和/或复制成本是相关的

template <typename T>
void reorder( std::vector<T> & data, std::vector<std::size_t> & order )
{
 std::size_t i,j,k;
  for(i = 0; i < order.size() - 1; ++i) {
    j = order[i];
    if(j != i) {
      for(k = i + 1; order[k] != i; ++k);
      std::swap(order[i],order[k]);
      std::swap(data[i],data[j]);
    }
  }
}

如果您的对象的创建成本很小并且内存不是问题(请参阅dribeas):

template <typename T>
void reorder( std::vector<T> & data, std::vector<std::size_t> const & order )
{
 std::vector<T> tmp;         // create an empty vector
 tmp.reserve( data.size() ); // ensure memory and avoid moves in the vector
 for ( std::size_t i = 0; i < order.size(); ++i ) {
  tmp.push_back( data[order[i]] );
 }
 data.swap( tmp );          // swap vector contents
}

请注意,运动答案中的两段代码会做不同的事情。

答案 10 :(得分:0)

我试图使用@ Potatoswatter的解决方案对第三个向量进行排序,并且在Armadillo的UITextView输出的索引向量上使用上述函数的输出真的很困惑。要从矢量输出从sort_index(下面的sort_index矢量)切换到可以与@ Potatoswatter解决方案(下面的arma_inds一起使用)的矢量输出,您可以执行以下操作:

new_inds

答案 11 :(得分:0)

使用O(1)空间要求进行重新排序是一项有趣的智力练习,但在99.9%的情况下,更简单的答案将满足您的需求:

void permute(vector<T>& values, const vector<size_t>& indices)  
{   
    vector<T> out;
    out.reserve(indices.size());
    for(size_t index: indices)
    {
        assert(0 <= index && index < values.size());
        out.push_back(values[index]);
    }
    values = std::move(out);
}

除了内存要求之外,我认为这种方式较慢的唯一方法是由于out的内存位于与valuesindices不同的缓存页面中。

答案 12 :(得分:0)

我想出了具有O(max_val - min_val + 1)空间复杂度的解决方案,但是它可以与std::sort集成,并受益于std::sort的{ {1}}体面的时间复杂度

O(n log n)

编写此代码时做出的以下假设:

  • 向量值从零开始。
  • 向量不包含重复的值。
  • 为了使用std::vector<int32_t> dense_vec = {1, 2, 3}; std::vector<int32_t> order = {1, 0, 2}; int32_t max_val = *std::max_element(dense_vec.begin(), dense_vec.end()); std::vector<int32_t> sparse_vec(max_val + 1); int32_t i = 0; for(int32_t j: dense_vec) { sparse_vec[j] = order[i]; i++; } std::sort(dense_vec.begin(), dense_vec.end(), [&sparse_vec](int32_t i1, int32_t i2) {return sparse_vec[i1] < sparse_vec[i2];});
  • ,我们有足够的记忆力可以牺牲

答案 13 :(得分:-1)

这应避免复制矢量:

void REORDER(vector<char>& vA, const vector<size_t>& vOrder)  
{   
    assert(vA.size() == vOrder.size()); 
    for(int i = 0; i < vOrder.size(); ++i)
        if (i < vOrder[i])
            swap(vA[i], vA[vOrder[i]]);
}