我想重新排序向量中的项目,使用另一个向量来指定顺序:
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()?
答案 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)额外的空间,并且不会突变重新排序向量。
chmike和rcgldr给出了使用 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
的内存位于与values
和indices
不同的缓存页面中。
答案 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]]);
}