目錄
首先,在說幾個排序算法之前,先自己寫一個簡單的工具類,判斷一個數列是否有序(以升序爲例),如果不是升序的數列,在出現亂序的地方把附近的兩個元素輸出一下:
/**
* 判斷一組數據是不是升序
* @param array 傳入一個需要判斷是否有序的數列
*/
public static void isSortedAsc(int[] array)
{
for (int i=1; i<array.length; i++)
{
if (array[i] < array[i-1]){
System.out.println("array["+(i-1)+"] = "+array[i-1]);
System.out.println("array["+i+"] = "+array[i]);
System.out.println("數據不是升序");
return;
}
}
System.out.println("數據是升序");
}
運行結果如下:
以下的排序都以升序爲例......
在寫一個主方法,用來測試排序的結果:
public static void main(String[] args) {
// 隨機生成10000個數 對他們進行排序
int[] array = new int[10000];
Random random = new Random();
for (int i = 0; i < array.length; i++) {
array[i] = random.nextInt(10000)+1;
}
System.out.println("排序前:" + Arrays.toString(array));
// 記錄時間
long start = System.currentTimeMillis();
// 這裏調用具體的排序方法
sort(array);
long end = System.currentTimeMillis();
System.out.println("運行時間:"+(end-start));
System.out.println("排序後:"+Arrays.toString(array));
SortUtils.isSortedAsc(array);
}
1、冒泡排序
冒泡排序是幾種排序算法中最常見的一種,其思想就是:給定一組數據,然後按順序兩兩數字進行比較,如果前面的一個元素比後面的大,就讓他們兩個進行交換,然後繼續朝後比較,從數列最左邊走到數列最右邊的過程稱爲一趟冒泡排序,經過一趟冒泡排序,會把最大的元素放到數列的最右邊。如下圖
當一趟冒泡排序走完之後,最大的數就放到了最右邊,然後現在我們就可以直接對剩下的9個數再進行冒泡排序:
當這一趟冒泡排序結束之後,第二大的數就會被挪到倒數第二個位置,如下圖:
通過以上分析,冒泡排序代碼如下:
public static void bubbleSort(int[] array){
// 外層循環表示的是變量j一共要進行多少趟冒泡排序
for (int i=0; i<array.length-1; i++) {
// 定義一個標誌,默認爲真
boolean flag = true;
for (int j=0; j<array.length-i-1; j++)
{
if (array[j] > array[j+1])
{
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
// 每趟排序之後又數據交換,就說明數組還不是有序的,就把標誌設置爲假
flag = false;
}
}
if (flag) {
// 如果其中某一趟排序的時候發現沒有一個數據交換,就說明數組已經有序了,就不用再進行排序了,可以直接退出循環
break;
}
}
}
使用上面的兩個測試類進行測試:
冒泡排序分析:
- 時間複雜度:O(n^2)
- 空間複雜度:O(1)
- 穩定性 :穩定
2、直接插入排序
思想:兩個下標 i, j,一個臨時變量tmp,i用來遍歷待排序序列,tmp存放arr[i],j=i-1,j朝0的方向遞減,將tmp中的值插入合適的位置。直接插入排序的思想就和我們打撲克牌的情況一樣,當我們拿到第一張牌的時候,由於此時手上只有一張牌,所以它一定是最小(或最大)的一張,當我們接到第二張牌的時候,如果它比第一張牌小,我們就把它放第一張牌的前邊,反之,就放到它後邊,以此類推。現在給我們一個數組,我們就默認它第一個元素一定是有序的,從第二個元素開始,如果第二個元素比第一個大,就不用管他,如果第二個元素小於第一個元素,就把第二個元素放第一個前面去,這時前兩個元素就有序了;然後到了第三個元素,如果第三個元素大於第二個元素,那麼第三個元素一定大於第一個元素,也就不用管它,如果它比第二個元素小,就把第二個元素挪到第三個元素的位置,然後第三個和第一個在比較,如果第三個元素大於第一個,就把它放第一個後面(第二個的位置),如果第三個小於第一個,就把它放第一個前面,以此類推,過程如下圖:
圖解: 這裏我們定義一個臨時變量tmp存儲當前元素
代碼:
public static void insertSort(int[] array){
// 從第二個元素開始 一次朝後走
for(int i=1; i<array.length; i++)
{
int tmp = array[i] ;
int j = i-1 ;
// j 從 i-1的位置開始朝回遍歷,將tmp插入到合適的位置
while (j>=0 && array[j] > tmp )
{
array[j+1] = array[j];
j--;
}
array[j+1] = tmp;
}
}
測試結果:
直接插入排序總結:
- 時間複雜度:O(n)
- 空間複雜度:O(1)
- 穩定性 :穩定
- 適用場景:適合用來排一些接近有序的數列,因爲如果數列有序,直接從左到右走一遍就行了,時間複雜度爲O(n),但是如果亂序或倒序,每走一步,它都會在往回走,把當前元素插入到合適的位置在往後走。
3.希爾排序
思想:希爾排序是對直接插入排序的一種優化;採用的是將數據分組,然後每組在組內進行直接插入排序
爲什麼要分組呢:試想一下加入現在有10000個數據要進行直接插入排序,時間複雜度爲 10000 * 10000 = 10000 0000 ;但是如果將這組數據分成100組 , 每組進行插入排序,時間複雜度就變成了 100 * 100 * 100 = 1 00 00 00 。直接少了兩個0 ......
那麼要怎麼分組呢:如果是讓我們這種普通人來分組,就下面這組數據,要平均分爲兩種,我們一開始想到的肯定是這樣
但是人家科學家分組就不一樣了,他們分組是這麼分的:
那麼這麼分組有什麼好處呢:加入我們對下面這組數據每組的組內進行直接插入排序:
組內排序之後,我們會發現,每一組在組內都是有序的,也就是說,每一組的左邊的數據都比較小,右邊的數據都比較大,整體也是一樣,這樣的話我們就可以認爲整個數組是接近有序的,上面已經說過,直接插入排序適合用來排接近有序的數列,所以現在在對整體進行直接插入排序就會快很多。
所以希爾排序的本質還是直接插入排序。
代碼:
public static void shellSort(int[] array, int gap) {
// i從gap的位置開始
for (int i=gap; i<array.length; i++)
{
// j的初始值爲i-gap
int j = i - gap;
// 定義一個臨時變量存儲array[i]的值
int tmp = array[i];
// j從i減gap的位置開始朝回遍歷,吧tmp放在合適的位置
while (j>=0 && array[j] > tmp)
{
array[j+gap] = array[j];
j -= gap;
}
array[j+gap] = tmp;
}
}
希爾排序分析:
- 時間複雜度:O(n^1.3 - n^1.5)
- 空間複雜度:O(1)
- 穩定性:不穩定
4、歸併排序
思想:歸併排序的思想同樣是將一個大的數列劃分成單個有序的數據(之後一個元素的數列一定是有序的),然後在把有序的小數列合併成一個大的有序數列,從而實現整體有序的結果,過程如下圖:
代碼:
private static void mergeSort(int[] array, int start, int end) {
int mid = (start+end)/2 ;
if (end == start)
{
return ;
}
// 朝左邊拆分
mergeSort(array, start, mid);
// 朝右邊拆分
mergeSort(array, mid+1, end);
// 拆分成單個的之後,歸併
merge(array, start, mid, end);
}
private static void merge(int[] array, int start, int mid, int end) {
int[] tmpArray = new int[array.length] ;
int i = start ; // 保留一個start的值,最後拷貝數組的時候使用
int start1 = start;
int end1 = mid;
int start2 = mid+1;
int end2 = end;
while (start1 <= end1 && start2 <= end2)
{
// 哪一個數組中的值小把哪一個的值拿到新數組中
if (array[start1] <= array[start2]){
tmpArray[start++] = array[start1++];
}else {
tmpArray[start++] = array[start2++];
}
}
// 如果第一個數組還沒有走完
while (start1 <= end1)
{
tmpArray[start++] = array[start1++];
}
// 如果第二個數組還沒有走完
while (start2 <= end2)
{
tmpArray[start++] = array[start2++];
}
// 數組拷貝
for ( ; i<=end; i++)
{
array[i] = tmpArray[i];
}
}
運行結果:
優化:
但是這麼寫仔細觀察會發現有一點問題,因爲每次遞歸都會創建一個和array一樣大的數組,但是隻用了其中的一小部分,所以可以將tmpArray在排序之前只創建一次,然後作爲參數傳遞就可以了,不需要在遞歸中創建。
private static void mergeSort_R(int[] array, int start, int end, int[] tmpArr)
{
if (start < end)
{
int mid = (start+end)/2 ;
mergeSort_R(array, start, mid, tmpArr);
mergeSort_R(array, mid+1, end, tmpArr);
merge(array, start, mid, end, tmpArr);
}
}
private static void merge(int[] array, int start, int mid, int end, int[] tmpArr) {
int pos = start;
int index = start;
int start2 = mid+1 ;
while (start <= mid && start2 <= end){
if (array[start] < array[start2]) {
tmpArr[index++] = array[start++];
}else {
tmpArr[index++] = array[start2++];
}
}
while (start <= mid){
tmpArr[index++] = array[start++];
}
while (start2 <= end){
tmpArr[index++] = array[start2++];
}
// 數組拷貝
for(int i=pos; i<=end; i++)
{
array[i] = tmpArr[i];
}
}
5. 快速排序
思想:和上面那種歸併排序的思想類似,都是將一個大的問題劃分爲多個小問題,快速排序是 :先選取一個基準值,每經過一趟冒泡排序,就可以將比基準大的放在基準值的右邊,比它小的放它左邊。然後在分別對基準值左邊的和基準值右邊的分別進行冒泡排序。直到整個數列有序。
那麼現在主要的問題就是怎麼將一個數組根據基準值分爲兩部分,使左邊的小於基準值,右邊的大於基準值。通常的方法是挖坑法
下面,再結合代碼看一下這個 ‘挖坑’ 的過程:
private static int partion(int[] arr, int left, int right)
{
// 保存第一個位置的元素
int tmp = arr[left];
while (left != right)
{
// 右邊的和tmp比較 如果右邊的比tmp大,就不用管,繼續朝左邊走
// 注意這裏得有等號,不然如果左邊等於tmp,右邊等於tmp,會出現死循環,下面等號也是一樣
while (left != right && arr[right] >= tmp)
{
right--;
}
// 如果右邊的數小於tmp,就把右邊的數賦給左邊的數
arr[left] = arr[right];
// 左邊的數和tmp比較,如果左邊的數大於tmp 就把左邊的數賦值給右邊的數
while (left != right && arr[left] <= tmp){
left++ ;
}
arr[right]= arr[left];
}
// 當left == right 把tmp的值賦給當前位置
arr[left] = tmp;
// 返回位置下標
return left;
}
最後通過遞歸的方式,讓返回位置的左邊也進行快速排序,右邊也進行快速排序:
public static void quickSort (int[] arr, int left, int rigth){
// 獲取關鍵字的位置
int mid = partion(arr, left, rigth);
// 如果關鍵字左邊還剩一個元素,就不用比了
if (mid-left > 1){
quickSort(arr, left, mid-1);
}
// 如果右邊還剩一個元素,也就不用比了
if (rigth-mid > 1){
quickSort(arr, mid+1, rigth);
}
}
結果測試:
快排優化1 --- 三數取中法
但是這樣的快排還是有一些問題,假如每次排序數列第一個剛好是最大的或最小的,就會使快速排序時間複雜度退化爲O(n^2),因此需要對這個快速排序進行優化,常見的優化方式有:隨機取基法,三數取中法
- 隨機取基法:就是隨機取一個數作爲基數,這樣就可以避免取到最大或最小的數作爲基數,但是由於是隨機的,所以這種方法完全是一種看人品的方法,所以不推薦使用
- 三數取中法:就是在第一個數,中間位置的數,最後位置的數 ,在這三個數中,取這三個數(按大小排序)的中間值作爲基準值,然後在進行快速排序,這樣可以保證至少每次取得基準值都不會是最大或最小的一個數,下面具體說說這種方法:
至於三數取中的實現,就有很多方式了,因爲只是對3個數的比較,可以使用if語句比較大小然後交換值,也可以將這三個數排序然後按順序賦值等方法:
/**
* 快排優化1
* 三數取中法 : 最後要達到的效果:array[mid] < array[low] <array[high]
*/
public static void medianOfThree(int[] array, int low, int high)
{
int mid = (low+high)/2 ;
int[] tmps = new int[]{array[mid], array[low], array[high]};
Arrays.sort(tmps);
array[mid] = tmps[0];
array[low] = tmps[1];
array[high] = tmps[2];
}
然後在每次要進行快排的操作時,都調用一下三數取中的方法即可;
快排優化2 :使用插入排序
當使用快排對數列進行排序時,排到一定程度數據就接近有序了,此時可以不必在使用遞歸的快排,而使用插入排序對數列進行排序:
public static void sort(int[] array, int start, int end){
// 優化方式1:先用三數取中法
medianOfThree(array, start, end);
// 優化方式2:當一個區間中剩餘數的個數小於等於16的時候,我們就認爲這個區間的數已經接近有序了,次數可以使用插入排序接着排
if (end-start+1 <= 16) // end-start+1 表示start到end之間的元素個數
{
insertSort(array, start, end);
}
int par = partion(array, start, end);
// 遞歸左邊 左邊還剩一個元素就不用排序了
if (par > start+1) {
sort(array, start, par-1);
}
if (par < end-1){
sort(array, par+1, end);
}
}
最後的運行結果:
6. 選擇排序
思想:選擇排序就是先選擇最小的一個數,和第一個數交換,然後在選擇第二小的數,和第二個位置的數交換,以此類推...
代碼實現:
public static void selectSort(int[] array)
{
// 把倒數第二大的數放在倒數第二個位置的時候,最大的數一定在最後的位置,就不用在朝後比較了
for (int i=0; i<array.length-1; i++){
// 從i+1d位置開始朝後找,有比i位置的數小的數就交換
for (int j = i+1; j<array.length; j++){
if (array[i] > array[j]){
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
測試結果: