最近遇到的幾個有意思問題,記錄分享一下。
1. 合併三個有序數組(合併K個有序數組)
假如有這麼三個數組:(arr包含的三個數組)
let arr = [
[1, 3, 5, 7],
[2, 4, 6],
[0, 8, 9, 10, 11],
];
問題:K個數組,總共N個元素,合併成一個有序數組。(以升序爲例)
大家一眼看上去肯定會想到歸併排序,合併兩個有序數組是歸併排序的最後一步的動作,通過兩個指針引導比較大小依次合併兩個有序序列。
問題是三個數組的話,用三指針去做輔助貌似思路上看似直白,實際寫代碼控制邊界的時候要做N多個if else判斷,代碼上很囉嗦。如果上升到合併四個數組,五個數組話,難道要要四指針,五指針,一個個合併的時候判斷每個數組是否到達邊界?
如果只是三個數組的話,我們可以先合併兩個數組,再合併剩下的,時間複雜度也是O(N)級別的。
但如果迴歸到這個問題的本質通用模型上,有K個有序數組呢?
一個直接便捷的辦法是concat所有數組,快速排序,平均時間複雜度有N Log(N)。這種方法最直接,但是肯定不應該是最優解,子數組在有序的情況下,應該有不需要比較的情況。
我的思路是利用堆構造優先隊列預處理下各個子數組,用一個cur記錄下每個數組的當前指針。初始化的時候,建立一個K大小的優先隊列,初始化元素爲每個數組的第一個元素和這個元素所在的數組序號,用seq表示,eg:K個這樣的node:{seq:0,value:xx}。每次取出top元素,根據此元素的seq,將子數組的cur指針後移。總共取N次,優先隊列二叉堆的每次調整時間複雜度爲LogK, 所以時間複雜度爲N log(K)。
K一般遠小於N的,所以此時間複雜度比concat+排序要好的。
代碼:
function PriorityQueue() {
// 方便計算,將第一位置空
this.list = [{}];
}
PriorityQueue.prototype.size = function () {
return this.list.length - 1;
};
PriorityQueue.prototype.empty = function () {
return this.list.length === 1;
};
PriorityQueue.prototype.push = function (data) {
this.list.push(data);
this._moveUp();
};
// 向上調整數
PriorityQueue.prototype._moveUp = function () {
let newPos = this.list.length - 1;
let parent = Math.floor(newPos / 2);
let isChange = true;
while (isChange) {
isChange = false;
//父子結點比較
// 注意這個問題構造的是對象元素,值大小在對象的value key上
if (this.list[parent].value > this.list[newPos].value) {
let temp = this.list[parent];
this.list[parent] = this.list[newPos];
this.list[newPos] = temp;
isChange = true;
newPos = parent;
parent = Math.floor(newPos / 2);
}
}
};
// 向下調整
PriorityQueue.prototype._moveDown = function () {
let newPos = 1;
let isChange = true;
while (isChange) {
isChange = false;
//父子結點比較
let leftSonPos = newPos * 2;
let rightSonPos = newPos * 2 + 1;
let leftSonVal = this.list[leftSonPos];
let rightSonVal = this.list[rightSonPos];
if (typeof leftSonVal === "undefined" && typeof rightSonVal) break;
let pos;
// 要注意有結點不存在的情況
if (
typeof leftSonVal !== "undefined" &&
typeof rightSonVal === "undefined"
) {
pos = leftSonVal.value < this.list[newPos].value ? leftSonPos : newPos;
} else if (
typeof leftSonVal === "undefined" &&
typeof rightSonVal !== "undefined"
) {
pos = rightSonVal.value < this.list[newPos].value ? rightSonPos : newPos;
} else {
pos = leftSonVal.value < rightSonVal.value ? leftSonPos : rightSonPos;
pos = this.list[newPos].value < this.list[pos].value ? newPos : pos;
}
if (pos === newPos) break;
let temp = this.list[pos];
this.list[pos] = this.list[newPos];
this.list[newPos] = temp;
isChange = true;
newPos = pos;
}
};
PriorityQueue.prototype.pop = function () {
let res = this.top();
this.list[1] = this.list[this.list.length - 1];
this.list.splice(this.list.length - 1, 1);
this._moveDown();
return res;
};
PriorityQueue.prototype.top = function () {
return this.list[1];
};
PriorityQueue.prototype.back = function () {
return this.list[this.list.length - 1];
};
let arr = [
[1, 3, 5, 7],
[2, 4, 6],
[0, 8, 9, 10, 11],
];
let arrObj = [
{ cur: 0, arr: [1, 3, 5, 7] },
{ cur: 0, arr: [2, 4, 6] },
{ cur: 0, arr: [0, 8, 9, 10, 11] },
];
// 如果使用三個指針比較麻煩,判斷大小還要判斷某個到達邊界情況。如果只是合併三個有序數組的話我們可以雙指針法先合併一個,然後再合併剩下的一個
// 問題可以直接引申到合併K個數組
// 思路即是建立一個K大小的最小堆,最小堆的元素初始化爲每個數組的最開始元素。取最小值後將這個元素所在的數組再放進堆裏即可。
function mergeArr(arrObj) {
let ans = [];
let queue = new PriorityQueue();
// 求n
let n = 0;
for (let i = 0; i < arrObj.length; i++) {
n += arrObj[i].arr.length;
}
// init 隊列,,初始化指針map
for (let i = 0; i < arrObj.length; i++) {
queue.push({ seq: i, value: arrObj[i].arr[0] });
arrObj[i].cur++;
}
while (n--) {
let top = queue.pop();
// console.log(top);
let arrIndex = top.seq;
let cur = arrObj[arrIndex].cur;
let curArr = arrObj[arrIndex].arr;
ans.push(top);
if (cur < curArr.length) {
queue.push({ seq: arrIndex, value: curArr[cur] });
arrObj[arrIndex].cur++;
}
}
console.log(ans);
return ans.map((v) => v.value);
}
mergeArr(arrObj);
因爲不一定是完全二叉樹,所以建堆的過程需要判斷是否有子節點。還有一點就是要預處理各個有序子數組,利用seq,cur等幫助合併。(關於堆,樹的算法基礎強烈推薦啊哈算法這本書,大學時期我都把這本書翻爛了=-=,學生的時候還是很感謝這本書的。。)
2. 打印目標節點值和葉子節點相同的所有路徑
前端常見的省區市這種三級數據結構,如果給出一個區的名字,找出所有和區名字相同的所有三級路徑結構。
eg:
let tree = {
name: "china",
children: [
{
name: "江蘇省",
children: [
{ name: "G市", children: [] },
{ name: "M市", children: [] },
],
},
{
name: "河南省",
children: [
{ name: "M市", children: [] },
{ name: "D市", children: [] },
],
},
{
name: "北京市",
children: [
{ name: "G區", children: [] },
{ name: "A區", children: [{ name: "M市", children: [] }] },
],
},
],
};
此問題迴歸到通用模型上爲多叉樹上打印目標節點值和葉子節點相同的所有路徑,我的思路用一個path數組維護路徑,每到一層push當前層節點,到葉子節點判斷此葉子節點值是否和目標值相同,相同則把當前path數組push進ans結果數組,每層遞歸結束離開此層時記得pop掉path的此層節點值。
代碼:
/**
假設有這麼一個tree的數據結構,給定一個葉子節點的名稱,打印出名稱和目標節點相同的的所有路徑
[[china,M市],[china,henan,M市],[china,beijing,A區,M市]]
*/
let tree = {
name: "china",
children: [
{
name: "jiangsu",
children: [
{ name: "G市", children: [] },
{ name: "M市", children: [] },
],
},
{
name: "henan",
children: [
{ name: "M市", children: [] },
{ name: "D市", children: [] },
],
},
{
name: "beijing",
children: [
{ name: "G區", children: [] },
{ name: "A區", children: [{ name: "M市", children: [] }] },
],
},
],
};
let ans = [];
function printPathArr(node, target, path = []) {
let children = node.children;
path.push(node.name);
if (children.length) {
for (let i = 0; i < children.length; i++) {
let curNode = children[i];
printPathArr(curNode, target, path);
}
}
if (!children.length && node.name === target) {
// 只有滿足這個條件才push到arr
ans.push(path.slice(0));
}
// 這裏每次pop,那麼上面的push也就要每次都要push,這樣路徑纔不亂
// 這一層結束,返回上一層
path.pop();
}
printPathArr(tree, "M市");
console.log(ans);
與此相同的問題模型還有二叉(多叉)樹中找到目標節點的所有祖先節點,和爲某一值的所有路徑等等。相似問題都可用我的上面方法靈活解決。