系列文章导引
开源项目
本系列所有文章都将会收录到GitHub
中统一收藏与管理,欢迎ISSUE
和Star
。
GitHub传送门:Kiner算法算题记
知识回顾
我们之前讨论过单调队列,知道单调队列是用来维护区间最值的神兵利器:维护区间最小值用单调递增队列,维护区间最大值用单调递减队列。
单调队列有两种操作:
入队操作:从尾部入队,入队时,为了保证队列的单调性,需要将队列中已有所有违反单调性的元素都删掉之后再将新元素入队
出队操作:当头部元素已经移出了滑动窗口时,需要将头部元素从头部出队。
那么,大家再来思考一件事,我们的队列和栈有啥关系吗?我们是不是可以把栈看成是一头被堵住的队列呢?由于便秘
,屁股
被堵住了出不去,所以这哥们只能通过抠喉把吃进去的东西吐出来才能好受一点,这就是我们的栈
。虽然这个比喻略显粗鄙,但还是很说明问题的。一语中的的道出栈的特点:从顶部入队,也从顶部出队,后进先出
。
其实,我们今天要讨论的单调栈
,就可以看成把屁股
堵住的单调队列
。
单调栈的特性(渣男本质)
如果你是一个情场老手,或许有这样一个感受:所有被我追到的女生,我在她们心里就是“男神”。而那些我追不到的女生,在我心里都是我的“女神”。所谓“得不到的永远在骚动,被偏爱的有恃无恐”。可见,很多男生反而是对那些很难追到手的女生念念不忘,而对于轻易到手的女生则是没那么上心,也就是我们常说的“渣男”。而我们今天要聊的单调栈,就是数据结构中的“渣男”。
在我们的单调栈中,所有在入栈时被“我”忽悠出去到处浪的“女生”(因违反单调性出栈的元素),我都是她们的“男神”,由于太轻易得手,反而不知道珍惜。而那个永远对我爱答不理,怎么忽悠都不动如风的女神(第一个不违反单调性的元素),我总是无可奈何却又念念不忘。
单调递增栈
此时,我们的单调栈是个“御姐控”,喜欢比自己大的,所有比自己小的小妹妹们都只能感叹“君生妾未生,妾生君已老”。
出栈的元素都比当前要入栈的元素小,即入栈元素是第一个比这些出栈元素小的元素。
利用单调递增栈,可以维护任意一个元素最近的比他小的元素,即维护元素最近小于关系
单调递减栈
此时,我们的单调栈是个“萝莉控”,喜欢比自己小的,所有比自己大的姐姐们都只能感叹“妾生君未生,君生妾已老”。
出栈的元素都比当前要入栈的元素大,即入栈元素是第一个比这些出栈元素大的元素。
利用单调递减栈,可以维护任意一个元素最近的比他大的元素,即维护元素最近大于关系
单调栈的代码实现
# 单调递增栈
idx: 0 1 2 3 4 5 6 7 8 9
┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
stack: 6 7 9 <━ 0
┗━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┛
val: 6 7 9 0 8 3 4 5 1 2
# 如上图,当0要入栈时,由于前面的6、7、9都违反了单调性,将他们依次弹出为[9,7,6],而0就是9最近的小于9的元素,也就是当前元素上一个最近的比自己大的元素就是栈顶元素
interface Res {
stack: number[],
prev: number[],
next: number[]
}
// 查找数组中每个数最近的小于这个数的索引
function main(arr: number[]): Res {
// 维护小于关系,用单调递增栈
const stack: number[] = [];
const prev: number[] = new Array(arr.length);
const next: number[] = new Array(arr.length);
for(let i=0;i<arr.length;i++) {
// 将元素压入单调栈之前,先检查一下待压入元素是否小于栈顶元素,因为我们要找最近小于目标元素的索引,因此,我们需要一个
// 单调递增的栈,因此,只要待压入元素小于栈顶元素,我们就要把栈顶元素弹出,以此来维护栈的单调性
// 弹出后,将弹出元素所对应的索引与当前元素关联,即代表该元素是距离当前元素最近的一个大于当前元素的数,
// 换句话来说,就是距离stack.pop()所在位置元素最近的小于这个元素的位置是i,也就是,栈顶元素后面最近的小于栈顶的元素就是i所在的元素
while(stack.length && arr[i] < arr[stack[stack.length-1]]) {
next[stack.pop()] = i;
}
// 如果栈为空,那么上一个元素是不存在的,所以是-1
if(stack.length === 0) prev[i] = -1;
// 否则当前元素上一个小于他的元素就是栈顶元素,即当前元素前面最近的比当前元素大的元素就是栈顶元素
else prev[i] = stack[stack.length - 1];
stack.push(i);
}
// 如果上面的循环结束,栈中仍然存在元素,那么其实是不可能存在下一个元素,我们把栈中剩余的所有元素弹出,并将该元素对应的下个元素的所以设置为数组长度
while(stack.length) next[stack.pop()] = arr.length;
return {
stack,
prev,
next
}
}
const arr = [6,7,9,0,8,3,4,5,1,2];
const res: Res = main(arr);
console.log(`indx:\t ${arr.map((item, idx) => idx).join('\t')}`);
console.log(`curr:\t ${arr.join('\t')}`);
console.log(`prev:\t ${res.prev.join('\t')}`);
console.log(`next:\t ${res.next.join('\t')}`);
// indx: 0 1 2 3 4 5 6 7 8 9
// curr: 6 7 9 0 8 3 4 5 1 2
// prev: -1 0 1 -1 3 3 5 6 3 8
// next: 3 3 3 10 5 8 8 8 10 10