最近遇到的几个有意思问题,记录分享一下。
假如有这么三个数组:(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等帮助合并。(关于堆,树的算法基础强烈推荐啊哈算法这本书,大学时期我都把这本书翻烂了=-=,学生的时候还是很感谢这本书的。。)
前端常见的省区市这种三级数据结构,如果给出一个区的名字,找出所有和区名字相同的所有三级路径结构。
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);
与此相同的问题模型还有二叉(多叉)树中找到目标节点的所有祖先节点,和为某一值的所有路径等等。相似问题都可用我的上面方法灵活解决。