算法: 描述一種有限、確定、有效的併合適計算機程序來實現用於解決問題的方法
一般與數據結構組合起來使用,算法提供方法思想,數據結構提供數據的組織方式
穩定性:排序中相等的元素保留之前的相對順序我們就是算法是穩定的。
排序算法有很多,包括插入排序,冒泡排序,堆排序,歸併排序,選擇排序,計數排序,基數排序,桶排序,快速排序等。插入排序,堆排序,選擇排序,歸併排序和快速排序,冒泡排序都是比較排序,它們通過對數組中的元素進行比較來實現排序,其他排序算法則是利用非比較的其他方法來獲得有關輸入數組的排序信息。
1.冒泡排序
分爲n-1趟,每趟確定一個數的最終排序位置,爲每次排序範圍的最後一個位置
從第一個元素開始,依次比較相鄰元素的大小,將大的元素往後傳。
冒泡是穩定的,因爲兩兩相等的元素是不會交換的,若兩個相等的元素沒有相鄰,那麼通過前面的兩兩交換把兩個元素相鄰起來,他們也不會交換。
冒泡的比較次數是固定的,1+2+...+(n-1) = n*(n-1)/2
Java描述:
public static void bubbleSort(int[] arr){
for(int i =1; i < arr.length-i; i++){
for(int j =0; j<arr.length-i; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
2.選擇排序
也是分爲n-1趟,每趟也確定一個最終排序位置,爲每次排序範圍的第一個位置
從第一個位置開始,依次往後比較相應位置上的元素大小,遇到更小的元素就與之交換。
選擇是不穩定的,舉例:在一趟選擇中,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就受到破壞了。
選擇的比較次數也是固定的,1+2+...+(n-1) = n*(n-1)/2
交換次數O(n),最好情況是,已經有序,交換0次;最壞情況交換n-1次,逆序交換n/2次。交換次數比冒泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。
Java描述:
public static void selectionSort(int[] arr){
for(int i = 0; i < arr.length - 1; j++){
int min = i;
for(int j = i + 1; j < arr.length; j++){
if (arr[min] > arr[j]){
min = j;
}
}
if (min != j){
int tmp = arr[min];
arr[min] = arr[i];
arr[i] = arr[min];
}
}
}
3.插入排序
插入排序是一個簡單直觀且穩定的排序算法。
也是分爲n-1趟,每趟插入一個元素到已有的有序隊列中,從而得到一個新的個數加一的有序隊列,適用於少量的數據的排序。
插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最後一個元素除外(讓數組多一個空間纔有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。
穩定的原因:
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。
Java描述:
public static int[] insertSort(int[] arr){
if (arr == null || arr.length < 2){
return arr;4.
}
for (int i = 1; i < arr.length; i++){
int temp = arr[i];
int index = i;
for (int j = i; j > 0; j--){
if (arr[j] < arr[j-1]){
arr[j] = arr[j-1];
index = j - 1;
}else{
break;
}
arr[index] = temp;
}
}
return arr;
}
4.希爾排序
希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
-
插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率。
-
但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位。
希爾排序的時間性能優於直接插入排序的原因:
①當文件初態基本有序時直接插入排序所需的比較和移動次數均較少。
②當n值較小時,n和 n^2 的差別也較小,即直接插入排序的最好時間複雜度O(n)和最壞時間複雜度0(n^2)差別不大。
③在希爾排序開始時增量較大,分組較多,每組的記錄數目少,故各組內直接插入較快,後來增量di逐漸縮小,分組數逐漸減少,而各組的記錄數目逐漸增多,但由於已經按di-1作爲距離排過序,使文件較接近於有序狀態,所以新的一趟排序過程也較快。
因此,希爾排序在效率上較直接插入排序有較大的改進。
Java描述:
public static void ShellSort(int[] arr){
int len = arr.length;
while (len != 1){
len /= 2;
for (int i = 0; i < len; i++){
for (int j = i + len; j < arr.length; j += len){
int temp = arr[j];
for (k = j - len; k >= 0 && arr[k] >temp; k -= len){
arr[k+len] = arr[k];
}
arr[k + len] = temp;
}
}
}
}
5.歸併排序
歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。
歸併操作的工作原理如下:
第一步:申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
第二步:設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
重複步驟3直到某一指針超出序列尾
將另一序列剩下的所有元素直接複製到合併序列尾
歸併排序是穩定的排序.即相等的元素的順序不會改變.如輸入記錄 1(1) 3(2) 2(3) 2(4) 5(5) (括號中是記錄的關鍵字)時輸出的 1(1) 2(3) 2(4) 3(2) 5(5) 中的2 和 2 是按輸入的順序.這對要排序數據包含多個信息而要按其中的某一個信息排序,要求其它信息儘量按輸入的順序排列時很重要。歸併排序的比較次數小於快速排序的比較次數,移動次數一般多於快速排序的移動次數。
速度僅次於快速排序,爲穩定排序算法,一般用於對總體無序,但是各子項相對有序的數列。
Java描述:
public static int[] mergeSort(int[] nums, int l , int h){
if (l == h){
return new int[] {nums[l]};
}
int mid = l + (h - l) / 2;
int[] leftArr = mergeSort(nums, l, mid); // 左有序數組
int[] rightArr = mergeSort(nums, mid + 1, h); // 右有序數組
int[] newArr = new int[h-l+1]; // 新有序數組
int m = 0, i = 0, j = 0;
while (i < leftArr.length && j < rightArr.length){
newArr[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : right[j++];
}
while (i < leftArr.length){
newArr[m++] = leftArr[i++];
}
while (j < rightArr.length){
newArr[m++] = rightArr[j++];
}
return newArr;
}
6.快速排序
快速排序(Quicksort)是對冒泡排序的一種改進。
它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
快速排序算法通過多次比較和交換來實現排序,其排序流程如下:
(1)首先設定一個分界值,通過該分界值將數組分爲左右兩個部分。
(2)將大於或者等於分界值的數據集中到數組右邊(所以是不穩定的),小於分界值的數據集中到數組的左邊。此時,左邊部分中各元素都小於或者等於分界值,而右邊部分中的元素都大於或者等於分界值。
(3)然後,左邊和右邊的數據可以獨立排序。對於左側的數組數據,又可以去一個分界值,將該部分分成左右兩部分,同樣在左邊放置較小值,右邊放置較大值。右側的數組數據也可以做類似處理。
(4)重複上述過程,可以看出,這是一個遞歸定義。通過遞歸將左側部分拍好序後,再遞歸排好右側部分的順序。當左、右兩個部分各數據排序完成後,整個數組的排序也就完成了。
1)設置兩個變量i、j,排序開始的時候:i=0,j=N-1;
2)以第一個數組元素作爲關鍵數據,賦值給key,即key=A[0];
3)從j開始向前搜索,即由後開始向前搜索(j--),找到第一個小於key的值A[j],將A[j]和A[i]的值交換;
4)從i開始向後搜索,即由前開始向後搜索(i++),找到第一個大於key的A[i],將A[i]和A[j]的值交換;
5)重複第3、4步,直到i=j; (3,4步中,沒找到符合條件的值,即3中A[j]不小於key,4中A[i]不大於key的時候改變j、i的值,使得j=j-1,i=i+1,直至找到爲止。找到符合條件的值,進行交換的時候i, j指針位置不變。另外,i==j這一過程一定正好是i+或j-完成的時候,此時令循環結束)。
快速排序的一次劃分算法從兩頭交替搜索,直到low和high重合,因此其時間複雜度是O(n);而整個快速排序算法的時間複雜度與劃分的趟數有關。
理想的情況是,每次劃分所選擇的中間數恰好將當前序列幾乎等分,經過log2n趟劃分,便可得到長度爲1的子表。這樣,整個算法的時間複雜度爲O(nlog2n)。
最壞的情況是,每次所選的中間數是當前序列中的最大或最小元素,這使得每次劃分所得的子表中一個爲空表,另一子表的長度爲原表的長度-1。這樣,長度爲n的數據表的快速排序需要經過n趟劃分,使得整個排序算法的時間複雜度爲O(n2)。
爲改善最壞情況下的時間性能,可採用其他方法選取中間數。通常採用“三者值取中”方法,即比較H->r[low].key、H->r[high].key與H->r[(1ow+high)/2].key,取三者中關鍵字爲中值的元素爲中間數。
可以證明,快速排序的平均時間複雜度也是O(nlog2n)。因此,該排序方法被認爲是目前最好的一種內部排序方法。
從空間性能上看,儘管快速排序只需要一個元素的輔助空間,但快速排序需要一個棧空間來實現遞歸。最好的情況下,即快速排序的每一趟排序都將元素序列均勻地分割成長度相近的兩個子表,所需棧的最大深度爲log2(n+1);但最壞的情況下,棧的最大深度爲n。這樣,快速排序的空間複雜度爲O(log2n))。
Java描述:
//方式一********************
public static int[] qsort(int arr[], int start, int end){
int pivot = arr[start];
int i = start;
int j = end;
while (i<j){
while ((i<j) && (arr[j] > pivot)){
j--;
}
while ((i<j) && arr[i] < pivot){
i++;
}
if ((arr[i] == arr[j]) && (i<j)){
i++;
}else{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
if (i - 1 > start){
arr = qsort(arr, start, i - 1);
}
if (j + 1 < end){
arr = qsort(arr, j + 1; end);
}
return arr;
}
//方式二(更高效)***********************TextendsComparable和SortUtil都是自己封裝的類,裏面重寫和實現了compareTo
//和swap方法
public <TextendsComparable<? super T>> T[] quickSort(T[] targetArr, int start, int end){
int i = start + 1, j = end;
T key = targetArr[start];
SortUtil<T> sUtil = new SortUtil<>();
if (start == end){
return targetArr;
}
while (true){
while (targetArr[j].compareTo(key) > 0){
j--;
}
while (targetArr[i].compareTo(key) < 0 && i < j){
i++;
}
if (i >= j){
break;
}
sUtil.swap(targetArr, i, j);
if (targetArr[i] == key){
j--;
}else{
i++;
}
}
sUtil.swap(targetArr, start, j); // 將關鍵數據放到中間來
if (start < i - 1){
this.quickSort(targetArr, start, i - 1);
}
if (j + 1 < end){
this.quickSort(targetArr, j + 1, end);
}
return targetArr;
}
//方法三*********************減少交換次數,提高效率
public <TextendsComparable<? super T>> void quickSort(T[] targetArr, int start, int end){
int i =start, j = end;
T key = targetArr[start];
while (i < j){
/*按j--方向遍歷目標數組,直到比key小的值爲止*/
while (j > i && targetArr[j].compareTo(key) > 0){
j--;
}
if (i < j){
/*targetArr[i]已經保存在key中,可將後面的數填入*/
targetArr[i] = targetArr[j];
i++;
}
/*按i++方向遍歷目標數組,直到比key大的值爲止*/
while (i < j && targetArr[i].compareTo(key) <= 0){
/*此處一定要小於等於零,假設數組之內有一億個1,0交替出現的話,而key的值又恰巧是1的話,那麼這個小於等於的作用就會使下面的if語句少執行一億次。*/
i++;
}
if (i < j){
/*targetArr[j]已保存在targetArr[i]中,可將前面的值填入*/
targetArr[j] = targetArr[i];
j--;
}
}
targetArr[i] = key;
if (start < i - 1){
this.quickSort(targetArr, start, i - 1);
}
if (j + 1 < end){
this.quickSort(targetArr, j + 1, end);
}
}
7.堆排序
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
在堆的數據結構中,堆中的最大值總是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。堆中定義以下幾種操作:
-
最大堆調整(Max Heapify):將堆的末端子節點作調整,使得子節點永遠小於父節點
-
創建最大堆(Build Max Heap):將堆中的所有數據重新排序
-
堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算
Java描述:
public static int[] heapSort(int[] arr){
//這裏元素的索引是從0開始的,所以最後一個非葉子結點array.length/2 - 1
for (int i = arr.length/2 - 1; i >= 0; i--){
adjustHeap(arr, i, arr.length); // 調整堆
}
// 上述邏輯,建堆結束
// 下面開始排序邏輯
for (int j = arr.length - 1; j > 0; j--){
// 元素交換,作用是去掉大頂堆
// 把大頂堆的根元素,放到數組的最後;換句話說,就是每一次的堆調整之後,都會有一個元素到達自己的最終位置
swap(arr, 0, j);
// 元素交換之後,毫無疑問,最後一個元素無需再考慮排序問題了。
// 接下來我們需要排序的,就是已經去掉了部分元素的堆了,這也是爲什麼此方法放在循環裏的原因
// 而這裏,實質上是自上而下,自左向右進行調整的
adjustHeap(arr, 0, j);
}
return arr;
}
public static void adjustHeap(int[] arr, int i, int length){
// 先把當前元素取出來,因爲當前元素可能要一直移動
int temp = arr[i];
for (int k = 2*i + 1; k < length; k = 2*k + 1){ //2*i+1爲左子樹i的左子樹(因爲i是從0開始的),2*k+1爲k的左子樹
// 讓k先指向子節點中最大的節點
if (k + 1 < length && arr[k] < arr[k+1]){ // 如果有右子樹,並且右子樹大於左子樹
k++;
}
//如果發現結點(左右子結點)大於根結點,則進行值的交換
if (arr[k] > temp){
swap(arr, i, k);
// 如果子節點更換了,那麼,以子節點爲根的子樹會受到影響,所以循環對子節點所在的數繼續進行判斷
}else{
break; // 不用交換,退出循環
}
}
}