1. 程式人生 > 實用技巧 >排序與複雜度分析

排序與複雜度分析

排序

桶排序

適用情況

元素值分佈相對集中的序列(需要設計良好的對映規則)

排序過程
  1. 按對映規則,申請桶陣列
  2. 遍歷,將元素分到對應的桶
  3. 桶內排序
  4. 遍歷,桶外合併
void bucketSort(int* arr, int len)
{
	int maximum = INT_MIN, minimum = INT_MAX;  //若保證arr全是非負數,可以簡化對映規則
	int index;
	for (int i = 0; i < len; ++i) {
		maximum = max(maximum, arr[i]);
		minimum = min(minimum, arr[i]);
	}

	int bucketSize = (maximum - minimum) / 10 + 1;  //確保把最小數對映到下標為0
	vector<int>* bucket = new vector<int>[bucketSize];

	for (int i = 0; i < len; i++)  //陣列入桶
	{
		index = arr[i] / 10 - minimum / 10 + 1;
		bucket[index].emplace_back(arr[i]);
	}

	for (int i = 0; i < bucketSize; ++i)  //桶內排序
		if (!bucket[i].empty())
			sort(bucket[i].begin(), bucket[i].end());

	index = 0;
	for (int i = 0; i < bucketSize; ++i)  //桶寫回陣列
		for (int j = 0; j < bucket[i].size(); ++j)
			arr[index++] = bucket[i][j];

	delete []bucket;
	bucket = nullptr;
}
說明
  • 若對映規則過鬆,元素集中在單桶內,則排序演算法退化成桶內比較排序演算法
  • 若對映規則過緊,每個元素分佈在不同的桶上,排序演算法退化成計數排序
  • 穩定性取決於桶內的排序演算法

插入排序

適用情況

資料量小,或基本有序

排序思路(整體而言是往一個新的有序的序列中插入新的元素直到所有元素有序)
  1. 下標從小到大,遍歷陣列
  2. 比較相鄰的兩個元素是否符合規則,不符合則交換,繼續內迴圈,若符合則退到外迴圈
template<class T>
void insertSort(T* arr, int len, bool flag) {  //插入排序
	for (int i = 1; i < len; ++i)
		for (int j = i - 1; j >= 0 && compare(arr[j + 1], arr[j], flag); --j)
			swap(arr[j + 1], arr[j]);
	return;
}
  • 說明:

compare函式是為了支援升序,降序兩種排序方式

template<class T>
bool compare(const T& x, const T& y, bool flag) {  //引用傳遞不產生臨時物件
	if (flag)
		return x < y;
	else
		return x > y;
}
動態排序過程

希爾排序

適用情況

同插入排序

排序思路

在插入排序外加一層迴圈,設定間隔將元素分組,組內插入排序。(下一次粒度更小的分組排序時元素相對有序)

template<class T>
void shellSort(T* arr, int len, bool flag) {  //希爾排序

	for (int gap = len / 2; gap > 0; gap /= 2)  //shell增量序列
		for (int i = gap; i < len; ++i)
			for (int j = i - gap; j >= 0 && compare(arr[j + gap], arr[j], flag); j -= gap)
				swap(arr[j + gap], arr[j]);
	//int k = 1;
	//while (pow(2, k) - 1 < len)
	//	++k;
	//while (int gap = static_cast<int>(pow(2, k) - 1) > 0) {  //Hibbard增量序列
	//	for (int i = gap; i < len; ++i) {
	//		for (int j = i - gap; j >= 0 && compare(arr[j + gap], arr[j], flag); j -= gap)
	//			swap(arr[j + gap], arr[j]);
	//	}
	//	--k;
	//}
	return;
}

氣泡排序

適用情況

排序效率倒數第一,而且當元素逆序時效果最差。

排序思路(每次選擇最大(最小)的元素置於末尾)
  1. 外迴圈遍歷陣列
  2. 內迴圈比較相鄰的元素,若不符合則發生交換

基礎版

template<class T>
void bubbleSort(T* arr, int len, bool flag)
{
	for (int i = 1; i < len; ++i) {
		for (int j = 0; j < len - i; ++j) {  //從後往前,每次迴圈把第k小的數放在k的位置上
			if (compare(arr[j + 1], arr[j], flag)) 
				swap(arr[j], arr[j + 1]);
		}
		//printArray(arr, 6);
	}
}

改進版:當序列已有序時不再排序(收效甚微)

template<class T>
void bubbleSort(T* arr, int len, bool flag)
{
	for (int i = 1; i < len; ++i) {
		bool exchange = false;  //是否當已經有序
		for (int j = 0; j < len -i; ++j) {  //從後往前,每次迴圈把第k小的數放在k的位置上
			if (compare(arr[j + 1], arr[j], flag)) {
				swap(arr[j], arr[j + 1]);
				exchange = true;
			}
		}
		printArray(arr, 6);
		if (!exchange)
			return;
	}
}
動態排序過程

快速排序

適用情況

大多數情況下第一名

排序思路(冒泡基礎之上的分治遞迴)
  1. 將序列劃分為左右序列,遞迴
  2. 劃分中,以最左元素為支點,從右向左選擇小於支點覆蓋到左序列,從左到右選擇大於支點覆蓋到右序列,直到序列下標相遇,返回相遇的下標。
template<class T>
int quickSortPartition(T* arr, int left, int right) {  //交替覆蓋
	int l = left, r = right, x = arr[left];
	while (l < r)
	{
		while (l < r && arr[r] >= x)  //從右向左尋找第一個小於x的數,不必考慮越界
			--r;
		if (l < r)
			arr[l++] = arr[r];  //替換左元素
		while (l < r && arr[l] <= x)  //從左向右尋找第一個大於x的數,不必考慮越界
			++l;
		if (l < r)
			arr[r--] = arr[l];  //替換右元素
	}
	arr[l] = x;  //寫回支點元素
	return l;
}
template<class T>
void quickSort(T* arr, int left, int right) {
	if (left >= right)
		return;
	int benchmark = quickSortPartition(arr, left, right);
	quickSort(arr, left, benchmark - 1);
	quickSort(arr, benchmark + 1, right);
}

輸入序列為7,3,8,5,1,2時執行情況如下

動態排序過程

歸併排序

適用情況

適合外部排序

排序思路
  1. 二路歸併將序列兩兩劃分,遞迴直到最後只有序列只有單個元素(分)
  2. 將相鄰序列兩兩合併,直到合成一個序列(治)
template<class T>
void merge(vector<T>& arr, int left, int mid, int right) {  //治

	vector<T> temp;  //臨時陣列空間
	temp.resize(right - left + 1);
	int i = left;  // i是左序列的下標
	int j = mid + 1;  // j是右序列的下標
	int t = 0;  // t是臨時陣列的下標

	while (i <= mid && j <= right) {  //掃描直到有序列結束
		if (arr[i] <= arr[j]) {  //左序列小,左序列下標為i的值寫入臨時陣列
			temp[t++] = arr[i++];
		}
		else {  //右序列小,右序列下標為j的值寫入臨時陣列
			temp[t++] = arr[j++];
		}
	}
	while (i <= mid) {  //若左序列未掃描完,餘下寫入臨時陣列
		temp[t++] = arr[i++];
	}

	while (j <= right) {  //若右序列未掃描完,餘下寫入臨時陣列
		temp[t++] = arr[j++];
	}

	t = 0;  //重置臨時陣列下標
	while (left <= right) {  // 將臨時陣列回寫到原序列中
		arr[left++] = temp[t++];
	}
}

template<class T>
void mergeSort(vector<T>& arr, int left, int right) {  //分
	if (left < right) {  //當left=right時已經時只有一個元素
		int mid = (left + right) / 2;
		mergeSort(arr, left, mid);
		mergeSort(arr, mid + 1, right);  //當left+right是奇數時,左邊多分一個
		merge(arr, left, mid, right);
	}
}
動態排序過程

選擇排序

適用情況

只需要Top的資料(任何序列排序都是O(N^2^))

排序原理

兩趟迴圈,每次最小記錄的下標,第k遍將第k小的數放在k的位置上

template<class T>
void selectSort(T* arr, int len, bool flag)
{
	for (int i = 0; i < len; ++i) {
		int index = i;
		for (int j = i + 1; j < len; ++j) {  //每次選擇最小的放在最前面
			if (compare(arr[j], arr[index], flag))
				index = j;
		}
        if(i != index)
		swap(arr[i], arr[index]);
	}
}
動態排序過程

堆排序

堆的實現blog

時空複雜度及穩定性

排序演算法 平均時間複雜度 最壞時間複雜度 最好時間複雜度 空間複雜度 穩定性
桶排序 O(M+N)
插入排序 O(N^2^) O(N^2^) O(N) O(1) 穩定
希爾排序 O(1) 不穩定
氣泡排序 O(N^2^) O(N^2^) O(N) O(1) 穩定
快速排序 O(Nlog~2~N) O(N^2^) O(Nlog~2~N) O(Nlog~2~N) 不穩定
歸併排序 O(Nlog~2~N) O(Nlog~2~N) O(Nlog~2~N) O(N) 穩定
選擇排序 O(N^2^) O(N^2^) O(N^2^) O(1) 不穩定
堆排序 O(Nlog~2~N) O(Nlog~2~N) O(Nlog~2~N) O(1) 不穩定