《編程珠璣》2.3節提出了向量旋轉問題,並給出幾種解決方案。c++標準庫中的
template <class ForwardIterator>
void rotate(ForwardIterator first, ForwardIterator middle, ForwardIterator last)
就解決了該問題,而且一般來說使用了其中效率最高的方法。接下來就分別看一下這幾個解決方案,並分別實現rotate函數。
問題:將一個n元向量x向左旋轉i個位置。
首先是最樸素的方法,將x的前i個元素複製到一個臨時數組中,然後將餘下的n-i個元素向左移動i個位置,最後將最初的i個元素從臨時數組中複製到x中餘下的位置。
template <class ForwardIt>
void rotate1(ForwardIt first, ForwardIt middle, ForwardIt last)
{
std::vector<typename std::iterator_traits<ForwardIt>::value_type> temp;
std::move(first, middle, std::back_inserter(temp));
std::move(temp.begin(), temp.end(), std::move(middle, last, first));
}
但是這個方法消耗了較多的存儲空間。
考慮將向量首尾相接,我們要做的相當於循環左移i位。詳細的步驟是:移動x[0]到臨時變量t,然後移動x[i]至x[0],x[2i]至x[i],依此類推(將x的所有下標對n取模),直至返回到x[0],此時從t取值。如果該過程沒有移動全部元素,比如6個元素的數組左移2位,就從x[1]再次移動,直到所有的元素都已經移動爲止。
由於std::rotate接受的是前向迭代器,該方法實現起來有些複雜:
template <class ForwardIt>
void rotate2(ForwardIt first, ForwardIt middle, ForwardIt last)
{
if(middle == last) return;
auto dis = std::distance(first, middle);
auto circle_advance = [first,last,dis](ForwardIt &it)
{
for(int i=0; i<dis; ++i)
if(++it == last) it = first;
};
ForwardIt write = first;
ForwardIt read = middle;
ForwardIt hole = write;
auto tmp = *hole;
auto counter = std::distance(first, last);
if(counter <= 0) return;
while(counter)
{
while(read != hole)
{
*write = *read;
write = read;
--counter;
circle_advance(read);
}
*write = tmp;
--counter;
write = ++read;
hole = write;
tmp = *hole;
circle_advance(read);
}
}
第三個方法比較巧妙了,將x分爲ab兩段,選擇x其實就是交換ab使之變爲ba。考慮兩種情況:
a比b短,將ab表示爲ab1b2,其中a與b2長度相同,最終我們需要的是b1b2a。先交換a與b2,向量變爲b2b1a,接下來只要交換b2b1即可。
a比b長,將ab表示爲a1a2b,其中a1與b長度相同,最終我們需要的是ba1a2。先交換a1與b,向量變爲ba2a1,接下來只要交換a2a1即可。
以上行爲可以用遞歸實現:
template <class ForwardIt>
void rotate3(ForwardIt first, ForwardIt middle, ForwardIt last)
{
if(first == middle) return;
if(middle == last) return;
ForwardIt oldmid = middle;
while(first != oldmid && middle != last)
{
std::iter_swap(first++, middle++);
}
rotate3(first, first == oldmid ? middle : oldmid, last);
}
很多實現就用這個方法實現的std::rotate,但可能沒有使用遞歸:
template <class ForwardIt>
void rotate4(ForwardIt first, ForwardIt middle, ForwardIt last)
{
ForwardIt next = middle;
while (first != next)
{
std::iter_swap (first++, next++);
if (next==last) next = middle;
else if (first==middle) middle = next;
}
}
這個方法足夠高效,但實現起來還是要小心,最後一個方法即簡單效率又不差:先對a求逆得到arb,然後對b求逆,得到arbr,最後對整體求逆,得到(arbr)r,此時恰好就是ba。
template <class ForwardIt>
void rotate5(ForwardIt first, ForwardIt middle, ForwardIt last)
{
std::reverse(first, middle);
std::reverse(middle, last);
std::reverse(first, last);
}
Ken Thompson主張把該方法當做一個常識,在1971年。。。
《編程珠璣》作者測試了後三種方法,後兩種方法明顯更優,求逆算法花費的時間很穩定,但比塊交換算法稍慢一些。
附上正確性測試代碼:
#include <iostream>
#include <vector>
#include <algorithm>
#include <vector>
template <class ForwardIt>
void myrotate(ForwardIt first, ForwardIt middle, ForwardIt last)
{
rotate2(first, middle, last);
}
int main()
{
std::vector<int> v {1,2,3,4,5,6,7,8,9};
myrotate(v.begin(), v.begin()+3, v.end());
std::cout << "a direct test : ";
for (int n: v)
std::cout << n << ' ';
std::cout << '\n';
v = {2, 4, 2, 0, 5, 10, 7, 3, 7, 1};
std::cout << "before sort : ";
for (int n: v)
std::cout << n << ' ';
std::cout << '\n';
// insertion sort
for (auto i = v.begin(); i != v.end(); ++i)
{
myrotate(std::upper_bound(v.begin(), i, *i), i, i+1);
}
std::cout << "after sort : ";
for (int n: v)
std::cout << n << ' ';
std::cout << '\n';
// simple rotation to the left
myrotate(v.begin(), v.begin() + 1, v.end());
std::cout << "simple rotate left : ";
for (int n: v)
std::cout << n << ' ';
std::cout << '\n';
// simple rotation to the right
myrotate(v.rbegin(), v.rbegin() + 1, v.rend());
std::cout << "simple rotate right : ";
for (int n: v)
std::cout << n << ' ';
std::cout << '\n';
return 0;
}
參考:
編程珠璣
cppreference.com
cplusplus.com