1. 程式人生 > >STL sort快排

STL sort快排

C++之所以得到這麼多人的喜歡,是因為它既具有面向物件的概念,又保持了C語言高效的特點。STL 排序演算法同樣需要保持高效。因此,對於不同的需求,STL提供的不同的函式,不同的函式,實現的演算法又不盡相同。

1.1 所有sort演算法介紹

所有的sort演算法的引數都需要輸入一個範圍,[begin, end)。這裡使用的迭代器(iterator)都需是隨機迭代器(RadomAccessIterator), 也就是說可以隨機訪問的迭代器,如:it+n什麼的。(partition 和stable_partition 除外)

如果你需要自己定義比較函式,你可以把你定義好的仿函式(functor)作為引數傳入。每種演算法都支援傳入比較函式。以下是所有STL sort演算法函式的名字列表:

函式名 功能描述
sort 對給定區間所有元素進行排序
stable_sort 對給定區間所有元素進行穩定排序
partial_sort 對給定區間所有元素部分排序
partial_sort_copy 對給定區間複製並排序
nth_element 找出給定區間的某個位置對應的元素
is_sorted 判斷一個區間是否已經排好序
partition 使得符合某個條件的元素放在前面
stable_partition 相對穩定的使得符合某個條件的元素放在前面
其中nth_element 是最不易理解的,實際上,這個函式是用來找出第幾個。例如:找出包含7個元素的陣列中排在中間那個數的值,此時,我可能不關心前面,也不關心後面,我只關心排在第四位的元素值是多少。

1.2 sort 中的比較函式

當你需要按照某種特定方式進行排序時,你需要給sort指定比較函式,否則程式會自動提供給你一個比較函式。
vector < int
> vect; //... sort(vect.begin(), vect.end()); //此時相當於呼叫 sort(vect.begin(), vect.end(), less<int>() );
上述例子中系統自己為sort提供了less仿函式。在STL中還提供了其他仿函式,以下是仿函式列表:
名稱 功能描述
equal_to 相等
not_equal_to 不相等
less 小於
greater 大於
less_equal 小於等於
greater_equal 大於等於
需要注意的是,這些函式不是都能適用於你的sort演算法,如何選擇,決定於你的應用。另外,不能直接寫入仿函式的名字,而是要寫其過載的()函式:
less<int>()
greater<int>()
當你的容器中元素時一些標準型別(int float char)或者string時,你可以直接使用這些函式模板。但如果你時自己定義的型別或者你需要按照其他方式排序,你可以有兩種方法來達到效果:一種是自己寫比較函式。另一種是過載型別的'<'操作賦。
#include <QtCore/QCoreApplication>
#include <iostream>
#include <algorithm>
#include <vector>
#include <functional>
using namespace std;

class myclass
{
public:
    myclass(int a, int b):first(a),second(b){}
    int first;
    int second;
    bool operator < (const myclass &m)const
    {
        return first < m.first;
    }
};
bool less_second(const myclass &m1, const myclass &m2)
{
    return m1.second < m2.second;
}
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    vector< myclass > vect;
    for(int i = 0; i < 10; i++)
    {
        myclass my(10 - i, i * 3);
        vect.push_back(my);
    }

    cout<<"原始資料:\n";
    for(int i = 0; i < vect.size(); i++)
    {
        cout<<"("<<vect[i].first<<","<<vect[i].second<<")\n";
    }
    sort(vect.begin(), vect.end());

    cout<<"第一次排序後(sort預設從小到大):\n"<<endl;

  for(int i = 0 ; i < vect.size(); i ++)
      {
    cout<<"("<<vect[i].first<<","<<vect[i].second<<")\n";
    }

    cout<<"第二次排序後(要想從到小排序就要過載):\n"<<endl;
    sort(vect.begin(), vect.end(), less_second);
  for(int i = 0 ; i < vect.size(); i ++)
    {
    cout<<"("<<vect[i].first<<","<<vect[i].second<<")\n";
}
  cout<<"end"<<endl;
    return a.exec();
}
執行結果:

Starting /home/jz/test/test...

原始資料:

(10,0)

(9,3)

(8,6)

(7,9)

(6,12)

(5,15)

(4,18)

(3,21)

(2,24)

(1,27)

第一次排序後(sort預設從小到大):

(1,27)

(2,24)

(3,21)

(4,18)

(5,15)

(6,12)

(7,9)

(8,6)

(9,3)

(10,0)

第二次排序後(要想從到小排序就要過載):

(10,0)

(9,3)

(8,6)

(7,9)

(6,12)

(5,15)

(4,18)

(3,21)

(2,24)

(1,27)

end

1.3 sort 的穩定性
你發現有sort和stable_sort,還有 partition 和stable_partition, 感到奇怪吧。其中的區別是,帶有stable的函式可保證相等元素的原本相對次序在排序後保持不變。或許你會問,既然相等,你還管他相對位置呢,也分不清楚誰是誰了?這裡需要弄清楚一個問題,這裡的相等,是指你提供的函式表示兩個元素相等,並不一定是一摸一樣的元素。

例如,如果你寫一個比較函式:

bool less_len(const string &str1, const string &str2)
{
        return str1.length() < str2.length();
}
此時,"apple" 和 "winter" 就是相等的,如果在"apple" 出現在"winter"前面,用帶stable的函式排序後,他們的次序一定不變,如果你使用的是不帶"stable"的函式排序,那麼排序完後,"Winter"有可能在"apple"的前面。

1.4 全排序

全排序即把所給定範圍所有的元素按照大小關係順序排列。用於全排序的函式有
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);

template <class RandomAccessIterator, class StrictWeakOrdering>
void sort(RandomAccessIterator first, RandomAccessIterator last,
StrictWeakOrdering comp);

template <class RandomAccessIterator>
void stable_sort(RandomAccessIterator first, RandomAccessIterator last);

template <class RandomAccessIterator, class StrictWeakOrdering>
void stable_sort(RandomAccessIterator first, RandomAccessIterator last, 
StrictWeakOrdering comp);
在第1,3種形式中,sort 和 stable_sort都沒有指定比較函式,系統會預設使用operator< 對區間[first,last)內的所有元素進行排序, 因此,如果你使用的型別義軍已經過載了operator<函式,那麼你可以省心了。第2, 4種形式,你可以隨意指定比較函式,應用更為靈活一些。來看看實際應用:

班上有10個學生,我想知道他們的成績排名。

#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>
#include <string>
using namespace std;

class student{
        public:
          student(const string &a, int b):name(a), score(b){}
          string name;
        int score;
        bool operator < (const student &m)const {
                return score< m.score;
          }
};

int main() {
          vector< student> vect;
          student st1("Tom", 74);
          vect.push_back(st1);
          st1.name="Jimy";
          st1.score=56;
          vect.push_back(st1);
          st1.name="Mary";
          st1.score=92;
          vect.push_back(st1);
          st1.name="Jessy";
          st1.score=85;
          vect.push_back(st1);
          st1.name="Jone";
          st1.score=56;
          vect.push_back(st1);
          st1.name="Bush";
          st1.score=52;
          vect.push_back(st1);
          st1.name="Winter";
          st1.score=77;
          vect.push_back(st1);
          st1.name="Andyer";
          st1.score=63;
          vect.push_back(st1);
          st1.name="Lily";
          st1.score=76;
          vect.push_back(st1);
          st1.name="Maryia";
          st1.score=89;
          vect.push_back(st1);
          cout<<"------before sort..."<<endl;
        for(int i = 0 ; i < vect.size(); i ++) cout<<vect[i].name<<":\t"<<vect[i].score<<endl;
          stable_sort(vect.begin(), vect.end(),less<student>());
          cout <<"-----after sort ...."<<endl;
        for(int i = 0 ; i < vect.size(); i ++) cout<<vect[i].name<<":\t"<<vect[i].score<<endl;
        return 0 ;
}
sort採用的是成熟的"快速排序演算法"(目前大部分STL版本已經不是採用簡單的快速排序,而是結合內插排序演算法)。注1,可以保證很好的平均效能、複雜度為n*log(n),由於單純的快速排序在理論上有最差的情況,效能很低,其演算法複雜度為n*n,但目前大部分的STL版本都已經在這方面做了優化,因此你可以放心使用。stable_sort採用的是"歸併排序",分派足夠記憶體是,其演算法複雜度為n*log(n), 否則其複雜度為n*log(n)*log(n),其優點是會保持相等元素之間的相對位置在排序前後保持一致。

1.5 區域性排序

區域性排序其實是為了減少不必要的操作而提供的排序方式。其函式原型為:
template <class RandomAccessIterator>
void partial_sort(RandomAccessIterator first, 
RandomAccessIterator middle,
RandomAccessIterator last);

template <class RandomAccessIterator, class StrictWeakOrdering>
void partial_sort(RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last, 
StrictWeakOrdering comp);

template <class InputIterator, class RandomAccessIterator>
RandomAccessIterator partial_sort_copy(InputIterator first, InputIterator last,
RandomAccessIterator result_first,
RandomAccessIterator result_last);

template <class InputIterator, class RandomAccessIterator, 
class StrictWeakOrdering>
RandomAccessIterator partial_sort_copy(InputIterator first, InputIterator last,
RandomAccessIterator result_first,
RandomAccessIterator result_last, Compare comp);
理解了sort 和stable_sort後,再來理解partial_sort 就比較容易了。先看看其用途: 班上有10個學生,我想知道分數最低的5名是哪些人。如果沒有partial_sort,你就需要用sort把所有人排好序,然後再取前5個。現在你只需要對分數最低5名排序,把上面的程式做如下修改:
stable_sort(vect.begin(), vect.end(),less<student>());
替換為:
partial_sort(vect.begin(), vect.begin()+5, vect.end(),less<student>());
這樣的好處知道了嗎?當資料量小的時候可能看不出優勢,如果是100萬學生,我想找分數最少的5個人......

partial_sort採用的堆排序(heapsort),它在任何情況下的複雜度都是n*log(n). 如果你希望用partial_sort來實現全排序,你只要讓middle=last就可以了。

partial_sort_copy其實是copy和partial_sort的組合。被排序(被複制)的數量是[first, last)和[result_first, result_last)中區間較小的那個。如果[result_first, result_last)區間大於[first, last)區間,那麼partial_sort相當於copy和sort的組合。

1.6 nth_element 指定元素排序

nth_element一個容易看懂但解釋比較麻煩的排序。用例子說會更方便:
班上有10個學生,我想知道分數排在倒數第4名的學生。
如果要滿足上述需求,可以用sort排好序,然後取第4位(因為是由小到大排), 更聰明的朋友會用partial_sort, 只排前4位,然後得到第4位。其實這是你還是浪費,因為前兩位你根本沒有必要排序,此時,你就需要nth_element:
template <class RandomAccessIterator>
void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last);

template <class RandomAccessIterator, class StrictWeakOrdering>
void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last, StrictWeakOrdering comp);
對於上述例項需求,你只需要按下面要求修改1.4中的程式:
stable_sort(vect.begin(), vect.end(),less<student>());
替換為:
nth_element(vect.begin(), vect.begin()+3, vect.end(),less<student>());
第四個是誰?Andyer,這個倒黴的傢伙。為什麼是begin()+3而不是+4? 我開始寫這篇文章的時候也沒有在意,後來在ilovevc 的提醒下,發現了這個問題。begin()是第一個,begin()+1是第二個,... begin()+3當然就是第四個了。

1.7 partition 和stable_partition

好像這兩個函式並不是用來排序的,'分類'演算法,會更加貼切一些。partition就是把一個區間中的元素按照某個條件分成兩類。其函式原型為:
template <class ForwardIterator, class Predicate>
ForwardIterator partition(ForwardIterator first,
ForwardIterator last, Predicate pred)
template <class ForwardIterator, class Predicate>
ForwardIterator stable_partition(ForwardIterator first, ForwardIterator last, 
Predicate pred);
看看應用吧:班上10個學生,計算所有沒有及格(低於60分)的學生。你只需要按照下面格式替換1.4中的程式:
stable_sort(vect.begin(), vect.end(),less<student>());
替換為:
student exam("pass", 60);
stable_partition(vect.begin(), vect.end(), bind2nd(less<student>(), exam));
使用的是stable_partition, 元素之間的相對次序是沒有變.

2 Sort 和容器

STL中標準容器主要vector, list, deque, string, set, multiset, map, multimay, 其中set, multiset, map, multimap都是以樹結構的方式儲存其元素詳細內容請參看:學習STL map, STL set之資料結構基礎. 因此在這些容器中,元素一直是有序的。

這些容器的迭代器型別並不是隨機型迭代器,因此,上述的那些排序函式,對於這些容器是不可用的。上述sort函式對於下列容器是可用的:

  • vector
  • string
  • deque
如果你自己定義的容器也支援隨機型迭代器,那麼使用排序演算法是沒有任何問題的。

對於list容器,list自帶一個sort成員函式list::sort(). 它和演算法函式中的sort差不多,但是list::sort是基於指標的方式排序,也就是說,所有的資料移動和比較都是此用指標的方式實現,因此排序後的迭代器一直保持有效(vector中sort後的迭代器會失效).

3 選擇合適的排序函式

如果你以前有用過C語言中的qsort, 想知道qsort和他們的比較,那我告訴你,qsort和sort是一樣的,因為他們採用的都是快速排序。從效率上看,以下幾種sort演算法的是一個排序,效率由高到低(耗時由小變大):

  1. partion
  2. stable_partition
  3. nth_element
  4. partial_sort
  5. sort
  6. stable_sort