比如數組A:
[
0: 5,
1: 2,
2: 4,
3: 3,
4: 1
]
排序后的結果為:[1, 2, 3, 4, 5],但是有時候會有需求想要保留排序前的位置到一個同位數組里,如前例則為:[4, 1, 3, 2, 0],因此就利用堆排序寫了一個單獨的數組排序過程加以實現。
代碼如下:
function arrayKeys(arr) {
var i = 0,
len = arr.length,
keys = [];
while (i < len) {
keys.push(i++);
}
return keys;
}
// 判斷變量是否為數組
function isArray(arr) {
return ({}).toString.call(arr).match(/^\[[^\s]+\s*([^\s]+)\]$/)[1] == 'Array';
}
// 堆排序
function heapSort(arr, keys, order) {
if (!isArray(arr) || !isArray(keys)) return ;
var order = (order + '').toLowerCase() == 'desc' ? order : 'asc';
// 交換位置
function changePos(arr, cur, left) {
var tmp;
tmp = arr[cur];
arr[cur] = arr[left];
arr[left] = tmp;
}
// 構造二叉堆
function heap(arr, start, end, isMax) {
var isMax = isMax == undefined ? true : isMax, // 是否最大堆,否為最小堆
cur = start, // 當前節點的位置
left = 2 * cur + 1; // 左孩子的位置
for (; left <= end; cur = left, left = 2 * left + 1) {
// left是左孩子,left + 1是右孩子
if (left < end && ((isMax && arr[left] < arr[left + 1]) || (!isMax && arr[left] > arr[left + 1]))) {
left++; // 左右子節點中取較大/小者
}
if ((isMax && arr[cur] >= arr[left]) || (!isMax && arr[cur] <= arr[left])) {
break;
} else {
// 原index跟隨排序同步進行
changePos(keys, cur, left);
changePos(arr, cur, left);
}
}
}
return (function () {
// 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個二叉堆
for (var len = arr.length, i = Math.floor(len / 2) - 1; i >= 0; i--) {
heap(arr, i, len - 1, order == 'asc');
}
// 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素
for (i = len - 1; i > 0; i--) {
changePos(keys, 0, i);
changePos(arr, 0, i);
// 調整arr[0...i - 1],使得arr[0...i - 1]仍然是一個最大/小堆
// 即,保證arr[i - 1]是arr[0...i - 1]中的最大/小值
heap(arr, 0, i - 1, order == 'asc');
}
})();
}
// 測試
var aa = [5, 2, 8, 9, 1, 3, 4, 7, 6];
var kk = arrayKeys(aa); // 原索引數組
heapSort(aa, kk, 'asc');
console.log(aa); // 排序后:[1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(kk); // 原索引:[4, 1, 5, 6, 0, 8, 7, 2, 3]
當然,也可以在確保安全的前提下把該方法寫入Array.prototype.heapSort,這樣就可以用數組直接調用了,代碼略微修改一下即可,如下:
Array.prototype.heapSort = function (keys, order) {
var keys = ({}).toString.call(keys) == '[object Array]' ? keys : [],
order = (order + '').toLowerCase() == 'desc' ? order : 'asc';
// 交換位置
function changePos(arr, cur, left) {
var tmp;
tmp = arr[cur];
arr[cur] = arr[left];
arr[left] = tmp;
}
// 構造二叉堆
function heap(arr, start, end, isMax) {
var isMax = isMax == undefined ? true : isMax, // 是否最大堆,否為最小堆
cur = start, // 當前節點的位置
left = 2 * cur + 1; // 左孩子的位置
for (; left <= end; cur = left, left = 2 * left + 1) {
// left是左孩子,left + 1是右孩子
if (left < end && ((isMax && arr[left] < arr[left + 1]) || (!isMax && arr[left] > arr[left + 1]))) {
left++; // 左右子節點中取較大/小者
}
if ((isMax && arr[cur] >= arr[left]) || (!isMax && arr[cur] <= arr[left])) {
break;
} else {
// 原index跟隨排序同步進行
changePos(keys, cur, left);
changePos(arr, cur, left);
}
}
}
return (function (arr) {
// 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個二叉堆
for (var len = arr.length, i = Math.floor(len / 2) - 1; i >= 0; i--) {
heap(arr, i, len - 1, order == 'asc');
}
// 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素
for (i = len - 1; i > 0; i--) {
changePos(keys, 0, i);
changePos(arr, 0, i);
// 調整arr[0...i - 1],使得arr[0...i - 1]仍然是一個最大/小堆
// 即,保證arr[i - 1]是arr[0...i - 1]中的最大/小值
heap(arr, 0, i - 1, order == 'asc');
}
})(this);
};
經過測試發現,在數組沒有相同元素的情況下,原索引的序列同排序后的數組是匹配的,但是在有相同元素的情況下,序列就比較亂,這是因為堆排序本來就不是一個穩定的排序,排序后所有元素的位置會被打亂。比如:數組[5, 5, 5, 5, 5],因為元素都一樣,所以排序過程中應該是只做了比較,卻並沒有交換原來位置的,但實際得到的索引卻是:[1, 2, 3, 4, 0],這也很好理解,因為堆排序假如是最大堆的話,根節點是要被交換到末尾的,而很明顯元素首位的5即是根節點,所以得到了前面那個顛倒的序列。
這樣一來就跟初衷有點不符了,所以我又重新模擬了一個穩定排序的算法----歸並排序,效果已經達到了預期。
代碼如下:
// 歸並排序(過程:從下向上)
function mergeSort(arr, key, order) {
if (!isArray(arr)) return [];
var key = isArray(key) ? key : [];
// 對數組arr做若干次合並:數組arr的總長度為len,將它分為若干個長度為gap的子數組;
// 將"每2個相鄰的子數組" 進行合並排序。
// len = 數組的長度,gap = 子數組的長度
function mergeGroups(arr, len, gap) {
// 對arr[0..len)做一趟歸並排序
// 將"每2個相鄰的子數組"進行合並排序
for (var i = 0; i + 2 * gap - 1 < len; i += gap * 2) {
merge(arr, i, i + gap - 1, i + 2 * gap - 1); // 歸並長度為len的兩個相鄰子數組
}
// 注意:
// 若i ≤ len - 1且i + gap - 1 ≥ len - 1時,則剩余一個子數組輪空,無須歸並
// 若i + gap - 1 < len - 1,則剩余一個子數組沒有配對
// 將該子數組合並到已排序的數組中
if (i + gap - 1 < len - 1) { // 尚有兩個子文件,其中后一個長度小於len - 1
merge(arr, i, i + gap - 1, len - 1); // 歸並最后兩個子數組
}
}
// 核心排序過程
function merge(arr, start, mid, end) {
var i = start; // 第1個有序區的索引,遍歷區間是:arr數組中的[start..mid]
var j = mid + 1; // 第2個有序區的索引,遍歷區間是:arr數組中的[mid + 1..end]
var aTmp = []; // 匯總2個有序區臨時數組
var kTmp = [];
var isAsc = (order + '').toLowerCase() !== 'desc';
/* 排序過程開始 */
while (i <= mid && j <= end) { // 遍歷2個有序區,當該while循環終止時,2個有序區必然有1個已經遍歷並排序完畢
if ((!isAsc && arr[i] <= arr[j]) || (isAsc && arr[i] >= arr[j])) { // 並逐個從2個有序區分別取1個數進行比較,將較小的數存到臨時數組aTmp中
aTmp.push(arr[i]);
kTmp.push(key[i++]);
} else {
aTmp.push(arr[j]);
kTmp.push(key[j++]);
}
}
// 將剩余有序區的剩余元素添加到臨時數組aTmp中
while (i <= mid) {
aTmp.push(arr[i]);
kTmp.push(key[i++]);
}
while (j <= end) {
aTmp.push(arr[j]);
kTmp.push(key[j++]);
} /*排序過程結束*/
var len = aTmp.length, k;
// 此時,aTmp數組是經過排序后的有序數列,然后將其重新整合到數組arr中
for (k = 0; k < len; k++) {
arr[start + k] = aTmp[k];
key[start + k] = kTmp[k];
}
}
// 歸並排序(從下往上)
return (function (arr) {
// 采用自底向上的方法,對arr[0..len)進行二路歸並排序
var len = arr.length;
if (len <= 0) return arr;
for (var i = 1; i < len; i *= 2) { // 共log2(len - 1)趟歸並
mergeGroups(arr, len, i); // 有序段長度 ≥ len時終止
}
})(arr);
}
// 數組原型鏈方法
Array.prototype.mergeSort = function (key, order) {
var key = ({}).toString.call(key) == '[object Array]' ? key : [];
function mergeGroups(arr, len, gap) {
for (var i = 0; i + 2 * gap - 1 < len; i += gap * 2) {
merge(arr, i, i + gap - 1, i + 2 * gap - 1);
}
if (i + gap - 1 < len - 1) {
merge(arr, i, i + gap - 1, len - 1);
}
}
// 核心排序過程
function merge(arr, start, mid, end) {
var i = start;
var j = mid + 1;
var aTmp = [];
var kTmp = [];
var isAsc = (order + '').toLowerCase() !== 'desc';
/* 排序過程開始 */
while (i <= mid && j <= end) {
if ((isAsc && arr[i] <= arr[j]) || (!isAsc && arr[i] >= arr[j])) {
aTmp.push(arr[i]);
kTmp.push(key[i++]);
} else {
aTmp.push(arr[j]);
kTmp.push(key[j++]);
}
}
while (i <= mid) {
aTmp.push(arr[i]);
kTmp.push(key[i++]);
}
while (j <= end) {
aTmp.push(arr[j]);
kTmp.push(key[j++]);
} /*排序過程結束*/
var len = aTmp.length, k;
for (k = 0; k < len; k++) {
arr[start + k] = aTmp[k];
key[start + k] = kTmp[k];
}
}
// 歸並排序(從下往上)
return (function (arr) {
var len = arr.length;
if (len <= 0) return arr;
for (var i = 1; i < len; i *= 2) {
mergeGroups(arr, len, i);
}
return arr;
})(this);
};
// 測試 var arr1 = [2, 2, 2, 2, 2, 2, 2, 2];
var arr2 = [5, 2, 7, 1, 2, 6, 6, 8]; var key1 = arrayKeys(arr1);
var key2 = arrayKeys(arr2);
console.log(arr1.mergeSort(key1)); // [2, 2, 2, 2, 2, 2, 2, 2]
console.log(key1); // [0, 1, 2, 3, 4, 5, 6, 7]
console.log(arr2.mergeSort(key2)); // [1, 2, 2, 5, 6, 6, 7, 8]
console.log(key2); // [3, 1, 4, 0, 5, 6, 2, 7]
