1. 程式人生 > >排序演算法之快速排序以及優化

排序演算法之快速排序以及優化

顧名思義,就是一種速度特別快的快速排序也叫分割槽排序,是目前應用最廣泛的排序演算法,人如其名,就是很快,而且快速排序演算法在空間上只使用一個小的輔助棧,其內部迴圈也很小,另外快排很容易實現,消耗的資源也很小。

【基本思想】

(1)任取待排序元素序列中的某個元素(例如取第一個元素)作為基準

(2)按照該元素的排序碼大小,將整個元素序列劃分為左右兩個子序列;左側序列中所有元素的排序碼都小於基準值,右側都大於或等於基準值,基準值在中間;

(3)然後分別對著兩個子序列重複執行上述方法,直到所有元素都排在相應的位置上為止

【圖解】

【程式碼實現】

int Pation1(int* array, const int left,const int right)
{
	int ret = left;
	int key = array[left];//基準元素
	for (int i = left + 1; i <= right-1; i++)//檢查整個序列,進行劃分
	{
		if (array[i] < key)
		{
			ret++;
			if (ret != i)
			{
				swap(array[ret], array[i]);//小於基準值的交換到後面去
			}
		}
	}
	array[left] = array[ret];
	array[ret] = key;//將基準值元素就位
	return ret;//返回到基準值元素位置
}
void QuickSort(int *array, int left, int right)
{
	if (left < right)//元素序列大於1時
	{
		size_t div = Pation1(array, left, right);//劃分
		QuickSort(array, left, div);//對左側進行同樣處理
		QuickSort(array, div + 1, right);//對右側進行同樣處理
	}
}

【測試】

void Print(int *array,int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
}
int main()
{
	int array[] = { 21,25,49,25,16,8 };
	Print(array, sizeof(array) / sizeof(array[0]));
	QuickSort(array, 0, sizeof(array) / sizeof(array[0]));
	Print(array, sizeof(array) / sizeof(array[0])); 
	system("pause");
	return 0;
}

【效能分析】

如上圖所知,快排的趟數取決於遞迴樹的深度

最理想的情況:如果每次劃分一個元素定位後,該元素的左側子序列與右側子序列的長度相同,則下一步時對於兩個長度減半的子序列進行排序

假設在n個元素中,對一個元素定位所需時間為O(n),若T(n)是對n個元素的序列進行排序所需的時間(這裡假設為理想狀態下),總的時間為:

最後可得快排的平均複雜度為,最優情況為最壞將到達

下面介紹最常用的三種快排方法~

一、左右指標法

【基本思想】

利用兩個指標,先設定一個基準值key,設定一個begin指標,從前向後依次找比基準值大的值,設定一個end指標,從後向前依次找比基準值小的值,找到之後將兩個值交換,直到begin跟end的位置重合為止,這時候還要判斷begin的位置是否到達right,如果沒有則與基準值交換

【圖解】

【程式碼實現】

size_t Potion1(int *arr, int left, int right)
{
	int begin = left;
	int end = right-1;
	int key = arr[end];
	while (begin < end)
	{
		while (begin < end&&arr[begin] <= key)//從左向右依次找比基準值大的位置
		{
			begin++;
		}
		while (begin<end&&arr[end] >= key)//從右向左依次找比基準值小的位置
		{
			end--;
		}
		if (begin < end)//如果找到了一對,就相互交換
		{
			swap(arr[begin], arr[end]);
		}
	}
	if (begin != right)//這時候判斷begin的位置是否到達right,如果沒有則與基準值交換
	{
		swap(arr[begin], arr[right-1]);
	}
	return begin;
}

二、挖坑法

【基本思想】顧名思義,就是挖坑啊

(1)先找一個基準值,將基準值儲存起來,將基準值所在的位置設定為坑;

(2)設定一個指標begin,從前向後找比基準值大的數字並將該數字填入坑中,那麼該數字之前所在的位置就變為新坑;

(3)再設定一個指標end,從後向前找比基準值小的數字,找到之後將該數字填入坑中;

(4)保證begin指標先走,end指標再走,然後迴圈(2)(3)步驟;

(5)直到left<right之後,將儲存的基準值放入begin所在的位置;

【圖解】

挖坑圖解

【程式碼實現】

size_t Potion2(int* arr, int left, int right)
{
	int begin = left;
	int end = right-1;
	int key = arr[right - 1];//儲存基準值
	while (begin < end)
	{
		while (begin < end&&arr[begin] <= key)
			begin++;//從前向後找比基準值大的
		if (begin < end)
			arr[end--] = arr[begin];//找到之後填坑
		while (begin < end && arr[end] >= key)
			end--;//從後往前找比基準值小的
		if (begin < end)
			arr[begin++] = arr[end];//找到之後填上個坑
	}
	arr[begin] = key;
	return begin;
}

三、前後指標法

【基本思想】

【程式碼實現】

int Potion3(int*arr, int left, int right)
{
	int key = arr[right - 1];
	int cur = left;
	int pre = cur - 1;
	while (cur < right)
	{
		if (arr[cur] < key&& ++pre != cur)
		{
			swap(arr[cur], arr[pre]);
		}
		cur++;
	}
	if (++pre != right)
	{
		swap(arr[pre], arr[right - 1]);
	}
	return pre;
}

四、非遞迴實現

#include<stack>
void QuickSortNor(int *arr, int size)
{
	int left = 0;
	int right = size;
	stack<int> s;
	s.push(right);
	s.push(left);
	while (!s.empty())
	{
		int begin = s.top();
		s.pop();
		int end = s.top();
		s.pop();
		if (begin < end)
		{
			int mid = Potion2(arr, begin, end);
			//儲存右邊區間
			s.push(end);
			s.push(mid + 1);
			//儲存左邊區間
			s.push(mid);
			s.push(begin);
		}
	}	
}

五、兩種優化方案

優化一:上述的幾種方法,我們都是以陣列最後一個元素為基準的,這樣勢必為造成誤差,例如陣列若為arr={1,6,5,3,4,6,7,8}或者arr={7,4,3,2,4,2,6,1},這樣的陣列在分割槽時,勢必會造成分割槽沒有作用,陣列還是集中在左邊區域或者右邊區域,那麼為了避免出現這種左右分割槽不均勻的情況出現,我們採用了一種叫做三數取中法的方式優化程式碼。

三數取中法程式碼實現

int GetMidNum(int* arr, int left, int right)//從陣列的最左邊,最右邊以及中間選擇一箇中間的數字作為基準值
{
	assert(arr);
	int mid = left + ((right-left) >> 1);
	if (arr[left] < arr[right - 1])
	{
		if (arr[left]>arr[mid])
			return left;
		else if (arr[right - 1] < arr[mid])
			return right - 1;
		else
			return mid;
	}
	else
	{
		if (arr[left] < arr[mid])
			return left;
		else if (arr[right - 1] > arr[mid])
			return right - 1;
		else
			return mid;
	}
}

優化二:插入排序+快速排序

void InsertSort(int *array, size_t size)//插入排序
{
	for (size_t i = 0; i < size; i++)
	{
		int key = array[i];
		int end = i - 1;
		while (end >= 0 && key < array[end])
		{
			array[end + 1] = array[end];
			end--;
		}
		array[end + 1] = key;//插入元素
	}
}
void QuickSort(int* arr, int left, int right)
{
	if (right-left < 5)//當分割槽小於一定數字之後
	{
		InsertSort(arr, right-left);
	}
	else
	{
		size_t mid = Potion2(arr, left, right);
		QuickSort(arr, left, mid);
		QuickSort(arr, mid + 1, right);

	}
}

五、完整的程式碼

#define _CRT_SECURE_NO_WARNINGS 1
using namespace std;
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<assert.h>
void InsertSort(int *array, size_t size)
{
	for (size_t i = 0; i < size; i++)
	{
		int key = array[i];
		int end = i - 1;
		while (end >= 0 && key < array[end])
		{
			array[end + 1] = array[end];
			end--;
		}
		array[end + 1] = key;//插入元素
	}
}
//三數取中法
int GetMidNum(int* arr, int left, int right)
{
	assert(arr);
	int mid = left + ((right-left) >> 1);
	if (arr[left] < arr[right - 1])
	{
		if (arr[left]>arr[mid])
			return left;
		else if (arr[right - 1] < arr[mid])
			return right - 1;
		else
			return mid;
	}
	else
	{
		if (arr[left] < arr[mid])
			return left;
		else if (arr[right - 1] > arr[mid])
			return right - 1;
		else
			return mid;
	}
}
//左右指標法
size_t Potion1(int *arr, int left, int right)
{
	int begin = left;
	int end = right-1;
	//int key = arr[end];
	int index = GetMidNum(arr, left, right);
	if (index != right-1)
	{
		swap(arr[index], arr[right - 1]);
	}
	int key = arr[right - 1];
	while (begin < end)
	{
		while (begin < end&&arr[begin] <= key)
		{
			begin++;
		}
		while (begin<end&&arr[end] >= key)
		{
			end--;
		}
		if (begin < end)
		{
			swap(arr[begin], arr[end]);
		}
	}
	if (begin != right)
	{
		swap(arr[begin], arr[right-1]);
	}
	return begin;
}
//挖坑法
size_t Potion2(int* arr, int left, int right)
{
	int begin = left;
	int end = right-1;
	int index = GetMidNum(arr, left, right);
	if (index != right - 1)
	{
		swap(arr[index], arr[right - 1]);
	}
	int key = arr[right - 1];
	while (begin < end)
	{
		while (begin < end&&arr[begin] <= key)
		{
			begin++;
		}
		if (begin < end)
			arr[end--] = arr[begin];

		while (begin < end && arr[end] >= key)
			end--;
		if (begin < end)
			arr[begin++] = arr[end];
	}
	arr[begin] = key;
	return begin;
}
//前後指標法
int Potion3(int*arr, int left, int right)
{
	//int key = arr[right - 1];
	int cur = left;
	int pre = cur - 1;
	int index = GetMidNum(arr, left, right);
	if (index != right - 1)
	{
		swap(arr[index], arr[right - 1]);
	}
	int key = arr[right - 1];
	while (cur < right)
	{
		if (arr[cur] < key&& ++pre != cur)
		{
			swap(arr[cur], arr[pre]);
		}
		cur++;
	}
	if (++pre != right)
	{
		swap(arr[pre], arr[right - 1]);
	}
	return pre;
}
void QuickSort(int* arr, int left, int right)
{
	if (right-left < 4)
	{
		InsertSort(arr, right-left);
	}
	else
	{
		size_t mid = Potion2(arr, left, right);
		QuickSort(arr, left, mid);
		QuickSort(arr, mid + 1, right);

	}
}
void Print(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[] = { 2, 0, 4, 6, 5, 7, -1, -3, 5, 8, 9,10};
	Print(arr, sizeof(arr) / sizeof(arr[0]));
	QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
	Print(arr,sizeof(arr) / sizeof(arr[0]));
	getchar();
	return 0;
}