冒泡排序(重溫經典算法系列)







最直接和原始的冒泡代碼

// 單個元素時肯定是有序的;故首元素單獨有序,從 [1,n) 爲待排元素所在的區間;
	// i 控制: n-1輪冒泡(即執行一系列兩兩元素對比的操作),第 i 輪能夠選出第 i 大(或者小)的元素值,n-1輪 選出 n-1 個較大(或者小)值之後,整個序列即可有序;
	for (int i = 1; i < n ; ++i) {
		// j 表示: 當前輪次所需要排序(對比大小)的每1對數的索引;
		for (int j = 1; j < n; ++j) {
			if (arr[j - 1] > arr[j]) {// 最終結果升序,故而前大後小時執行元素交換;不取等號是爲了穩定排序;
				std::swap(arr[j - 1], arr[j]);
			}
		}
	}


雙層循環的控制表達不唯一

// 外層i 控制 n-1 輪即可; 
	for (int i = 0; i < n - 1; ++i) {
		// j 表示: 待排序元素區間即可;
		for (int j = n - 1; j > 0; --j) {
			if (arr[j - 1] > arr[j]) {
				std::swap(arr[j - 1], arr[j]);
			}
		}
	}


思考:原始代碼贅餘工作問題


原始代碼的設計問題所在:
原始代碼執行了N^2次元素掃描:每一輪都從開頭掃描至末尾

每一輪冒泡都會加長有序元素區間段:
自前往後冒泡則出現在末端,自後往前則出現在前段;

代碼優化:
雙側循環可跳過對該區間元素的掃描,
僅掃描無序元素區間,這便是代碼優化的理論依據


雙層控制條件語句的優化


從前到後掃描

for (int i = 0; i < n - 1; ++i) {//冒泡次數;
	for (int j = 0; j < n - 1 - i; ++j) {//自前向後冒泡比較;
	//Todo ;
	}
}

自後往前掃描

for (int i = 0; i < n - 1; ++i) {}
	for (int j = n - 1 - i; j > 0; --j) {}//自後往前冒泡比較;

循環控制條件的總結(綜述)

無論具體循環的條件如何設置,本質依舊都是確保a的索引在[0,n)範圍;
其次要確保:
外層控制 n-1 輪冒泡,內層控制待排序元素區間即可;
使用<符號而非是<=符號時:
0與n-1配對使用,1與n配對使用.


問題之二與代碼的繼續優化


問題描述及其優化

若當前輪次的冒泡使得整個序列已經全部有序,或者待排序的數據本身就是有序的,
則後續執行的冒泡操作都是贅餘工作;
優化辦法:
引入 flag標誌即可,
記錄當前輪次冒泡是否有元素交換操作,無交換則表明已序列全部有序.

加入有序標誌的簡單冒泡

template <typename T>
void BubbleSort(T* arr, const std::size_t n) {
	assert(arr);
	bool notSortedFlag = true;
	//也可在外層循環內部聲明 bool sortedFlag=true;之後每輪內層循環結束都判斷一次flag,若重置true爲false不被執行則說明已經有序,此時藉助break跳出循環;
	for (std::size_t i = n - 1; i && notSortedFlag; --i) {
		notSortedFlag = false;
		for (std::size_t j = 0; j < i; ++j) {
			if (arr[j] > arr[j + 1]) {
				notSortedFlag = true;
				std::swap(arr[j], arr[j + 1]);
			}
			//該部分代碼再次改進如下;
		}
	}
}

溫馨提示:
assert(arr);
該斷言語句需要使用頭文件(C++#include <cassert>


問題之三:掃描部分有序數列導致贅餘工作


之前是序列整體有序之後的贅餘工作問題;
若給出的待排序數列直接是前段或者後段部分有序的,則
也會因爲執行了對部分有序元素的區間的掃描而導致工作贅餘.
優化辦法:
引入哨兵標誌,記錄有序區間與無序區間段的邊界,
每一輪冒泡中,最後執行交換操作的索引位置即爲有序與無序的分界處.

加入有序標誌與有無序邊界記錄的進一步優化版冒泡

自左往右掃描,則左段無序、右段有序;
反之,類比可知.
template <typename T>
void BubbleSort2(T* arr, const std::size_t n) {
	assert(arr);
	std::size_t lastExchangeIndex = 0;
	std::size_t sortedBorder = n - 1;//有序無序分界,最開始默認全無序;
	//n-1次冒泡獲得n-1個最值;
	for (std::size_t i = 0; i < n - 1; ++i) {//冒泡次數仍是n-1趟;
		bool isSortedFlag = true;
		for (std::size_t j = 0; j < sortedBorder; ++j) {
			if (arr[j] > arr[j + 1]) {
				isSortedFlag = false;
				std::swap(arr[j], arr[j + 1]);
				lastExchangeIndex = j;
			}
		}
		sortedBorder = lastExchangeIndex;
		if (isSortedFlag) { break; }//未發生交換時flag未被重置爲false,表明數列已經有序;
	}
}



冒泡優化終極版本:雙端冒泡法

進一步優化的雙向冒泡法(同快排設計思想),同時刷選最大、最小值往兩端靠攏;
template <typename T>
void HeadTailBubbleSort(T* arr, const std::size_t n) {
	assert(arr);
	//雙端掃描,無序區間爲中間部分,且不斷縮減;
	std::size_t leftIndex = 0;//該索引往左側有序;
	std::size_t rightIndex = n - 1;//該索引往右側有序;

	//n-1次冒泡獲得n-1個最值;
	while (leftIndex < rightIndex) {
		std::size_t j = rightIndex;
		rightIndex = 0;
		bool isSortedFlag = true;//假設有序;

		for (std::size_t i = leftIndex; i < j; ++i) {
			//自前向後掃描;
			if (arr[i] > arr[i + 1]) {
				std::swap(arr[i], arr[i + 1]);
				rightIndex = i;
				isSortedFlag = false;//實際無序;
			}
			//std::cout << "自前向後掃描: i = " << i << " j = "<< j << " maxIndex = " << maxIndex << std::endl;
			//RandomArrayFuntionHelper::PrintArray(arr, n);
		}
		if (isSortedFlag) { break; }

		isSortedFlag = true;
		j = leftIndex;
		leftIndex = 0;
		for (std::size_t i = rightIndex; i > j; --i) {
			//自後往前掃描;

			if (arr[i - 1] > arr[i]) {
				std::swap(arr[i - 1], arr[i]);//此處切勿出錯;
				leftIndex = i;
				isSortedFlag = false;
			}
			//std::cout << "自後往前掃描: i = " << i << " j = " << j << " minIndex = " << maxIndex << std::endl;
			//RandomArrayFuntionHelper::PrintArray(arr, n);	
		}
		if (isSortedFlag) { break; }
	}
}


參考材料


冒泡算法逐步改進的思想,請參考文章:
冒泡排序的算法分析與改進

冒泡算法排序過程的圖形化演示,請參考:
排序算法過程演示



交流方式
QQ —— 2636105163(南國爛柯者)
溫馨提示:
轉載請註明出處!!
2020年3月29日 20:17:43

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章