Data Structures in C++:排序算法

主要參考:



交換

std::swap()可以直接使用,也可以自己實現:

template <class T>
void swap(const T& a, T const& b)
{
	T temp = a;
	a = b;
	b = temp;
}

關於const,值得說明的是:

  • const T x; 定義一個常量x,其值不可修改

  • const T* p; 定義一個指針變量p,其指向對象*p不可修改,指針p可修改
  • T const* p; 與上等同
  • T* const p = &var; 定義一個指針變量p,其指向對象*p可修改,指針p不可修改
  • const T* const p = &var; 定義一個指針變量p,其指向對象*p和指針p都不可修改

  • const T& a = var 定義一個引用變量a,其值不可修改
  • T const& a = var 與上等同
  • T& const a = var 無意義

  • bool empty() const; const修飾表明該成員函數不修改數據成員

冒泡排序

比較n-1趟,每趟比較n-i-1次,每次從頭開始,大數逐次移到末尾. 下圖是每次將小數移至最前端。
在這裏插入圖片描述

template<class T>
void BubbleSort(T* a, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        for (int j = 0; j < n - i - 1; j++)
        {
            if (a[j] > a[j + 1])
                swap(a[j], a[j + 1]);
        }
    }
}

選擇排序

減少交換次數,只操作下標
在這裏插入圖片描述

template<class T>
void selectSort(T* a, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        int min = i;
        for (int j = i + 1; j < n; j++)
        {
            if (a[min] > a[j])
                min = j;
        }
        if (min != i)
            swap(a[i], a[min]);
    }
}

插入排序

在要排序的一組數中,假定前n-1個數已經排好序,現在將第n個數插到前面的有序數列中,使得這n個數也是排好順序的。如此反覆循環,直到全部排好順序。
在這裏插入圖片描述
在這裏插入圖片描述

template<class T>
void insertSort(T* a, int n)
{
    for (int right = 1; right < n; right++)
    {
        int left = right - 1;
        T temp = a[right];
        while (temp < a[left] and left >= 0)
        {
            a[left + 1] = a[left];
            left--;
        }
        int middle = left + 1;
        a[middle] = temp;
    }
}

快速排序 ★★★

遞歸的排序算法,平均時間 O(nlogn)\mathcal{O}(nlogn) 性能是目前最好的,應用最廣泛; 但需要工作棧存放遞歸調用的執行環境,平均棧深 O(logn)\mathcal{O}(logn),是一種不穩定的排序算法.

快速排序 的思想----分治法非常實用,因此很多軟件公司的筆試面試,包括像騰訊,微軟等知名IT公司都喜歡考這個,還有大大小的程序方面的考試如軟考,考研中也常常出現快速排序的身影。

基本思想:(分治)

  • 先從數列中取出一個數作爲key值;
  • 將比這個數小的數全部放在它的左邊,大於或等於它的數全部放在它的右邊;
  • 對左右兩個小數列重複第二步,直至各區間只有1個數。
template<class T>
void quickSort(T* a, const int left, const int right)
{
    if (left >= right) return;  // 待排序數列只有一個元素時,結束遞歸
    else
    {
        // 選樞軸
        if (right - left == 1);
        else
        {   // 中值放在左側
            int middle = (left + right) / 2;
            if (a[left] > a[middle])
                swap(a[middle], a[left]);
            if (a[middle] > a[right])
                swap(a[middle], a[right]);
            if (a[left] < a[middle])
                swap(a[middle], a[left]);
        }
        int pivot = left;

        // 一次劃分
        int i{ left }, j{ right + 1 };
        while (i < j)
        {
            do i++; while (a[i] < a[pivot]);    // 從左掃描
            do j--; while (a[j] > a[pivot]);    // 從右掃描
            if (i < j) swap(a[i], a[j]);
        }
        swap(a[pivot], a[j]);                   // 最左側爲初始樞軸
        pivot = j;

        // 遞歸
        quickSort(a, left, pivot - 1);
        quickSort(a, pivot + 1, right);
    }
}

歸併排序

歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法的一個非常典型的應用。

首先考慮引子:如何將2個有序數列合併?這個非常簡單,只要從比較2個數列的第一個數,誰小就先取誰,取了後就比較下一個數,如果有數列爲空,那直接將另一個數列的數據依次取出即可。

引子
以下是兩個有序數組a[ ], b[ ]的歸併排序(最簡單)
在這裏插入圖片描述

解決了上面的合併有序數列問題,再來看歸併排序,其的基本思路是:將數組分成2組A,B,如果這2組組內的數據都是有序的,那麼就可以很方便的將這2組數據進行排序。如何讓這2組組內數據有序了?可以將A,B組各自再分成2組。依次類推,當分出來的小組只有1個數據時,可以認爲這個小組組內已經達到了有序,然後再合併相鄰的2個小組就可以了。這樣通過先 遞歸 的分解數列,再合併數列就完成了歸併排序。

在這裏插入圖片描述

  • 遞歸式
    在這裏插入圖片描述
// 一次歸併排序, 輸入爲已排序數組 a[l,...,m]  a[m+1,...,n]
template <class T>
void merge(T* a, const int l, const int m, const int n)
{
    // 申請輔助空間
    int length = n - l + 1;
    assert(length > 1);   // 消除C6386警告
    T* temp = new T[length];
    // 遍歷等長元素              
    int i = l, j = m + 1, k = 0; 
    while (i <= m and j <= n)
    {
        if (a[i] <= a[j])
        {
            temp[k++] = a[i++];
        }
        else
        {
            temp[k++] = a[j++];
        }
    }
    // 複製其餘元素
    copy(a + i, a + m + 1, temp + k);
    copy(a + j, a + n + 1, temp + k);
    // 釋放輔助空間
    copy(temp, temp + length, a + l);
    delete[] temp;
}

template <class T>
void mergeSort(T* a, int left, int right)
{
    if (left >= right)
        return;
    int mid = (left + right) / 2;
    mergeSort(a, left, mid);
    mergeSort(a, mid + 1, right);
    merge(a, left, mid, right);
}
  • 迭代式
    在這裏插入圖片描述

以下代碼的缺點:頻繁申請/釋放內存

// 一次歸併排序, 輸入爲已排序數組 a[l,...,m]  a[m+1,...,n]
template <class T>
void merge(T* a, const int l, const int m, const int n)
{
    // 申請輔助空間
    int length = n - l + 1;
    assert(length > 1);   // 消除C6386警告
    T* temp = new T[length];
    // 遍歷等長元素              
    int i = l, j = m + 1, k = 0; 
    while (i <= m and j <= n)
    {
        if (a[i] <= a[j])
        {
            temp[k++] = a[i++];
        }
        else
        {
            temp[k++] = a[j++];
        }
    }
    // 複製其餘元素
    copy(a + i, a + m + 1, temp + k);
    copy(a + j, a + n + 1, temp + k);
    // 釋放輔助空間
    copy(temp, temp + length, a + l);
    delete[] temp;
}

// 一趟歸併排序
template <class T>
void mergePass(T* a, int n, int h)
{
    int i= 1;
    while (n - i + 1 >= 2 * h)
    {
        merge(a, i, i + h - 1, i + 2 * h - 1);
        i += 2 * h;
    }
    if (n - i + 1 > h)
        merge(a, i, i + h - 1, n);
    else
        ;// do nothing
}

// 歸併排序(迭代式)
template <class T>
void mergeSort(T* a, int n)
{
    if (a == nullptr or n <= 0) return;
    // 最初歸併長度爲1
    for (int h = 1; h < n; h *= 2)
    {
        mergePass(a, n, h);
    }
}

改進:只申請一次內存

// 一次歸併排序, 輸入爲已排序數組 a[l,...,m]  a[m+1,...,n]
template <class T>
void merge(T* a, T* result, const int l, const int m, const int n)
{
    // 遍歷等長元素
    int i = l, j = m + 1, k = l;
    while (i <= m and j <= n)
    {
        if (a[i] <= a[j])
        {
            result[k++] = a[i++];
        }
        else
        {
            result[k++] = a[j++];
        }
    }
    // 複製其餘元素
    copy(a + i, a + m + 1, result + k);
    copy(a + j, a + n + 1, result + k);
}

// 一趟歸併排序
template <class T>
void mergePass(T* a, T* result, int n, int h)
{
    int i = 1;   
    while (n - i + 1 >= 2 * h)
    {
        merge(a, result, i, i + h - 1, i + 2 * h - 1);
        i += 2 * h;
    }
    if (n - i + 1 > h)
        merge(a, result, i, i + h - 1, n);
    else
        copy(a + i, a + n + 1, result + i);
}

// 歸併排序(迭代式)
template <class T>
void mergeSort(T* a, int n)
{
    if (a == nullptr or n <= 0) return;
    T* temp = new T[n + 1];
    int h = 1;  // 最初歸併長度爲1
    while (h < n)
    {
        mergePass(a, temp, n, h);
        h *= 2;
        mergePass(temp, a, n, h);
        h *= 2;
    }
    delete[] temp;
}

基數排序(桶、箱)

基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶排序”(bucket sort)或“箱排序”(bin sort),顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法屬於穩定性的排序,其時間複雜度爲O (nlog®m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。

基數排序是建立在“箱排序”的基礎上的。箱排序是創建數組A[MaxValue];然後將每個數放到相應的位置上(例如17放在下標17的數組位置);最後遍歷數組,即爲排序後的結果。
.
存在的問題: 當序列中存在較大值時,BinSort 的排序方法會浪費大量的空間開銷。
在這裏插入圖片描述

基數排序是在BinSort的基礎上,通過基數的限制來減少空間的開銷。所謂基數,是指進制,十進制數列的基數爲10,二進制爲2,八進制爲8.
在這裏插入圖片描述
可以用鏈表或者數組來構造“箱”,這裏使用STL中的鏈表完成:

template<class T>
int maxdigit(const T a, int n)
{
    int digits{ 1 }, ratio{ 10 };
    for (int i = 0; i < n; ++i)
    {
        while (a[i] > ratio)
        {
            ++digits;
            ratio *= 10;
        }
    }
    return digits;
}

template<class T>
void radixSort(T* a, int n)
{
    std::list<T> bins[10];  // 十進制需要十個箱
    int digits = maxdigit(a, n);
    for (int d = 0, radix = 1; d < digits; ++d, radix *= 10)
    {
        // 將每個數放入對應箱中
        for (int i = 0; i < n; ++i)
        {
            int index = (a[i] / radix) % 10;
            bins[index].push_back(a[i]);
        }

        // 從10個箱中按順序取出,每取一個就刪除一個
        for (int i = 0, j = 0; i < 10; ++i)
        {
            while (not bins[i].empty())
            {
                a[j++] = bins[i].front();
                bins[i].pop_front();
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章