算法代碼:
/**
* Created by CLY on 2017/3/17.
*/
package pers.cly.sorting;
/**
* 排序工具類,里面包含各種排序方法
*/
public class Sorting {
/**
* 名稱:插入排序-直接插入排序
* 描述:每次將一個待排序的元素與已排序的元素進行逐一比較,直到找到合適的位置按大小插入。
* 時間復雜度:平均O(n^2),最壞O(n^2)
* 穩定性:穩定
* @param array 待排數組
*/
public void straightInsertionSort(int[] array){
//取arr[0]作為初始的順序序列,從arr[1]開始和順序序列進行比較。
for (int i = 1; i < array.length; i++) {
//每次先與當前順序序列的最大的數比,如果比他小則表示需要插入。
// 如果比當前順序序列里的最大數還要大,則不必插入,直接進行下一次循環。
if(array[i] < array[i-1]){
int temp = array[i];//先將待插數存入temp
int j;
//待插數據的前一個數其實就是當前順序序列的最大數,所以先和前一個數比,
// 如果比最大數小,則最大數后移一位,然后繼續比。
for(j = i-1; j >= 0 && array[j] > temp; j --){
array[j+1] = array[j];//把比temp大或相等的元素全部往后移動一個位置
}
array[j+1] = temp;//把待排序的元素temp插入騰出位置的(j+1)
}
}
}
//================================================================================================
/**
* 名稱:插入排序-希爾排序
* 描述:把整個序列分成若干個子序列,分別進行直接插入排序。這算是“一趟希爾排序”
* 時間復雜度:平均O(n^1.5),最壞O(n^2)
* 穩定性:不穩定
* @param array 待排數組
* @param incrementNum 初始增量
*/
public void shellSort(int[] array,int incrementNum){
//從初始增量開始循環,每次增量減少一倍
for (int increment = incrementNum; increment > 0; increment /= 2) {
//下面就是一個修改過的直接插入排序
for (int i = increment; i < array.length; i++) {
if(array[i] < array[i-increment]){
int temp = array[i];
int j;
for(j = i-increment; j >= 0 && array[j] > temp; j -=increment){
array[j+increment] = array[j];
}
array[j+increment] = temp;
}
}
}
}
//================================================================================================
/**
* 名稱:交換排序-冒泡排序
* 描述:第一趟,第一個和第二個比,第二個再和第三個比···,第一趟完后,最大的數會被排到最后。
* 時間復雜度:平均O(n^2),最壞O(n^2)
* 穩定性:穩定
* @param array 待排數組
*/
public void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {//最多做n-1趟排序
//隨着i的一次次循環,j每次都少一次循環(因為后面的書都是排好序的)
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) { //如果前一位大於后一位,則把大的放前面
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
//================================================================================================
/**
* 名稱:交換排序-快速排序
* 描述:選第一個數作為“樞軸”,
* 將樞軸與序列另一端的數比較,樞軸大於它,就換位,小於就再和另一端的倒數第二個數比較··
* 第一次換位完了后依舊和另一邊的比,但判斷標准得顛倒,變成“如果樞軸小於它,就換位”
* 一輪比完了,樞軸就到了中間,左邊比它小,右邊比它大。
* 之后樞軸兩邊的序列繼續進行快排。
* 時間復雜度:平均O(nlogn),最壞O(n^2)
* 穩定性:不穩定
* @param array 待排數組
* @param low 開始位置(初始為0,因為一開始選[0]作為樞軸)
* @param high 結束位置(初始為數組最后一個數)
*/
public void quickSort(int[] array,int low,int high){
int start = low;//開始位置(前端)
int end = high;//結束位置(后端)
int key = array[low];//關鍵值,也就是樞軸。第一次從位置0開始取,一輪排完會后排到中間。
while(end>start){//
//現在關鍵值在“前端”,從后往前比較,要找到小於關鍵值的值
//如果比關鍵值大,則比較下一個,直到有比關鍵值小的交換位置,然后又從前往后比較
while(end>start&&array[end]>=key)
end--;//如果最后一個值大於關鍵值,則end往前移一位,拿倒數第二個比···
//由於之前的end--,現在是往前移了一位了,
// 如果這時候剛好比關鍵值小,則將小的值和關鍵值交換位置。
if(array[end]<=key){
int temp = array[end];
array[end] = array[start];
array[start] = temp;
}
//現在關鍵值在后端,從前往后比較,要找到大於關鍵值的值
//如果比關鍵值小,則比較下一個,直到有比關鍵值大的交換位置
while(end>start&&array[start]<=key)
//從前端開始找,如果前端的值比目前處在后端的關鍵值小,
// 則start++,將前端位置往后移一位
start++;
//由於前端往后移了一位,就再比一次,
// 如果此時前端值剛好比關鍵值大,則交換位置,把關鍵值交換到前端。
if(array[start]>=key){
int temp = array[start];
array[start] = array[end];
array[end] = temp;
}
//此時第一次循環比較結束,關鍵值(樞軸)的位置已經確定了。
// 左邊的值都比關鍵值小,右邊的值都比關鍵值大,
// 但是兩邊的順序還有可能是不一樣的,進行下面的遞歸調用
}
//遞歸,此時分別對樞軸兩邊進行快排
//此時low是初始時的開始位置,start則++了好幾次,low位至start位構成了左邊序列。
// low作為左邊序列的起始位,start其實是樞軸的位置,所以start-1就是左邊序列的結束位
if(start>low) quickSort(array,low,start-1);
//此時end是樞軸的位置,所以end+1是右邊序列的起始位,high是最初的結束為
// (也就是最后一個數,期間改變的是end,high沒變),所以high就是右邊序列的結束位
if(end<high) quickSort(array,end+1,high);
}
//================================================================================================
/**
* 名稱:選擇排序-直接選擇排序
* 描述:先從頭到尾掃一遍,找到最小的數,和第一個交換,
* 然后從第二個開始找,找到最小的,和第二個位置交換·····
* 時間復雜度:平均O(n^2),最壞O(n^2)
* 穩定性:穩定
* @param array 待排數組
*/
public void selectionSort(int[] array){
int len=array.length;
int small;//一次比較中最小的下標。
int temp;
//初始從第0位開始找,此位放最小的數,之后第1位放第二小的數·····
for(int i=0;i<len-1;i++){
small=i;//該此比較中最小的下標
//把假定最小下標后的下標的值與該值循環比較,每次都將更小的下標賦給small,
// 一輪下來,small里就是真正的最小下標
for(int j=i+1;j<len;j++){
//如果找到比“最小下標”小的值,則將該下標改成最小下標
if(array[j]<array[small]){
small=j;
}
}
//查詢一輪下來,判斷small是否變更,如果沒變就表示目前i位就是最小的,不用換位。否則換位
if(i!=small){
//將目前找到的最小元素臨時裝到temp中
temp=array[small];
//此處沒有直接將i位於small位交換,
// 因為直接交換可能會導致相同的數據元素位置發生變化,引起排序不穩定。
//將第i位至第samll-1位的元素集體向后移一位
// (這樣就剛好把第samll位蓋住了,順序也不會發生改變,也保證了穩定性)
for(int k=small;k>i;k--){
array[k]=array[k-1];
}
array[i]=temp;//將目前最小值賦值給第i位
}
}
}
//================================================================================================
/**
* 名稱:選擇排序-堆排序
* 描述:首先,此序列對應的一維數組可看成是一個完全二叉樹
* 其結構是:如果最頂端為最大值,則“所有的非終端結點的值都大於等於其左右的孩子結點的值”。
* 將這個完全二叉樹經過一遍排序后,其頂端元素為最大值(為最大值則表示最終結果是從小到大排。
* 如果頂端為最小值,則表示最終數組結果為從大到小排)。
* 之后將樹頂元素和最后一個元素交換位置,交換完后,最后一個元素變成了最小的值。
* 接着再將除最后一個元素以外的元素看成是完全二叉樹,對這個完全二叉樹再進行這樣的排序···
* 最終該完全二叉樹(也就是一維數組)達到了從小到大的順序。
* 時間復雜度:平均O(nlogn),最壞O(nlogn)
* 穩定性:不穩定
* @param array 待排數組
*/
public void heapSort(int[] array){
//整個過程分為兩步,
// 第一步:先構成一個根節點為最大元素的堆
//在完全二叉樹中,第i位結點的左孩子結點剛好在(i*2+1)位,右孩子結點在(i*2+2)位。
//所以此處(array.length-1)/2,
// 才能找到完全二叉樹中的“最末端的非終端結點”(該結點以后的結點都是葉子結點)
int half = (array.length-1) / 2;
//堆排序的做法就是“以一個非終端結點和它的左右孩子結點”為一個“三角單位”,
// 找出該三角單位中最大的數,將該數置於此三角單位的頂結點處。
// 從最尾部的“三角單位”排起,一直排到根節點和其左右孩子結點組成的“三角單位”,
// 此時根節點上的最大數就是整個完全二叉樹的最大數。
int len= array.length;
//此處的i=half就是從“最末端的非終端結點”所組成的“三角單位”開始排起,
// 每排完一個非終端結點,就i--,找他上一個非終端節點接着排序,直到排到根節點為止。
for (int i = half; i >= 0; i--) {
//根據傳入的非終端結點,找到“以該非終端結點為頂部結點以及他的左右孩子結點”,
// 找出三者中的最大數,將其換到該三角單位的頂部結點位。
heapAdjust(array,len, i);
}
//第二步:此時第一輪的排序已經完成,現在有一個根節點為最大元素的堆,
//我們需要把目前的根節點和末尾元素替換。
// 然后重新進行一輪排序,找出第二大的元素,放到倒數第二位·····
//每循環一次,“需要被排序的完全二叉樹長度就減一”(因為尾部都是排好序的了)
for (int i = array.length - 1; i >= 1; i--) {
int temp = array[0];
array[0] = array[i];
array[i] = temp;
heapAdjust(array, i, 0);
}
}
//構建局部最大頂堆,其中array是待排數組。
// heapSize是目前“需要被排的完全二叉樹長度”
// (因為一輪排完,最大值就放到尾端了,這樣需要被排的堆長度就減少了1)。
// index是任一個“非終端節點位”
private void heapAdjust(int[] array, int heapSize, int index) {
int left = index * 2 + 1;//index位的左孩子結點位
int right = index * 2 + 2;//index位的右孩子結點位
//最大的結點位(可能是頂位也可能是左右孩子結點位,只要是最大的數,其位置就也是這個)
int largest = index;
//左孩子結點不能超過目前堆的長度,且如果左孩子結點大於頂點,就將最大結點位改成左孩子結點位
if (left < heapSize && array[left] > array[index]) {
largest = left;
}
//右孩子結點不能超過目前堆的長度,
// 且如果右孩子結點大於“最大結點位上的元素”,就將最大結點位改成右孩子結點位
if (right < heapSize && array[right] > array[largest]) {
largest = right;
}
//如果發現原來的頂端結點位已經不是最大結點位了,則將左、右孩子結點中最大的元素與頂端元素換位
if (index != largest) {
//將頂端元素暫存
int temp = array[index];
//將最大結點位上的元素放到頂端位上
array[index] = array[largest];
//將舊頂端元素存到之前的最大頂點位上(因為最大頂點位其實是左右孩子結點中最大的孩子結點位)
array[largest] = temp;
//由於左、右孩子結點中的一個已經被替換,
// 所以有可能破壞了“以舊孩子結點為頂端結點的局部三角排序順序”,
// “要以被替換的結點為頂點”重新做一次調整。
heapAdjust(array, heapSize, largest);
}
}
//================================================================================================
/**
* 名稱:歸並排序
* 描述:假設初始序列含有n個元素,則可看成序列含有n個子序列,每個子序列長度為1,
* 然后兩兩合並並排序,得到(n/2)個長度為2(或者最后一個長度可能為1)的有序子序列。
* 再兩兩合並並排序·····,如此重復直到得到一個長度為n的有序序列為止。
* 時間復雜度:平均O(nlogn),最壞O(nlogn)
* 穩定性:穩定
* @param array 待排數組
* @param left 待排數組的左邊起始位
* @param right 待排數組的右邊結束位
*/
public void mergingSort(int[] array, int left, int right){
if (left<right){
//找到傳入數組的中間位
int center = (left + right)/2;
//將中間位作為“左邊邊界”遞歸,這樣就能不斷二分左邊數組
mergingSort(array, left, center);
//找到和左邊數組對應的右邊部分
mergingSort(array, center + 1, right);
//根據相關數據可以確定需要排序的范圍,進行排序
merge(array, left, center, right);
}
}
/**
* 相當於有三個數組:
* leftPos到leftEnd組成了第一個待排數組,
* rightPos到rightEnd組成了第二個待排數組。
* 還有一個空的數組用來臨時存儲結果。
*
* 每次將兩個待排數組的最靠前項相比較,
* 將其中最小的一項放入空數組中,
* 然后該最小項所在數組的靠前下標+1
*
* 即有a[],b[]兩個數組比較,
* 先比較a[0],b[0],發現a[0]小,就將a[0]放入result[],再比較a[1]和b[0]······
*
* 其中兩個待排數組自身肯定是有序的(因為他們也是經過現有步驟排出來的)
*
* 當其中某個待排數組排完后,
* 就表示“另一個待排數組的剩余項肯定大於之前所有的排序項”,又因為剩余項是有序的,
*
* 所以可以將剩余項全部按序裝入臨時結果數組。
* 之后用結果數組覆蓋掉原數組
*
* @param array 待排數組
* @param leftPos 待排數組的左邊部分的起始位
* @param leftEnd 待排數組左邊部分的結束位
* @param rightEnd 待排數組右邊部分的結束位
*/
private void merge(int[] array, int leftPos, int leftEnd, int rightEnd) {
int[] tmpArr = new int[rightEnd+1];//臨時的結果數組,將排序后的結果存放其中
int rightPos = leftEnd + 1;//位於右邊的待排數組開始位
int tmpPos = leftPos; //臨時數組的存儲位,每存一個,就后移一位
int tmp = leftPos;//排序的起始位置
// 從兩個數組中取出最小的放入臨時數組
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if ((array[leftPos] <= array[rightPos])){
tmpArr[tmpPos++] = array[leftPos++];
}else {
tmpArr[tmpPos++] = array[rightPos++];
}
}
//此時肯定有一個待排數組已經排完了,現在查找那個數組排完了,
// 並將另一個數組的剩余部分裝入臨時數組
while (rightPos <= rightEnd) {
tmpArr[tmpPos++] = array[rightPos++];
}
while (leftPos <= leftEnd) {
tmpArr[tmpPos++] = array[leftPos++];
}
// 將臨時數組中的內容復制回原數組
for (int i = tmp; i <= rightEnd; i++) {
array[i] = tmpArr[i];
}
}
//================================================================================================
/**
* 名稱:基數排序
* 描述:在描述基數排序前先描述一下“桶排序”:
* 桶排序:
* 假設待排數組{A1,A1,A3·····}中所有的數都小於“M”,
* 則建立一個長度為M的數組count[M],初始化為全0。
* 當讀取A1時,將count[A1]增1(初始為0,現在為1),當讀取A2時,將count[A2]增1····
* 之后count[M]中的每一個非0項的順序就是排序結果。
* 基數排序:
* 對於數組中的所有項的“每一位數”都進行桶排序。
* 比如先對所有項的“個位”進行桶排序,根據個位的桶排序的結果,對各個項進行一次排序。
* 之后再對十位進行桶排序··········
* 時間復雜度(d代表長度,n代表關鍵字個數,r代表關鍵字的基數):平均O(d(n+rd)),最壞O(d(n+rd))
* @param array 待排數組
* @param len_max 待排數組項的最高位位數,如待排數組={2,23,233,2333},則len_max為4(2333的位數)
* 穩定性:穩定
*/
public void radixSort(int[] array,int len_max){
/**
* 一般的桶排序的count[]只是一維數組,
* 里面的每項(0-9)的具體值代表了該數出現的次數,
* 如count[2]=3,代表2這個“項”,出現了3次。
*
* 但現在是“從個位開始”,每一位都要做桶排序。
* 如果還是使用count[],
* 那么count[2]=3只能代表“在所有項的第n位桶排序中”2這個數“作為第n位數”出現了3次。
*
* 顯然,正常的流程是:
* 先查找“個位”的count[0]=n,將這些“個位為0”的數從第0位開始放入數組。
* 再看“個位”的count[1]=m,將這些“個位為1”的數緊挨着剛剛插入的數插進來。·····
* “個位”的第一輪排序完后,原數組相當於“依據個位大小,進行了一次排序”,
* 接下來就要“依據十位大小再進行一次排序了”····。
*
* 綜上流程能夠發現,第n位中count[2]=3,這個2代表了“3個n位為2的數”,
* 我們要排序就必須知道“這3個數具體是什么”,然后把這些數按序放入原數組。
* 所以引入二維數組count[][],
* 第一維的下標代表了該位數“具體是幾”,所以范圍是0-9。
* 第二維的下標代表了該位數相同的值“第幾次出現”,
* (如個位桶排序時,count[2][34]就代表了第34個“個位為2的數”。)
* 第二維中存儲的是具體的某個數
*/
int[][] count = new int[10][array.length];
//該數組frequency[n]=m,用來計算“某位的桶排序”中“n這個數第m次出現”。
// 所以n的范圍只能是0-9,而m最多可能是原數組的長度(當該數組某一位的值都是同一個數時)
int[] frequency = new int[10];
int now_digit = 1;//當前排序的是各項的第幾位數(從第一位(個位)開始排)
int n = 1;//用來計算當前位的具體值
//從個位開始排,然后再排十位·····
while (now_digit<=len_max){
//根據原數組中各項的“now_digit位”,進行桶排序。
for (int i=0;i<array.length;i++){
//找到具體某位的值。如n=1時,找到的就是個位的值。n=10時,找到的就是十位的值
int digit = ((array[i] / n) % 10);
count[digit][frequency[digit]]=array[i];
frequency[digit]++;
}
/**
* 現在所有的項已經根據桶排序規則存入count[][]中,現在需要按序再存回原數組。思路如下:
* count[][]中第一個下標意味着“當前位”的具體值,為0-9.
* 所以應該將count[0][n]中的各個數排在前面,count[1][m]中的各項跟在后面·····
* count[0][]中存了多個“當前位為0的數”,
* 而count[][]的第二個下標表示“被存儲的數”是“第幾個下標為0的數”。
* 如:當前位為“個位”排序時,count[0][1]=21表示21是第一個“個位為0的數”,
* count[0][6]=341表示341是第6個“個位為0的數”
*/
//把數據存在原數組的什么位置(起始的存儲位置自然是0,然后每存一個數后移一位)
int k = 0;
//從count[i=0][]開始找,然后找count[i++][]····
for(int i = 0; i < 10; i++) {
//frequency[i]代表了“i這個數第幾次出現”,所以為0就表示沒出現過,也就不用排了
if(frequency[i] != 0){
//從第0次出現開始找,每次都裝入原數組
for(int j = 0; j < frequency[i]; j++) {
//j代表了“位值為i”的數是第幾個,count[i][j]代表了該數
array[k] = count[i][j];
k++;
}
}
//“當前位數”為i的數已經存完,需要初始化,否則下一“位數為i”的數存時會出錯。
frequency[i] = 0;
}
//每循一次,用來計算當前位的具體值的n做+10處理
n *= 10;
//每循一次,當前位數+1
now_digit++;
}
}
//一個簡單的桶排序
private void bucketSort(){
int[] array_demo = {2,5,7,3,1,6,8,4,2,1,3,7,9,4,2,5,0};
int[] count = new int[10];
int m=0;
for (int i:array_demo){
count[i]++;
}
for (int i=0;i<10;i++){
if (count[i]!=0){
for (int k=0;k<count[i];k++){
array_demo[m] = i;
m++;
}
}
}
}
}
測試代碼:
/**
* 運行測試各種排序方法
*/
public class Main {
public static void main(String[] args) {
Sorting sorting_util = new Sorting();
//直接插入排序
int[] param_straightInsertionSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.straightInsertionSort(param_straightInsertionSort);
printResult("插入排序-直接插入排序:",param_straightInsertionSort);
//希爾排序
int[] param_shellSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
int incrementNum = param_shellSort.length/2;//增量
sorting_util.shellSort(param_shellSort,incrementNum);
printResult("插入排序-希爾排序:",param_shellSort);
//冒泡排序
int[] param_bubbleSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.bubbleSort(param_bubbleSort);
printResult("交換排序-冒泡排序:",param_bubbleSort);
//快速排序
int[] param_quickSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.quickSort(param_quickSort,0,param_quickSort.length-1);
printResult("交換排序-快速排序:",param_quickSort);
//直接選擇排序
int[] param_selectionSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.selectionSort(param_selectionSort);
printResult("選擇排序-直接選擇排序:",param_selectionSort);
//堆排序
int[] param_heapSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.heapSort(param_heapSort);
printResult("選擇排序-堆排序:",param_heapSort);
//歸並排序
int[] param_mergingSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.mergingSort(param_mergingSort, 0, param_mergingSort.length-1);
printResult("歸並排序:",param_mergingSort);
//基數排序
int[] param_radixSort= {6,2,8,5,324,23423,56,2,87,3,42,436};
sorting_util.radixSort(param_radixSort,5);
printResult("基數排序:",param_radixSort);
}
/**
* 將排序結果展示出來
* @param sort_type 用來描述排序方法
* @param array_result 排序結果數組
*/
public static void printResult(String sort_type,int[] array_result){
System.out.print(sort_type);
for (int i:array_result){
System.out.print(i+",");
}
System.out.println();
}
}
速度測試:
(1)
隨機數范圍:0-100
隨機數數量:100
結果:
8種算法速度均在0ms左右,沒有顯著差別。
(2)
隨機數范圍:0-1000
隨機數數量:1000
結果:
希爾排序、快速排序、堆排序、基數排序:0ms左右
直接插入排序、冒泡排序、直接選擇排序、歸並排序:1ms-10ms
(3)
隨機數范圍:0-10000
隨機數數量:10000
結果:
希爾排序、快速排序、堆排序、基數排序:0ms-10ms
直接插入排序:10ms-20ms
直接選擇排序、歸並排序:50ms左右
冒泡排序:150ms左右
(4)
隨機數范圍:0-100000
隨機數數量:100000
結果:
希爾排序、快速排序、堆排序、基數排序:10ms-20ms
直接插入排序:1100ms左右
歸並排序:2000ms左右
直接選擇排序:5000ms左右
冒泡排序:14500ms左右
(5)
隨機數范圍:0-10000000
隨機數數量:10000000
結果:
基數排序:690ms左右
快速排序:1300ms左右
希爾排序:2600ms左右
堆排序:2900ms左右
直接插入排序、冒泡排序、直接選擇排序、歸並排序:速度過慢。
