1. 定义
栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同一端,称作栈顶,另一端就叫栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
下图模拟了一个栈的push与pop操作。
2. 模拟一个栈
JavaScript本身是没有栈的,所以我们尝试用JS来模拟一个栈的实现。
一个基本栈包含如下方法
push(element(s)):添加一个(或几个)新元素到栈顶。
pop():移除栈顶的元素,同时返回被移除的元素。
peek():返回栈顶的元素,不对栈做任何修改(不会移除栈顶的元素,仅仅返回它)。
isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。
clear():移除栈里的所有元素。
size():返回栈里的元素个数。该方法和数组的 length 属性很类似。
2.1 数组实现
我们首先尝试用数组实现一个栈。我们可以通过数组的push和pop方法模拟栈的添加与删除操作。
class Stack {
constructor() {
this._items = [];
}
push(element) {
this._items.push(element);
}
pop() {
return this._items.pop();
}
peek() {
return this._items[this._items.length - 1];
}
isEmpty() {
return this._items.length === 0;
}
clear() {
this._items = [];
}
size() {
return this._items.length;
}
}
2.2 对象实现
上面我们用数组实现了栈的模拟操作,但是数组有两个问题,首先数组大部分操作都是O(n)的,另外我们的栈是不固定大小的,使用数组会占用一段连续的内存空间,对内存是一种浪费。
基于数组的以上问题,我们考虑用对象来模拟实现。
class Stack {
constructor() {
this._count = 0;
this._items = {};
}
push (element) {
this._items[this._count] = element;
this._count++;
}
pop() {
if (this.isEmpty()) {
return undefined;
}
this._count--;
const result = this._items[this._count];
delete this._items[this._count];
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this._items[this._count - 1];
}
isEmpty() {
return this._count === 0;
}
clear() {
this._items = {};
this._count = 0;
// LIFO 原则
// while (!this.isEmpty()) {
// this.pop();
// }
}
size() {
return this._count;
}
}
2.3 用链表实现
JavaScript对象是基于散列表+数组实现的(字典有散列表部分有详细讲解),所以还是需要连续的内存空间,为了减少空间的占用,我们考虑使用链表实现(链表不需要连续的内存空间,链表部分详细讲解)。使用单链表,通过在链表顶端插人来实现 push,删除链表顶端元素实现 pop。
interface INodeList {
value: number
next: INodeList | null
}
class MyStack {
private head: INodeList | null = null
private len: number = 0
add(x: number) {
const newNode: INodeList = {
value: x,
next: this.head,
}
this.head = newNode;
// 记录长度
this.len++
}
delete(): number | null {
const headNode = this.head
if (headNode == null) return null
if (this.len <= 0) return null
// 取值
const value = headNode.value
// 处理 head
this.head = headNode.next
// 记录长度
this.len--
return value
}
get length(): number {
return this.len;
}
}
由于栈是一个表,因此任何实现表的方法都能实现栈。
另外可以参考下Java栈的源码实现 JAVA stack源码
3. 常见的栈问题
3.1 从十进制到二进制
上图是一个十进制到二进制的计算,我们可以发现后出来的余数要排到前面。这符合栈后入先出的特点,把余数依次入栈,然后再出栈,就可以实现余数倒序输出。
代码如下:
function decimalToBinary(decNumber) {
const remStack = [];
let number = decNumber, rem, binaryString = '';
while(number > 0) {
rem = Math.floor(number % 2); // 取余
remStack.push(rem);
number = Math.floor(number / 2);
}
while (remStack.length) {
binaryString += +remStack.pop();
}
return binaryString;
}
console.log(decimalToBinary(233)); // 11101001
console.log(decimalToBinary(10)); // 1010
console.log(decimalToBinary(1000)); // 1111101000
console.log(decimalToBinary(1000.99)); // 1111101000
3.2 十进制转任意进制
解决了十进制转二进制,我们再升级一下刚才的问题,十进制转任意进制。很简单,我们只需要再设置一个进制的参数base替换2即可。
function baseConverter(decNumber, base) {
const remStack = [];
const digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let number = decNumber, rem, binaryString = '';
if(!(base >= 2 && base <= 36)) { return '' };
while(number > 0) {
rem = Math.floor(number % base);
remStack.push(rem);
number = Math.floor(number / base);
}
while(remStack.length) {
binaryString += digits[remStack.pop()]
}
return binaryString;
}
console.log(baseConverter(100345, 2)); // 11000011111111001
console.log(baseConverter(100345, 8)); // 303771
console.log(baseConverter(100345, 16)); // 187F9
console.log(baseConverter(100345, 35)); // 2BW0
3.3 函数调用堆栈
最后调用的函数,最先执行完。
JS解释器使用栈来控制函数的调用顺序。
3.4 回文检查器
回文是正反都能读通的单词、词组、数或一系列字符的序列,例如 madam或 racecar。
解题思路:如果字符倒序和正序相等,就是回文。考虑使用栈。遍历输入的字符,依次押入栈中,然后再依次出栈,即可获取倒序字符。如果与输入字符相等,即是回文。代码如下:
var isPalindrome = function(x) {
let startString = x + '';
if (startString == null || (startString != null && startString.length === 0)) {
return false
}
const stack = [];
let i = 0;
let lowerString = '';
while(i < startString.length) {
stack.push(startString[i++]);
}
while(stack.length) {
lowerString += stack.pop();
}
return lowerString === startString;
};
4. 单调栈问题
单调栈是指栈内元素保持递增或递减。一般用来处理next greater element(下一个更大的元素)问题。
例题:输入一个数组,返回一个等长数组,对应索引存储着元数组中下一个比它大的元素,如果没有,返回-1。如输入[2, 1, 2, 4, 3], 返回[4, 2, 4, -1, -1]。
如果把数组里的数组比作人站队拍成一列,那么第一个能看到的人就是下一个最大元素,如下图所示:
代码模版如下:
var nextGreaterElement = function (nums) {
const len = nums.length;
const result = new Array(len).fill(-1); // 存放答案的数组
const stack = [];
for (let i = len - 1; i >= 0; i--) { // 倒着往栈里放
// 判定高矮
while (stack.length && stack[stack.length - 1] <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
stack.pop();
}
// nums[i] 身后的第一个高的
result[i] = stack.length ? stack[stack.length - 1] : -1;
// 进队,接受之后的⾝⾼判定吧
stack.push(nums[i]);
}
return result;
}
for 循环之所以要从后往前扫描元素,是因为我们借助的是栈的结构,倒着⼊栈,其实是正着出栈。while 循环是把两个“⾼个”元素之间的元素排除,因为他们的存在没有意义,前⾯挡着个“更⾼”的元素,所以他们不可能被作为后续进来的元素的 Next Great Number 了。 这个算法的时间复杂度不是 O(n^2),而是 O(n)。因为从整体来看:总共有 n 个元素,每个元素都被 push ⼊栈了⼀次,⽽最多会被 pop ⼀次。所以总的计算规模是和元素规模 n 成正⽐的,也就是 O(n) 的复杂度
以上摘自labuladong的算法小抄
单调栈相关题目:
每日温度
下一个更大元素I
下一个更大元素II
5. leetcode相关题目
5.1 easy
1. 有效的括号
原题地址:有效的括号
难度:简单
2. 最小栈
原题地址:最小栈
难度:简单
题解:最小栈
3. 用栈实现队列
原题地址:用栈实现队列
难度:简单
题解:用栈实现队列
4. 用两个栈实现队列
原题地址:用两个栈实现队列
难度:简单
题解:用两个栈实现队列
5. 删除字符串中的所有相邻重复项
难度:简单
6. 下一个更大元素 I
难度:简单
5.2 medium
1. 下一个更大元素II
难度:中等
2. 每日温度
原题地址:每日温度
难度:中等
题解:每日温度(单调栈)
3. 二叉树的前序遍历
原题地址:二叉树的前序遍历
难度:中等
难度:中等
5.3 hard
1. 柱状图中最大的矩形
原题地址:柱状图中最大的矩形
难度:困难
2. 接雨水
原题地址: 接雨水
难度:困难
题解:接雨水(单调栈解法)
以上题解源码请参考:leetcode刷题之路
持续更新ing...