1. 程式人生 > >七、排序(1)

七、排序(1)

一、排序的概述

1、最經典、最常用的排序方法:

  • 氣泡排序、插入排序、選擇排序、歸併排序、快速排序、計數排序、基數排序、桶排序。

2、分類:

排序方法 時間複雜度 是否基於比較 適用性
1 冒泡、插入、選擇 O(n2) 適合小規模資料排序
2 快排、歸併 O(nlogn) 適合大規模的資料排序
3 桶、計數、基數 O(n) ×

思考: 插入排序和氣泡排序具有相同的時間複雜度,但在實際開發中,更傾向於使用插入排序?
解:冒泡程式中的交換操作:需要3個賦值操作,而插入排序的移動操作:只需一個賦值操作。

3、分析“排序演算法”

(1)執行效率

  • 最好情況、最壞情況、平均情況時間複雜度
    • 原始資料的形態——有序度
  • 時間複雜度的係數、常數、低階
    • 對同一階時間複雜度的排序演算法效能對比時,需考慮係數、常數、低階
  • 比較 “次數和交換(或移動)” 次數

(2)記憶體消耗——空間複雜度

原地排序(sorted in place)——特指空間複雜度為O(1)的排序演算法

(3)穩定性

穩定性:若待排序的序列中存在值相等的元素,經過排序之後,相等元素之間原有的先後順序不變

穩定性重要性:可針對物件的多種屬性進行有優先順序的排序。

示例:電商交易系統中的“訂單”排序。

  • 目標:訂單有兩個屬性,一個是下單時間,另一個是訂單金額。希望按照金額從小到大對訂單資料排序。對於金額相同的訂單,希望按照金額從小到大對訂單資料排序。
  • 實現:藉助穩定排序演算法。先按照下單時間給訂單排序,注意是按照下單時間,不是金額。排序完成之後,我們用穩定排序演算法,按照訂單金額重新排序。兩遍排序之後,我們得到的訂單資料就是按照金額從小到大排序,金額相同的訂單按照下單時間從早到晚排序的。
    在這裡插入圖片描述

二、冒泡演算法(Bubble Sort)

1、基本思想

兩兩比較相鄰記錄的關鍵字,若反序則交換,直到沒有反序的記錄為止。

2、示例

目標:陣列{4,5,6,3,2,1}——從小到大排序
過程:

  • 第一次冒泡操作的詳細過程:
    在這裡插入圖片描述
  • 完成排序的所有操作:
    在這裡插入圖片描述

3、優化

當某次冒泡操作已經沒有資料交換時,說明已經達到完全有序,不需要再繼續執行後面的冒泡操作。
在這裡插入圖片描述

4、實現

(1)基於陣列的實現

#include <iostream>
using namespace std;

/*
 * 氣泡排序——從小到大排序
 *
 * 引數說明:
 *     a -- 待排序的陣列
 *     n -- 陣列的長度
 */
void bubbleSort(int *a, int n)
{
    int i,j,tmp;
    for(i = 0; i < n-1; i++)
    {
        for(j = 0; j < n-1-i;j++)
        {
            if(a[j] > a[j+1])
            {
                // 交換 a[j] 與 a[j+1]
                tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = tmp;
            }
        }
    }
}

/*
 * 氣泡排序(改進版)——從小到大排序
 *
 * 引數說明:
 *     a -- 待排序的陣列
 *     n -- 陣列的長度
 */
void bubbleSort1(int *a, int n)
{
    int i,j,tmp;
    int flag;          // 標記
    for(i = 0; i < n-1; i++)
    {
        flag = 0;
        for(j = 0; j < n-1-i;j++)
        {
            if(a[j] > a[j+1])
            {
                tmp = a[j];
                a[j] = a[j+1];
                a[j+1] = tmp;
                flag = 1;
            }
        }
        // 若沒發生交換,則說明數列已有序。
        if (flag == 0)
            break;
    }
}

int main()
{
    int i;
    int a[] = {2,4,3,1,6,5};
    int len = (sizeof(a)/sizeof(a[0]));

    cout<<"Before Sort:";
    for(i = 0; i < len; i++)
        cout<< a[i]<<" ";
    cout<<endl;

    // 傳統方法
    bubbleSort(a, len);   // 傳統方法
    // 改進方法:即若內迴圈中不發生交換,則說明數列已有序
    // bubbleSort1(a,len);

    cout<<"After Sort:";
    for(i = 0; i < len; i++)
        cout<< a[i]<<" ";
    cout<<endl;
    
    return 0;
}

(2)基於C++模板實現氣泡排序

/*
 * 基於C++模板實現:氣泡排序——從小到大排序
 *
 * 引數說明:
 *     a -- 待排序的陣列
 *     n -- 陣列的長度
 */
#include <iostream>
using namespace std;
template<typename T>
//整數或浮點數皆可使用
void bubble_sort(T arr[], int len)
{
    int i, j;  T temp;
    for (i = 0; i < len - 1; i++)
        for (j = 0; j < len - 1 - i; j++)
        if (arr[j] > arr[j + 1])
        {
            temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
}

int main()
{
    int arr[] = { 61, 17, 29, 22, 34, 60, 72, 21, 50, 1, 62 };
    int len = (int) sizeof(arr) / sizeof(*arr);
    bubble_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;

    float arrf[] = { 17.5, 19.1, 0.6, 1.9, 10.5, 12.4, 3.8, 19.7, 1.5, 25.4, 28.6, 4.4, 23.8, 5.4 };
    len = (int) sizeof(arrf) / sizeof(*arrf);
    bubble_sort(arrf, len);
    for (int i = 0; i < len; i++)
        cout << arrf[i] << ' ';
    return 0;
}

5、效能分析

(1)原地排序演算法

  • 冒泡過程中只涉及相鄰資料的交換操作 ==》需要常量級的臨時空間,即空間複雜度為 O(1)

(2)穩定排序演算法

  • 為保證氣泡排序演算法的穩定性 ==》當有相鄰的兩個元素大小相等的時候,不做交換 ==》穩定

(3)時間複雜度

  • 最好情況時間複雜度(有序時):O(n)
  • 最壞情況時間複雜度(逆序時):O(n2)
  • 平均情況時間複雜度:O(n2)
    • 最壞情況下,初始狀態的有序度是 0,所以要進行 n*(n-1)/2 次交換。最好情況下,初始狀態的有序度是 n*(n-1)/2,就不需要進行交換。
    • 平均情況下,需要 n*(n-1)/4 次交換操作。

平均情況時間複雜度(加權平均期望時間複雜度)——通過“有序度” 和 “逆序度” 兩個概念進行分析

  1. 有序度:陣列中具有有序關係的元素對的個數。
    有序元素對用數學表示式表示:a[i] <= a[j], 如果 i < j
    示例:
    {2,4,3,1,5,6} =》有序度為11,分別為: (2,4)、(2,3)、(2,5)、(2,6)、(4,5)、(4,6)、(3,5)、(3,6)、(1,5)、(1,6)、(5,6)
    {1,2,3,4,5,6}=》有序度為n*(n-1)/2,稱為滿有序度
  2. 逆序度:逆序元素對:a[i] > a[j], 如果 i < j。
  3. 逆序度 = 滿有序度 - 有序度。
  4. 排序的過程是一種增加有序度,減少逆序度的過程,最後達到滿有序度。
  5. 交換次數總是確定的,即為逆序度,也就是:n*(n-1)/2 – 初始有序度
    在這裡插入圖片描述

三、插入排序(Insertion Sort)

1、基本思想

將一個記錄插入到已經排好序的有序表中,從而得到一個新的、記錄數增1的有序表。

(1)過程概述

插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

(2)具體演算法描述:

  1. 從第一個元素開始,該元素可以認為已經被排序
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
  5. 將新元素插入到該位置後
  6. 重複步驟2~5

2、示例

原資料:{4,5,6,1,3,2}
排序過程:其中左側是已排序區間,右側是未排序區間。
在這裡插入圖片描述
分析:滿有序度是 n*(n-1)/2=15,初始序列的有序度是 5,所以逆序度是 10。插入排序中,資料移動的個數總和也等於10 = 3+3+4。

3、實現

void insertSort(int a[], int len)
{	
	int tmp,j;
	for(int i = 1; i < len; i++)
	{
		tmp = a[i]
		int j = i -1;
		while(tmp < a[j])
		{
			a[j+1] = a[j];
			j--;
		}	
		a[j+1] = tmp;
	}
}

4、分析

(1)原地排序演算法

  • 空間複雜度為 O(1) ==》原地排序演算法

(2)穩定排序演算法

  • 對於值相同的元素,可以選擇將後面出現的元素,插入到前面出現元素的後面。==》保持原有前後順序,也就是穩定的排序演算法

(3)時間複雜度

  • 最好情況時間複雜度(資料已經有序):O(n)
  • 最壞情況時間複雜度:O(n2)
  • 平均時間複雜度:O(n2)
    • 對於插入排序來說,每次插入操作(時間複雜度為O(n))都相當於在陣列中插入一個數據,迴圈執行 n 次插入操作

四、選擇排序(Selection Sort)

1、基本思想

從未排序區間中找到最小的元素,將其放在已排序區間的末尾,也就是與未排序區間的第一個元素進行交換。

在這裡插入圖片描述
分析:
(1)原地排序演算法 《= 空間複雜度:O(1)
(2)最好情況時間複雜度、最壞情況和平均情況時間複雜度都為 O(n2
(3)非穩定排序:選擇排序每次都要找剩餘未排序元素中的最小值,並和前面的元素交換位置,這樣破壞了穩定性。

2、實現

void selectSort(int a[], int len)
{	
	int min,tmp;
	for(int i = 0; i < len - 1;i++)
	{
		min = i; // 假設最小元素的下標
		for(int j = i + 1; j < n; j++)
		{
			if(a[j] < a[min])
				min = j;
		}
		// 若陣列中真的存在比假設的元素還要小的元素,則交換
		if(i != min)
		{
			tmp = a[i];
			a[i] = a[min];
			a[min] = tmp;
		}
	}
}

歸併排序和快速排序都用到了分治思想,非常巧妙。
==》可以借鑑這個思想,來解決非排序的問題
比如:如何在 O(n) 的時間複雜度內查詢一個無序陣列中的第 K 大元素?