對7種排序算法的總結

排序算法是最基本的知識了,面試中也是常考的知識點,尤其是快速排序考察最多,也要求手寫出來,並分析他的時間複雜度。下面是我對所學到的知識的一個總結。

排序算法總共分爲5大類:
第一類:插入排序 直接插入、希爾排序
第二類:選擇排序簡單選擇排序 堆排序
第三類:交換排序冒泡排序 快速排序
第四類:歸併排序
第五類:基數排序(考察很少,知道概念就行)

在這裏插入圖片描述

直接插入排序

方法:假設元素從小到大排序,先固定第一個元素,讓第二個元素開始往後查找,把後面的元素放到一個臨時變量裏面拎出來,讓他和他前面的元素比較,如果比前面的元素小,就把被比較的元素向後面移動一格,並且指針向前移動一位,確保臨時變量是依次和前面的元素比較的。此方法需要兩個指針。填充位置爲每次比較排好序的元素。
需要注意確定邊界值,j指針>=0
時間複雜度計算:(1+n)*n/2=O(n^2)相當於九九乘法表的圖形
在這裏插入圖片描述

#include <iostream>
using namespace std;

void StraightInsertion(int a[], int length)
{
	int i = 1, j = 0;
	int temp = 0;
	/*當前排到第幾個元素*/
	for (i;i<length;i++)
	{
		/*保存當前的元素*/
		temp = a[i];
		/*依次檢索是否小於前面的元素*/
		j = i - 1;
		while (temp < a[j] && j >= 0)
		{
			/*當前被檢測元素向後移動一格*/
			a[j + 1] = a[j];
			j--;
		}
		
		/*找到插入位置*/
		
			a[j + 1] = temp;
	}
}


int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	StraightInsertion(a, 10);

	return 0;
}

希爾排序

方法:把整個一串數按照/2進行分組,10個數/2先分成5組,然後5/2=2,再分成兩組,直到爲1,這樣再進行組內直接插入排序。只是單純的比直接插入排序多了一個分組的步驟。
代碼和直接插入幾乎一樣,只不過把j移動的距離從1改爲組的個數h
平均時間複雜度提高了,可以做到部分有序,再進行直接插入
這裏注意:組間是交叉進行組內比較的,不是一個組進行完進行下一個組。
該算法是不穩定的。

在這裏插入圖片描述

#include <iostream>
using namespace std;

void shellSort(int a[], int length)
{
	int i = 0, j = 0, temp = 0, h = length/2;/*組的個數*/
	//分組
	for (h;h>=1;h/=2)
	{
		/*各組交替比較組內的元素 也就是使用直接插入排序*/
		for (i = h; i < length; i++)
		{
			temp = a[i];
			j = i - h;
			while (temp < a[j] && j >= 0)
			{
				/*當前被檢測元素在組內向後移動一個元素*/
				a[j + h] = a[j];
				j -= h;
			}
			/*找到插入位置*/
			a[j + h] = temp;
		}
	}
}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	
	shellSort(a, 10);
	return 0;
}

簡單選擇排序

思想:從一堆無序的數字中,先選擇一個最小的,然後選擇次小的,一次類推。。是最簡單的排序,符合人們的正常思維。
方法:需要額外開闢一個數組,用來存儲拿出來的元素。首先讓一串數字的首元素默認是當前最小值,記錄他的標號,用min_index表示,然後讓後面的元素依次和標號所只的元素比較,若比他小,記錄下標號,先不着急交換,因爲沒有比較完,後面可能有更小的,直到比較到頭,然後此時
min_index是最小的值的標號,然後將第一個元素和其交換,此時最小的找到,接着找次小的,此時i指針後移一位,j接着從i的下一個開始往後捋。(i的範圍是倒數第二個數字,不用再比較最後歐一個元素,因爲前面都是小的了,自然最後的是最大的值)。
該算法不穩定,對未排序的算法進行了未知的修改。
在這裏插入圖片描述

#include <iostream>
using namespace std;

void exchange(int a[], int i, int j)
{
	int temp = 0;
	temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}
void selectionSort(int a[], int n)
{
	int i, j, min_index;
	for ( i = 0; i < n - 1; i++)
	{
		min_index = i;
		for (j=i+1;j<n;j++)
		{
			if (a[j] < a[min_index])
				/*找到本次最小值*/
				min_index = j;
		}
		if (min_index != i)
			/*交換*/
			exchange(a, min_index, i);

	}

}



int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	selectionSort(a, 10);
	
	return 0;
}

堆排序

需要用到樹的結構,需要的是一個完全二叉樹。
步驟:
1。建立一個大根/小根堆
2.堆化
3.直接選擇排序
若從小到大排序,先建立一個大根堆,然後按層遍歷到一個數組,然後將收尾元素交換,此時保證了最後的元素是最大的值,之後讓iCurront指針向前移動一位,將前面的元素進行堆化,用到遞歸。遞歸的終止條件是該元素是葉子結點,沒有左孩子了,也就是說該元素的左孩子已經是排好序的無效結點了。如何找到那個有效結點呢?公式:(n-1)/2 n代表最後的元素下標
該算法不穩定。

在這裏插入圖片描述

#include <iostream>
using namespace std;

int iCurrentPos = 0;

void exchange(int a[], int b, int c)
{
	int temp = 0;
	temp = a[b];
	a[b] = a[c];
	a[c] = temp;
}
void heapify(int a[], int indexRoot)
{
	int indexL = indexRoot * 2 + 1, indexR = indexL + 1;
	int indexMax = indexRoot;
	/*終止條件,當時葉子節點的時候 如何判斷是否是葉子節點,
	就是看他的左孩子是否是無效的節點
	*/
	if (indexL > iCurrentPos)
		return;
	/*比較左子節點*/
	if (a[indexL] > a[indexMax])
		indexMax = indexL;
	/*比較右子節點*/
	if (a[indexR] > a[indexMax] && indexR < iCurrentPos)
		indexMax = indexR;
	/*交換結點*/
	if (indexMax != indexRoot)
	{
		exchange(a, indexRoot, indexMax);
		heapify(a, indexMax);
	}
	
}

/*建立一個大根堆*/
void createHeap(int a[], int n) //n爲第n個元素
{
	/*找到有效的結點*/
	int i = (n - 1) / 2;
	for (i; i >= 0; --i)
	{
		heapify(a, i);

	}
}

/*堆排序*/

void heapSort(int a[], int n) /*n指的總共的元素個數*/
{
	iCurrentPos = n - 1;
	createHeap(a, n - 1);
	for (; iCurrentPos > 0; --iCurrentPos, heapify(a, 0))
		exchange(a, 0, iCurrentPos);

}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	heapSort(a, 10);
	
	return 0;
}

冒泡排序

冒泡排序就是兩兩交換,然後每一次遍歷都會讓最大放在數組的最後。然後接着從頭倆倆比較。
在這裏插入圖片描述

#include <iostream>
using namespace std;

void exchange(int a[], int i, int j)
{
	int temp = 0;
	temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}
void SimpleExchange(int a[], int length)
{
	int i = 0,/*已排序數量*/ j = 1;
	for (i = 0; i < length - 1; i++)
	{
		for (j = 1; j < length - i; j++)
		{
			/*檢查是否交換*/
			if (a[j] < a[j - 1])
				exchange(a,j - 1, j);
		}
	}
}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	SimpleExchange(a, 10);

	return 0;
}

快速排序

典型的遞歸的算法。目的是找到一個軸pivot,讓左邊的都比他小,右邊的都比他大。
如何找到這個值的位置呢?
讓頭尾各指一個指針,把首元素的值拿出來,讓他作爲軸,位置空缺,讓尾元素和他比較,是大的就留在位置不動,然後移動j指針向前,直到找到一個元素的值比第一個小,就讓其放在空缺的位置,此時他的位置空缺,再讓i指針向後移動,和拿出的那個首元素接着比較,若小,就不動,指針後移,直到找到比他大的元素,放在空白位置。然後尾指針往前移動。。。直到 i j指針重合就是首元素該插入的位置,就是中間元素,保證了左側的都是小的,右側都是大的.然後左右遞歸。
快排的時間複雜度分析
在這裏插入圖片描述
最佳時間複雜度是nlogn 每次分割成兩半,分多少層呢?就是N不斷的/2,也就是logn層,每層的從左到右交換都是n的操作,所以時間複雜度最佳是nlogn
最壞時間複雜度n^2 在最壞的情況下,待排序的序列爲正序或者逆序,每次劃分只得到一個比上一次劃分少一個記錄的子序列。也就是說每次都是1個數和其餘要進行遞歸的序列。這樣就需要n層,每層是n的操作 即複雜度是n^2
平均複雜度就是第一次劃分是最佳的劃分,但是左右側就是有最壞的情況,平均下來還是nlogn
空間複雜度就是每分一層,都會有一些臨時變量存在,這樣就是看有多少層,就是nlongn
快排的性能和什麼有關係?
和pivot選取的方法有關係。pivot是可以隨便選擇的,但是要注意選到的值是中間的值,還是過大或者過小的。要是極值可能就會得到最壞的時間複雜度。
那如何避免最壞時間複雜度呢如何優化快排呢
1.常用的方法就是三數取中。就是找到頭,中間和尾的元素,哪個是中間值,就把哪個作爲軸。
2.直接插入
由於是遞歸程序,每一次遞歸都要開闢棧幀,當遞歸到序列裏的值不是很多時,我們可以採用直接插入排序來完成,從而避免這些棧幀的消耗。
還有一種方法,用的不是很多
讓第一個元素的左邊放一個可以擴展的區域,假設最後的元素是軸,從第一個元素開始比較,如果比軸小,就把他包括進這個區域,如果大了,就把軸前面的元素和這個元素做交換,此時檢查交換後的這個元素是不是還是比軸大,若還是就把他接着和軸前一個的前一個交換。。直到和後面交換的元素重合,然後軸剩下了,放在什麼位置呢?就把他和區的下一個位置做交換。
具體還有好幾種方法,就不一一列舉了。這篇博客說的很明白。
詳見可見博客地址

如今的快排和直接插入聯合使用了。快排當到一定的深度,量少,個數少趨於有序的時候就不佔有優勢,適合直接插入,所以將兩者聯合使用效果更佳。微軟目前使用的就是這樣的排序算法。

#include <iostream>
using namespace std;

void QuickSort(int a[], int min,int max)
{
	/*遞歸的結束條件*/
	if (min >= max)
		return;
	int pivot = a[min]; 
	int _min = min, _max = max;
	while (min < max)
	{
		/*從大端開始比較*/
		while (a[max] >= pivot && min<max)
			max--;
		/*如果不再大於等於pivot 就進行交換*/
		if (min < max)
		{
			a[min] = a[max];
			min++;
		}
		/*從小端開始比較*/
		while (a[min] <= pivot && min < max)
			min++;
		/*如果不再小於等於pivot,就交換*/
		if (min < max)
		{
			a[max] = a[min];
			max--;
		}

			
	}

	/*找到中樞的位置pivot*/
	a[min] = pivot;
	/*遞歸左側*/
	QuickSort(a, _min, min - 1);
	/*遞歸右側*/
	QuickSort(a, max + 1, _max);

}

int main(void)
{
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	QuickSort(a,0,9);
	
	return 0;
}

歸併排序

視頻鏈接
歸併算法和前幾種都不一樣,是一個特殊的算法,他就是兩個步驟,先分解,再整合。
Divide and Conquer(分而治之)

實現思路:我們需要開闢一個數組,然後將分解出的數組在頭尾均設立指針,左側最小,左側最大,右側最小,右側最大。數組需要遍歷的元素就是左側最小到右側最大。
歸併時分三種情況討論:
1.當左右兩側均沒有到末尾,就比較左右的誰最小,放到數組的相應位置。然後移動指針和數組的指針。
2.當左側到了頭,就說明右側有小的,就把右側的元素給數組並移動指針。
3.當右側到了頭,說明左側有小的。把左側的元素給數組並移動指針。
在這裏插入圖片描述

#include <iostream>
using namespace std;


void merge(int a[], int min_l, int max_l, int min_r, int max_r,int temp[])
{
	int k, i = min_l, j = min_r;
	for (k=min_l;k<=max_r;k++)
	{
		/*左右兩側均沒到結尾*/
		if (i <= max_l && j <= max_r)
		{
			if (a[i] <= a[j])
				temp[k] = a[i++];
			else
				temp[k] = a[j++];
		}
		/*左側到了結尾*/
		else if (i>max_l)
			temp[k] = a[j++];
		/*右側到結尾*/
		else
			temp[k] = a[i++];

	}
	/*把值付給數組a 數據填回*/
	for (k=min_l;k<=max_r;k++)
	{
		a[k] = temp[k];

	}
}
void MergingSort(int a[],int min,int max,int temp[])
{
	 /*終止條件*/
	if (min>=max)
		return;

	/*divide*/
	int middle = min+(max - min) / 2;
	MergingSort(a, min, middle,temp);
	MergingSort(a, middle + 1, max,temp);
	/*conquer*/
	merge(a, min, middle, middle + 1, max,temp);
}


int main(void)
{
	int b[10] = {0};
	int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
	MergingSort(a, 0, 9, b);
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章