目录
目录
一、栈
1,定义:
栈(Stack)是一种遵循后进先出(Last-In-First-Out,LIFO)原则的数据结构。它类似于现实生活中的一叠盘子,你只能从最上面放入和取出盘子。
2,性质:
局限性:
- 元素的插入操作(入栈)和删除操作(出栈)仅限于栈顶。
- 栈中的元素没有索引,只能在栈顶进行插入和删除操作。
高效性:
- 栈是一种高效的数据结构,插入和删除的时间复杂度为 O(1)。
3,Java中Stack类中的常用方法:
push(E item)
:将元素推入栈顶,即插入操作。pop()
:从栈顶删除并返回元素,即删除操作。peek()
:返回栈顶元素,但不执行删除操作。empty()
:判断栈是否为空。search(Object o)
:查找指定元素在栈中的位置,返回距离栈顶的距离。
4,栈的简单应用:
通过栈的特殊性质,发现在进行字符串有规律的操作时,具有较好的表现。
eg:
1,括号匹配
当某种算法需要一个循环遍历时,且该操作在每一次都会从当前元素开始涉及到有限个的元素时可以考虑使用栈。
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(ch == '{'||ch == '(' || ch == '['){
stack.push(ch);
}else if(!stack.empty()){ //({})
char temp = stack.peek();
switch (ch) {
case '}':
if (temp != '{')return false;
break;
case ')':
if (temp != '(')return false;
break;
case ']':
if (temp != '[')return false;
break;
}
stack.pop();
}else { // ()()}
return false;
}
}
return stack.empty(); //{()
}
}
2,最简路经
涉及到每一次遍历时都需要进行对当前元素进行判断,以及是否加入或从当前元素开始向前删除一个,故可以考虑栈。
class Solution {
// ../ 返回上一级目录
// ./ 当前的目录
// // 视为一个 /
// / 在结尾
public String simplifyPath(String path) {
String[] arr = path.split("/");
Stack<String> stack = new Stack<>();
for(String str : arr) {
if(str.equals("..") && !stack.isEmpty()) stack.pop();
if(!str.equals(".") && !str.equals("..")&& !str.equals("")) stack.push(str);
}
StringBuilder sb = new StringBuilder();
while(!stack.isEmpty()){
sb.insert(0,stack.pop());
sb.insert(0,"/");
}
if(sb.length() == 0) sb.append("/");
return sb.toString();
}
}
二、队列
1,定义:
队列(Queue)是一种具有特定操作规则的线性数据结构,遵循先进先出(FIFO)的原则。在队列中,元素的插入和删除操作都是在队列的末尾进行的,而访问或删除元素操作则是在队列的开头进行的。
2,不同队列的性质:
1,普通队列(Queue):
普通队列是最基本的队列形式,遵循先进先出(FIFO)的原则。
新元素被添加到队列的末尾,也称为队尾(rear),而队列的头部(front)用于删除元素或访问队列中的元素。
2,循环队列(Circular Queue):
循环队列是一种特殊的队列,使用数组实现。与普通队列不同的是,循环队列的队尾指针(rear)和队头指针(front)在到达数组的尾部后会跳转到数组的起始位置。
这样可以充分利用数组空间,避免队列插入时数组空间的浪费。
3,双端队列(Deque):
双端队列是一种具有双端操作的队列形式,允许在队头和队尾进行插入和删除操作。即可以在队列的两端进行元素的入队和出队操作。
双端队列可以作为栈和队列的一种泛化结构使用,提供了更灵活的操作。
3,Java中Queue接口的常用方法:
offer(E e):将指定的元素插入队列的尾部(也即入队操作)。如果插入成功,则返回true;如果队列已满,无法插入,则返回false。
add(E e): 同offer(E e),但由于容量导致插入失败则抛出异常--
IllegalStateException。
poll():删除并返回队列头部的元素(也即出队操作)。如果队列为空,则返回null。
remove(): 同poll(),但如果队列为空则抛出异常--
NoSuchElementException。
peek():返回队列头部的元素,但不对队列进行修改。如果队列为空,则返回null。
element():同peek(),但如果队列为空则抛出异常--
NoSuchElementException。
4,Java中Deque接口的常用方法:
- 添加元素:
- offerFirst(E e):将指定元素插入到双端队列的头部。如果插入成功,则返回true;如果队列已满,无法插入,则返回false。
- offerLast(E e):将指定元素插入到双端队列的尾部。如果插入成功,则返回true;如果队列已满,无法插入,则返回false。
- 获取并删除元素:
- pollFirst():删除并返回双端队列的头部元素。如果队列为空,则返回null。
- pollLast():删除并返回双端队列的尾部元素。如果队列为空,则返回null。
- 获取但不删除元素:
- peekFirst():返回双端队列的头部元素,但不对队列进行修改。如果队列为空,则返回null。
- peekLast():返回双端队列的尾部元素,但不对队列进行修改。如果队列为空,则返回null。
- 其他方法:
- remove(Object o):从双端队列中删除指定的元素。
- contains(Object o):检查双端队列是否包含指定的元素。
- size():返回双端队列中元素的个数。
- isEmpty():检查双端队列是否为空。
- iterator():返回一个在双端队列上进行迭代的迭代器
5, 队列的简单应用:
约瑟夫问题:有n个人围坐在一圈,从1到n编号。从第一个人开始,按照一定的规则逐个被移除,直到最后只剩下一个人。给定n和一个指定的计数值m,求最后剩下的那个人的编号。
分析一下,正常数组结构去思考的话,可以解决但过于繁琐,比如第三人出列但往后元素就得靠前或者当前位置进行标记,但换一种思路,这种循环遍历数据如同一个滤器不断绕圈,属于先进后出可不就是站队吗,至于如何循环起来了,正常站队是队伍不动,滤器循环,但换一种思路让滤器不动,队伍自己去跑起来,队首接受检查后自己去对尾排队,直到遇到符合要求的便不再排队。
public class YueSeFu {
public static int Yuesefu(int n,int m){
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 1; i <= n; i++) {
queue.offer(i);//初始化;
}
while (queue.size()>1){
for (int i = 1; i < m; i++) {
queue.offer(queue.poll());//重新站队
}
queue.poll();//出列;
}
return queue.peek();
}
}
三,栈与队列的综合应用:
1,单调栈:
下一个元素更大<循环数组>:
与普通寻找下一个更大元素思路并未区别,但因为其原理仍是为当前所遍历元素向前寻找配偶(即配偶的第一个更大元素为当前所遍历元素)。
循环数组的下一个更大元素可以在此元素之前,这个可以看作是两个数组拼接而成,因为此寻找非循环数组的下一个更大元素的算法只须循环一次。
在普通算法中遍历完得到的数据只有找到或者没找到不存在如果往后再拼接一个数组的话就需要修改原先某个值,即:栈中所剩余的元素为未匹配到配偶的,因此可以想到带着这个初始栈重新遍历一次数组,也就是打开壁垒,让所遍历的元素在向前寻找的途中可以寻找数组后面的元素。注意第二次循环不需要入栈,清理完栈中元素即可。
public int[] nextGreaterElements(int[] nums) {
int [] result = new int[nums.length];
Stack<Integer> stack = new Stack<>();
IntStream.range(0, result.length).//控制索引范围。
forEach(i -> result[i] = -1);
for (int i = 0; i < nums.length; i++) {
while (!stack.isEmpty()&&nums[stack.peek()]<nums[i]){
result[stack.pop()]=nums[i];
}
stack.push(i);
}
int index = 0;
while (!stack.isEmpty()){//上一个循环结束后栈中剩下的元素就是下一个更大的元素不在此元素后面而在前面故重新循环。
while (index<nums.length&&nums[stack.peek()]<nums[index]){
result[stack.pop()]=nums[index];
}
if(index>=nums.length)break;//可能遍历完也没有更大的;
index++;
}
return result;
}
删除重复数字后最大值
对于此问题,需要知道决定数的大小由高位决定<eg:45234与41234,决定删除哪一个4时显然由高位决定>在每一次抉择时保证当前选择为数的最大值(每一个抉择相互独立互不干扰)
由于结果唯一且每一次的选择唯一,那么每一次选择最大值即可获取局部最大值,故可有算法:每一次选择最大值的删除方式即可获得总体最大值。
因权重大小从前往后,所以数组从前往后遍历
当有重复数字的元素分开时,直接与后一个元素进行比较,小则去,大则留。
当重复数字连在一片时,第一个元素的去留可能由第二个元素的去留决定,以此类推(即这个局部每一个元素都可变)。《将局部看作一个整体》
但选择仍然是唯一的且必须是当前的最优解。因为在一个局部为保证整个局部为当前状态的最优解,只有降序这一种排列。(由此可以考虑单调栈,仅仅是局部的单调)
public static int deleteSameNum(String str){
int [] time = new int[10];
str.chars().forEach(num -> time[num - '0']++);
Stack<Character> stack = new Stack<>();
Set<Character> set = new HashSet<>();
for (char ch:str.toCharArray()){
if(set.contains(ch)){
time[ch - '0']--;// 第一次未被删除,遍历第二次时,可能连着被删除,如果次数不变,第三次可能被删除;
// eg:2 3 1 5 1 3 4
//其中3第一次遇到1不被删,遇到5一直删到3,最后因为次数不变,遇到4被删除;
}else {
while (!stack.isEmpty()&&stack.peek()<ch&&time[stack.peek() - '0']>1){
char top = stack.pop();
time[top-'0']--;//第一次被删后,如果次数不改变,那么第二次仍然可能被删;
set.remove(top);
}
set.add(ch);
stack.push(ch);
}
}
String s = stack.stream().map(String::valueOf).collect(Collectors.joining());
return Integer.parseInt(s);
}
2,单调队列:
队列的最大值<动态>:
对于队列先进先出的特点,为寻找每一次变动的当前最大值,并不需要每一次都去寻找最大值,在队列中只能从前一个一个删,从后一个一个加,如果只考虑加的话,运用单调栈的特点则可以解决这个问题,即不论加多少都能保证栈底元素为最大值。但如果是删除操作的话需要从队首删除,此时有两种情况,删除元素是否为当前队列的最大值(是否等于栈底元素(注意:无需考虑此栈底元素与队列元素是否只是值相等,因为如果队列有两个相等的元素且恰好是某一状态的最大值,那么栈底元素与倒数第二个元素都是它,删除一个还有第二个,单调栈只删除比其小的元素。)),不是则忽略,是的话那么需要双端队列进行栈底删除。
class MaxQueue {
Queue<Integer> queue;
LinkedList<Integer> monotonicQueue;
public MaxQueue() {
queue = new ArrayDeque<>();
monotonicQueue = new LinkedList<>();
}
public int max_value() {//得到最大值
while (queue.isEmpty()){
return -1;
}
return monotonicQueue.peekFirst();
}
public void push_back(int value) {//添加元素
queue.offer(value);
while (!monotonicQueue.isEmpty()&&monotonicQueue.peekLast() < value){
monotonicQueue.pollLast();
}
monotonicQueue.offerLast(value);
}
public int pop_front() {//删除元素
if(queue.isEmpty()){
return -1;
}int team = queue.poll();
if(team == monotonicQueue.peekFirst()){
monotonicQueue.pollFirst();
}
return team;
}
}
滑动窗口:
思路同上。
public int[] maxSlidingWindow(int[] nums, int k) {
int [] max = new int[nums.length-k+1];
int j=0;
Deque<Integer> queue = new ArrayDeque<>();
for (int i=0;i<nums.length;i++){
while (!queue.isEmpty()&&queue.peekLast()<nums[i]){
queue.pollLast();
}
queue.addLast(nums[i]);
if(i>=k-1){
int maxQueue = queue.peekFirst();
max[j++] = maxQueue;
if(nums[i - k + 1] == maxQueue){
queue.pollFirst();
}
}
}
return max;
}