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

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