目录
栈的特点
栈的数据特点是先进后出(FILO),数据特定非常明显当然解决的问题特定也非常明显,一般需要提供 peek(查看)、push(压栈)、pop(出栈)方法、也可以提供empty判断、search元素方法。可以认为栈就是受限的线性表,栈可以基于数组实现称为顺序栈,也可以基于链表实现称为链式栈。从功能上来说数组和链表本身都能实现栈的功能,但是其暴露了太多的接口,所以很多时候变得不可控也更容易出错,对比一下栈本身只暴露了三个接口【限制也是优势】。
Java中是 java.util.Stack就是一个顺序栈,默认长度为10。基于线程安全的 Vector 实现,底层使用syncronized关键字实现,如果我们的方法处理本身就是线程安全的,我们有最求极致性能,则可以考虑自己实现一个 Stack。
数据结构的特殊性,也决定了处理问题的特殊性,使用场景整理如下:
栈的使用场景
1)、浏览器的前进后退功能
浏览器的前进和后退功能本身就可以基于两个栈进行实现,该功能涉及到的操作包括:
- 浏览了一个或者多个页面;
- 在某一步进行前进或者后退功能,如果不能前进或者后退时,将按钮置灰不能点击;
- 在某一步时,新访问了一个地址(此时需要将本来可能通过后退功能查询的所有地址删除)
2)、LeetCode20 有效的括号
解决思路就还是使用栈,只是处理时可以利用散列表的数据结构(Java中的HashMap),使用key和value分布存储前、后括号,利用其读写时间复杂度近似 O(1)。遍历字符判断:
1、直接使用Map的containsKey判断是否为前括号,是则直接压栈;
2、再判断是否为空(没有左括号,直接进来就是右括号肯定是返回flase);或者从栈中获取一个元素,再去map中根据key获取value匹配;
public class StackForBrackets20 {
private static Map<Character, Character> CHAR_MAP = null;
static {
HashMap<Character, Character> tempForGc = Maps.newHashMap();
tempForGc.put(']', '[');
tempForGc.put('}', '{');
tempForGc.put(')', '(');
CHAR_MAP = Collections.unmodifiableMap(tempForGc);
}
public static void main(String[] args) {
String str = "{[][]()}{{{}}}}";
System.out.println(StackForBrackets20.isValid(str));
}
public static Boolean isValid(String str) {
if (str == null || str.length() == 0) {
return true;
}
Stack<Character> stack = new Stack<>();
for (int i = 0; i < str.length(); i++) {
Character c = str.charAt(i);
if (!CHAR_MAP.containsKey(c)) {
stack.push(c);
continue;
}
if (stack.isEmpty() || c.equals(stack.pop())) {
return false;
}
}
return stack.isEmpty();
}
}
3)、LeetCode232 用栈实现队列
栈的特点是FILO,而队列的特点则是FIFO,那么压栈、出栈两次就回到了FIFO【123压栈出栈后变成321,321再压栈出栈后就变成了123】。所以需要使用两个栈(分别为 input、output):
入栈(push)方法:将元素依次压入栈 input
出栈(pop)方法:判断如果栈B为空,则将栈A中的元素全部出栈并压入栈B;否则直接从栈B出栈一个元素;
判空(empty)方法:判断两个栈是否都为空;
查看(peek)方法:处理逻辑与出队方法相同,只是最后返回的是 B栈的peek方法;
public class StackToQueue232 {
private static Stack<Integer> input = null;
private static Stack<Integer> output = null;
/** Initialize your data structure here. */
public StackToQueue232() {
input = new Stack<Integer>();
output = new Stack<Integer>();
}
/** Push element x to the back of queue. */
public void push(int x) {
input.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if (output.empty()) {
while (!input.empty()) {
output.push(input.pop());
}
}
return output.pop();
}
/** Get the front element. */
public int peek() {
if (output.empty()) {
while (!input.empty()) {
output.push(input.pop());
}
}
return output.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return input.empty() && output.empty();
}
}
4)、函数调用栈
编程里面的方法本身就是栈的结果,里面是每个栈帧。方法调用就是压栈的过程,当所以栈都空了的时候就是方法调用完成。只是当方法调用时抛出异常,直接出栈,然后去异常表中获取信息返回。
5)、算术表达式求值
针对我们编程是 int x = 123 + 5 * 3 - 2; 对于编译器来说就是一个字符串,使用两个栈对其进行操作,分别存储数字和操作符,遍历字符:
1、如果是数字则压入数字栈;如果下一个还是数字则从数字栈中获取栈顶元素,12 = 1 * 10 + 2;下次 123 = 12 * 10 + 3;
2、如果操作符栈为空则直接压栈;否则 peek 栈顶操作符与当前操作符进行对比优先级:如果当前的优先级高于栈顶操作符也是直接压栈;否则从操作符栈 pop 该栈顶操作符,在从数字栈 pop 两个数字,进行计算将结果再压入数字栈;
6)、自己实现的接口性能耗时
现在虽然微服务为我们提供了很多的分布式链表追踪,比如 Spring Cloud Sleuth和Zipkin的基本概念。但是很多时候我们自己开发,或者测试环境等想基于简单的开发(比如在方法上添加一个枚举,就可以打印耗时),并且对节点的多个子方法添加注解就获取到了每一步的耗时(方便分析问题)。自己就基于栈的数据结构、Spring Aop、使用简单的注解配置就可以将接口性能耗时打印到日志中,代码包括:
/**
* 开启方法耗时, 默认打印的监控名称{@link TimeConsume#taskName()} 为: 方法所在类名#方法名称
* @author kevin
* @date 2020/8/23 10:14
* @since 1.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(TimeConsumeActionConfig.class)
public @interface EnableTimeConsume {
}
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeConsume {
/**
* 任务名称,用于记录时间
*
* @return taskName 任务名称
*/
String taskName() default "";
/**
* 打印时间消耗, 一般在执行流程的最后一步打印
* 为防止流氓扫描注入,开关失效
*
* @return 是否打印日志
*/
boolean print() default true;
}
/**
* 统计方法执行时间,可以支持注解的方法嵌套,自己保证会被动态代理切中
* @author kevin
* @date 2020/8/23 10:25
* @since 1.0.0
* @see org.springframework.context.annotation.EnableAspectJAutoProxy
*/
@Aspect
@Slf4j
public class TimeConsumeAction {
/**
* 名称分割符
*/
private static final String SEPARATOR_NAME = "#";
/**
* 方法调用计时器
*/
private static final ThreadLocal<Stack<TimeConsumeStopWatch>> MONITOR_THREAD_LOCAL = ThreadLocal.withInitial(Stack::new);
/**
* 只切面 TimeConsume 注解标注的方法
*/
@Pointcut("@annotation(自己修改一下路径.TimeConsume)")
private void timeConsumeAspect() {
}
/**
* Aop环绕
* @param pjp 切入点信息
* @return 代理对象
* @throws Throwable 执行异常
*/
@Around("timeConsumeAspect()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
TimeConsume timeConsume = getTimeConsume(pjp);
if (timeConsume.print()) {
String taskName = StringUtil.isBlank(timeConsume.taskName()) ? getDefaultName(pjp) : timeConsume.taskName();
TimeConsumeStopWatch stopWatch = new TimeConsumeStopWatch(taskName);
stopWatch.start(taskName);
MONITOR_THREAD_LOCAL.get().push(stopWatch);
}
return pjp.proceed();
}
/**
* Aop后置方法
* @param pjp 切入点信息
* @throws Throwable
*/
@After("timeConsumeAspect()")
public void after(JoinPoint pjp) {
TimeConsume timeConsume = getTimeConsume(pjp);
if (timeConsume.print()) {
TimeConsumeStopWatch stopWatch = MONITOR_THREAD_LOCAL.get().pop();
stopWatch.stop();
// LOG_THREAD_LOCAL.get().append(stopWatch.shortSummary()).append(SEPARATOR);
// 最外层(可能多个)出时,调用remove方法进行gc防止内存溢出
if (MONITOR_THREAD_LOCAL.get().empty()) {
log.info(stopWatch.shortSummary());
MONITOR_THREAD_LOCAL.remove();
// LOG_THREAD_LOCAL.remove();
}
}
}
/**
* 获取默认task名称
* @param pjp 切入点信息
* @return 默认task名称
*/
private String getDefaultName(JoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
return methodSignature.getDeclaringType().getSimpleName() + SEPARATOR_NAME + methodSignature.getMethod().getName();
}
/**
* 获取注解的参数信息
* @param pjp 切入点信息
* @return 注解信息
*/
private TimeConsume getTimeConsume(JoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
return method.getAnnotation(TimeConsume.class);
}
}
/**
* 根据 spring boot 配置启动是否加载
* @author kevin
* @date 2020/9/4 17:07
* @since 1.0.0
*/
public class TimeConsumeActionConfig {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "time.consume", name = "enable", havingValue = "true")
public TimeConsumeAction timeConsumeAction() {
return new TimeConsumeAction();
}
}
public class TimeConsumeStopWatch extends StopWatch {
/**
* 无参数构造,让父类处理
*/
public TimeConsumeStopWatch() {
super();
}
/**
* 有参数构造,让父类处理
* @param id 计时器名称
*/
public TimeConsumeStopWatch(String id) {
super(id);
}
@Override
public String shortSummary() {
return "'" + this.getId() + "': " + this.getTotalTimeMillis() /*/ 1000000*/ + "ms";
}
}
使用:
1、spring boot最好放到@SpringBoot启动类上,添加@EnableTimeConsume、或者让Spring 可以扫描到
2、在需要的方法上添加 @TimeConsume注解,可以自定义名称默认使用 类名 + 方法名(如:'AAService#getaa': 17ms ; 'ABService#getabc': 13ms ; 'AAAService#process': 945ms ;)
3、自己保证方法被AOP切中,可以参考(SpringAop源码(七)- 嵌套调用问题分析与解决)