《算法》第四版algs4:字符串排序算法C++實現

1.鍵索引計數法
適用於小整數鍵的簡單排序。
具有穩定性(穩定性:一個排序算法能夠保留數組中相同元素的相對位置,則它是穩定的)
突破了NlogN的排序算法時間下限:因爲它這裏不需要比較鍵,只需要訪問數據即可。

#include <vector>
#include <string>
#include <iostream>

struct info {
    info() {info(0, "");}
    info(int k, std::string v): key(k), val(v) {}
    int key;
    std::string val;
};

void printVector(const std::vector<info>& vec) {
    for (const auto &item : vec)
        std::cout << item.key << " " << item.val << std::endl;
    std::cout << std::endl;
}

int main() {
    std::vector<info> a{info(2, "Anderson"), info(3, "Brown"), info(3, "Davis"), info(4, "Garcia"), info(1, "Harris"), info(3, "Jackson")};
    std::cout << "before sorting: " << std::endl;
    printVector(a);

    int R = 5;
    int N = a.size();
    std::vector<info> aux(N);
    std::vector<int> count(R+1);
    for (int i = 0; i < N; ++i)
        count[a[i].key + 1]++;
    for (int r = 0; r < R; r++)
        count[r+1] += count[r];
    for (int i = 0; i < N; ++i)
        aux[count[a[i].key]++] = a[i];
    for (int i = 0; i < N; ++i)
        a[i] = aux[i];
    
    std::cout << "after sorting: " << std::endl;
    printVector(a);
}

2.低位優先的字符串排序LSD
適用於鍵的長度都相等的字符串排序應用。
基本思想:如果字符串的長度均爲W,那就從右向左以每個位置的字符作爲鍵,用鍵索引計數法將字符串排序W遍,且該排序方法是穩定的。
這種方法行得通的基礎是:鍵索引計數法是穩定的。
方法等價於進行W輪鍵索引計數法。

#include <iostream>
#include <vector>
#include <string>
#include <limits.h>

using namespace std;

void printVec(const vector<string> &a);
void LSDSort(vector<string> &a, int W);

int main() {
    vector<string> a;
    string s;
    while (cin >> s)
        a.push_back(s);
    cout << "before sorting:" << endl;
    printVec(a);

    LSDSort(a, a[0].size());

    cout << "after sorting:" << endl;
    printVec(a);
}

void printVec(const vector<string> &a) {
    for (const auto &item : a)
        cout << item << " ";
    cout << endl;
}

void LSDSort(vector<string> &a, int W) {
    int N = a.size();
    int R = CHAR_MAX;
    vector<string> aux(N);

    for (int d = W-1; d >= 0; d--) {
        vector<int> count(R+1);
        for (int i = 0; i < N; ++i)
            count[(int)a[i][d] + 1]++;
        for (int r = 0; r < R; ++r)
            count[r+1] += count[r];
        for (int i = 0; i < N; ++i)
            aux[count[(int)a[i][d]]++] = a[i];
        for (int i = 0; i < N; ++i)
            a[i] = aux[i];
    }
}

3.高位優先的字符串排序MSD
從左到右檢查鍵中的字符。
吸引人的地方在於它們不一定要檢查所有的輸入就能完成排序。
與快排類似,它們都會將需要排序的數組切分爲獨立的部分並遞歸的用相同的方法處理子數組來完成排序。區別之處在於MSD算法在切分時僅使用鍵的第一個字符,而快排的比較會涉及鍵的全部。

大致過程是將所有字符串按照首字母排序,然後遞歸地再將每個首字母所對應的子數組排序(忽略首字母)。
因爲每個字符串的長度不等,所以需要注意到達字符串末尾的情況。做法是把到達末尾的字符串放到它所在的子數組的最前面,使用的一個charAt()函數,當到達末尾時, 返回-1。

下面是使用MSD要注意的,總結起來兩點:1.小數組要切換到插入排序2.注意數據中如果相同前綴的鍵太多(甚至鍵全部相同),MSD效率很低
(需要注意,對於較大的字母表,MSD可能很危險,可能會消耗很多的時間空間。

小數組的處理:對於MSD尤其重要。假設將一個數百萬個不同的ASCII字符串(R=256)排序並且不對小數組做任何處理,那麼每個字符串最終都會產生一個只有他自己的子數組,因此需要將數百萬個大小爲1的子數組排序。更重要的是,每次都需要產生一個258個元素的count數組,這裏的代價是最高的。使用unicode時(R=65536),排序可能減慢上千倍。因此,將小數組切換到插入排序對於MSD來說是必須的。在長度小於等於10時將子數組切換到插入排序能夠將運行時間降低爲原來的十分之一。

MSD對於含有大量等值鍵的子數組排序會比較慢,因爲很難產生小數組然後切換到插入排序,而會檢查相同鍵中的每一個字符。MSD的最壞情況就是所有鍵都相同,大量含有相同前綴的鍵也會產生同樣問題。

因爲count數組不能在遞歸方法之外創建(不像aux數組),所以空間也是個問題。

MSD的性能主要取決於數據。對於隨機輸入,MSD只會檢查一部分的字符(足以區別字符串),運行時間是亞線性的;對於非隨機輸入,仍然可能亞線性,但是需要檢查的字符要更多;最壞情況,即所有鍵相同,MSD會檢查所有鍵中的所有字符。

所以MSD應用的主要挑戰在於處理數據中的非隨機因素。)

#include <iostream>
#include <vector>
#include <string>
#include <limits.h>

using namespace std;

vector<string> aux;
int R = CHAR_MAX;
int M = 15;

void printVec(const vector<string> &a);
void MSDSort(vector<string> &a, int lo, int hi, int d);
void insertionSort(vector<string> &a, int lo, int hi, int d);
bool smaller(string &v, string &w, int d);
int charAt(const string &s, int d);


int main() {
    vector<string> a;
    string s;
    while (cin >> s)
        a.push_back(s);
    cout << "before sorting:" << endl;
    printVec(a);

    aux = vector<string>(a.size());
    MSDSort(a, 0, a.size()-1, 0);

    cout << "after sorting:" << endl;
    printVec(a);
}

void printVec(const vector<string> &a) {
    for (const auto &item : a)
        cout << item << " ";
    cout << endl;
}

int charAt(const string &s, int d) {
    if (d < s.size())
        return (int)s[d];
    else
        return -1;
}

void MSDSort(vector<string> &a, int lo, int hi, int d) {
    if (hi <= lo + M) {
        insertionSort(a, lo, hi, d);
        return;
    }
    auto count = vector<int>(R+2, 0);
    for (int i = lo; i <= hi; ++i)
        count[charAt(a[i], d)+2]++;//如果已經超出了長度,則放到最開始
    for (int r = 0; r < R+1; r++)
        count[r+1] += count[r];
    for (int i = lo; i <= hi; ++i)
        aux[count[charAt(a[i], d)+1]++] = a[i];
    for (int i = lo; i <= hi; ++i)
        a[i] = aux[i-lo];
    
    for (int r = 0; r < R; r++)
        MSDSort(a, lo+count[r], lo+count[r+1]-1, d+1);
    
}

void insertionSort(vector<string> &a, int lo, int hi, int d) {
    using std::swap;
    for (int i = lo; i <= hi; i++)
        for (int j = i; j > lo && smaller(a[j], a[j-1], d); --j)
            swap(a[j], a[j-1]);
}

bool smaller(string &v, string &w, int d) {
    return v.substr(d) < w.substr(d);
}

4.三向字符串快速排序
根據鍵的首字母進行三向切分,僅在中間子數組中的下一個字符繼續遞歸調用。
是快排和MSD的結合。
相對於MSD,優點是能夠很好地處理等值鍵、有較長公共前綴的鍵、取值範圍較小的鍵和小數組——所有MSD不擅長的各種情況。特別是能夠適應鍵的不同部分的不同結構。和快排一樣,也不需要額外的空間,也是它相對於MSD的一大優點,MSD在統計頻率以及使用輔助數組時都需要額外空間。相對於MSD的一個缺點是,因爲三向字符串快速排序只將數組切分爲三部分,因此當相應的MSD產生的非空切分較多時,就需要進行較多次的三向切分才能取得相同的效果,所以移動的數據量就會變大。
同樣的,可以對小數組進行特殊處理,使用插入排序,但重要性遠不如它在MSD中來的高。
和快排一樣,爲了預防最壞情況,最好在排序前將數組打亂。

#include <vector>
#include <string>
#include <iostream>

using namespace std;

void Quick3string(vector<string> &a, int lo, int hi, int d);
void printVec(const vector<string> &a);
int charAt(const string &s, int d);

int main() {
    vector<string> a;
    string s;
    while (cin >> s)
        a.push_back(s);
    cout << "before sorting:" << endl;
    printVec(a);

    Quick3string(a, 0, a.size()-1, 0);

    cout << "after sorting:" << endl;
    printVec(a);
}

void printVec(const vector<string> &a) {
    for (const auto &item : a)
        cout << item << " ";
    cout << endl;
}

int charAt(const string &s, int d) {
    if (d < s.size())
        return (int)s[d];
    else
        return -1;
}

void Quick3string(vector<string> &a, int lo, int hi, int d) {
    using std::swap;
    if (hi <= lo) return;
    int lt = lo, gt = hi;
    int v = charAt(a[lo], d);
    int i = lo + 1;
    while (i <= gt) {
        int t = charAt(a[i], d);
        if (t < v) swap(a[lt++], a[i++]);
        else if (t > v) swap(a[i], a[gt--]);
        else i++;
    }
    Quick3string(a, lo, lt-1, d);
    if (v >= 0) Quick3string(a, lt, gt, d+1);
    Quick3string(a, gt+1, hi, d);
}

至此algs4上的5.1章字符串排序算法實現完畢,在書上的471頁有各個字符串排序算法的比較,需要看一下。

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