1. 程式人生 > 其它 >【演算法】十大經典排序演算法

【演算法】十大經典排序演算法

寫在前面

排序是演算法的必修課

也是基礎的第一課

其實就排序本身而言沒有什麼值得學習的

很多程式語言自帶排序函式,例如Java語言的Arrays.sort()函式,基本上可以直接用

所以我們在刷演算法題的時候很少自己寫排序程式碼

但是排序仍然值得大家去學習

因為排序並沒有大家想的這麼簡單,只是將亂序變為有序,其實不同的排序演算法裡包含著不同的思想

例如氣泡排序就是暴力演算法的思想,歸併排序就是分治演算法的思想

在本頁部落格裡大家更多的是體會排序演算法所包含的各種思想,在後續的學習中還會繼續深入地和大家討論

氣泡排序

氣泡排序(Bubble Sort)是基於交換的排序,每次遍歷需要排序的元素,依次比較相鄰的兩個元素的大小,如果前一個元素大於後一個元素則兩者交換,保證最後一個數字一定是最大的(假設按照從小到大排序),即最後一個元素已經排好序,下一輪只需要保證前面 n-1

 個元素的順序即可。

之所以稱為冒泡,是因為最大/最小的數,每一次都往後面冒,就像是水裡面的氣泡一樣。

排序(假設從小到大)的步驟如下:

  1. 從頭開始,比較相鄰的兩個數,如果第一個數比第二個數大,那麼就交換它們位置。
  2. 從開始到最後一對比較完成,一輪結束後,最後一個元素的位置已經確定。
  3. 除了最後一個元素以外,前面的所有未排好序的元素重複前面兩個步驟。
  4. 重複前面 1 ~ 3 步驟,直到所有元素都已經排好序。

例如,我們需要對陣列 [98,90,34,56,21] 進行從小到大排序,每一次都需要將陣列最大的移動到陣列尾部。那麼排序的過程如下動圖所示:

交換具體邏輯如下圖所示:

接下來兩輪排序確定好了第二個和第三個的位置,其實這個陣列已經完成排序了,一共 5 個數,冒泡 4 次即可。

紫色表示已經排好的元素,橙紅色表示正在比較/交換的元素,可以看出前面兩次排序之後,已經確定好了最大兩個數的位置。

氣泡排序Java程式碼

public class BubbleSort {
	public static void bubbleSort(int[] nums) {
		int size=nums.length;
		for(int i=0;i<size-1;i++) {
			System.out.println("第"+(i+1)+"輪交換開始");
			for(int j=0;j<size-1-i;j++) {
				if(nums[j]>nums[j+1]) {
					int temp=nums[j+1];
					nums[j+1]=nums[j];
					nums[j]=temp;
				}
			printf(nums);
			}
		}
		
	}
	public static void printf(int[] nums) {
	    for (int num : nums) {
	        System.out.print(num + " ");
	    }
	    System.out.println("");
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[]nums = new int[]{98,90,34,56,21};
	    printf(nums);
	    bubbleSort(nums);
	    
	}

}

氣泡排序Java程式碼執行結果

選擇排序

前面說的氣泡排序是每一輪比較確定最後一個元素,中間過程不斷地交換。而選擇排序就是每次選擇剩下的元素中最小的那個元素,與當前索引位置的元素交換,直到所有的索引位置都選擇完成。

排序的步驟如下:

  • 從第一個元素開始,遍歷其後面的元素,找出其後面比它更小的且最小的元素,若有,則兩者交換,保證第一個元素最小。
  • 對第二個元素一樣,遍歷其後面的元素,找出其後面比它更小的且最小的元素,若存在,則兩者交換,保證第二個元素在未排序的數中(除了第一個元素)最小。
  • 依次類推,直到最後一個元素,那麼陣列就已經排好序了。

比如,現在我們需要對 [98,90,34,56,21] 進行排序,動態排序過程如下:

靜態排序過程如下:

前面兩輪選擇排序已經分別將 21 和 34 選擇出來,放到最前面的位置。

剩下的排序是確定 56 和 90 的位置,最後一個 98 自然就是最大的數,不需要再排序。

選擇排序Java程式碼

public class SelectionSort {
	public static void printf(int[] nums) {
	    for (int num : nums) {
	        System.out.print(num + " ");
	    }
	    System.out.println("");
	}
	public static void selectionSort(int []nums) {
		int times=0;
		int size=nums.length;
		int minIndex,temp;
		for(int i=0;i<size-1;i++) {
			System.out.print("第" + (i + 1) + "輪選擇開始:");
			minIndex=i;
			for(int j=i+1;j<size;j++) {
				times++;
				if(nums[j]<nums[minIndex]) {
					minIndex=j;
				}
			}
			System.out.println("交換 "+nums[i]+"和"+nums[minIndex]);
			temp=nums[i];
			nums[i]=nums[minIndex];
			nums[minIndex]=temp;
			printf(nums);
		}
		System.out.println("比較次數:"+times);
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		    int[]nums = new int[]{98,90,34,56,21};
		    printf(nums);
		    selectionSort(new int[]{98,90,34,56,21});
	}

}

選擇排序Java程式碼執行結果

插入排序

選擇排序是每次選擇出最小的放到已經排好的陣列後面,而插入排序是依次選擇一個元素,插入到前面已經排好序的陣列中間,確保它處於正確的位置,當然,這是需要已經排好的順序陣列不斷移動。步驟描述如下:

  1. 從第一個元素開始,可以認為第一個元素已經排好順序。
  2. 取出後面一個元素 n,在前面已經排好順序的數組裡從尾部往頭部遍歷,假設正在遍歷的元素為 nums[i],如果 num[i] > n,那麼將 nums[i] 移動到後面一個位置,直到找到已經排序的元素小於或者等於新元素的位置,將 n 放到新騰空出來的位置上。如果沒有找到,那麼 nums[i] 就是最小的元素,放在第一個位置。
  3. 重複上面的步驟 2,直到所有元素都插入到正確的位置。

以陣列 [98,90,34,56,21] 為例,動態排序過程如下:

具體的排序過程如下:

第一次假設第一個元素已經排好,第二個元素 90 往前面查詢插入位置,正好查詢到 98 的位置插入,第二輪是 34 選擇插入位置,選擇了第一個元素 90 的位置插入,其後面的元素後移。

第三輪排序則是 56 選擇適合自己的位置插入,第四輪是最後一個元素 21 往前查詢適合的位置插入:

插入排序Java程式碼

public class InsertionSort {
	public static void printf(int[] nums) {
	    for (int num : nums) {
	        System.out.print(num + " ");
	    }
	    System.out.println("");
	}
	
	public static void insertionSort(int[] nums) {
		if(nums==null) {
			return;
		}
		int size=nums.length;
		int index,temp;
		for(int i=1;i<size;i++) {
			// 當前選擇插入的元素前面一個索引值
			index=i-1;
			// 當前需要插入的元素
			temp=nums[i];
			while(index>=0&&nums[index]>temp) {
				nums[index+1]=nums[index];
				index--;
			}
			// 插入空出來的位置
			nums[index+1]=temp;
			System.out.print("第" + (i) + "輪插入結果:");
			printf(nums);
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		    int[]nums = new int[]{98,90,34,56,21};
		    printf(nums);
		    insertionSort(nums);
	}

}

插入排序Java程式碼執行結果

希爾排序

希爾排序(Shell's Sort)又稱“縮小增量排序”(Diminishing Increment Sort),是插入排序的一種更高效的改進版本,同時該演算法是首次衝破 O(n^2) 的演算法之一。

插入排序的痛點在於不管是否是大部分有序,都會對元素進行比較,如果最小數在陣列末尾,想要把它移動到陣列的頭部是比較費勁的。希爾排序是在陣列中採用跳躍式分組,按照某個增量 gap 進行分組,分為若干組,每一組分別進行插入排序。再逐步將增量 gap 縮小,再每一組進行插入排序,迴圈這個過程,直到增量為 1。

希爾排序基本步驟如下:

  1. 選擇一個增量 gap,一般開始是陣列的一半,將陣列元素按照間隔為 gap 分為若干個小組。
  2. 對每一個小組進行插入排序。
  3. 將 gap 縮小為一半,重新分組,重複步驟 2(直到 gap 為 1 的時候基本有序,稍微調整一下即可)。

以陣列 [98,90,34,56,21,11,43,61] 為例子,排序的動圖如下:

同樣以陣列 [98,90,34,56,21,11,43,61] 為例子,元素個數為 8,首次 gap 為 4,元素分為 4 組,同顏色視為一組,對相同顏色進行插入排序,這樣保證了大致位置上大的元素在後面,小的元素在前面。

第二輪希爾排序,gap = 4/2 = 2,則元素可以分為兩組,同顏色視為一組,仍是對同組的進行插入排序:

最後一輪,gap= 2/2 =1,則所有元素視為一組,相當於對所有元素進行插入排序,這時候元素已經基本有序,只需要做小範圍的調整即可。

希爾排序是非穩定排序演算法,每一組的排序,都確保了這一組的資料基本有序,整體上也是基本有序。

希爾排序Java程式碼

public class ShellSort {
	public static void printf(int[] nums) {
	    for (int num : nums) {
	        System.out.print(num + " ");
	    }
	    System.out.println("");
	}
	public static void shellSort(int[] nums) {
		int times=1;
		for(int gap=nums.length/2;gap>0;gap/=2) {
			System.out.print("第" + (times++) + "輪希爾排序, gap= " + gap + " ,結果:");
		
		for(int i = gap;i<nums.length;i++) {
			int j=i;
			int temp=nums[j];
			if(nums[j]<nums[j-gap]) {
				while(j-gap>=0&&temp<nums[j-gap]) {
					nums[j]=nums[j-gap];
					j-=gap;
				}
				nums[j]=temp;
			}
		}
		printf(nums);
		}
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		 int[] nums = new int[]{98, 90, 34, 56, 21, 11, 43, 61};
		 printf(nums);
		 shellSort(nums);
	}

}

希爾排序Java程式碼執行結果

快速排序

快速排序比較有趣,選擇陣列的一個數作為基準數,一趟排序,將陣列分割成為兩部分,一部分均小於/等於基準數,另外一部分大於/等於基準數。然後分別對基準數的左右兩部分繼續排序,直到陣列有序。這體現了分而治之的思想,其中還應用到挖坑填數的策略。

演算法的步驟如下:

  1. 從陣列中挑一個元素作為基準數,一般情況下我們選擇第一個 nums[i],儲存為 standardNum,可以理解為 nums[i] 坑位的數被拎出來了,留下空的坑位。
  2. 取陣列的左邊界索引指標 i,右邊界索引指標 jj 從右邊往左邊,尋找到比 standardNum 小的數,停下來,寫到 nums[i] 的坑位,nums[j] 的坑位空出來。 索引指標i 從左邊往右邊找,尋找比 standardNum 大的數,停下來,寫到 nums[j] 的坑位,這個時候,num[i] 的坑位空出來(前提是 i 和 j 不相撞)。
  3. 上面的 i 和 j 迴圈步驟 2,直到兩個索引指標 i 和 j 相撞,將基準值 standardNum 寫到坑位 nums[i] 中,這時候,standardNum 左邊的數都小於等於它本身,右邊的數都大於等於它本身。
  4. 分別對 standardNum 左邊的子陣列和右邊的子陣列,迴圈執行前面的 1,2,3,直到不可再分,並且有序。

以陣列 [61,90,34,56,21,11,43,68] 為例,動態排序過程如下:

具體的排序過程如下:

第一輪排序是所有元素,以第一個數 61 為基準值,排序完成則左邊的數都小於等於 61,右邊的數都大於等於 61。

分別對 61 左邊的數 [ 43,11,34,56,21 ] 和右邊的數 [ 90,68 ] 分別進行快速排序,這裡體現了分治的思想。首先我們來看左邊 [ 43,11,34,56,21 ] 的排序。

左邊又確定了以 43 為分割的陣列 [ 21,11,34 ] 以及 [ 64 ],由於遞迴的原因,再次先對左邊 [ 21,11,34 ] 進行排序:

左邊 [ 21,11,34 ] 排序後,以 21 為分割線,左右各自只有一個數,自然已經停止,上面 43 的右邊也只有一個元素,所以也已經是有序的。

至此,61 以及左邊都是有序的,再對 61 右邊的 [ 90,68 ] 進行快速排序:

快速排序Java程式碼

public class QuickSort {
	public static void printf(int[] nums) {
	    for (int num : nums) {
	        System.out.print(num + " ");
	    }
	    System.out.println("");
	}
	public static void quickSort(int[] nums) {
		quickSort(nums,0,nums.length-1);
	}
	public static void quickSort(int nums[],int left,int right) {
		System.out.println("[left,right]:["+left+","+right+"]");
		if(left<right) {
			int i=left,j=right,standardNum=nums[left];
			while(i<j) {
				while(i<j&&nums[j]>=standardNum) {
					j--;
				
			}
			System.out.print("standardNum:"+standardNum+",第1個小於等於standardNum的數:"+nums[j]);
			if(i<j) {
				nums[i]=nums[j];
				i++;
			}
			while(i<j&&nums[i]<standardNum) {
				i++;
			}
			System.out.println(",第1個大於等於standardNum的數:"+nums[i]);
			if(i<j) {
				nums[j]=nums[i];
				j--;
			}
		}
		
		nums[i]=standardNum;
		printf(nums);
		quickSort(nums,left,i-1);
		printf(nums);
		quickSort(nums,i+1,right);
	}
	}

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] nums = new int[]{61, 90, 34, 56, 21, 11, 43, 68};
	    printf(nums);
	    quickSort(nums);
	}

}

快速排序Java程式碼執行結果

計數排序

計數排序,不是基於比較,而是基於計數,比較適合元素數值相近且為整數的情況。

計數排序步驟如下:

  • 遍歷陣列,找出最大值和最小值。
  • 根據最大值和最小值,初始化對應的統計元素數量的陣列。
  • 遍歷元素,統計元素個數到新的陣列。
  • 遍歷統計的陣列,按照順序輸出排序的陣列元素。

假設有幾個青少年,他們年齡很靠近,分別是 11、9、11、 13、12、14、15、13,現在需要給他們按照年齡排序。首先先遍歷一遍,找出最小的 min 和最大的元素 max,建立一個大小為 max - min + 1 的陣列,再遍歷一次,統計數字個數,寫到陣列中。

然後再遍歷一次統計陣列,將每個元素置為前面一個元素加上自身,為什麼這樣做呢?

這是為了讓統計陣列儲存的元素值等於相應整數的最終排序位置,這樣我們就可以做到穩定排序,比如下面的 15 對應的是 8,也就是 15 在陣列中出現是第 8 個元素,從後面開始遍歷,我們就可以保持穩定性。

比如原陣列從後往前遍歷到 13 的時候, 13 對應的位置是 6,那麼此時從後往前遍歷到的第一個 13 就是在第 6 個元素位置。後面再遇到 13,就放到第 5 個元素位置,不會打亂它們的相對位置。

具體過程如下:

計數排序Java程式碼

public class CountSort {

	  public static void countSort(int[] nums) {
	    int max = nums[0];
	    int min = nums[0];
	    for (int i = 1; i < nums.length; i++) {
	      if (nums[i] > max) {
	        max = nums[i];
	      }
	      if (nums[i] < min) {
	        min = nums[i];
	      }
	    }
	    System.out.println("min:" + min + ",max:" + max);
	    int count = max - min;
	    int[] countNums = new int[count + 1];
	    for (int i = 0; i < nums.length; i++) {
	      countNums[nums[i] - min]++;
	    }
	    System.out.print("countNums: ");
	    printf(countNums);
	    int sum = 0;
	    // 後面的元素等於前面元素加上自身
	    for (int i = 0; i < count + 1; i++) {
	      sum += countNums[i];
	      countNums[i] = sum;
	    }
	    System.out.print("countNums: ");
	    printf(countNums);
	    int[] newNums = new int[nums.length];
	    for (int i = nums.length - 1; i >= 0; i--) {
	      /**
	       * nums[i] - min 表示原陣列 nums 裡面第i位置對應的數在統計數組裡面的位置索引
	       */
	      newNums[countNums[nums[i] - min] - 1] = nums[i];
	      countNums[nums[i] - min]--;
	    }
	    printf(newNums);
	  }

		public static void printf(int[] nums) {
		    for (int num : nums) {
		        System.out.print(num + " ");
		    }
		    System.out.println("");
		}
	  
	  
	  public static void main(String[] args) {
		    int[] nums = new int[]{11, 9, 11, 13, 19, 14, 16, 14, 8, 17};
		    printf(nums);
		    countSort(nums);
		}
	}

計數排序Java程式碼執行結果

桶排序

桶排序,是指用多個桶儲存元素,每個桶有一個儲存範圍,先將元素按照範圍放到各個桶中,每個桶中是一個子陣列,然後再對每個子陣列進行排序,最後合併子陣列,成為最終有序的陣列。這其實和計數排序很相似,只不過計數排序每個桶只有一個元素,而且桶儲存的值為該元素出現的次數。

桶排序的具體步驟:

  • 遍歷陣列,查詢陣列的最大最小值,設定桶的區間(非必需),初始化一定數量的桶,每個桶對應一定的數值區間。
  • 遍歷陣列,將每一個數,放到對應的桶中。
  • 對每一個非空的桶進行分別排序(桶內部的排序可以選擇 JDK 自帶排序)。
  • 將桶中的子陣列拼接成為最終的排序陣列。

以陣列 [98,90,34,56,21,11,43,61] 為例,桶排序的動態過程:

先遍歷查找出 max 為 98, min 為 11,陣列大小為 8,( 98 - 11 )/8 + 1 = 11,桶的個數為 11。先把元素按照區間放進去,對每一個桶分別排序,然後再把桶的元素連起來放在陣列中,排序就完成了。

桶排序Java程式碼

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class BucketSort {

  public static void bucketSort(int[] nums) {
    // 遍歷陣列獲取最大最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int i = 0; i < nums.length; i++) {
      max = Math.max(max, nums[i]);
      min = Math.min(min, nums[i]);
    }

    // 計算桶的數量
    int bucketNum = (max - min) / nums.length + 1;
    System.out.println(
      "最小:" + min + ",最大:" + max + ",桶的數量:" + bucketNum
    );
    List<List<Integer>> buckets = new ArrayList<List<Integer>>(bucketNum);
    for (int i = 0; i < bucketNum; i++) {
      buckets.add(new ArrayList<Integer>());
    }

    // 將每個元素放入桶
    for (int i = 0; i < nums.length; i++) {
      int num = (nums[i] - min) / (nums.length);
      buckets.get(num).add(nums[i]);
    }

    // 對每個桶內部進行排序
    for (int i = 0; i < buckets.size(); i++) {
      Collections.sort(buckets.get(i));
    }

    // 將桶的元素複製到陣列中
    int index = 0;
    for (int i = 0; i < buckets.size(); i++) {
      for (int j = 0; j < buckets.get(i).size(); j++) {
        nums[index++] = buckets.get(i).get(j);
      }
    }
  }
  
  public static void main(String[] args) {
	    int[] nums = new int[]{98, 90, 34, 56, 21, 11, 43, 61};
	    printf(nums);
	    bucketSort(nums);
	    printf(nums);
	}
  
  public static void printf(int[] nums) {
    for (int num : nums) {
      System.out.print(num + " ");
    }
    System.out.println("");
  }
}

桶排序Java程式碼執行結果

堆排序

堆排序,就是利用大頂堆或者小頂堆來設計的排序演算法,是一種選擇排序。堆是一種完全二叉樹:

  • 大頂堆:每個節點的數值都大於或者等於其左右孩子節點的數值。
  • 小頂堆:每個節點的數值都小於或者等於其左右孩子節點的數值。

我們一般使用陣列來對堆結構進行儲存,下面我們只說大頂堆(元素按照從小到大排序),假設陣列為 nums[],則第 i 個數滿足:num[i] >= nums[2i+1] 且 num[i] >= nums[2i+2],第 i 個數在堆上的左節點就是陣列中下標索引 2i+1 的元素,其右節點就是陣列中下標索引 2i+2 的元素。

排序的思路為:

  • 將無序的陣列構建出一個大頂堆,也就是上面的元素比下面的元素大。
  • 將堆頂的元素和堆的最末尾的元素交換,將最大元素下沉到陣列的最後面(末端)。
  • 重新調整前面的順序,繼續交換堆頂的元素和當前末尾的元素,直到所有元素全部下沉。

倘若一個數組為 [11,21,34,43,56,61,90,98],動態的過程如下:

樹結構形象的結構如下:

首先需要先初始化堆,也叫堆化過程,就是用父節點和子節點對比,我們採取的是大的數往上冒,小的元素往下沉,執行該操作的是每一個非葉子節點與其左右子節點分別對比,從下到上,從右到左。其中我們上面發現交換的有 43 和 34,61 和 56。

經過上面的調整,已經是一個最大堆,我們每次取最大的那個元素(堆頂的元素)和最後的元素交換,然後調整:

堆排序Java程式碼

public class HeapSort {

	  public static void heapSort(int[] nums) {
	    // 首先需要構建最大堆
	    for (int i = nums.length / 2 - 1; i >= 0; i--) {
	      // 從第一個非葉子結點調整結構,大的往上走
	      adjustHeap(nums, i, nums.length);
	    }
	    printf(nums);
	    System.out.println("-----------------------------");
	    // 交換元素和調整
	    for (int j = nums.length - 1; j > 0; j--) {
	      // 將堆頂元素與末尾元素交換
	      swap(nums, 0, j);
	      // 重新調整,大的元素往上交換
	      adjustHeap(nums, 0, j);
	      printf(nums);
	      System.out.println("-----------------------------");
	    }
	  }

	  /**
	   * 調整大頂堆
	   */
	  public static void adjustHeap(int[] nums, int i, int length) {
	    // 取出當前元素
	    int temp = nums[i];
	    //從左節點開始
	    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
	      // 如果右節點更大,那麼指向右節點
	      if (k + 1 < length && nums[k] < nums[k + 1]) {
	        k++;
	      }
	      // 子節點的值直接給父節點
	      if (nums[k] > temp) {
	        nums[i] = nums[k];
	        i = k;
	      } else {
	        break;
	      }
	      printf(nums);
	    }
	    // 最後將最上面的節點置,放到當前的節點
	    nums[i] = temp;
	  }

	  /**
	   * 交換元素
	   */
	  public static void swap(int[] nums, int a, int b) {
	    int temp = nums[a];
	    nums[a] = nums[b];
	    nums[b] = temp;
	  }

	  public static void printf(int[] nums) {
	    for (int num : nums) {
	      System.out.print(num + " ");
	    }
	    System.out.println("");
	  }
	  
	  public static void main(String[] args) {
		    int[] nums = new int[]{98, 90, 34, 56, 21, 11, 43, 61};
		    printf(nums);
		    heapSort(nums);
		    printf(nums);
		}
	}

堆排序Java程式碼結果顯示

基數排序

基數排序比較特殊,特殊在它只能用在整數(自然數)排序,而且不是基於比較的,其原理是將整數按照位分成不同的數字,按照每個數各位值逐步排序。何為高位,比如 81,1 就是低位, 8 就是高位。 分為高位優先和低位優先,先比較高位就是高位優先,先比較低位就是低位優先。下面我們講高位優先。

主要的步驟如下:

  • 將所有元素統一稱為統一數位長度,前面補 0。
  • 從最高位開始,依次排序,從最高位到最低位遍歷完,陣列就是有序的。

以陣列 [98,90,34,56,21,11,43,61,39] 為例,動態的排序過程如下:

具體的流程,初始化桶:

先按照個位排序,然後從後面往前面取出數值,這裡很想前面的計數排序,也很像桶排序。個位排序完之後,除了個位不同而其他位置不同的數已經保持了相對位置的排序。

再按照十位排序,也是如個位般,放到各個桶裡面去,再取出,這樣把所有位數都遍歷完之後,取出的陣列就是有序的。

基數排序Java程式碼

public class RadixSort {

	  private static void radixSort(int[] nums) {
	    int max = nums[0];
	    // 指數,從個位到十位到百位...
	    int exp;
	    // 遍歷得到最大值
	    for (int num : nums) {
	      if (num > max) {
	        max = num;
	      }
	    }
	    // 從個位開始,對陣列每一位進行排序
	    for (exp = 1; max / exp > 0; exp = exp * 10) {
	      // 臨時陣列
	      int[] tempNums = new int[nums.length];
	      // 數值 0-9,桶的個數固定為 10
	      int[] buckets = new int[10];
	      // buckets 中儲存的其實是資料出現的次數
	      for (int value : nums) {
	        buckets[(value / exp) % 10]++;
	      }
	      // 每一個值等於前面的元素次數加上自身(類似計數排序)
	      for (int i = 1; i < 10; i++) {
	        buckets[i] += buckets[i - 1];
	      }
	      // 從後往前遍歷,將元素寫會臨時陣列
	      for (int i = nums.length - 1; i >= 0; i--) {
	        tempNums[buckets[(nums[i] / exp) % 10] - 1] = nums[i];
	        buckets[(nums[i] / exp) % 10]--;
	      }
	      // 將有序元素 tempNums 賦給 nums
	      System.arraycopy(tempNums, 0, nums, 0, nums.length);
	      printf(nums);
	    }
	  }
	  public static void printf(int[] nums) {
		    for (int num : nums) {
		        System.out.print(num + " ");
		    }
		    System.out.println("");
		}
	  public static void main(String[] args) {
		    int[] nums = new int[]{98, 90, 34, 56, 21, 11, 43, 61, 39};
		    printf(nums);
		    radixSort(nums);
		}
	}

基數排序Java程式碼結果顯示

實驗總結

  • 氣泡排序:基本最慢,時間複雜度最好為 O(n),最壞為 O(n2),平均時間複雜度為 O(n2),空間複雜度為 O(1),穩定排序演算法。
  • 選擇排序:時間複雜度很穩定,最好最壞或者平均都是 O(n2),空間複雜度為 O(1),可以做到穩定排序。
  • 插入排序:時間複雜度最好為 O(n),最壞為 O(n2),平均時間複雜度為 O(n2),空間複雜度為 O(1),穩定排序演算法。
  • 希爾排序:希爾增量下最壞的情況時間複雜度是 O(n2),最好的時間複雜度是 O(n) (也就是陣列已經有序),平均時間複雜度是 O(n3/2),屬於不穩定排序。
  • 快速排序:時間複雜度最差的情況是 O(n2),平均時間複雜度為 O(nlogn),空間複雜度,雖然快排本身沒有申請額外的空間,但是遞迴需要使用棧空間,遞迴數的深度是 log2n,空間複雜度也就是 O( log2n),屬於不穩定排序。
  • 歸併排序:排序複雜度為 O(nlog2n),不存在好壞的情況,但是代價就是需要申請額外的空間,申請空間的大小最大為 n,所以空間複雜度為 O(n),屬於穩定排序。
  • 計數排序:時間複雜度為 O(n+k),申請了一個統計陣列和一個新陣列,空間複雜度為 O(n+k),沒有所謂最好最壞,都是一個複雜度,一般適用於小範圍整數排序,屬於穩定排序。
  • 桶排序:最好情況時間複雜度 O(n),最壞情況時間複雜度為 O(n2),平均的時間複雜度為 O(n+k)。由於在中間過程中會申請桶的數量 m,所以空間複雜度為 O(n+m),穩定性決定於桶內部排序。
  • 堆排序:時間複雜度為 O(nlogn),沒有申請額外的空間,空間複雜度為 O(1),屬於不穩定排序。
  • 基數排序:時間複雜度為 O(d(2n))。一般只使用於整數排序,不適合小數或者文字排序。由於需要申請桶的空間,假設有 k 個桶(上面是 10 個桶),則空間複雜度為 O(n+k),一般 k 較小,所以近似為 O(n),屬於穩定排序。

每一種排序,都有其優缺點,我們應該根據場景選擇合適的排序演算法。

關於時間複雜度,我們一般使用大 O 表示法,它是一種體現演算法時間複雜度的計法,通俗來講,就是隨著問題規模的增長,演算法執行的指令數也在增長,時間複雜度越高,則執行時間增長越快。常見的演算法時間複雜度由好到壞依次為: Ο(1) < Ο(log2n) < Ο(n) < Ο(nlog2n) < Ο(n^2) < Ο(n^3) < … < Ο(2^n) < Ο(n!) ,一個優秀的演算法,自然少不了對低時間複雜度的追求。

但是我們也不能自然也不能忽略空間複雜度,也就是隨著問題規模的增長,計算過程中所需要的儲存空間增長的速度(增長率),其計算方式與時間複雜度類似。時間複雜度和空間複雜度是息息相關的兩個概念,隨著計算機空間越拉越大,不少的演算法傾向於以空間換時間,這也是取捨的策略。

講完這麼多種排序,我們平時並非都能去實踐,重要的是演算法演變的過程以及設計思路。