您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)Java數(shù)據(jù)結(jié)構(gòu)常見幾大排序是什么的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
排序是將一批無序的記錄(數(shù)據(jù))重新排列成按關(guān)鍵字有序的記錄序列的過程。
排序分為內(nèi)部排序和外部排序
若整個(gè)排序過程不需要訪問外存便能完成,則稱此類排序問題為內(nèi)部排序。
反之,若參加排序的記錄數(shù)量很大,整個(gè)序列的排序過程不可能在內(nèi)存中完成,則稱此類排序問題為外部排序。
內(nèi)部排序的過程是一個(gè)逐步擴(kuò)大記錄的有序序列長度的過程
假定在待排序的記錄序列中,存在多個(gè)具有相同的關(guān)鍵字的記錄,若經(jīng)過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩(wěn)定的;否則稱為不穩(wěn)定的。
總結(jié)起來就是,如果一個(gè)數(shù)據(jù)在排序過程中發(fā)生了跳躍行為,則為不穩(wěn)定的排序;反之,則是穩(wěn)定的排序。
時(shí)間復(fù)雜度:一個(gè)算法執(zhí)行所耗費(fèi)的時(shí)間
空間復(fù)雜度:運(yùn)行完一個(gè)程序所需的內(nèi)存大小。
直接插入排序的基本操作是講一個(gè)記錄插入到已經(jīng)排好序的有序表中,從而得到一個(gè)新的、記錄數(shù)增1的有序表。
代碼:
public static void insertSort(int[] array) { for (int i = 1; i < array.length; i++) { int tmp = array[i]; int j = i-1; for (; j >= 0 ; j--) { if(array[j] > tmp) { array[j+1] = array[j]; }else { break; } } //j回退到了小于0的地方 array[j+1] = tmp; } }
流程圖解:
時(shí)間和復(fù)雜度分析: 我們在實(shí)現(xiàn)的這個(gè)排序算法的時(shí)候,只借助了一個(gè)輔助的記錄空間。
所以最好的情況就是我們原來需要的排序的元素之間就是有序的,比如說{1,2,3,4,5,6},那么我們需要比較的次數(shù),其實(shí)就是臨近兩個(gè)元素的比較,因此沒有移動(dòng)的記錄,時(shí)間復(fù)雜度為O(n);
最壞的情況就是元素都是逆序的,如{6,5,4,3,2,1},所以都需要比較,移動(dòng)次數(shù)達(dá)到O(n^2).
穩(wěn)定性:穩(wěn)定
插入排序,初始數(shù)據(jù)越接近有序,時(shí)間效率越高
希爾排序法又稱縮小增量法。希爾排序法的基本思想是:先選定一個(gè)整數(shù),把待排序文件中所有記錄分成個(gè)組,所有距離為的記錄分在同一組內(nèi),并對每一組內(nèi)的記錄進(jìn)行排序。然后,重復(fù)上述分組和排序的工作。當(dāng)?shù)竭_(dá)=1時(shí),所有記錄在統(tǒng)一組內(nèi)排好序。
希爾對記錄的分組,不是簡單地“逐段分割”,而是將相隔某個(gè)“增量”的記錄分成一組。
在嚴(yán)薇敏的《數(shù)據(jù)結(jié)構(gòu)》里面是這樣說的:
上面的話可能有點(diǎn)難理解,下面我們通過畫圖來了解一下希爾排序的本質(zhì)。
希爾排序跟直接插入排序有點(diǎn)相似,可以說是直接插入排序的升級版。不同在于,希爾排序的比較方式變成了跳躍式的。比如說下面這組數(shù)據(jù)的第一次排序。
最終排序完成了。
我們來看一下希爾排序的代碼:
public static void shell(int[] array,int gap) { for (int i = gap; i < array.length; i++ ) { int tmp = array[i]; int j = i-gap; for (; j >= 0 ; j -= gap) { if(array[j] > tmp) { array[j+gap] = array[j]; }else { break; } } array[j+gap] = tmp; } } //按照5、3、1分組 public static void shellSort1(int[] array) { int[] drr = {5,3,1}; for (int i = 0; i < drr.length; i++) { shell(array,drr[i]); } }
是不是跟直接插入排序很像?所以我們才說,希爾排序是直接插入排序的升級版。它不是隨便分組后各自排序,而是將相隔某個(gè)增量gap的記錄組成一個(gè)子序列,實(shí)現(xiàn)跳躍式移動(dòng),使得排序的效率提高了。
所以,選取“增量”是非常關(guān)鍵的一步,值得一提的是,選取什么樣的“增量”,目前為止尚沒有一個(gè)沒完美的增量序列。
復(fù)雜度分析:
時(shí)間復(fù)雜度[和增量有關(guān)系的]:O(n^1.3 - n^1.5)。
空間復(fù)雜度:O(1)
穩(wěn)定性:不穩(wěn)定的。
簡單選擇排序:通過n-i次關(guān)鍵字間的比較,從n-i+1個(gè)記錄中選出關(guān)鍵字最小的記錄,并和第 i(1 ≤ i ≤n) 個(gè)記錄交換
public static void selectSort1(int[] array) { for (int i = 0; i < array.length; i++) { for (int j = i+1; j < array.length; j++) { //1 2 3 4 5 6 if(array[j] < array[i]) { swap(array,i,j); } } } } public static void swap(int[] array,int i,int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp;
有時(shí)候,我們可能不需要循環(huán)那么多次,循環(huán)一兩次就有序了,如果在有序的序列中繼續(xù)循環(huán),則會造成時(shí)間的浪費(fèi)。為避免這種情況,所以我們可以對代碼進(jìn)行稍稍改進(jìn):
public static void selectSort(int[] array) { for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i+1; j < array.length; j++) { //找到最小值下標(biāo) if(array[j] < array[minIndex]) { minIndex = j; } } swap(array,i,minIndex); } }
復(fù)雜度分析:
時(shí)間復(fù)雜度:O(n^2)
空間復(fù)雜度:同接下介紹的冒泡排序一樣,只有在兩個(gè)記錄交換時(shí)需要一個(gè)輔助空間,所以空間復(fù)雜度為O(1).
穩(wěn)定性:穩(wěn)定
堆排序的基本原理也是選擇排序,只是不在使用遍歷的方式查找無序區(qū)間的最大的數(shù),而是通過堆來選擇無序區(qū)間的最大的數(shù)。
我上一篇文章有詳細(xì)介紹,這里就不再說了,大家感興趣可以去了解一下。 Java數(shù)據(jù)結(jié)構(gòu)之優(yōu)先級隊(duì)列(堆)圖文詳解
這里也給一下代碼:
public static void heapSort(int[] array) { //1、建堆 O(N) createHeap(array); int end = array.length-1; //2、交換然后調(diào)整 O(N * log N) while (end > 0) { swap(array,0,end); shiftDown(array,0,end); end--; } } public static void createHeap(int[] array) { for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) { shiftDown(array,parent,array.length); } } public static void shiftDown(int[] array,int parent,int len) { int child = 2*parent+1;//左孩子下標(biāo) while (child < len) { if(child+1 < len && array[child] < array[child+1]) { child++; } //child下標(biāo) 就是左右孩子最大值的下標(biāo) if(array[child] > array[parent]) { swap(array,child,parent); parent = child; child = 2*parent+1; }else { break; } } }
復(fù)雜度分析:
時(shí)間復(fù)雜度:O(n * log2(n))——2指的是以2為底
空間復(fù)雜度:O(1)
穩(wěn)定性:不穩(wěn)定
【注意】
堆排序只能用于順序結(jié)構(gòu),不能用于鏈?zhǔn)浇Y(jié)構(gòu)
由于建初堆的時(shí)候所需比較的次數(shù)較多,因此記錄次數(shù)較少時(shí)不宜采用。
冒泡排序是一種交換排序,它的基本思想是:兩兩比較相鄰記錄的關(guān)鍵字,如果反序則交換,直到?jīng)]有反序的記錄為止。
代碼:
public static void bubbleSort(int[] array) { for (int i = 0; i < array.length-1; i++) { for (int j = 0; j < array.length-1; j++) { if(array[j] > array[j+1]) { swap(array,j+1,j); } } } } public static void swap(int[] array,int i,int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp;
復(fù)雜度分析:
時(shí)間復(fù)雜度:O(N^2)
空間復(fù)雜度:O(1)
穩(wěn)定性:穩(wěn)定。
快速排序是冒泡排序的升級版,它們都屬于交換排序類,即它也是通過不斷比較和移動(dòng)交換來實(shí)現(xiàn)排序,不同的是,快排的實(shí)現(xiàn)增大的了記錄的比較和移動(dòng)的距離。
快排將關(guān)鍵字較大的數(shù)據(jù)從前面直接移到后面,關(guān)鍵字較小的記錄從后面直接移到前面,從而減少了總的比較次數(shù)和移動(dòng)交換次數(shù)。
快排的基本思想:
通過一趟排序?qū)⒋判虻臄?shù)據(jù)分割成獨(dú)立的兩部分,其中一部分比另一部小,然后再分別對這兩部分記錄并再次進(jìn)行排序,以達(dá)到整個(gè)序列有序。
我們翻譯翻譯一下上面的那段話:
首先,你得有一個(gè)中間數(shù),比它小的放一邊,比它大的放一邊。這個(gè)數(shù),我們稱為基準(zhǔn)值。
采用分治思想,對左右兩個(gè)小區(qū)間按照同樣的方式處理,直到小區(qū)間的長度 == 1,代表已經(jīng)有序,或者小區(qū)間的長度 == 0,代表沒有數(shù)據(jù)。
可能大家看到這里也還是有點(diǎn)迷惑,我們直接上代碼。
public static void quickSort(int[] array) { quick(array,0,array.length-1); } public static void quick(int[] array,int left,int right) { if(left >= right) { return; } int pivot = partition(array,left,right);//基準(zhǔn) quick(array,left,pivot-1); quick(array,pivot+1,right); }
上面的代碼是不是有點(diǎn)像二叉樹的遍歷?
這二者確實(shí)有相似之處,我們后面再講。
上面還有一個(gè)partition函數(shù),這個(gè)函數(shù)是我們快速排序最關(guān)鍵的地方。
/** * 找基準(zhǔn) * @param array 待排序數(shù)組對象 * @param start左邊界 * @param end右邊界 * @return 基準(zhǔn)值下標(biāo) */ private static int partition(int[] array,int start,int end) { int tmp = array[start]; while (start < end) { while (start < end && array[end] >= tmp) { end--; } //end下標(biāo)就遇到了 < tmp的值 array[start] = array[end]; while (start < end && array[start] <= tmp) { start++; } //start下標(biāo)就遇到了 > tmp的值 array[end] = array[start]; } array[start] = tmp; return start; }
我們下面通過圖解模擬一下函數(shù)的運(yùn)行過程:
可以看到,當(dāng)?shù)谝惠喿咄曛?,?shù)據(jù)由基準(zhǔn)值分成了兩部分。
之后,我們再次對左右兩部分完成同樣的操作,如下:
一直遞歸下來,跟二叉樹的遍歷類似。
復(fù)雜度分析:
時(shí)間復(fù)雜度:
最好情況下:O(nlogn)
最壞情況下:O(n^2).
空間復(fù)雜度:O(logn)
穩(wěn)定性:不穩(wěn)定
快排優(yōu)化
不知大家看上面的圖解的時(shí)候有沒有一點(diǎn)困惑,就是我們這基準(zhǔn)選得不好,導(dǎo)致第一趟遞歸下來的效果不好,變成了如下圖:
如果我們有一種辦法,先找到相對居中的那個(gè)數(shù)字,那么整個(gè)排序的時(shí)間復(fù)雜度是不是大大減小了。
于是,有人提出了隨機(jī)選取一個(gè)值來作為基準(zhǔn),稱為隨機(jī)選取法。
這種做法,得看運(yùn)氣,因?yàn)榧偃邕x的好,剛剛選取中間值,那么,性能大大提高;如果隨機(jī)得不好,比如隨機(jī)到最小值或者最大值,那么性能則變成最差了。
所以有人提出一個(gè)新的方法,三數(shù)取中:
取三個(gè)關(guān)鍵字先進(jìn)行排序,將中間數(shù)作為基準(zhǔn),一般取左端,右端和中間三個(gè)數(shù)。
如果運(yùn)用三數(shù)取中這種方法的話,第一次比較的結(jié)果如下:
可以看到,11基本上與中間的數(shù)字相差不遠(yuǎn)了,性能大大提高。
所以,這里我們再寫一個(gè)找基準(zhǔn)的代碼:
/** * 找基準(zhǔn) 三數(shù)取中法 * @param array 待排序數(shù)組對象 * @param left 左邊界 * @param right 右邊界 * @return 基準(zhǔn)值下標(biāo) */ private static int findMidValIndex(int[] array,int start,int end) { int mid = start + ((end-start) >>> 1); if(array[start] < array[end]) { if(array[mid] < array[start]) { return start; }else if(array[mid] > array[end]) { return end; }else { return mid; } }else { if(array[mid] > array[start]) { return start; }else if(array[mid] < array[end]) { return end; }else { return mid; } } }
前面quick函數(shù)改動(dòng)一下,如下:
public static void quick(int[] array,int left,int right) { if(left >= right) { return; } //采用三數(shù)取中法找基準(zhǔn)值 int midValIndex = findMidValIndex(array,left,right); swap(array,midValIndex,left); int pivot = partition(array,left,right);//基準(zhǔn) quick(array,left,pivot-1); quick(array,pivot+1,right); }
其實(shí),優(yōu)化到這里已經(jīng)很棒了 。但是,我們還能優(yōu)化。
這里先插播一個(gè)知識點(diǎn):
直接插入是簡單排序中性能最好的。 所以如果要我們要排序的數(shù)組非常小,直接插入排序會更好。 原因在于,快速排序用到了遞歸操作,在大量的數(shù)據(jù)排序時(shí),這點(diǎn)性能影響相對它的整體算法優(yōu)勢而言是可以忽略的。但是如果數(shù)組只有不多的數(shù)據(jù)需要排序,就有點(diǎn)大材小用了。
因此,我們在這里的優(yōu)化是:
增加一個(gè)判斷,當(dāng) right-left+1 不大于某個(gè)常數(shù)時(shí),就用直接插入排序,這個(gè)常數(shù)是具體情況而定。這樣我們就能保證最大化地利用兩種排序的優(yōu)勢來完成排序工作了。
/** * 優(yōu)化,加入插入排序 * @param array 待排序數(shù)組對象 * @param left 左邊界 * @param right 右邊界 * @return 基準(zhǔn)值下標(biāo) */ public static void quick(int[] array,int left,int right) { if(left >= right) { return; } //加一個(gè)判斷,如果區(qū)間內(nèi)的數(shù)據(jù),在排序的過程當(dāng)中,小于某個(gè)范圍了,可以使用直接插入排序 if(right-left+1 <= 1400) { //使用直接插入排序 insertSort2(array,left,right); return; } //1、找基準(zhǔn)之前,我們找到中間大小的值-使用三數(shù)取中法 int midValIndex = findMidValIndex(array,left,right); swap(array,midValIndex,left); int pivot = partition(array,left,right);//基準(zhǔn) quick(array,left,pivot-1); quick(array,pivot+1,right); } //插入排序 public static void insertSort(int[] array,int start,int end) { for (int i = 1; i <= end; i++) { int tmp = array[i]; int j = i-1; for (; j >= start ; j--) { if(array[j] > tmp) { array[j+1] = array[j]; }else { break; } } array[j+1] = tmp; } }
我們都知道,遞歸對性能是有一定影響的。所以我們也可以采用非遞歸的方式來實(shí)現(xiàn)快速排序
/** * 快速排序非遞歸實(shí)現(xiàn) * @param array 待排序數(shù)組 */ public static void quickSort(int[] array) { Stack<Integer> stack = new Stack<>(); int left = 0; int right = array.length-1; int pivot = partition(array,left,right); if(pivot > left+1) { stack.push(left); stack.push(pivot-1); } if(pivot < right-1) { stack.push(pivot+1); stack.push(right); } while (!stack.isEmpty()) { right = stack.pop(); left = stack.pop(); pivot = partition(array,left,right); if(pivot > left+1) { //左邊有2個(gè)元素 stack.push(left); stack.push(pivot-1); } if(pivot < right-1) { //右邊有2個(gè)元素 stack.push(pivot+1); stack.push(right); } } }
非遞歸復(fù)雜度分析:
時(shí)間復(fù)雜度:
最壞情況下: O(n^2)
平均情況下:O(nlogn)
空間復(fù)雜度:
最壞情況下:O(n)
平均情況下:O(logn)
穩(wěn)定性:不穩(wěn)定
歸并排序(MERGE-SORT)是建立在歸并操作上的一種有效的排序算法,該算法是采用分治法(Divide an Conquer)的一個(gè)非常典型的應(yīng)用。將已有序的子序列合并,得到完全有序的序列;即先使每個(gè)子序列有序,再使子序列段間有序。若將兩個(gè)有序表合并成一個(gè)有序表,稱為二路歸并
直接上代碼:
//調(diào)用了mergeSortInternal函數(shù) public static void mergeSort1(int[] array) { mergeSortInternal(array,0,array.length-1); } private static void mergeSortInternal(int[] array,int low,int high) { if(low>=high) { return; } int mid = low + ((high-low) >>> 1);//>>>無符號右移1位。就是除以2,找中間值 //左邊遞歸 mergeSortInternal(array,low,mid); //右邊遞歸 mergeSortInternal(array,mid+1,high); //合并兩邊數(shù)組 merge(array,low,mid,high); }
mergeSortInternal函數(shù)的圖解:
其實(shí)就是對半分開數(shù)組
這里這個(gè)merge函數(shù)是歸并排序里面的關(guān)鍵,無論是采用遞歸還是非遞歸都必須采用到這部分的函數(shù)。
而其本質(zhì),其實(shí)就是合并兩個(gè)數(shù)組,并使其有序起來。
merge函數(shù)的代碼:
private static void merge(int[] array,int low,int mid,int high) { int[] tmp = new int[high-low+1]; int k = 0;// int s1 = low; int e1 = mid; int s2 = mid+1; int e2 = high; while (s1 <= e1 && s2 <= e2) { if(array[s1] <= array[s2]) { tmp[k++] = array[s1++]; }else { tmp[k++] = array[s2++]; } } while (s1 <= e1) { tmp[k++] = array[s1++]; } while (s2 <= e2) { tmp[k++] = array[s2++]; } //拷貝tmp數(shù)組的元素 放入原來的數(shù)組array當(dāng)中 for (int i = 0; i < k; i++) { array[i+low] = tmp[i]; } }
歸并排序除了用遞歸的方式來實(shí)現(xiàn),也可以用非遞歸的方式來實(shí)現(xiàn)。
public static void mergeSort(int[] array) { int nums = 1;//每組的數(shù)據(jù)個(gè)數(shù) while (nums < array.length) { //數(shù)組每次都要進(jìn)行遍歷,確定要?dú)w并的區(qū)間 for (int i = 0; i < array.length; i += nums*2) { int left = i; int mid = left+nums-1; //防止越界 if(mid >= array.length) { mid = array.length-1; } int right = mid+nums; //防止越界 if(right >= array.length) { right = array.length-1; } //小標(biāo)確定之后,進(jìn)行合并 merge(array,left,mid,right); } nums *= 2;數(shù)組合并后,以1-2-4-8-16-進(jìn)行循環(huán) } }
圖解如下:
感謝各位的閱讀!關(guān)于“Java數(shù)據(jù)結(jié)構(gòu)常見幾大排序是什么”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。