栈与队列-JS
一、基础知识
在JavaScript中,栈(Stack)和队列(Queue)是两种基本的数据结构,它们用于存储和操作数据。尽管JavaScript没有内置的栈和队列对象,但我们可以使用数组来模拟这两种数据结构的行为。
栈(Stack)
栈是一种后进先出(LIFO, Last In First Out)的数据结构。这意味着最后添加到栈中的元素将是第一个被移除的元素。
基本操作:
push(item)
:将一个元素添加到栈顶。pop()
:移除并返回栈顶的元素。peek()
:返回栈顶的元素,但不移除它。isEmpty()
:检查栈是否为空。size()
:返回栈中的元素数量。
使用数组模拟栈的示例:
let stack = [];
// 添加元素
stack.push('apple');
stack.push('banana');
stack.push('cherry');
// 移除并获取元素
let topItem = stack.pop(); // 'cherry'
// 查看栈顶元素
let peekItem = stack[stack.length - 1]; // 'banana'
// 检查是否为空
let isEmpty = stack.length === 0; // false
// 获取栈的大小
let size = stack.length; // 2
队列(Queue)
队列是一种先进先出(FIFO, First In First Out)的数据结构。这意味着第一个添加到队列中的元素将是第一个被移除的元素。
基本操作:
enqueue(item)
:在队列末尾添加一个元素。dequeue()
:移除并返回队列前端的元素。front()
或peek()
:返回队列前端的元素,但不移除它。isEmpty()
:检查队列是否为空。size()
:返回队列中的元素数量。
使用数组模拟队列的示例:
let queue = [];
// 添加元素
queue.push('apple');
queue.push('banana');
queue.push('cherry');
// 移除并获取元素
let frontItem = queue.shift(); // 'apple'
// 查看队列前端元素
let peekItem = queue[0]; // 'banana'
// 检查是否为空
let isEmpty = queue.length === 0; // false
// 获取队列的大小
let size = queue.length; // 2
注意:在使用数组模拟队列时,shift()
方法用于移除数组的第一个元素,这可能会导致性能问题,因为需要移动数组中的所有其他元素。在需要高性能的场景下,可以使用双端队列(如 ArrayBuffer
与 TypedArray
结合使用)或其他数据结构来优化。
在JavaScript中,还可以使用类或对象来创建更正式的栈和队列实现,这样可以封装数据和行为,提供更清晰的接口
二、20.有效的括号
20. 有效的括号
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
提示:
1 <= s.length <= 104
s
仅由括号'()[]{}'
组成
/**
* @param {string} s
* @return {boolean}
*/
var isValid = function (s) {
const stack = [];
let temp;
//字符串转为数组
s = s.split('');
for (let i of s) {
if (i === '(' || i === '{' || i === '[') {
stack.push(i)
} else {
temp = stack.pop();
if (i === ')') {
if (temp !== '(') return false;
}
if (i === ']') {
if (temp !== '[') return false;
}
if (i === '}') {
if (temp !== '{') return false;
}
}
}
if(stack.length===0) return true;
if(stack.length!==0) return false;
};
三、1047.删除字符串中的所有相邻重复项
1047. 删除字符串中的所有相邻重复项
提示
给出由小写字母组成的字符串
s
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在
s
上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca" 输出:"ca" 解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。提示:
1 <= s.length <= 105
s
仅由小写英文字母组成。
使用栈
/**
* @param {string} s
* @return {string}
*/
var removeDuplicates = function(s) {
// 字符串转数组
s=s.split('');
let stack=[];
for(let i of s){
let temp=stack.pop()
// 如果相等,就是出栈
// 如果不相等,就是把出栈比较的元素也入栈,新元素也入栈。
if(temp!==i){
stack.push(temp);
stack.push(i);
}
}
return stack.join('');
};
更简单 ,字符串也可以直接通过下标读取每个元素,但是不能通过下标修改。
var removeDuplicates = function(s) {
const result = []
for(const i of s){
if(i === result[result.length-1]){
result.pop()
}else{
result.push(i)
}
}
return result.join('')
};
四、150.逆波兰表达式求值
150. 逆波兰表达式求值
给你一个字符串数组
tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
示例 1:
输入:tokens = ["2","1","+","3","*"] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9示例 2:
输入:tokens = ["4","13","5","/","+"] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6示例 3:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] 输出:22 解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 = ((10 * (6 / (12 * -11))) + 17) + 5 = ((10 * (6 / -132)) + 17) + 5 = ((10 * 0) + 17) + 5 = (0 + 17) + 5 = 17 + 5 = 22提示:
1 <= tokens.length <= 104
tokens[i]
是一个算符("+"
、"-"
、"*"
或"/"
),或是在范围[-200, 200]
内的一个整数逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如
( 1 + 2 ) * ( 3 + 4 )
。- 该算式的逆波兰表达式写法为
( ( 1 2 + ) ( 3 4 + ) * )
。逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成
1 2 + 3 4 + *
也可以依据次序计算出正确结果。- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
注意点:
- 类型转换:在处理运算时,从栈中弹出的元素是字符串类型,需要转换为数字类型进行运算。讲字符转为数组parseInt() Number()也可以
- 除法处理:JavaScript 中除法可能导致浮点数,而通常 RPN 的结果需要是整数。应当对结果进行取整处理。
- 返回值:最后返回的结果应该是数字类型。
parseInt()
和Number()
都是 JavaScript 中用于将字符串转换为数字的函数,但它们在处理转换时有一些区别:
解析方式:
parseInt()
:解析字符串中的整数部分。如果字符串开头不是数字或符号,则返回NaN
。它可以接受第二个参数作为基数(radix),用于指定数字的进制。Number()
:尝试将整个字符串转换为数字。如果字符串包含任何非数字字符(除了小数点和指数符号),则返回NaN
。处理非数字字符:
parseInt()
:会从字符串的开始位置解析数字,直到遇到第一个非数字字符为止。例如,parseInt('123abc')
会返回123
。Number()
:要求整个字符串必须是有效的数字表示,否则返回NaN
。例如,Number('123abc')
会返回NaN
。基数(Radix):
parseInt()
:可以指定基数,例如parseInt('1010', 2)
会将二进制的1010
转换为十进制的10
。Number()
:不接收基数参数,总是将字符串视为十进制数。空字符串:
parseInt()
:对于空字符串,返回NaN
。Number()
:对于空字符串,返回0
。性能:
parseInt()
:通常比Number()
快,因为它不需要解析整个字符串,只要找到第一个非数字字符就可以停止解析。Number()
:需要检查整个字符串,以确保没有非法字符。特殊值:
parseInt()
:可以解析以0x
开头的十六进制字符串,例如parseInt('0x10')
会返回16
。Number()
:不会将十六进制字符串转换为十进制数,例如Number('0x10')
会返回NaN
。在实际使用中,选择
parseInt()
还是Number()
取决于你的具体需求。如果你只需要字符串中的整数部分,或者需要处理特定进制的数字,parseInt()
是更好的选择。如果你需要确保整个字符串是一个有效的数字,那么Number()
更合适。
if+parseInt
/**
* @param {string[]} tokens
* @return {number}
*/
var evalRPN = function (tokens) {
let stack = [];
let temp, num1, num2;
for (let i of tokens) {
if (i === '+' || i === '-' || i === '*' || i === '/') {
// 栈的最后一个元素
num1 = parseInt(stack.pop());
// 栈的倒数第二个元素
num2 = parseInt(stack.pop());
if (i === '+') {
temp = num1 + num2;
}
if (i === '-') {
temp = num2 - num1;
}
if (i === '*') {
temp = num2 * num1;
}
if (i === '/') {
temp = Math.trunc(num2 / num1);
}
// 计算结果入栈
stack.push(temp);
} else {
stack.push(parseInt(i))
}
}
return stack.pop();
}
switch+Number
var evalRPN = function (tokens) {
const stack = [];
for (const token of tokens) {
if (isNaN(Number(token))) { // 非数字
const n2 = stack.pop(); // 出栈两个数字
const n1 = stack.pop();
switch (token) { // 判断运算符类型,算出新数入栈
case "+":
stack.push(n1 + n2);
break;
case "-":
stack.push(n1 - n2);
break;
case "*":
stack.push(n1 * n2);
break;
case "/":
stack.push(n1 / n2 | 0);
break;
}
} else { // 数字
stack.push(Number(token));
}
}
return stack[0]; // 因没有遇到运算符而待在栈中的结果
};
五、239.滑动窗口最大值
239. 滑动窗口最大值
尝试过
困难
相关标签
相关企业
提示
给你一个整数数组
nums
,有一个大小为k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的k
个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7] 解释: 滑动窗口的位置 最大值 --------------- ----- [1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7示例 2:
输入:nums = [1], k = 1 输出:[1]提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
方法一、暴力法(超时了)
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
function maxSlidingWindow(nums, k) {
const result = [];
for (let i = 0; i <= nums.length - k; i++) {
let max = -Infinity;
for (let j = i; j < i + k; j++) {
max = Math.max(max, nums[j]);
}
result.push(max);
}
return result;
}
方法二、使用 Map 统计频率并找出前 K 个高频元素
JavaScript Map 详解
Map 是 ES6 引入的一种新的数据结构,它类似于对象(Object),但提供了更强大的键值对存储功能。
1. Map 基本特性
- 键值对集合:存储键值对,类似 Object
- 键的类型:可以是任意值(对象、函数、原始值)
- 顺序保证:Map 会记住键的原始插入顺序
- 大小可获取:通过 size 属性直接获取元素数量
- 高性能:在频繁增删键值对的场景下表现优于 Object
2. Map 与 Object 的区别
特性 Map Object 键的类型 任意值 只能是 String 或 Symbol 顺序 按插入顺序 ES6后也有顺序但不完全可靠 大小 size 属性获取 需要手动计算 原型 无原型链 有原型链可能引起键名冲突 默认键 无 有默认属性如 toString 性能 频繁增删时更优 静态键值对时更优 3. Map 的基本操作
创建 Map
const map = new Map(); // 空Map const mapWithValues = new Map([ ['key1', 'value1'], ['key2', 'value2'] ]);
添加/更新元素
map.set('name', 'Alice'); map.set(123, 'number key'); map.set({}, 'object key');
获取元素
map.get('name'); // 'Alice' map.get('nonexistent'); // undefined
检查键是否存在
map.has('name'); // true
删除元素
map.delete('name'); // 返回布尔值表示是否删除成功
清空 Map
map.clear(); // 移除所有键值对
获取大小
map.size; // 返回Map中键值对的数量
4. Map 的迭代方法
forEach 方法
map.forEach((value, key) => { console.log(key, value); });
for...of 循环
for (const [key, value] of map) { console.log(key, value); }
获取键、值、条目
map.keys(); // 返回键的迭代器 map.values(); // 返回值的迭代器 map.entries(); // 返回键值对的迭代器
5. Map 的高级用法
使用对象作为键
const objKey = { id: 1 }; map.set(objKey, 'value associated with object'); console.log(map.get(objKey)); // 'value associated with object'
链式调用
map.set('a', 1) .set('b', 2) .set('c', 3);
转换数组
// Map转数组 const arr = Array.from(map); // 或使用展开运算符 const arr2 = [...map];
合并 Map
const first = new Map([[1, 'one'], [2, 'two']]); const second = new Map([[2, 'two-new'], [3, 'three']]); const merged = new Map([...first, ...second]); // 结果: {1 => "one", 2 => "two-new", 3 => "three"}
6. 性能考虑
- 查找速度:Map 的查找操作接近 O(1) 复杂度
- 插入速度:Map 的插入操作通常比 Object 快
- 内存占用:Map 通常比 Object 占用更多内存
- 大量数据:当键值对数量很大时,Map 的性能优势更明显
7. 使用场景
- 需要键不是字符串/符号的情况
- 需要维护插入顺序的场景
- 频繁增删键值对的场景
- 需要知道数据大小的场景
- 避免与原型属性冲突的场景
8. 注意事项
- 键的比较:Map 使用 "SameValueZero" 算法比较键(类似于 ===,但 NaN 等于 NaN)
- 内存泄漏:使用对象作为键时,即使对象被设为 null,Map 仍会保留引用
- 序列化:Map 不能直接 JSON.stringify,需要先转换为数组
- 浏览器兼容性:所有现代浏览器都支持 Map,但旧版 IE 不支持
9. 实际应用示例
统计词频
function wordFrequency(text) { const words = text.split(/\s+/); const frequency = new Map(); for (const word of words) { const count = frequency.get(word) || 0; frequency.set(word, count + 1); } return frequency; }
缓存实现
class SimpleCache { constructor() { this.cache = new Map(); } set(key, value) { this.cache.set(key, { value, timestamp: Date.now() }); } get(key) { const entry = this.cache.get(key); return entry ? entry.value : null; } clearExpired(expireTime) { const now = Date.now(); for (const [key, entry] of this.cache) { if (now - entry.timestamp > expireTime) { this.cache.delete(key); } } } }
Map 是 JavaScript 中非常强大且灵活的数据结构,特别适合需要复杂键或有序键值对的场景。理解并熟练使用 Map 可以显著提高代码的质量和性能。
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
// 1. 使用Map统计频率
const frequencyMap = new Map();
for (const num of nums) {
frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1);
}
// 2. 将Map转换为数组并排序
const sortedEntries = Array.from(frequencyMap.entries())
.sort((a, b) => b[1] - a[1]);
// 3. 提取前k个高频元素
return sortedEntries.slice(0, k).map(entry => entry[0]);
};