数据结构与算法
栈与队列入门
栈
-
栈的常用操作
方法 描述 时间复杂度 push() 元素入栈(添加至栈顶) O(1) pop() 栈顶元素出栈 O(1) peek() 访问栈顶元素 O(1) -
栈的实现
- 基于数组实现
- 基于链表实现
- 实现方式 : hello算法-栈的实现
-
两种实现方式对比
- 时间效率
- 基于数组实现的栈在触发扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
- 基于链表实现的栈可以提供更加稳定的效率表现。
- 空间效率
- 基于数组实现的栈可能造成一定的空间浪费。
- 因此链表节点占用的空间相对较大。
- 时间效率
-
Java内置的栈类
/* 初始化栈 */ Stack<Integer> stack = new Stack<>(); /* 元素入栈 */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); /* 访问栈顶元素 */ int peek = stack.peek(); /* 元素出栈 */ int pop = stack.pop(); /* 获取栈的长度 */ int size = stack.size(); /* 判断是否为空 */ boolean isEmpty = stack.isEmpty();
队列
- 队列常用操作
方法 描述 时间复杂度 push() 元素入队,即将元素添加至队尾 O(1) pop() 队首元素出队 O(1) peek() 访问队首元素 O(1) - 队列实现
- 基于链表实现
- 基于数组实现 (环形数组)
- 实现方式 : hello算法-队列实现
- 两种方式效率对比与栈相同
- Java内置的队列类
/* 初始化队列 */ Queue<Integer> queue = new LinkedList<>(); /* 元素入队 */ queue.offer(1); queue.offer(3); queue.offer(2); queue.offer(5); queue.offer(4); /* 访问队首元素 */ int peek = queue.peek(); /* 元素出队 */ int pop = queue.poll(); /* 获取队列的长度 */ int size = queue.size(); /* 判断队列是否为空 */ boolean isEmpty = queue.isEmpty();
双向队列
- 双向队列常用操作
方法名 描述 时间复杂度 push_first() 将元素添加至队首 O(1) push_last() 将元素添加至队尾 O(1) pop_first() 删除队首元素 O(1) pop_last() 删除队尾元素 O(1) peek_first() 访问队首元素 O(1) peek_last() 访问队尾元素 O(1) - 双向队列实现
- 基于双向链表的实现
- 基于数组的实现 (环形数组)
- 双向队列的优势
- 双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。
- 软件需要在栈底(队首)执行删除操作, 栈无法实现该功能, 此时就需要使用双向队列来替代栈。
- Java内置的双向队列类
/* 初始化双向队列 */ Deque<Integer> deque = new LinkedList<>(); /* 元素入队 */ deque.offerLast(2); // 添加至队尾 deque.offerLast(5); deque.offerLast(4); deque.offerFirst(3); // 添加至队首 deque.offerFirst(1); /* 访问元素 */ int peekFirst = deque.peekFirst(); // 队首元素 int peekLast = deque.peekLast(); // 队尾元素 /* 元素出队 */ int popFirst = deque.pollFirst(); // 队首元素出队 int popLast = deque.pollLast(); // 队尾元素出队 /* 获取双向队列的长度 */ int size = deque.size(); /* 判断双向队列是否为空 */ boolean isEmpty = deque.isEmpty();
力扣题目
232.用栈实现队列
题目链接 : 232.用栈实现队列
class MyQueue {
// 定义两个栈 一个用于存入 一个用于弹出
Stack<Integer> stackIn; // 进栈
Stack<Integer> stackOut; // 出栈
// 构造 初始化
public MyQueue() {
stackIn = new Stack<>();
stackOut = new Stack<>();
}
// 加入元素时直接加入到进栈
public void push(int x) {
stackIn.push(x);
}
// 从出栈中弹出数据, 若出栈中空了就把进栈的数据全部推到出栈中
public int pop() {
dumpstackIn();
return stackOut.pop();
}
// 从出栈中弹出数据, 若出栈中空了就把进栈的数据全部推到出栈中
public int peek() {
dumpstackIn();
return stackOut.peek();
}
// 若两个栈都空了就没有数据了
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
// 如果出栈空了 就把进栈全部压到出栈中
private void dumpstackIn(){
if (!stackOut.isEmpty()){
return;
}
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
思路
1. 使用栈来模式队列的行为需要两个栈,一个输入栈,一个输出栈
2. 每次从输入栈压数据到输出栈都要全部压出, 保证输出栈可以第一个拿出来的数据是当前的队首数据
代码随想录提供思路 : 代码随想录
225. 用队列实现栈
题目链接 : 225. 用队列实现栈
class MyStack {
// 使用一个队列实现栈
Queue<Integer> queue;
// 构造方法
public MyStack() {
// 使用双链表实现类 实现队列
queue = new LinkedList<>();
}
// 将每次添加的元素都放在队列首部, 相当于栈顶, 之后出栈顺序就和此队列相同了
public void push(int x) {
queue.offer(x);
int size = queue.size();
while (size-- > 1) {
queue.offer(queue.poll());
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
思路
1. 使用一个队列实现栈, 因为队列和栈的主要区别就是进出顺序不同, 所以添加元素后调换队列内部元素顺序使其满足栈的出栈顺序要求
代码随想录提供思路 : 代码随想录 (涵盖其他思路)
20. 有效的括号
题目链接 : 20. 有效的括号
class Solution {
// 注意点
// 1. 括号必须一一对应左括号对应右括号
// 2. 括号之间要有顺序, 不能错落进去, 是有规律的
public boolean isValid(String s) {
// 使用栈实现
Stack<Character> stack = new Stack<>();
char ch; // 接收字符串的每个字符
for (int i = 0; i < s.length(); i++) {
ch = s.charAt(i);
// 检测到左括号, 就在栈内存入对应的右括号 (易于匹配)
if (ch == '(') {
stack.push(')');
} else if (ch == '{') {
stack.push('}');
} else if (ch == '[') {
stack.push(']');
// 1. 栈为空,没有匹配的字符,说明右括号没有找到对应的左括号 return false
// 2. 发现栈顶与要匹配的字符不同 return false
} else if (stack.isEmpty() || stack.peek() != ch) {
return false;
// 除上述条件之外的情况就是栈顶元素与要匹配的字符相同的情况
} else {
stack.pop();
}
}
// 栈为空, 说明都正常匹配了 返回true
return stack.isEmpty();
}
}
思路
1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。 —对应最后栈不为空的情况
2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 —对应栈顶与匹配的字符不同的情况
3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。 —对应循环结束前栈空了的情况
代码随想录提供思路 : 代码随想录
1047. 删除字符串中的所有相邻重复项
题目链接 : 1047. 删除字符串中的所有相邻重复项
class Solution {
public String removeDuplicates(String s) {
// 使用字符串代替栈的操作
StringBuilder stringBuilder = new StringBuilder();
// 循环将字符串中的字符判断后存入容器中
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int length = stringBuilder.length();
// 若容器栈顶元素和当前索引的字符相同 表明这两者相连 则删除此元素
if (length > 0 && stringBuilder.charAt(length - 1) == c) {
stringBuilder.deleteCharAt(length - 1);
}
// 添加元素
else {
stringBuilder.append(c);
}
}
// 返回容器内的字符串
return stringBuilder.toString();
}
}
思路
1. 字符串内的字符可以逐一与栈顶元素进行判断之后进入栈中, 从而删除相邻重复项
2. 最后要输出栈内的字符串, 可以直接用字符串容器代替栈实现栈的基本操作
代码随想录提供思路 : 代码随想录 (涵盖其他思路)
150. 逆波兰表达式求值
题目链接 : 150. 逆波兰表达式求值
相关知识 : 逆波兰式
class Solution {
public int evalRPN(String[] tokens) {
// 使用整型栈实现
Stack<Integer> stack = new Stack<>();
// 判断为数字就存入栈中 判断为运算符就从栈中拿出栈顶的两个数字运算后把结果再填入栈顶
for (String s : tokens) {
if ("+".equals(s)) {
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();
}
}
思路
1. 逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。
2. 遇到一个运算符就把最近的两个数字拿出来进行前后运算, 结果返回栈中成为新的数字
3. 其实逆波兰表达式相当于是二叉树中的后序遍历, 把运算符作为中间节点, 按照后序遍历的规则画出一个二叉树。
代码随想录提供思路 : 代码随想录
239. 滑动窗口最大值
题目链接 : 239. 滑动窗口最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// 定义双向队列为滑块 更加自由操作滑块左右数据
Deque<Integer> deque = new LinkedList<>();
// 定义接收数据的结果数组
int[] result = new int[nums.length - k + 1];
// 循环将数据填入滑块中 也将滑块中左端的数据扔出来
for (int i = 0; i < nums.length; i++) {
// 如果滑块不为空 且 要添加的索引对应的数值比滑块入口处的数字索引对应的数值大 抛出入口处的数字
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 如果滑块不为空 且 滑块左端数字索引已经不满足要求的索引范围 抛出出口处的数字
while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 若要添加的数字比滑块入口的数字小, 就加进去
deque.offer(i);
// 当滑块内数字足够时, 滑块出口处的数字索引对应的数值一定是最大值, 存入结果数组中
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
思路
1. 只需要将滑块中的最大值保留在队列中即可, 后面添加的数值若是大, 就把小的排出去然后再填入
2. 通过索引判断滑块离开的索引位置是否应该被抛出
3. 滑块装满后每次移动都将出口处的最大值存入结果数组中
代码随想录提供思路 : 代码随想录 (涵盖其他思路)
347. 前 K 个高频元素 ( !!! 不是很懂实现原因 )
题目链接 : 347. 前 K 个高频元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 使用 map 集合将数组转为某个数字和其对应的出现次数
Map<Integer, Integer> map = new HashMap<>();
// 用于接收出现次数最多的 k 个数字
int[] result = new int[k];
// 实现小顶堆的优先级队列
PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(o -> o[1]));
// 将数组转化为 map 集合
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
// 将map集合内的数字导入优先级队列中
map.forEach((key, value) -> {
priorityQueue.offer(new int[]{key, value});
// 若优先级队列中的数字个数为 k + 1 时, 抛出头节点最小的数字重新加入
// 最后一步循环会抛出最小的数字以至于最后队列中只有 k 个数字
if (priorityQueue.size() > k) {
priorityQueue.poll();
}
});
// 将优先级队列中留下的 k 个数字放入结果数组中
for (int i = k - 1; i >= 0; i--) {
result[i] = priorityQueue.poll()[0];
}
return result;
}
}
思路
1. 优先级队列是一个披着队列外衣的堆
2. 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。
3. 优先级队列提供大小顶堆的选择,
4. 快排要将map转换为int[]的结构,然后对整个数组进行排序,不如优先级队列可以只维护k个数值的排序
代码随想录提供思路 : 代码随想录 (涵盖其他思路) (大小顶堆)