栈和队列
介绍:
栈(Stack)和队列(Queue)是计算机科学中常用的数据结构,它们用于管理和组织数据。
栈(Stack):
- 定义: 栈是一种线性数据结构,它遵循 Last In, First Out(LIFO)原则,即最后进入栈的元素是第一个被移除的。
- 操作:
- 压入(Push): 将元素添加到栈的顶部。
- 弹出(Pop): 从栈的顶部移除元素。
- 查看栈顶元素(Top): 获取但不移除栈顶的元素。
- 应用:
- 用于实现递归算法的调用栈。
- 表达式求值。
- 浏览器的前进和后退功能。
- 实现: 可以使用数组或链表实现栈。
队列(Queue):
- 定义: 队列是一种线性数据结构,它遵循 First In, First Out(FIFO)原则,即最先进入队列的元素是第一个被移除的。
- 操作:
- 入队(Enqueue): 将元素添加到队列的尾部。
- 出队(Dequeue): 从队列的头部移除元素。
- 查看队头元素(Front): 获取但不移除队列头部的元素。
- 应用:
- 任务调度。
- 广度优先搜索算法。
- 打印队列。
- 实现: 可以使用数组或链表实现队列,也有特殊类型如双端队列(Deque)。
两者特点:
- 限制访问: 栈和队列都限制了数据的访问方式,栈只允许从顶部访问,队列只允许从前端访问。
- 实现: 可以使用数组或链表等数据结构来实现栈和队列。
- 基本操作: 压入/入队、弹出/出队、查看栈顶/队头元素等是它们的基本操作。
入门经典示例:
有效的括号20
基本思路l:
遍历字符串,当遇到左括号时,将其推入栈中;当遇到右括号时,检查栈顶元素是否是相匹配的左括号,如果是,则将栈顶元素弹出,否则返回 false。最后,如果栈为空,说明所有括号都匹配成功,返回 true;否则,返回 false。
class Solution {
public boolean isValid(String s) {
// 当字符串长度为奇数的时候,属于无效情况
// 条件说明了长度至少为 1,所以不需要在判空
if (s.length() % 2 == 1) {
return false;
}
//构建栈
Stack<Character> stack = new Stack<Character>();
//由左向右遍历字符串
for(char c : s.toCharArray()){
if(c == '('){
stack.push(')');
}else if(c == '['){
stack.push(']');
}else if( c == '{'){
stack.push('}');
//列举三种左括号情况,当是左括号时直接压入栈中
}else if( stack.isEmpty() || c != stack.pop()){
//除去左括号的情况,当最新的字符不等于刚放入栈中的元素时,返回false,
//当并没有匹配,而右侧直接是空时直接结束;表明有多余的右括号
//其实省略了一种情况,即c=stack.pop()时,表明正常,不进行任何操作;
return false;
}
}
//当完美运行完毕后,栈肯定为空
return stack.isEmpty();
}
}
删除字符串中所有相邻重复项1047
使用java来写
想象一下入栈字符串,之后每个字符出栈都需要先和上一个比较是否相同,若相同两者都消去,不同入栈
方法一:使用可变字符串直接模拟栈来操作节省空间和时间
class Solution {
public String removeDuplicates(String s) {
// 将 res 当做栈
// 也可以用 StringBuilder 来修改字符串,速度更快
// StringBuilder res = new StringBuilder();
StringBuffer res = new StringBuffer(); //StringBuffer 指可变性字符串类型,初始长度为0
// top为 res 的下标
int top = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
if (top >= 0 && res.charAt(top) == c) {
res.deleteCharAt(top);
top--;
// 否则,将该字符 入栈,同时top++
} else {
res.append(c);
top++;
}
}
return res.toString();//将 StringBuffer(或者换成 StringBuilder)类型的 res 转换为普通的字符串类型(String)。这是因为 res 是一个可变的字符串缓冲区,而有些情况下可能需要返回一个不可变的字符串。
}
}
方法二:常规栈操作,遍历字符串,将结果存入栈中,最后将栈中元素转化为可变字符串,再输出定长字符串
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek() == c) {
stack.pop(); // 如果栈不为空且栈顶元素与当前字符相同,则弹出栈顶元素
} else {
stack.push(c); // 否则将当前字符压入栈中
}
}
StringBuilder result = new StringBuilder();
while (!stack.isEmpty()) {
result.insert(0, stack.pop()); // 从栈底开始构建最终字符串
}
return result.toString();
}
}
逆波兰表达式150
根据 逆波兰表示法,求表达式的值。其实就是后缀表达式,我们日常使用的是中缀表达式
有效的运算符包括 + , - , * , / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
- 输入: [“2”, “1”, “+”, “3”, " * "]
- 输出: 9
- 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
知识拓展:这样的一个计算,使用中缀表达式,计算机计算起来会很复杂(因为,*,/,括号的存在),但是使用后缀表达式就很简单,直接从左到右计算,使用二叉树可以很直观的表现出计算的过程(左,右,中),而栈也可以很轻松计算。
即,当识别到符号时吗,根据符号的定义,从栈中出栈两个元素进行操作,并将结果再存入栈中;
java:
关键部分代码写法:
for (String s : tokens) {
if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
stack.push(stack.pop() + stack.pop()); // 注意 - 和/ 需要特殊处理,stack会自动识别并弹出已经出现过的数,所以stack.pop()+stack.pop()其实就是将栈最外层两个数相加,前边的数是外侧第一个数,后边的是外侧第二个数;按照这个原理依次写逻辑
} else if ("-".equals(s)) {
stack.push(-stack.pop() + stack.pop());
} else if ("*".equals(s)) {
stack.push(stack.pop() * stack.pop());
} else if ("/".equals(s)) {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
}
else {
stack.push(Integer.valueOf(s));//数字的情况,将字符串转换成数字加入到栈中;
}
代码:
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList();
for (String s : tokens) {
if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
stack.push(stack.pop() + stack.pop()); // 注意 - 和/ 需要特殊处理
} else if ("-".equals(s)) {
stack.push(-stack.pop() + stack.pop());
} else if ("*".equals(s)) {
stack.push(stack.pop() * stack.pop());
} else if ("/".equals(s)) {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
} else {
stack.push(Integer.valueOf(s));//数字的情况,将字符串转换成数字加入到栈中;
}
}
return stack.pop();
}
}
滑动窗口最大值239
单调队列类型题目
如果无法理解单调队列可以看下边的解释:
摘自立扣评论:
单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此.....就拿此题来说,队头最大,往队尾方向单调......有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝.....这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国......然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城......历史总是轮回的。
太对了,对于单调队列的解释一目了然
这道题目解法:
牢记从头到尾是单调减小,最大数都存到头结点,新加入的数要和头结点比较大小,大的话直接清空队列,小的话,放在头结点后边,同时还要一直维护一个滑动窗口,头结点值划到最左端时,要去掉头结点,即最大值。
//利用双端队列手动实现单调队列
/**
* 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
* 单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (--> head) (右边为头结点,元素存的是下标)
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for(int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while(!deque.isEmpty() && deque.peek() < i - k + 1){
deque.poll();
}
// 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
// 将窗口内的最大值加入结果数组;
if(i >= k - 1){
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}