重新梳理一下歸併排序以及一些相關的東西。
對於歸併排序大家如果需要回憶下是個什麼東西的話,可以點擊這個鏈接,裏面有各種排序的動畫演示以及講解,比我再用文字贅述一遍要好得多,功能相當強大。
先給出歸併排序的js代碼實現:
function mergeSort(arr, l, r) {
if (l === r) {
return;
}
let mid = Math.floor((r + l) / 2);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
function merge(arr, l, mid, r) {
let leftIndex = l;
let rightIndex = mid + 1;
let helpArr = [];
while(leftIndex <= mid && rightIndex <= r) {
let leftItem = arr[leftIndex];
let rightItem = arr[rightIndex];
if (leftItem < rightItem) {
helpArr.push(leftItem);
leftIndex++;
} else {
helpArr.push(rightItem);
rightIndex++;
}
}
// 這倆循環只會進去一個,因爲經過上面的比較,要麼左邊部分走完了,要麼右邊部分走完了
while(leftIndex <= mid) {
helpArr.push(arr[leftIndex]);
leftIndex++;
}
while(rightIndex <= r) {
helpArr.push(arr[rightIndex]);
rightIndex++;
}
for (let index = 0; index < helpArr.length; index++) {
arr[l] = helpArr[index];
l++;
}
}
如何估計歸併排序的時間複雜度呢?
由於上面採用了遞歸寫法,我們使用master公式對遞歸進行時間複雜度估算,以下是公式詳情。
<p>T(n) = a*T(n/b) + O(n^d)<br/>
(1)、log(b, a) > d => 複雜度爲O(n^log(b, a))<br/>
(2)、log(b, a) = d => 複雜度爲O(n^d*logn)<br/>
(3)、log(b, a) < d => 複雜度爲O(n^d)</p>
a代表遞歸的次數,由於在mergeSort中調用了兩次mergeSort,所以歸併排序中a = 2。<br/>
b代表樣本量被劃分幾份,由於我們對樣本量是一分爲二將數組分爲left和right部分,所以歸併排序中b = 2。<br/>
O(n^d)代表其他操作的時間複雜度,所以在歸併排序中主要是merge這個函數,相當於是執行了一次數組遍歷,則爲O(n)。<br/>
a = 2,b = 2,d = 1根據master公式,複雜度爲nlogn。
我們知道冒泡排序、選擇排序、插入排序的時間複雜度都是O(n^2),當樣本量比較大的時候,n^2比之nlogn差了可不是一星半點。這是爲什麼呢?因爲在其他三種排序中,會浪費元素之間的比較,比如冒泡排序冒泡比較一輪只定位了一個元素,下一輪冒泡又只定位一個元素,會浪費元素之間的相互比較;而歸併排序通過分治,由小到大進行比較合併的過程中,上一次比較合併的元素不會再次發生比較,有序的區域成規模增長,這樣就不會浪費比較,節省了時間。
由歸併排序引入數組小和問題和數組逆序對問題。
根據小和的題目要求,我們思考一下可以發現,在歸併排序過程中,left和right部分進行比較合併的時候,其實就可以找到左邊部分比右邊部分小的數,意思就是說我們可以很方便的在merge這個函數執行過程中來計算數組的小和且會快很多,因爲合併的時候左右兩遍都是有序的,如果一個數比右邊的第一個數字小,我們可以得知這個數字肯定比右邊全部的數字都小。
舉個例子,比如left = [1,2,3],right = [4,5,6],1小於4,說明右邊三個數都比1大,假如說小和等於sum,那麼sum就要加1 * 3。
代碼實現一下小和:
function smallSum(arr) {
if (!arr || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
function mergeSort(arr, l, r) {
if (l === r) {
return 0;
}
let mid = Math.floor((l + r)/2);
return mergeSort(arr, l, mid)
+ mergeSort(arr, mid + 1, r)
+ merge(arr, l, mid, r)
}
function merge(arr, l, mid, r) {
let leftIndex = l;
let rightIndex = mid + 1;
let helpArr = [];
let sum = 0;
while(leftIndex <= mid && rightIndex <= r) {
let leftItem = arr[leftIndex];
let rightItem = arr[rightIndex];
if (leftItem < rightItem) {
/**相對於歸併排序增加的部分**/
let tempSum = (r - rightIndex + 1) * leftItem
sum += tempSum;
/***************************/
helpArr.push(leftItem);
leftIndex++;
} else {
helpArr.push(rightItem);
rightIndex++;
}
}
/**這部分和歸併排序merge函數一樣**/
return sum;
}
對於小和都知道如何使用歸併排序進行求解之後,逆序對其實和小和是一樣的,只是反過來了而已,以下直接貼出代碼。
function inversePairs(arr) {
if (!arr || arr.length < 2) {
return [];
}
return mergeSort(arr, 0, arr.length - 1);
}
function mergeSort(arr, l, r) {
if (l === r) {
return [];
}
let mid = Math.floor((l + r)/2);
return [
...mergeSort(arr, l, mid),
...mergeSort(arr, mid + 1, r),
...merge(arr, l, mid, r)
];
}
function merge(arr, l, mid, r) {
let leftIndex = l;
let rightIndex = mid + 1;
let helpArr = [];
let res = [];
while(leftIndex <= mid && rightIndex <= r) {
let leftItem = arr[leftIndex];
let rightItem = arr[rightIndex];
if (leftItem < rightItem) {
helpArr.push(leftItem);
leftIndex++;
} else {
/**相對於歸併排序增加的部分**/
res.push([leftItem, rightItem]);
/***************************/
helpArr.push(rightItem);
rightIndex++;
}
}
/**這部分和歸併排序merge函數一樣**/
return res;
}
以上是對歸併排序這部分內容進行的一些回顧和總結,希望能加深自己對它的理解,能在其他更多的地方將其運用上;如果有不正確的地方,大家可以踊躍指出,我將及時改正。