std::rotate的几种实现方法

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章