【經典排序算法】(一)--- 比較類排序(冒泡排序,快速排序 ,簡單插入排序,希爾排序,簡單選擇排序,堆排序,歸併排序)

【經典排序算法】(一)--- 比較類排序(冒泡排序,快速排序 ,簡單插入排序,希爾排序,簡單選擇排序,堆排序,歸併排序

概述

經過比較來決定元素間的相對次序,因爲其時間複雜度不能突破O(nlogn),所以也稱爲非線性時間比較類排序。ios

算法複雜度

排序方法 時間複雜度(平均) 時間複雜度(最壞) 時間複雜度(最好) 空間複雜度 穩定性
冒泡排序 O(n^2) O(n^2) O(n) O(1) 穩定
快速排序 O(nlog2n) O(n^2) O(nlog2n) O(n) 不穩定
插入排序 O(n^2) O(n^2) O(n) O(1) 穩定
希爾排序 O(n^1.3) O(n^2) O(n) O(1) 不穩定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不穩定
歸併排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 穩定

相關概念

  • 穩定:若是a本來在b前面,而a=b,排序以後a仍然在b的前面。
  • 不穩定:若是a本來在b的前面,而a=b,排序以後 a 可能會出如今 b 的後面。
  • 時間複雜度:對排序數據的總的操做次數。反映當n變化時,操做次數呈現什麼規律。
  • 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。

一. 交換排序

1. 冒泡排序

  • 介紹:
    冒泡排序是一種簡單的排序算法。它重複地走訪過要排序的數列,一次比較兩個元素,若是它們的順序錯誤就把它們交換過來。走訪數列的工做是重複地進行直到沒有再須要交換,也就是說該數列已經排序完成。這個算法的名字由來是由於越小的元素會經由交換慢慢「浮」到數列的頂端。web

  • 算法描述:算法

  1. 比較相鄰的元素。若是第一個比第二個大,就交換它們兩個;
  2. 對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
  3. 針對全部的元素重複以上的步驟,除了最後一個;

重複步驟1~3,直到排序完成。
shell

  • 代碼演示:
#include <iostream>
using namespace std;

int bubbleSort(int arr[],int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		for (int j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	bubbleSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

固然,咱們能夠再優化一下冒泡排序:數組

int bubbleSort(int arr[],int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		bool flag = true;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				flag = false;
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
		if (flag)
		{
			break;
		}
	}
	return 0;
}

這樣,當後面已經排序好了的狀況下,咱們就不須要再去遍歷後面的元素了。數據結構

2. 快速排序

  • 介紹:
    快速排序的基本思想:經過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另外一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
  • 算法描述:
    快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述以下:
  1. 從數列中挑出一個元素,稱爲 「基準」;
  2. 從新排序數列,全部元素比基準值小的擺放在基準前面,全部元素比基準值大的擺在基準的後面(相同的數能夠到任一邊)。在這個分區退出以後,該基準就處於數列的中間位置。這個稱爲分區操做;
  3. 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
  • 代碼演示:
#include <iostream>
using namespace std;

int swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
	return 0;
}

int quicksort(int arr[], int left, int right)
{
	if (left >= right)
	{
		return 0;
	}
	int i = left;
	int j = right + 1;
	int key =arr[left];//設置基準值
	while (true)
	{
		//從左向右找比key大的值
		while (arr[++i] < key && i != right);
		//從右向左找比key小的值
		while (arr[--j] > key && j != left);
		if (i >= j)
		{
			break;
		}
		//交換ij對應的值
		swap(&arr[i], &arr[j]);
	}
	//將基準值(中樞值)與j所對應的值交換,由於arr[J]是小於基準中下標最大的
	swap(&arr[left], &arr[j]);
	quicksort(arr, left, j - 1);
	quicksort(arr, j + 1, right);
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	quicksort(arr, 0, n - 1);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

雖然咱們實現了快速排序,可是卻存在兩個問題:ide

  1. 若是出現最壞的狀況(上面我是將第一個設爲基準的,若是前面的幾個元素有序,那麼我上面這個方法的時間複雜度就會很高。)
  2. 因爲快速排序通常是由遞歸實現的,若是待排序的規模較小,遞歸的反作用就會凸顯出來,效果會變得不好.

解決以上問題的方法:svg

  1. 一種比較好的方法是將arr[left],arr[right],arr[(left+right)/2]三者關鍵字的中值做爲基準,這樣有可能避免在基本有序的序列中進行快速排序時間複雜度出現最壞狀況的問題。
  2. 當規模較小時就不須要遞歸了,直接調用簡單插入排序就能解決這個問題。

代碼演示:函數

#include <iostream>
#include <algorithm>
using namespace std;
//交換
int swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
	return 0;
}
//簡單插入排序
int insertsort(int arr[], int n)
{
	int temp, i, j;
	for (i = 1; i < n; i++)
	{
		temp = arr[i];
		for (j = i; j > 0 && arr[j - 1] > temp; j--)
		{
			arr[j] = arr[j - 1];
		}
		arr[j] = temp;
	}
	return 0;
}
//得到基準值
int median3(int arr[], int left, int right)
{
	int center = (left + right) / 2;
	if (arr[left] > arr[center])
	{
		swap(&arr[left], &arr[center]);
	}
	if (arr[left] > arr[right])
	{
		swap(&arr[left], &arr[right]);
	}
	if (arr[center] > arr[right])
	{
		swap(&arr[center], &arr[right]);
	}
	//此時arr[left]<arr[center]<arr[right]
	swap(&arr[center], &arr[right - 1]);//已知arr[right]>arr[center]
	return arr[right - 1];//只需考慮arr[left+1]到arr[right-2]的值,因此返回arr[right-1]
}
//快排
int quicksort(int arr[], int left, int right)
{
	int key, i, j;
	if (right - left > 3)//當元素較多時調用快速排序
	{
		key = median3(arr, left, right);//選擇基準
		i = left;
		j = right - 1;
		while (true)
		{
			while (arr[++i] < key);//從左向右找比key大的值
			while (arr[--j] > key);//從右向左找比key小的值
			if (i > j)
			{
				break;
			}
			swap(&arr[i], &arr[j]);
		}
		swap(&arr[i], &arr[right - 1]);//將基準值與按順序第一個大於基準值的元素交換,保證前面小於後面大於基準
		quicksort(arr, left, i - 1);//遞歸解決左邊的
		quicksort(arr, i + 1, right);//遞歸解決右邊的
	}
	else//當元素較少時,直接用簡單插入排序
	{
		insertsort(arr + left, right - left + 1);
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	quicksort(arr, 0, n - 1);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:
優化

二. 插入排序

1. 簡單插入排序

  • 介紹:
    插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工做原理是經過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。
  • 算法描述:
    通常來講,插入排序都採用in-place在數組上實現。具體算法描述以下:
  1. 從第一個元素開始,該元素能夠認爲已經被排序;
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描;
  3. 若是該元素(已排序)大於新元素,將該元素移到下一位置;
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  5. 將新元素插入到該位置後;
  6. 重複步驟2~5。
  • 代碼演示:
#include <iostream>
using namespace std;

int insertionSort(int arr[], int n)
{
	int temp, i, j;
	for (i = 1; i < n; i++)
	{
		temp = arr[i];
		for (j = i; j > 0 && arr[j - 1] > temp; j--)
		{
			arr[j] = arr[j - 1];
		}
		arr[j] = temp;
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	insertionSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

2. 希爾排序

  • 介紹:
    1959年Shell發明,第一個突破O(n2)的排序算法,是簡單插入排序的改進版。它與插入排序的不一樣之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
  • 算法描述:
    先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
  1. 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列個數k,對序列進行k 趟排序;
  3. 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列做爲一個表來處理,表長度即爲整個序列的長度。
  • 代碼演示:
#include <iostream>
using namespace std;

int shellSort(int arr[], int n)
{
	for (int gap = n / 2; gap > 0; gap--)//肯定增量
	{
		for (int i = gap; i < n; i += gap)//每次都間隔增量,將這些排序
		{
			int temp = arr[i];
			int j;
			for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
			{
				arr[j] = arr[j - gap];
			}
			arr[j] = temp;
		}
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	shellSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

三. 選擇排序

1. 簡單選擇排序

  • 介紹:
    選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工做原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到全部元素均排序完畢。
  • 算法描述:
    n個記錄的直接選擇排序可通過n-1趟直接選擇排序獲得有序結果。具體算法描述以下:
  1. 初始狀態:無序區爲R[1…n],有序區爲空;
  2. 第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別爲R[1…i-1]和R(i…n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1…i]和R[i+1…n)分別變爲記錄個數增長1個的新有序區和記錄個數減小1個的新無序區;
  3. n-1趟結束,數組有序化了。
  • 代碼演示:
#include <iostream>
using namespace std;

int selectionSort(int arr[], int n)
{
	for (int i = 0; i < n; i++)
	{
		int minIndex = i;
		for (int j = i + 1; j < n; j++)
		{
			if (arr[minIndex] > arr[j])
			{
				minIndex = j;
			}
		}
		int temp = arr[minIndex];
		arr[minIndex] = arr[i];
		arr[i] = temp;
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	selectionSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

2. 堆排序

  • 介紹:
    堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。
  • 算法描述:
    將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區;
    將堆頂元素R[1]與最後一個元素R[n]交換,此時獲得新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且知足R[1,2…n-1]<=R[n];
    因爲交換後新的堆頂R[1]可能違反堆的性質,所以須要對當前無序區(R1,R2,……Rn-1)調整爲新堆,而後再次將R[1]與無序區最後一個元素交換,獲得新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重複此過程直到有序區的元素個數爲n-1,則整個排序過程完成。
  • 代碼演示:
#include <iostream>
#include <algorithm>
using namespace std;

int percDown(int arr[], int p, int n)//創建最大堆
{
	int parent, child;//標記根結點和孩子結點
	int x;
	x = arr[p];
	for (parent = p; (parent * 2 + 1) < n; parent = child)
	{
		child = parent * 2 + 1;//得到孩子結點的下標
		if ((child != n - 1) && (arr[child] < arr[child + 1]))
		{
			child++;//指向左右子結點的較大者
		}
		if (x >= arr[child])//找到合適的位置,即根結點大於子結點
		{
			break;
		}
		else
		{
			arr[parent] = arr[child];//反之交換
		}
	}
	arr[parent] = x;
	return 0;
}

int swap(int *x, int *y)//交換
{
	int temp = *x;
	*x = *y;
	*y = temp;
	return 0;
}

int heapSort(int arr[], int n)//堆排序
{
	for (int i = n / 2 - 1; i >= 0; i--)
	{
		percDown(arr, i, n);//創建最大堆
	}
	for (int i = n - 1; i > 0; i--)
	{
		swap(&arr[0], &arr[i]);//將最大堆中的最大值放在arr[i]
		percDown(arr, 0, i);//而後用0-i建立最大堆獲取最大值,即刪除最大堆頂
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	heapSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下:

四. 歸併排序

1. 二路歸併排序

  • 介紹:
    歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
  • 算法描述
  1. 把長度爲n的輸入序列分紅兩個長度爲n/2的子序列;
  2. 對這兩個子序列分別採用歸併排序;
  3. 將兩個排序好的子序列合併成一個最終的排序序列。
  • 代碼演示:
#include <iostream>
using namespace std;
//將arr[left]-arr[center-1]和arr[center]-arr[right]歸併成一個序列
int merge(int arr[], int tmparr[], int left, int center, int right)
{
	int l_end = center - 1;//左邊終點的位置
	int tmp = left;//有序序列的起始位置
	int len = right - left + 1;//總長度
	while (left <= l_end && center <= right)
	{
		if (arr[left] <= arr[center])//將小的元素先複製到tmparr中
		{
			tmparr[tmp++] = arr[left++];//將左邊的元素複製到tmparr中
		}
		else
		{
			tmparr[tmp++] = arr[center++];//將右邊的元素複製到tmparr中
		}
		
	}
	
	while (left <= l_end)//直接將左邊剩下的全部元素都複製到tmparr中
	{
		tmparr[tmp++] = arr[left++];
	}
	while (center <= right)//直接將右邊剩下的全部元素都複製到tmparr中
	{
		tmparr[tmp++] = arr[center++];
	}
	//將有序的tmparr複製回arr中
	for (int i = 0; i < len; i++,right--)
	{
		arr[right] = tmparr[right];
	}
	return 0;
}


int msort(int arr[], int tmparr[], int left, int right)
{
	int center;
	if (left < right)
	{
		center = (left + right) / 2;
		msort(arr, tmparr, left, center);//遞歸解決左邊
		msort(arr, tmparr, center + 1, right);//遞歸解決右邊
		merge(arr, tmparr, left, center + 1, right);//合併兩段有序序列並排序
	}
	return 0;
}

int mergeSort(int arr[], int n)
{
	int *tmparr;
	//建立一個數組,以便歸併
	tmparr = (int *)malloc(n * sizeof(int));

	if (tmparr != NULL)
	{
		msort(arr, tmparr, 0, n - 1);
		//釋放空間
		free(tmparr);
	}
	else
	{
		cout << "空間不足" << endl;
	}
	return 0;
}

int main()
{
	int arr[1000];
	int n;
	cout << "請輸入數組中元素的個數:";
	cin >> n;
	cout << "請輸入元素: " << endl;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	cout << "排序前:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	mergeSort(arr, n);
	cout << "排序後:" << endl;
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

運行結果以下: