八種常用排序演算法

Rabbit_Judy發表於2019-03-29

01 演算法分類

image

02 時間複雜度

image

03 相關概念

穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。

不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面。

時間複雜度:對排序資料的總的操作次數。反映當n變化時,操作次數呈現什麼規律。

空間複雜度:是指演算法在計算機內執行時所需儲存空間的度量,它也是資料規模n的函式。

1、氣泡排序(Bubble Sort)

氣泡排序是一種簡單的排序演算法。它迴圈遍歷要排序的數列,一次比較兩個元素,如果前者比後者大就交換彼此的位置。

1.1 演算法描述

  • 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
  • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
  • 針對所有的元素重複以上的步驟,除了最後一個;
  • 重複步驟1~3,直到排序完成。

1.2 動畫演示

image

1.3 程式碼實現

//氣泡排序
/**
 * 5,7,2,9,4,1,0,5,7		共需要比較length-1輪
 * 5,7,2,9,4,1,0,5,7	
 * 5,2,7,9,4,1,0,5,7
 * 5,2,7,4,1,0,5,7,9
 * 2,5   
 */
public static void bubbleSort(int[]  arr) {
	//控制共比較多少輪
	for(int i=0;i<arr.length-1;i++) {
		//控制比較的次數
		for(int j=0;j<arr.length-1-i;j++) {
			if(arr[j]>arr[j+1]) {
				int temp=arr[j];
				arr[j]=arr[j+1];
				arr[j+1]=temp;
			}
		}
	}	
}

複製程式碼

2、快速排序(Quick Sort)

基本思想:通過一次排序將待排數列分隔成獨立的兩個數列,其中一部分數列的關鍵字均比另一部分數列的關鍵字小,則可分別對這兩部分數列繼續進行排序,以達到整個數列有序。

2.1 演算法描述

快速排序使用分治法來把一個串(list)分為兩個子串(sub-lists)。具體演算法描述如下:

  • 從數列中挑出一個元素,稱為 “基準”(pivot);
  • 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;
  • 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

2.2 動畫演示

image

2.3 程式碼實現

//快速排序
public static void quickSort(int[] arr,int start,int end) {
	if(start<end) {
		//把陣列中的第0個數字做為標準數
		int stard=arr[start];
		//記錄需要排序的下標
		int low=start;
		int high=end;
		//迴圈找比標準數大的數和比標準數小的數
		while(low<high) {
			//右邊的數字比標準數大
			while(low<high&&stard<=arr[high]) {
				high--;
			}
			//使用右邊的數字替換左邊的數
			arr[low]=arr[high];
			//如果左邊的數字比標準數小
			while(low<high&&arr[low]<=stard) {
				low++;
			}
			arr[high]=arr[low];
		}
		//把標準數賦給低所在的位置的元素
		arr[low]=stard;
		//處理所有的小的數字
		quickSort(arr, start, low);
		//處理所有的大的數字
		quickSort(arr, low+1, end);
	}
}
複製程式碼

3、插入排序(Insertion Sort)

工作原理:通過構建有序數列,對於未排序元素,在已排序數列中從後向前掃描,找到相應位置並插入。

3.1 演算法描述

一般來說,插入排序都採用in-place在陣列上實現。具體演算法描述如下:

  • 從第一個元素開始,該元素可以認為已經被排序;
  • 取出下一個元素,在已經排序的元素數列中從後向前掃描;
  • 如果該元素(已排序)大於新元素,將該元素移到下一位置;
  • 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  • 將新元素插入到該位置後;
  • 重複步驟2~5。

3.2 動畫演示

image

3.3 程式碼實現

//插入排序
public static void insertSort(int[] arr) {
	//遍歷所有的數字
	for(int i=1;i<arr.length;i++) {
		//如果當前數字比前一個數字小
		if(arr[i]<arr[i-1]) {
			//把當前遍歷數字存起來
			int temp=arr[i];
			int j;
			//遍歷當前數字前面所有的數字
			for(j=i-1;j>=0&&temp<arr[j];j--) {
				//把前一個數字賦給後一個數字
				arr[j+1]=arr[j];
			}
			//把臨時變數(外層for迴圈的當前元素)賦給不滿足條件的後一個元素
			arr[j+1]=temp;
		}
	}
}
複製程式碼

3.4 演算法分析

插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

4、希爾排序(Shell Sort)

1959年Shell發明,第一個突破O(n2)的排序演算法,是簡單插入排序的改進版。它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。

4.1 演算法描述

先將整個待排序的記錄數列分割成為若干子數列分別進行直接插入排序,具體演算法描述:

  • 選擇一個增量數列x1,x2,…,xk,其中xi>xj,xk=1;
  • 按增量數列個數k,對數列進行k 次排序;
  • 每次排序,根據對應的增量ti,將待排數列分割成若干長度為m 的子數列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個數列作為一個表來處理,表長度即為整個數列的長度。

4.2 動畫演示

image

4.3 程式碼實現

//希爾排序
public static void shellSort(int[] arr) {
	int k = 1;
	// 遍歷所有的步長
	for (int d = arr.length / 2; d > 0; d /= 2) {
		// 遍歷所有有元素
		for (int i = d; i < arr.length; i++) {
			// 遍歷本組中所有的元素
			for (int j = i - d; j >= 0; j -= d) {
				// 如果當前元素大於加上步長後的那個元素
				if (arr[j] > arr[j + d]) {
					int temp = arr[j];
					arr[j] = arr[j + d];
					arr[j + d] = temp;
				}
			}
		}
		System.out.println("第" + k + "次排序結果:" + Arrays.toString(arr));
		k++;
	}
}
複製程式碼

4.4 演算法分析

希爾排序的核心在於間隔數列的設定。既可以提前設定好間隔數列,也可以動態的定義間隔數列。動態定義間隔數列的演算法是《演算法(第4版)》的合著者Robert Sedgewick提出的。

5、選擇排序(Select Sort)

工作原理:首先在未排序數列中找到最小(大)元素,存放到排序數列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序數列的末尾。以此類推,直到所有元素均排序完畢。

5.1 演算法描述

n個記錄的直接選擇排序可經過n-1次直接選擇排序得到有序結果。具體演算法描述如下:

  • 初始狀態:無序區為R[1..n],有序區為空;
  • 第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個的新無序區;
  • n-1次結束,陣列有序化了。

5.2 動畫演示

image

5.3 程式碼實現

//選擇排序
public static void selectSort(int[] arr) {
	//遍歷所有的數
	for(int i=0;i<arr.length;i++) {
		int minIndex=i;
		//把當前遍歷的數和後面所有的數依次進行比較,並記錄下最小的數的下標
		for(int j=i+1;j<arr.length;j++) {
			//如果後面比較的數比記錄的最小的數小。
			if(arr[minIndex]>arr[j]) {
				//記錄下最小的那個數的下標
				minIndex=j;
			}
		}
		//如果最小的數和當前遍歷數的下標不一致,說明下標為minIndex的數比當前遍歷的數更小。
		if(i!=minIndex) {
			int temp=arr[i];
			arr[i]=arr[minIndex];
			arr[minIndex]=temp;
		}
	}
}
複製程式碼

5.4 演算法分析

表現最穩定的排序演算法之一,因為無論什麼資料進去都是O(n2)的時間複雜度,所以用到它的時候,資料規模越小越好。唯一的好處可能就是不佔用額外的記憶體空間了吧。理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。

6、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

6.1 演算法描述

  • 將初始待排序關鍵字數列(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,則整個排序過程完成。

6.2 動畫演示

image

6.3 程式碼實現

//堆排序
public static void heapSort(int[] arr) {
	//開始位置是最後一個非葉子節點,即最後一個節點的父節點
	int start = (arr.length-1)/2;
	//調整為大頂堆
	for(int i=start;i>=0;i--) {
		maxHeap(arr, arr.length, i);
	}
	//先把陣列中的第0個和堆中的最後一個數交換位置,再把前面的處理為大頂堆
	for(int i=arr.length-1;i>0;i--) {
		int temp = arr[0];
		arr[0]=arr[i];
		arr[i]=temp;
		maxHeap(arr, i, 0);
	}
}

public static void maxHeap(int[] arr,int size,int index) {
	//左子節點
	int leftNode = 2*index+1;
	//右子節點
	int rightNode = 2*index+2;
	int max = index;
	//和兩個子節點分別對比,找出最大的節點
	if(leftNode<size&&arr[leftNode]>arr[max]) {
		max=leftNode;
	}
	if(rightNode<size&&arr[rightNode]>arr[max]) {
		max=rightNode;
	}
	//交換位置
	if(max!=index) {
		int temp=arr[index];
		arr[index]=arr[max];
		arr[max]=temp;
		//交換位置以後,可能會破壞之前排好的堆,所以,之前的排好的堆需要重新調整
		maxHeap(arr, size, max);
	}
}
複製程式碼

7、歸併排序(Merge Sort)

歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子數列合併,得到完全有序的數列;即先使每個子數列有序,再使子數列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。

7.1 演算法描述

  • 把長度為n的輸入數列分成兩個長度為n/2的子數列;
  • 對這兩個子數列分別採用歸併排序;
  • 將兩個排序好的子數列合併成一個最終的排序數列。

7.2 動畫演示

image

7.3 程式碼實現

//歸併排序
public static void mergeSort(int[] arr,int low,int high) {
	int middle=(high+low)/2;
	if(low<high) {
		//處理左邊
		mergeSort(arr, low, middle);
		//處理右邊
		mergeSort(arr, middle+1, high);
		//歸併
		merge(arr,low,middle,high);
	}
}

public static void merge(int[] arr,int low,int middle, int high) {
	//用於儲存歸併後的臨時陣列
	int[] temp = new int[high-low+1];
	//記錄第一個陣列中需要遍歷的下標
	int i=low;
	//記錄第二個陣列中需要遍歷的下標
	int j=middle+1;
	//用於記錄在臨時陣列中存放的下標
	int index=0;
	//遍歷兩個陣列取出小的數字,放入臨時陣列中
	while(i<=middle&&j<=high) {
		//第一個陣列的資料更小
		if(arr[i]<=arr[j]) {
			//把小的資料放入臨時陣列中
			temp[index]=arr[i];
			//讓下標向後移一位;
			i++;
		}else {
			temp[index]=arr[j];
			j++;
		}
		index++;
	}
	//處理多餘的資料
	while(j<=high) {
		temp[index]=arr[j];
		j++;
		index++;
	}
	while(i<=middle) {
		temp[index]=arr[i];
		i++;
		index++;
	}
	//把臨時陣列中的資料重新存入原陣列
	for(int k=0;k<temp.length;k++) {
		arr[k+low]=temp[k];
	}
}
複製程式碼

7.4 演算法分析

歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的效能不受輸入資料的影響,但表現比選擇排序好的多,因為始終都是O(nlogn)的時間複雜度。代價是需要額外的記憶體空間。

8、基數排序(Radix Sort)

基數排序是按照低位優先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。

8.1 演算法描述

取得陣列中的最大數,並取得位數; arr為原始陣列,從最低位開始取每個位組成radix陣列; 對radix進行計數排序(利用計數排序適用於小範圍數的特點);

8.2 動畫演示

image

8.3 程式碼實現

//基數排序(普通方法實現)
public static void  radixSort(int[] arr) {
	//存最陣列中最大的數字
	int max=Integer.MIN_VALUE;
	for(int i=0;i<arr.length;i++) {
		if(arr[i]>max) {
			max=arr[i];
		}
	}
	//計算最大數字是幾位數
	int maxLength = (max+"").length();
	//用於臨時儲存資料的陣列
	int[][] temp = new int[10][arr.length];
	//用於記錄在temp中相應的陣列中存放的數字的數量
	int[] counts = new int[10];
	//根據最大長度的數決定比較的次數
	for(int i=0,n=1;i<maxLength;i++,n*=10) {
		//把每一個數字分別計算餘數
		for(int j=0;j<arr.length;j++) {
			//計算餘數
			int ys = arr[j]/n%10;
			//把當前遍歷的資料放入指定的陣列中
			temp[ys][counts[ys]] = arr[j];
			//記錄數量
			counts[ys]++;
		}
		//記錄取的元素需要放的位置
		int index=0;
		//把數字取出來
		for(int k=0;k<counts.length;k++) {
			//記錄數量的陣列中當前餘數記錄的數量不為0
			if(counts[k]!=0) {
				//迴圈取出元素
				for(int l=0;l<counts[k];l++) {
					//取出元素
					arr[index] = temp[k][l];
					//記錄下一個位置
					index++;
				}
				//把數量置為0
				counts[k]=0;
			}
		}
	}
}
複製程式碼
//基數排序(用佇列實現)
public static void  radixQueueSort(int[] arr) {
	//存最陣列中最大的數字
	int max=Integer.MIN_VALUE;
	for(int i=0;i<arr.length;i++) {
		if(arr[i]>max) {
			max=arr[i];
		}
	}
	//計算最大數字是幾位數
	int maxLength = (max+"").length();
	//用於臨時儲存資料的佇列的陣列
	MyQueue[] temp = new MyQueue[10];
	//為佇列陣列賦值
	for(int i=0;i<temp.length;i++) {
		temp[i]=new MyQueue();
	}
	//根據最大長度的數決定比較的次數
	for(int i=0,n=1;i<maxLength;i++,n*=10) {
		//把每一個數字分別計算餘數
		for(int j=0;j<arr.length;j++) {
			//計算餘數
			int ys = arr[j]/n%10;
			//把當前遍歷的資料放入指定的佇列中
			temp[ys].add(arr[j]);
		}
		//記錄取的元素需要放的位置
		int index=0;
		//把所有佇列中的數字取出來
		for(int k=0;k<temp.length;k++) {
			//迴圈取出元素
			while(!temp[k].isEmpty()) {
				//取出元素
				arr[index] = temp[k].poll();
				//記錄下一個位置
				index++;
			}
		}
	}
}
複製程式碼

8.4 演算法分析

基數排序基於分別排序,分別收集,所以是穩定的。但基數排序的效能比桶排序要略差,每一次關鍵字的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的關鍵字數列又需要O(n)的時間複雜度。假如待排資料可以分為d個關鍵字,則基數排序的時間複雜度將是O(d*2n),當然d要遠遠小於n,因此基本上還是線性級別的。

基數排序的空間複雜度為O(n+k),其中k為桶的數量。一般來說n>>k,因此額外空間需要大概n個左右。

相關文章