《编程珠玑》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
#include
#include
#include
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