数据结构 - 栈

目录

栈的特点

栈的使用场景

1)、浏览器的前进后退功能

2)、LeetCode20 有效的括号

3)、LeetCode232 用栈实现队列

4)、函数调用栈

5)、算术表达式求值

6)、自己实现的接口性能耗时


栈的特点

            

    栈的数据特点是先进后出(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源码(七)- 嵌套调用问题分析与解决

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值