java实现归并排序(详解)
主要思想
歸并排序和快速排序都是基于分而治之的算法思想。
歸并排序先將待排序的數組不斷拆分,直到拆分到區間里只剩下一個元素的時候。不能再拆分的時候。這個時候我們再想辦法合并兩個有序的數組,得到長度更長的有序數組。當前合并好的有序數組為下一輪得到更長的有序數組做好了準備。一層一層的合并回去,直到整個數組有序。
在這各整個過程當中,不停做的事情就是拆分問題和組合子問題的解,在這里很顯然我們是需要使用遞歸來實現歸并排序。
如何合并兩個有序數組
在這里我們是一個一個的選出來的。
先選出兩個數組里最小的元素,然后再選出兩個數組中第二小的元素,因為兩個數組都是有序的,所以兩個數組里的最小的元素肯定是出現在兩個數組的開頭。
所以我們只需要比較兩個數組中開頭的兩個元素,較小的那一個就是兩個數組中最小的那一個,我們就將它拿出來放到一個新的地方,接著選出兩個元素中第二小的元素,依然是比較這個時候兩個數組開頭的兩個元素,誰較小,誰就是數組中第二小的,依次向下進行。
直到將所有的元素選擇出來,我們就把兩個有序的數組成功合并為一個更長的有序數組。
在我們實際應用的時候,兩個有序數組是兩個緊挨著的兩個有序空間。
在使用的時候,我們先把原始數組要合并的部分復制到一個新的區域,然后在復制出來的輔助數組上設置兩個下標 i 和 j ,分別指向兩個有序區間的第一個元素,然后再在輸入數組上設置一個變量k用于賦值。
首先比較 i 和 j 所指向的元素,我把就把小的數賦值為k,然后向后移動下標。
這也是個典型的空間換時間的一種做法。
在開始時那為了演示,使用的是同時拆分同時合并的,但在歸并排序當中,實際使用的是按照深度優先的順序完成歸并排序的。
按照深度優先的順序進行:
0 ~ 7分為0 ~ 3和4 ~ 7兩組;
然后0 ~ 3分為0 ~ 1和2 ~ 3兩組;
然后0 ~ 1分為 0和1;
再然后0和1整理為有序數組;
再又對2 ~ 3分為 2 和 3;
對2 和3 進行整理為有序數組;
再將 0 ~ 1和2 ~ 3進行整理為有序數組;
再對 4 ~ 7分為 4 ~ 5和 6 ~ 7兩組;
。。。。。
最后整理成為有序數組。
深度優先遍歷只要可以繼續拆分問題,就先拆分問題;直到只有一個元素時不能拆分的時候,才回到之前它走過的地方走另一條還沒有走過的路徑繼續走下去。直到走完所有的可能走的路徑。因此稱為深度優先遍歷
代碼實現:
import java.util.Arrays;public class Demo912_2 {public static void main(String[] args) {int[] nums = {-1, 2, -8, -10}; //給定一個數組int[] after = sortArray(nums); //的帶排序后的數組System.out.println(Arrays.toString(after)); //打印輸出得到數組}private static int[] sortArray(int[] nums) {int len = nums.length;int[] temp = new int[len];mergeSort(nums,0,len-1,temp);return nums;}/*** 遞歸函數對nums[left...right]進行歸并排序* @param nums 原數組* @param left 左邊的索引* @param right 右邊記錄索引位置* @param temp*/private static void mergeSort(int[] nums, int left, int right, int[] temp) {if (left == right){//當拆分到數組當中只要一個值的時候,結束遞歸return;}int mid = (left+right)/2; //找到下次要拆分的中間值mergeSort(nums,left,mid,temp);//記錄樹左邊的mergeSort(nums,mid+1,right,temp);//記錄樹右邊的//合并兩個區間for (int i = left; i <= right; i++) {temp[i] = nums[i]; //temp就是輔助列表,新列表的需要排序的值就是從輔助列表中拿到的}int i = left; //給輔助數組里面的值標點int j = mid +1;for (int k = left; k <= right ; k++) {//k 就為當前要插入的位置if (i == mid + 1){nums[k] = temp[j];j++;}else if (j == right+1){nums[k] = temp[i];i++;}else if (temp[i] <= temp[j]){nums[k] = temp[i];i++;}else {nums[k] = temp[j];j++;}}} }時間復雜度:
對于復雜度分析來講,可以忽略這個常數項,而且對數的底我們也不在乎。
所以我們最終的時間復雜度為:
歸并排序的優化
第一個優化:
可以在小區間內使用插入排序。就是在判斷當left == right這里使用一個區間的插入排序,當數組長度較小的時候,使用插入排序。
在這里我們將這個值設置為16。當數組的長度小于16的時候,就使用插入排序,提高代碼執行效率。
第二個優化:
就是在合并兩個有序的數組之前,我們先看看這兩個數組拼接起來,是不是已經成為了有序的數組。
最終代碼:
歸并排序后兩種優化結束后的最終代碼:
import java.util.Arrays;public class Demo912_2 {public static void main(String[] args) {int[] nums = {-1, 2, -8, -10}; //給定一個數組int[] after = sortArray(nums); //的帶排序后的數組System.out.println(Arrays.toString(after)); //打印輸出得到數組}private static int[] sortArray(int[] nums) {int len = nums.length;int[] temp = new int[len];mergeSort(nums, 0, len - 1, temp);return nums;}/*** 遞歸函數對nums[left...right]進行歸并排序** @param nums 原數組* @param left 左邊的索引* @param right 右邊記錄索引位置* @param temp*/private static void mergeSort(int[] nums, int left, int right, int[] temp) {if (right - left < 16) {//當拆分到數組長度小于16的時候,直接插入排序來提高代碼運行速度insertionSort(nums, right, left);return;}int mid = (left + right) / 2; //找到下次要拆分的中間值mergeSort(nums, left, mid, temp);//記錄樹左邊的mergeSort(nums, mid + 1, right, temp);//記錄樹右邊的if (nums[mid] <= nums[mid + 1]) {//因為整數除法是向下取整,所以這里不會代寫mid + 1越界return;}//合并兩個區間for (int i = left; i <= right; i++) {temp[i] = nums[i]; //temp就是輔助列表,新列表的需要排序的值就是從輔助列表中拿到的}int i = left; //給輔助數組里面的值標點int j = mid + 1;for (int k = left; k <= right; k++) {//k 就為當前要插入的位置if (i == mid + 1) {nums[k] = temp[j];j++;} else if (j == right + 1) {nums[k] = temp[i];i++;} else if (temp[i] <= temp[j]) {nums[k] = temp[i];i++;} else {nums[k] = temp[j];j++;}}}/*** 執行插入排序* @param nums* @param right* @param left*/private static void insertionSort(int[] nums, int right, int left) {for (int i = left + 1; i <= right; i++) {int temp = nums[i];int j;for (j = i; j > left; j--) {if (nums[j - 1] > temp) {nums[j] = nums[j - 1];} else {break;}}nums[j] = temp;}} }總結
歸并排序采用的是分治算法。
分治算法每一次執行都可以分為三個步驟:
O(N logN)比O(N^2)快多少?
一段代碼演示:
我們這采用的是遞歸來實現歸并排序,還有自底向上的歸并排序和原地完成的歸并排序。
在理論自底向上的歸并排序是比采用遞歸快一丟丟的,但是考慮到學習成本來講,就沒必要了╰(●’?’●)╮ ?(?′0`?)?
在上面的代碼中,我們使用了 int mid = (left + right) / 2;來找出當前數組中的中間值.
在這里我們可以采用一個防止整型溢出的寫法:
int mid = left +(right - left)/ 2;
也可以使用一個無符號右移的方式,理論上更快一點點:
int mid = left + (right - left) >> 1;
也可以直接寫成:
int mid =(right - left) >> 1;
因為即使left +right會造成溢出,但右移以后,無符號的高位依然會補0,所以結論依然正確。
練習:
劍指offer 51. 數組中的逆序對
LeetCode315、計算右側小于當前元素的個數
總結
以上是生活随笔為你收集整理的java实现归并排序(详解)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php 303,HTTP 的重定向301
- 下一篇: 几个常用的JS代码.