算法学习 (门徒计划)1-3 递归与栈 学习笔记

算法学习 (门徒计划)1-3 递归与栈 学习笔记

  • 前言
  • 递归与栈
    • 栈(Stack)
      • 栈的基本概念
      • 栈的基本操作
    • 栈的概念和应用
      • 例题:括号匹配 leetcode—20
        • 解题思路
        • 示例代码
      • 解题后的思维衍生
      • 例题:基本计算器 leetcode—224
        • 解题思路
        • 示例代码
      • 继续课程的内容
      • 栈的应用场景(简述线程池)
      • 例题 表达式求值
        • 解题思路
          • 重点:双栈法
  • 经典例题-栈的基本操作
    • 面试题03.04 化栈为队
      • 解题思路
    • leetcode-682
      • 解题思路
    • leetcode-844. 比较含退格的字符串
      • 解题思路
    • leetcode-946. 验证栈序列
      • 解题思路
      • 示例代码
  • 经典例题-栈结构扩展应用
    • leecode-20 题有效的括号(已经做过)
    • leetcode-1021 删除最外层的括号
      • 解题思路
      • 示例代码
    • leetcode-1249 移除无效的括号
      • 解题思路
      • 示例代码
    • leetcode-145. 二叉树的后序遍历
      • 解题思路
      • 示例代码
    • leetcode-331. 验证二叉树的前序序列化
      • 解题思路
      • 示例代码
    • leetcode-227. 基本计算器 II
      • 例题:基本计算器 leetcode—224
        • 解题思路 (双栈法)
  • (插入自身思想)调整计划
    • leetcode-636. 函数的独占时间
      • 解题思路
    • leetcode-1124. 表现良好的最长时间段
  • 彩蛋习题
  • 总结

前言

3月18日,开课吧门徒计划算法课第三讲学习笔记。
本课讲递归和栈。
课题为:
1-3 递归与栈(Stack):解决表达式求值
(由于自身精力问题,本次只记录重点和次重点,尽量给算法题目配图)

来自3月30日
(学习完毕,记录过于详细了,并且没有多余精力为算法配图,下次需要改进)

递归与栈

栈(Stack)

栈的基本概念

栈是一种基础的线性数据存储结构
栈可以想象成一个网球桶,最先放进去的球最后取出来。
栈的概念配图:
算法学习 (门徒计划)1-3 递归与栈 学习笔记_第1张图片

栈的基本操作

对于入栈的概念:
栈顶指针加1,存入目标元素
对于出栈的概念:
栈顶指针减1,提取目标元素(提取,等效于剪贴,先复制,再删除原始值)

(概念仅做参考实际使用时可以变通)

进栈,出栈,判空,元素遍历,长度获取演示代码

public static void test() throws Exception {
    	//以栈内数据结构是字符串类型为例子
    	//下方定义的这个栈无长度限制
    	Stack <String> stack = new Stack <String>();
    	//入栈
    	String in = stack.push("123");   //返回的是入栈的内容
    	boolean runRes = stack.add("45");//返回的是true或false
    	//在这种情况下不需要关注返回值
    	System.out.println(in);
    	System.out.println(runRes);
    	
    	System.out.println(".....");
    	//遍历栈内元素,遍历的方式为从栈底向栈顶遍历
    	for(String str: stack) {
    		System.out.println(str);
    	}
    	System.out.println(".....");
    	
    	//出栈
    	String out = stack.pop();  //输出并移出栈顶元素
    	String out2 = stack.peek();//仅输出栈顶元素
    	
    	//第一次出栈删除元素,第二次出栈不删除元素仅读取
    	System.out.println(out);
    	System.out.println(out2);
    	
    	//判断是否为空
    	boolean isEmpty=  stack.isEmpty();
    	System.out.println(isEmpty);
    	
    	//获取栈长度(压入2个元素,移出了一个)
    	Integer len = stack.size();
    	System.out.println(len);
    }

打印如下:

123
true

123
45

45
123
false
1

栈的概念和应用

栈及相关概念常用于处理需要进行层次处理的场景。(队列常用于需要顺次处理的场景)

递归是栈的一种使用方式,因为每一层深入的递归都是新的层次,而对于上层而言都是确实是视作一个整体

举一个例子,有效括号的判断

(课上讲的例题是下方例题的简化版本,不完全相同)

例题:括号匹配 leetcode—20

来源:括号匹配 leetcode—20

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。

解题思路

先不用栈,先考虑这个问题怎么从逻辑角度处理,这个问题显然是一个层次结构问题,在假设所有括号都正确闭合的情况下,那么任意一对闭合的括号内部区域也是可以正确闭合的,因此当发现某一个左括号后,就意味着期望在右侧合理的位置上发现一个右括号

由于括号的种类并不相同因此不能只用一个数字来做加减,这个表示左括号数字的字符除了要表示括号的数量还需要表示括号的种类,还需要表示括号的出现顺序,因此可以指定一个字符类型数组,通过移动指针的方式来为括号进行匹配,如果遇到左括号就存入数组,并且指针右移,如果遇到右括号就取得数组最右端的内容如果相同就,指针左移一位,并清除,此时如果不相同就反馈为失败。最终如果所有数组指针没有归位,或者数组在归位时,遇到了取的要求,也判断失败。

(那么这种数组像什么?)

(像栈)

因此用栈来实现,遇到左括号入栈,右括号出栈,出栈前比较是否和存入的匹配。其余同理。最后判断某一次不匹配或者栈为空无法出栈或者结束时栈不为空,都为失败,其余为成功。

(记录用栈的解题思路)

(课上的例题括号类型相同因此可以考虑不用栈,改为一个整形变量存储栈的长度,这是因为括号类型相同不需要存储括号种类所以只需要进行出入栈动作,此时只和长度有关因此可以用一个变量代替栈这个对象)

(课上先讲了简单的题目用于活跃大脑,但是我没有必要记录因此直接解题)

示例代码

(以下我对于例题20的解法,但是性能不佳,先记录答案)

class Solution {
    public boolean isValid(String s) {
        //去除空格
        s=s.replaceAll(" +","");
        System.out.println(s);      //test
        char [] sc = s.toCharArray();

        Stack <Character> stack = new Stack <Character>();

        for(int i=0;i<sc.length;i++){
            if( isleft_1(sc[i])||
                isleft_2(sc[i])||
                isleft_3(sc[i])){
                stack.add(sc[i]);
            }else{
                if(!stack.isEmpty()){
                    Character c = stack.pop();
                    if(!chickBracket(c,sc[i]))
                        return false;
                }else
                    return false;
                
            }
        }
        if(!stack.isEmpty())
            return false;
        return true;
    }

    private boolean isleft_1(char c){
        if(c=='(')
            return true;
        else
            return false;
    }
    private boolean isleft_2(char c){
        if(c=='[')
            return true;
        else
            return false;
    }
    private boolean isleft_3(char c){
        if(c=='{')
            return true;
        else
            return false;
    }
    private boolean chickBracket(char c,char nsc){
        switch(nsc){
            case ')': 
                if(isleft_1(c))
                    return true;
                else
                    return false;

            case ']':
                if(isleft_2(c))
                    return true;
                else
                    return false;

            case '}':
                if(isleft_3(c))
                    return true;
                else
                    return false;

            default: 
            return false;
        }
    }
}

(下方是一个更为优秀的解法,和我的空间复杂度近似但是时间远胜于我)

这个方案通过使用map键值对的方式快速的进行了左右括号的匹配,性能远胜于我的用逻辑语句分析判断的性能,值得学习。

class Solution {
    private static final Map<Character,Character> map = new HashMap<Character,Character>(){{
        put('{','}'); put('[',']'); put('(',')'); put('?','?');
    }};
    public boolean isValid(String s) {
        if(s.length() > 0 && !map.containsKey(s.charAt(0))) return false;
        LinkedList<Character> stack = new LinkedList<Character>() {{ add('?'); }};
        for(Character c : s.toCharArray()){
            if(map.containsKey(c)) stack.addLast(c);
            else if(map.get(stack.removeLast()) != c) return false;
        }
        return stack.size() == 1;
    }
}

解题后的思维衍生

课上讲完题目后拓展了一下题目的思维,括号序列,本身也是一个层级结构,在试图执行左括号(理解为某一个函数的调用)时需要找到右括号,并内部的括号需要成对(理解为某一个函数内部有很多子函数),因此括号序列就和项目中的各种模块的代码很像,括号序列对于左右匹配用栈来实现,项目中不同函数的顺次排布或者是嵌套内在也是用栈来实现的

(如何用栈实现的就暂时不记录了,后续如果没忘记再记录一下)

这道题目的意义:不在于实际的知识点在于看待问题的思维方式

(关于这种思维方式,我认为我现在是掌握了,下一次看的时候我还能掌握吗)

最后对于这道题目做出了一个总结:

栈,可以处理具有完全包含关系的问题

(对此我的理解是:栈,可以用于处理有层次结构的问题,不知道和课上讲的内容是否一致)
(层次结构,就是包含了顺次执行的动作位,和每个动作作为整体依然可以有自身内在的动作位)

对此我自己举一个例子:

举一个例子:数学算式求值。

在数学运算式中不同的运算符号有不同的优先级,并且还有括号可以实现约定更高的优先级,每一次都需要先进行高优先级的计算,再将结果去参与下一轮运算。

例题:基本计算器 leetcode—224

来源:基本计算器 leetcode—224

(这是一道困难的题目,我自行解一遍,限时2h)

题目描述:给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

示例:

示例 1:

输入:s = “1 + 1”
输出:2
示例 2:

输入:s = " 2-1 + 2 "
输出:3
示例 3:

输入:s = “(1+(4+5+2)-3)+(6+8)”
输出:23

提示:

  • 1 <= s.length <= 3 * 105
  • s 由数字、’+’、’-’、’(’、’)’、和 ’ ’ 组成
  • s 表示一个有效的表达式

解题思路

首先, 没有乘法和除法,这题还是简单的,对于整个式子复杂的地方主要在于括号的管理。

那么拆分所有元素可以获取到的信息为,值,运算符,括号

从左到右依次计算,每一组都理解为值和运算符,当遇到括号时提取括号内字符串的整体,将整个内容作为一个s使用计算器进行计算,将返回值作为合成值和运算符。

那么在哪里使用栈的思想呢?
(主要是想利用一下刚学的知识)
在进行括号对的判断上,可以利用栈的思想,遇到左括号入栈,遇到右括号出栈。并且整体对于自身函数的调用(递归)就是栈的思想。

(此处我对于栈的使用时错误的,并且也没有找到解决方案最终我采用了递归方案进行求解)

(解出来了,但是性能很差,解题用了117分钟)

示例代码

(一套性能不好的方案,没有使用栈:)



class Solution {
    static final char add = '+';
    static final char subtraction = '-';
    static final char leftBracket= '(';
    static final char rightBracket = ')';  
    public int calculate(String s) {
        s=s.replaceAll(" +","");
        System.out.println(s);      //test
        char [] sc = s.toCharArray();
        return myCalculate(sc,0,sc.length);
    }
    private int myCalculate(char [] sc,int start,int end) {

        int getFormula = 0;           
        //0表示未定义
        //1表示正在试图获取值   
        //2表示正在试图获取运算符
        //3表示正在试图获取值
        //3如果结束就进行一次运算
        //当遇到括号时另外进行考虑
        //Stack  bracket = new Stack ();
        int leftNum = 0;
        int rightNum = 0;
        boolean isAdd = false;

        for(int i = start;i<end;i++){
            if(getFormula==0){ //此时在试图寻找算式的左侧
                Integer num = isNum(sc[i]);

                System.out.println(num);        //test

                if(num!=null){
                    leftNum = num;
                    getFormula = 1;             //找到值尝试获取左值
                }else if(sc[i] == subtraction){
                    getFormula = 3;             //直接遇到负号认为左值为0且进行减法
                    isAdd = false;
                }else if (sc[i] == leftBracket){
                    int nowIndex = i+1;
                    int brackets = 0x1;
                    int renum = 0;
                    for(i++;i<end;i++){
                        if(sc[i] == leftBracket)
                            ++brackets;
                        if(sc[i] == rightBracket){
                            --brackets;
                            if(brackets == 0){
                                renum  = myCalculate  (sc,nowIndex,i);
                                break;
                            }
                        }   
                    }
                    leftNum = renum;
                    getFormula = 2;     //尝试获取运算符
                }else{
                    i--;
                    getFormula = 2;     //尝试获取运算符
                }
            }else if(getFormula==1){
                Integer num = isNum(sc[i]);
                if(num!=null){
                    leftNum = leftNum*10;
                    leftNum +=num;
                }else{
                    i--;
                    getFormula = 2;             //值获取完毕尝试获取运算符
                }                
            }else if(getFormula==2){
                getFormula = 3;                 
                if(sc[i] == subtraction)
                    isAdd = false;
                else
                    isAdd = true;
            }else if(getFormula==3){            //尝试获取右值
                Integer num = isNum(sc[i]);

                System.out.println(num);        //test

                if(num!=null){
                    rightNum = rightNum*10;
                    rightNum +=num;
                }else if (sc[i] == leftBracket){
                    int nowIndex = i+1;
                    int brackets = 0x1;
                    int renum = 0;
                    for(i++;i<end;i++){
                        if(sc[i] == leftBracket)
                            ++brackets;
                        if(sc[i] == rightBracket){
                            --brackets;
                            if(brackets == 0){
                                renum  = myCalculate  (sc,nowIndex,i);
                                break;
                            }
                        }   
                    }
                    rightNum = renum;
                    getFormula = 4;     //获取完毕进行计算
                }else{
                    getFormula = 4;     //获取完毕进行计算
                    //指针退回一位,尝试获取运算符
                    i--;
                }
            }
            //如果有必要进行计算
            if(getFormula == 4){
                if(isAdd)
                    leftNum = leftNum + rightNum;
                else
                    leftNum = leftNum - rightNum;

                getFormula = 2; //准备获取运算符
                rightNum = 0;
            }
        }
        if(getFormula ==3){ //并未发现结尾,说明已经到达结尾
            if(isAdd)
                leftNum = leftNum + rightNum;
            else
                leftNum = leftNum - rightNum;
        }


        System.out.println(getFormula);
        System.out.println(rightNum);
        
        return leftNum;
    }

    private Integer isNum(char c){
        if(c>='0'&&c<='9')
            return c-'0';
        else
            return null;
    }

}

继续课程的内容

(接下来课程讲了一下栈的实现,略过这一阶段)
(关于栈的思想,我在解题20时,描述过,并未有什么复杂度,而对于实现我没有兴趣做演示,因此略过这一段,但是要求一点,如果要我设计一个栈结构,我需要立刻能设计出来

栈的应用场景(简述线程池)

栈的概念讲完了接下来讲栈的应用场景

系统的线程池中,栈常和任务队列配合解决有层次结构的问题。
(任务队列负责来任务放入线程空间,栈负责层级切分任务,负责函数间的调用关系

栈常用于存储函数中的局部变量,当一部分函数结束时,删除其内需要持有的变量值,符合先进后出的理念
(用之前讲的栈的思维去理解)

因此线程空间也被称为线程栈
(线程空间解决问题的方式是栈)

在系统中线程生成时开辟的线程栈是有默认大小的,为8mb
(详细的本次略过)

因此对于有限存储空间,就会发生超限的可能性,这种可能性发生时的现象叫做:爆栈
(栈的溢出)

常见的爆栈是线程池的递归爆栈。
(使用空间过多超出了划分的范围)

回顾一个概念:多线程编程
当进行多线程编程时,对于系统会有额外的负担。因为一个线程默认就是8mb的空间,线程越多空间需求越多
(同时需要使用的空间增加了)

例题 表达式求值

关于这道题目用递归的解法略,前面我找了道题目224,用递归解完了,
接下来记录用双栈的解法

例题我打算采用上面选的224

(下方记录课上的思路)
(我的思路用递归解决了,但是想不用栈怎么解决,所以记录课上的思路很重要)

解题思路

首先关于表达式的计算,一个表达式都可以被理解为是两个运算单元和一个运算符的结合

(这部分和我的想法一致)
(但是课上对于这个表达式的理解没有采用横向排布而是采用了树形的立体排布)

(到这一步反思我是怎么思考的,我采用了横向排布,将这个需要合成的对象视作一个整体算式用我定义的一个计算表达式的方法去计算)
(所以我对于表示的切分不够具体,不够统筹)

当出现层次结构比如有乘法这种有更高优先级运算能力的方式或者是括号定义更高优先级运算能力的方式

因此接下来关键的步骤就是确定优先级最低的运算符的位置
(到这里跟我的解法已经不一致了)

于是提出了一个递归式的表达式求解方法,先获得优先级最低的运算符的位置,然后把原始的表达式拆成2部分,左式和右式。然后每一个式子再进行表达式求值

(也可以理解为拆成,左,运算符,右,3部分,看怎么分配)

关于这个解法,最关键的内容就是提供优先级判断的方式,因此定义括号内的算数优先级为根据括号的层数附加优先级,而基础的算数优先级自身带有基础的权重值,例如一层括号加权重100,而基础的乘除法优先级为2,加减法优先级为1。

(这种解题的方式就是层级的轮次解题了,首先获得各个单元的元素,然后再为其中的部分元素附加优先级)

完成这一步后,就可以将原始的式子拆解3份进行递归计算(左,符号,右,这种就是利用了二叉树的思想)

(接下来的方式我也会,问题在于怎么进行权重的记录和判断呢,怎么最简单的提取数字进行元素和值的管理呢)

(运算符好处理,进行字符判断即可,如果遇到左括号则接下来所有运算符优先级加一重权重,如果是右括号则减一重权重

(数字可以使用库函数进行读取,也可以自行设计函数进行读取,读取完毕后封装一个数组)
(运算符的优先级同样封装一个数组)

此时课上对于优先级的计算方式不是进行封装的而是每一个轮次用遍历的方式对于期望提取运算符的单元组进行重新获取最小优先级,如果是相同优先级取最后一个再进行切分(或者选择第一个,相同优先级不需要考虑先后,这是数学的分配率

(至此我明确了课上的解题思路比我最初的方案更为优秀,同样是递归的指针想向下传递,这种解决的思路更具有层次性,但是这种思路目前还没有根本上的区别)

(我的方案是顺次计算和结果作为左值去寻找右值计算遇到括号则是视为整体进行递归,而课上的解法是先切分,递归切分至最小单元再参与计算,显然课上的思维更为先进,但是代码我就不写了没有必要)

这套方案更有优秀的部分就是可以直接合成数字并且可以解决负数的问题,合成数字的方案为字符串转数字,合成负数的方式为理解为0减去某值,0来自于0或者0长度字符串。

重点:双栈法

(接下来就应该将如何用双栈解这道题目了。重点)

(先没讲,在例题227,课时4:12:00的时候讲了)

经典例题-栈的基本操作

面试题03.04 化栈为队

来源:化栈为队 leetcode—面试题03.04

实现一个MyQueue类,该类用两个栈来实现一个队列。

示例:

MyQueue queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek();  // 返回 1
queue.pop();   // 返回 1
queue.empty(); // 返回 false

说明:

你只能使用标准的栈操作 – 也就是只有 push to top, peek/pop from top, size 和 is empty 操作是合法的。
你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。

解题思路

(我先来解一下)

这题很有意思,用两个栈实现一个队列的效果。
那么首先复习一下队列,队列的特征是先进先出,栈的特征是先进后出。

因为只能使用栈的存储结构,所以数据必须存在一个或者2个栈里,那么假设有一个栈内存放了3个数据单元,此时需要提取最先存放的一个单元怎么做?那么只能出栈两次将栈底的元素提取出来,那么之前出栈的元素去哪呢。当然是存入另一个栈中了,但是提取完毕后需要将栈内的元素进行复原。

对此我已经提出解决方案,对于任一个时刻所需要进行新数据加入都是加入栈顶,对于旧数据的提取都是获取栈底,栈底元素想要获取只能使用出栈,而那些提前出栈的元素就存入另一个栈,等提取完毕后,再复原。

(本题主要难在思路,方案可行后,就看课上有没有更好的方案,如果没有,那我就不写代码了)

(课上的方案更好,我想复杂了,没有必要进行复原,只需要在需要提取元素时,如果用于出元素的栈为空,就把入元素的栈导入出元素的栈)

(我在设计的时候还在试图维护原始栈的形态,但是对外部整体而言,原始栈的维护是没有必要的,和课上的方案一比我的两个方案都显得不足,但是整体差别不大就不写代码加快进度了)

(但是这题更重要的是一种封装的思维方式,将一些复杂变化封装为整体,使得对外表现变得简单)

leetcode-682

来源:leetcode-682 棒球比赛

(简单题目,如果是简单题目除非我明显不足,否则我一律略写代码)

你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。

比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:

  1. 整数 x - 表示本回合新获得分数 x
  2. “+” - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
  3. “D” - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
  4. “C” - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。

请你返回记录中所有得分的总和。

解题思路

(先记录我的想法)

非常简单,分为2步顺次进行

一步是依次的进行分数入栈操作,一步是依次的进行分值累加。
当遇到操作符时进行出栈,并将操作结果入栈作为本回合的分数,

(这里我存着一个分歧,是否需要将结果入栈呢?也就是分数有效的情况下本回合得分需要作为有效分数吗)

(对于这一点代码里只需要进行测试即可)

当得分无效时,理解为本回合得到负分数,并且结果不入栈。

最终返回分值的累加。

(看看课上的思路是什么如果没有必要则依然不写代码)

(课上将结果入栈了)

(我设计时候漏了一个情况如果提取了2个元素,则第一个提取的元素需要再入栈,这是可以想到的,但是我忘记了,因为我初期的设计是一个数组用指针进行数据管控)

(综上代码略写)

leetcode-844. 比较含退格的字符串

来源:leetcode-844. 比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:S = “ab#c”, T = “ad#c”
输出:true
解释:S 和 T 都会变成 “ac”。

解题思路

(非常简单,答案略写)

设计两个栈,进行字符的入栈,当遇到退格时改为出栈一个元素
最后全部入栈完毕后,比较两个栈内元素是否相等即可

leetcode-946. 验证栈序列

来源:leetcode-946. 验证栈序列

给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。

示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:

push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

解题思路

(本题为中等题目,并且我觉得有意思因此写一下代码)

本题是一个入栈序列和一个出栈序列,需要比较的是如果按入栈序列入栈是否可以按出栈序列出栈。

解题关键在于出栈的动作,根据出栈的动作和入栈序列去猜想站内的元素

比如某一个元素出栈则这个元素在入栈序列的前的所有元素都需要入栈,此时栈内就已经有元素了,然后尝试进行下一个元素的出栈,依然是要求这个元素前的所有元素进行入栈,如果出现了矛盾比如这个元素无法出栈(已经在更深的位置),那么就认为这个序列不可行。

最终当出栈的序列全部完成出栈动作且没有矛盾时,认为可行。

但是我猜想本题可以允许出栈序列为空的情况,也就是不进行出栈

(我的思路设计完毕,尝试解题)

(看了一遍课上的思路跟我的区别在于先判断栈顶元素,再考虑是否入栈,的确是性能更高的方案,但是我认为对我没有重写的必要)

示例代码

(我的方案如下)

class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        if(popped.length==0)
            return true;
        Stack <Integer> stack = new Stack<Integer>();


        for(int i = 0,push_i=0;i<popped.length;i++){
            //需要出栈的元素
            int p = popped[i];
            boolean getDataFlag = false;

            for(int j=push_i ;j<pushed.length;j++){
                //找到元素了
                if(pushed[j] == p){
                    //找到本次出栈的元素并且将其他提前入栈的元素按顺序存入
                    getDataFlag = true;
                    for(int k=push_i;k<j;k++)
                        stack.add(pushed[k]);
                    //标记已经入栈的元素
                    push_i = j+1;
                    break;
                }
            }
            if(getDataFlag){
                //想要的元素入栈再出栈,本次出栈成功
                continue;
            }else{
                Integer data = stack.pop();
                if(data!=null &&data == p){
                    //出栈得到想要的元素,则本次出栈成功
                }else
                    return false;
            }

        }
        //通过了所有判断
        return true;
    }
}

经典例题-栈结构扩展应用

leecode-20 题有效的括号(已经做过)

上文做了,跳过不写

leetcode-1021 删除最外层的括号

来源:leetcode-1021 删除最外层的括号

有效括号字符串为空 ("")、"(" + A + “)” 或 A + B,其中 A 和 B 都是有效的括号字符串,+ 代表字符串的连接。例如,"","()","(())()" 和 “(()(()))” 都是有效的括号字符串。

如果有效字符串 S 非空,且不存在将其拆分为 S = A+B 的方法,我们称其为原语(primitive),其中 A 和 B 都是非空有效括号字符串。

给出一个非空有效字符串 S,考虑将其进行原语化分解,使得:S = P_1 + P_2 + … + P_k,其中 P_i 是有效括号字符串原语。

对 S 进行原语化分解,删除分解中每个原语字符串的最外层括号,返回 S 。

解题思路

(简单题,如果想明白了就不写代码了)

(想不太明白,写一遍代码,思路如下)

首先输入的目标是一个S,那么就需要根据括号组进行分解合并成由若干括号横向组合的字串组

然后再将这些括号组拆分掉最外侧的括号,也就是删除最左和最右的字符,

最后再将拆除完毕的括号组合并,输出

因此本题的关键在于如何拆分成有效括号组,这很简单,进行左右长度的累计即可

(想明白了,但是代码还是写一下)

(后续我写完了再想了一下,还有一个方案为当记录的括号对数目为1时就进行截取,这样可能会性能更好因为可以比我少一重循环,但是比较简单就不记录)

示例代码

(我的第一套方案,性能还是不太好,但是逻辑还是清晰稳定的)

class Solution {
    public String removeOuterParentheses(String S) {
        char [] sc = S.toCharArray();

        //存放源语的索引
        ArrayList <Brackets> brackets = new ArrayList <Brackets> ();

        int bracketTemp = 0;
        //单个源语对象
        Brackets bracket  = new  Brackets();
        for(int i=0;i<sc.length;i++){
            if(bracketTemp!=0){
                if(sc[i]==')'){
                    bracketTemp --;
                }else{
                    bracketTemp ++;
                }
                //括号闭合存入对象
                if(bracketTemp==0){
                    bracket.right_i = i;
                    brackets.add(bracket);
                }
            }else{
                if(sc[i]==')'){
                    bracketTemp --;
                }else{
                    bracketTemp ++;
                }
                //首次左括号创建新对象
                if(bracketTemp == 1){
                    bracket = new  Brackets();
                    bracket.left_i = i;
                }
            }
        }

        String res = "";
        for(int i= 0;i<brackets.size();i++){
            bracket = brackets.get(i);
            //截取输出
            res += S.substring(bracket.left_i+1, bracket.right_i);
        }
        return res;
    }


    class Brackets {
        int left_i;
        int right_i;
    }
}

leetcode-1249 移除无效的括号

来源:leetcode-1249 移除无效的括号

给你一个由 ‘(’、’)’ 和小写字母组成的字符串 s。

你需要从字符串中删除最少数目的 ‘(’ 或者 ‘)’ (可以删除任意位置的括号),使得剩下的「括号字符串」有效。

请返回任意一个合法字符串。

有效「括号字符串」应当符合以下 任意一条 要求:

  • 空字符串或只包含小写字母的字符串
  • 可以被写作 AB(A 连接 B)的字符串,其中 A 和 B 都是有效「括号字符串」
  • 可以被写作 (A) 的字符串,其中 A 是一个有效的「括号字符串」

示例 1:

输入:s = “lee(t©o)de)”
输出:“lee(t©o)de”
解释:“lee(t(co)de)” , “lee(t©ode)” 也是一个可行答案。

解题思路

(本题为中等题目,尝试做一下)

本题并不复杂核心要求是对于字符串进行左右括号的平衡,因为括号种类相同所以没有必要使用栈来解题,只需要使用栈的长度的概念。

对于括号平衡的方式为进行删减也就是如果我统计了所有的括号数目,最终得出哪些括号不可成对,在这种情况下删除这些括号足以。

(这里我感觉困惑,是否有思考的漏洞不得而知)

进一步分析后得知,关键在于两侧的括号不可通过删除内侧的括号来实现平衡,因此先删除外侧的不可成对的括号,然后对于内部的括号进行统筹,先删除成对时右侧不可成对的括号,再删除成对时左侧不可成对的括号,这套方案需要来回遍历4次。

可以简化一下,直接进行完整的统筹,直接删除不可能成对的括号(试图成对时计算结果为负数的括号)这样只需要来回遍历2次,

示例代码

(这次我的性能不错,很好)

class Solution {
    public String minRemoveToMakeValid(String s) {
        char [] sc = s.toCharArray();

        boolean  [] errBracket = new boolean [sc.length] ;

        int temp = 0;

        for(int i=0;i<sc.length;i++){
            if(!errBracket[i]){
                if(sc[i]==')'){
                    temp--;
                }else if(sc[i]=='('){
                    temp++;
                }
            }

            if(temp<0){
                temp = 0;
                errBracket[i] = true;
            }   
        }

        temp = 0;     
        for(int i=sc.length-1;i>=0;i--){
            if(!errBracket[i]){
                if(sc[i]==')'){
                    temp--;
                }else if(sc[i]=='('){
                    temp++;
                }
            }

            if(temp>0){
                temp = 0;
                errBracket[i] = true;
            }   
        }



        StringBuilder sb = new StringBuilder();
        for(int i=0;i<sc.length;i++){
            if(!errBracket[i]){
                sb.append(sc[i]);
            }
        }

        return sb.toString();
    }
}

leetcode-145. 二叉树的后序遍历

来源:leetcode-145. 二叉树的后序遍历

给定一个二叉树,返回它的 后序 遍历。

示例:

算法学习 (门徒计划)1-3 递归与栈 学习笔记_第2张图片

输出: [3,2,1]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?

解题思路

(中等难度,并且必须使用迭代,做一下)

首先关于这个题目,需要明白什么是二叉树的后续遍历

(我也忘了,记录一下:首先遍历左子树,然后遍历右子树,最后遍历访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点。)

也就是说对于期望获得的这个二叉树对象,先进行左侧的遍历再进行右侧的遍历,最后获取到根节点。的确,甩递归非常简单,对于每一个对象进行递归即可。

对此要明确,虽然不使用递归但是可以使用栈,递归本质上也是栈,既然是栈那就简单了,首先将打算后遍历的元素压入栈,持续压入,直到当前元素是最先遍历的元素为止,然后进行出栈,出栈后这个元素也是有先后关系的,再持续将元素入栈直到最后的元素是需要最先遍历的元素。

但是需要注意出栈后的元素是可能有左右支的,因此判断时会试图再次入栈,所以,入栈前,需要拆分当前节点的左右支关联关系。

如此就是迭代,思路设计完毕,开始解题。

(但是这个思路有缺陷,需要破坏原本的二叉树

(解完后看看课上的思维是否和我的一致)

(以下是课上的思维)

首先,讨论下后序遍历的概念为:先左再右最后根。
然后,讨论以下中序遍历的概念:先左再根最后右

(这部分只是用于了解这个概念,跟题目无关)

接着,讨论一下这道题目用递归的解法,用递归的解法引申出栈的概念

(的确如此)

(到这里和我的方法一样,接下来重点看,怎么解决某个元素的下位节点都被遍历的情况,我的方案是拆解二叉树使得遍历过的节点没有下节点,递归的方案是用逻辑顺序使得遍历的节点已经考虑过下节点)

递归的方案底层也是一个栈,所以目标是采用栈的方式模拟递归的写法,接下来会讲一个通用的办法来模拟系统栈

(模拟系统栈,值得研究一下)

首先这个系统栈内部就要存相关节点的地址,然后准备一个逻辑栈用于存储当前节点的动作位。

(原来如此,对于节点栈我有进行设计,但是逻辑栈我没有设计,我通过对原始栈的拆解来实现这个逻辑功能,所以这种方式需要更多的内存空间但是可以保留原始的数据结构)

(我明白这部分想法了,看看课接下来讲的是否跟我的想法一致)

这种方式就是一种不错的技巧一个栈存数据信息,一个栈存动作信息

(确实不错)

依次的根据逻辑去入栈和出栈,定义动作信息为试图执行某个动作,注意这个动作不是必须执行的,而是试图执行,动作定义为0试图获取左支,1为试图获取右支,2为获取本节点的值。

(这部分跟我的想法不一致,我是对于每一个节点都进行左右中的入栈动作,然后试图输出栈顶值,因此需要解决每一个节点重复入栈需求,而这个方案为明确每个节点需求的执行情况,并且根据需求的运行结果进行处理,详细的差别看下方的示例代码)

(当然代码都是自己写的,我只借鉴思想)

(这套方案的提出核心在于对递归函数的理解,那么我的方案的缺陷是对于递归的不理解吗?我认为不是,而是我没有想到用额外的空间去保存动作位

(对此其实有第三种写法,我的思路可以进一步优化用动作栈来管理某一个节点是否需要被进一步遍历的问题,但是我不写)

示例代码

(以下是我的思路,性能还可以)

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {

        ArrayList <Integer> resList = new ArrayList <Integer>();

        Stack <TreeNode> stack = new Stack <TreeNode>();
        
        if(root==null)
            return resList;

        stack.add(root);

        while(!stack.isEmpty()){
            //获取当前的二叉树对象,并取得左右枝干
            TreeNode nowRoot = stack.peek();
            TreeNode left = nowRoot.left;
            TreeNode right = nowRoot.right;

            //拆解二叉树,防止重复处理
            nowRoot.left = null;
            nowRoot.right = null;

            int endFlag = 0;
            //先不遍历右枝干
            if(right!=null){
                //先入栈
                stack.add(right);
            }else{
                endFlag ++;
            }

            //先遍历左枝干
            if(left!=null){
                //遍历前先入栈
                stack.add(left);
            }else{
                endFlag ++;
            }
            //本节点无下属节点,可以进行提取了
            if( endFlag == 2){
                resList.add(nowRoot.val);
                stack.pop(); //删除这个节点
            }               
        }
        return resList;
    }
}

(以下是课上的思路,性能和我的一样)

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {

        ArrayList <Integer> resList = new ArrayList <Integer>();

        Stack <TreeNode> stack = new Stack <TreeNode>();
        Stack <Integer> state_stack = new Stack <Integer>();
        //定义一下0表示试图获得左、1表示试图右,2表示获取完毕返回根

        if(root==null)
            return resList;

        stack.add(root);
        state_stack.add(0);

        while(!stack.isEmpty()){
            //获取当前的二叉树对象,并取得左右枝干
            TreeNode nowRoot = stack.peek();
            TreeNode left = nowRoot.left;
            TreeNode right = nowRoot.right;

            int state = state_stack.pop();
            switch(state){
                case 0:
                    state_stack.add(state+1);
                    //先遍历左枝干
                    if(left!=null){
                        //遍历前先入栈
                        stack.add(left);
                        state_stack.add(0);
                    }else{
                        //左为空,试图遍历右
                    }
                break;
                case 1:
                    state_stack.add(state+1);
                    if(right!=null){
                        //先入栈
                        stack.add(right);
                        state_stack.add(0);
                    }else{
                        //右为空,可以返回根
                    }
                break;
                case 2:
                    //返回根
                    resList.add(nowRoot.val);
                    stack.pop(); //删除这个节点
                break;
            }
        }
        return resList;
    }
}

leetcode-331. 验证二叉树的前序序列化

来源:leetcode-331. 验证二叉树的前序序列化

序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。


     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #

例如,上面的二叉树可以被序列化为字符串 “9,3,4,#,#,1,#,#,2,#,6,#,#”,其中 # 代表一个空节点。

给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。

每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。

你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 “1,3” 。

示例 1:

输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true

示例 2:

输入: "1,#"
输出: false

解题思路

(中等题目,做一下)

首先本题很有意思,依然是如果能重构二叉树就能很轻易的获得答案,但是不允许进行重构,对此根据上一题的思路,假设重构了二叉树怎么解。

如果重构了二叉树,那就是当成在前序遍历的样子依次获得左右枝干。

所以就是要模拟一个二叉树,模拟的方案类似与构建二叉树

因为二叉树是有层次结构的所以解决这个问题也需要用栈进行配合,持续的进行入栈,并记录当前的动作位,每一个数字都是二叉树的根,每一个数的下一个数都是上一个二叉树的左支,一直保持这个状态,直到遇到空,空以后的下一个数就是上一个二叉树的右支,但是并且本身也是下一个二叉树的根,因此继续往下时还是认为是左支,除非为空

如果右侧遇到空就说明上一个二叉树节点已经到达末端,需要获取再上一个(按着我的逻辑空也当做节点只不过是末端节点)

因此设计代码准备两个栈,或者一个栈也行,关键是要有动作栈用于获取上一个动作位,如果上一个动作位是求左则试图做,如果上一个动作位是求右则试图右,但是获得时都是获得了根,获得根后,先求左再求右。

(因为前序遍历是根,左,右)

所以一个动作栈就够了

(我想明白了,开始写代码)

(写完了花了1小时,中途写错了改了半小时bug)

(接下来看课上的思维)

首先观察和思考这个模型就可以发现,连续的两个#号和前面的一个数字可以构成一个最小二叉树单元,并且不需要向下衍生,所以可以理解为一个#号。
(这需要一点想象力,想想#号不是空的意思而是不需要向下追溯的意思)

如此可以对于原始的数组序列进行多重的合并,每3个合并为一组,最终,如果能够全部合并那么就是合格的模型,如果不能那就是不合格的模型

(好简单啊,我就是想不到这种方案)
(但是我的编程也成功了,只能说多练习,我的思路是从构建二叉树角度分析的,所以我没有错误,只是做的不够好)

那么下一步就是尝试进行阶段性的字符串处理了

(接下来我自己写)

示例代码

(我的方案性能一般)

class Solution {
    public boolean isValidSerialization(String preorder) {
        Stack <Integer> stack = new Stack <Integer>();
        
        //间隔一位有效
        String [] cpreorder =  preorder.split(",");

        //空二叉树
        if(cpreorder.length <= 0||
            (cpreorder.length == 1&&cpreorder[0].equals("#"))
        ){
            return true;
        }
        
        //定义动作位,0表示在寻找左支,1表示在寻找右支
        //确保初始根节点
        if(cpreorder[0].equals("#"))
            return false;
        stack.add(0);
        //开始模拟二叉树
        for(int i=1;i<cpreorder.length;i++){
            //获取动作位
            //追溯为空
            if(stack.isEmpty())   
                return false;
            Integer state = stack.peek();
            if(state!=null){
                //对于末端
                if(cpreorder[i].equals("#")){
                    switch (state){
                        case 0:
                            //改为寻找右支
                            stack.pop();
                            stack.add(1);
                        break;
                        case 1:
                            //回到上一层节点
                            Integer bef_state = stack.pop(); 

                            for(;bef_state==1;)  //对于上一个找右支的诉求持续向上
                            {
                                if(stack.isEmpty()) 
                                    break; 
                                bef_state = stack.pop();
                            }
                            //最终某一个节点原本打算要找左支或者上方没有要求(已经到达初始根)
                            if(bef_state == 0){
                                //改为试图获取右枝
                                stack.add(1);
                            }
                        break;
                    }
                }else{
                    //对于每一个节点都理解为根,然后要寻找左支
                    stack.add(0);
                }                   
            }else
                return false;
        }
        if(stack.isEmpty())
            return true;
        return false;
    }
}

(课上更好的方案)
(写一遍学习一下)
(但是性能更差了,所以我写的还不错)

class Solution {
    public boolean isValidSerialization(String preorder) {
        Stack <Boolean> stack = new Stack <Boolean>();
        
        //间隔一位有效
        String [] cpreorder =  preorder.split(",");

        //获取空标志位,获取末端标志位
        boolean [] endFlags = new boolean [cpreorder.length];

        //转换判断关系减小复杂度
        for(int i=0;i<cpreorder.length;i++){
            if(cpreorder[i].equals("#"))
                endFlags[i] = true;
        }
        
        if(endFlags.length>=2&&endFlags[0]){
            //首位为空
            return false;
        }
        

        //为了方便取数字用数组代替栈
        ArrayList <Boolean> endArr = new ArrayList <Boolean>();

        for(int i=0;i<endFlags.length;i++){
            //stack.add(endFlags[i]);//入栈
            endArr.add(endFlags[i]);//入栈
            int len = endArr.size();
            //当连续为末位时,合并
            while(len>=3&&
            endArr.get(len-1)&&
            endArr.get(len-2)){
                endArr.remove(len-3);
                endArr.remove(len-3);
                endArr.remove(len-3);
                endArr.add(true);
                len = endArr.size();
            }

            if(len == 2&&
            endArr.get(len-1)&&
            endArr.get(len-2)){
                return false;
            }
        }

        if(endArr.size()==1&&endArr.get(0)){
            return true;
        }
        for(boolean b: endFlags)
        System.out.println("ed+"+endArr.size()+b);
        return false;
    }
}

leetcode-227. 基本计算器 II

来源:leetcode-227. 基本计算器 II

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。

(这道题我不做,改为做224,并且把两道题目的难度合在一起)

例题:基本计算器 leetcode—224

来源:基本计算器 leetcode—224

我改一下这道题目改成之前课上讲的概念:

题目描述:给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
(为了降低难度除法用整形除法做,也就是不产生小数)

示例:

示例 1:

输入:s = “1 + 1”
输出:2
示例 2:

输入:s = " 2-1 * 2 "
输出:0
示例 3:

输入:s = “(1+(4+5/2)*3)-(6-8)”
输出:21

提示:

  • 1 <= s.length <= 3 * 105
  • s 由数字、’+’、’-’、’*’、’/’、’(’、’)’、和 ’ ’ 组成
  • s 表示一个有效的表达式

解题思路 (双栈法)

(现在是困难题目了,来练习一下吧,之前用了递归的方案求解)
(我现在要的是栈的方式,通过课上学的内容,自己来写一遍解决方案)
(需要注意的是,目前这个题目已经变化了)

(先记录课上的思维,因为我不会双栈法

首先把表达式拆成2部分,一部分只包含操作数,另一部分只包含运算符。
关于运算符优先级的内容本课学了,如果忘了去前面讲概念的地方看.

但是在拆表达式之前,要先加入括号,这些括号不影响计算而是用于跟着括号进行栈操作,左括号为入栈请求,将数字和运算符入各自的栈,右括号为出栈请求将内容出栈。

(我没有理解)

入栈的方式是这样的,左括号遇到后将接下来的每一个数字和运算符入各自的栈
右括号遇到后从数字栈取2个数字,从运算栈取一个运算符,进行计算,将结果存入数字栈。

(理解了,就相当于取括号内的内容进行计算,而之所以需要加辅助括号就是为了方便出入栈的判断

(用辅助单元将判断对象分割为小单元)

然后讨论了一下这个方法的原理。

(这个方法的原理是切分最小单元并计算,并将结果作为下一次计算的目标)

(那么问题来了,怎么为字符串加括号呢)

括号其实没有加的必要,主要是用于辅助思考的,现在如果不加括号会有什么结果呢,
当不加括号的时候如果遇到低优先级的运算符,就不会进行结果的运算而是先进行入栈了
因此在不主动加括号的情况下,如果遇到低优先级的运算符,需要先进行前一个高优先级的运算符的运算,而如果有括号那就默认接下来所有的优先级都比之前的要高

(我觉得合理,可以不加)

(所以这个方案在实际计算的过程中其实是从右向左运算的?)

(不,可以这么设计,如果下一个运算符的优先级不大于上一个的优先级,就立即进行计算,比如连续的加减法运算先左还是先右没有区别,但是先做更符合数学习惯)

最后当所有元素进行了计算和入栈后,只需要进行对栈内元素进行清除即可,此时理解为遇到了一连串的辅助右括号,需要进行:从数字栈取2个数字,从运算栈取一个运算符,进行计算,将结果存入数字栈。 至于右括号的数量,就看运算符栈的长度即可。

最终数字栈只有一个元素,运算栈为空,这个数字栈最后的元素就是所求的表达式的值

(目前我理解了,之后有空就做一下)

(插入自身思想)调整计划

到目前为止6小时的课程,我已经学完了4小时,但是我至少花费了20个小时的时间进行学习,我没有那么多资源可以供我调配,但是我的记忆力真的很差,但我的时间又没那么多,所以剩下2小时的课程,我要求我2小时学完,而不是再花8小时认真的做整理,那么我应该怎么做,我记录题目如果会做就写思路,如果不会做就认真上课,但是不写代码了,我没有那个时间来学习了。

leetcode-636. 函数的独占时间

来源:leetcode-636. 函数的独占时间

给出一个非抢占单线程CPU的 n 个函数运行日志,找到函数的独占时间。

每个函数都有一个唯一的 Id,从 0 到 n-1,函数可能会递归调用或者被其他函数调用。

日志是具有以下格式的字符串:function_id:start_or_end:timestamp。
例如:
"0:start:0" 表示函数 0 从 0 时刻开始运行。
"0:end:0" 表示函数 0 在 0 时刻结束。

函数的独占时间定义是在该方法中花费的时间,调用其他函数花费的时间不算该函数的独占时间。你需要根据函数的 Id 有序地返回每个函数的独占时间。

示例 1:

输入:
n = 2
logs = 
["0:start:0",
 "1:start:2",
 "1:end:5",
 "0:end:6"]
输出:[3, 4]
说明:
函数 0 在时刻 0 开始,在执行了  2个时间单位结束于时刻 1。
现在函数 0 调用函数 1,函数 1 在时刻 2 开始,执行 4 个时间单位后结束于时刻 5。
函数 0 再次在时刻 6 开始执行,并在时刻 6 结束运行,从而执行了 1 个时间单位。
所以函数 0 总共的执行了 2 +1 =3 个时间单位,函数 1 总共执行了 4 个时间单位。

说明:

  1. 输入的日志会根据时间戳排序,而不是根据日志Id排序。
  2. 你的输出会根据函数Id排序,也就意味着你的输出数组中序号为 0 的元素相当于函数 0 的执行时间。
  3. 两个函数不会在同时开始或结束。
  4. 函数允许被递归调用,直到运行结束。
  5. 1 <= n <= 100

解题思路

(本题为中等题,如果我想明白了并且和课上思路差不多就不写代码了)

本题很有意思,但是如果想明白了就不难,首先这是一个层次问题,每个函数的开始和结束就是这个函数的运行时间,但是要求计算函数的独占运行时间,这里很奇怪,如果一个函数调用自己,那这个自己调用自己算多少时间呢?是算外侧的时间加上内侧的时间还是不加上内侧的时间呢。

我的理解是表层指针实际走过的时间,表层指针行走时最内侧的函数标号是什么那么就为什么函数计算使用时间。

既然用了层级的思想,本题就要用栈来解,定义一个栈用于存放任务,开始则入栈,结束则出栈,每次出入栈时,都为当前栈顶任务累计一次任务执行时间(也就是栈顶元素存在的时间)

(想明白了,接下来看课上讲什么)

(和我的想法一致跳过本题节约时间)

leetcode-1124. 表现良好的最长时间段

来源:leetcode-1124. 表现良好的最长时间段

给你一份工作时间表 hours,上面记录着某一位员工每天的工作小时数。

我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。

所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格 大于「不劳累的天数」。

请你返回「表现良好时间段」的最大长度。

示例 1:


输入:hours = [9,9,6,0,6,6,9]
输出:3
解释:最长的表现良好时间段是 [9,9,6]

这题很恶心,我不做,这题暗示员工只有每天工作劳累才叫表现良好,暗示996才是合理的。

彩蛋习题

在学习群有一个文件,里面存储一系列出入栈的动作,此时先提取全部的出栈序列
然后将每个出栈的元素乘以索引加1(或者理解为索引从1开始计算)
最后将结果作为答案。
(题目很简单,不是学员也做不了)

总结

此时是3月30日,学了太久,太慢了,但是一定是很认真的。
所有题目我全部都自己做出来了,不需要听讲,(但是我课也听了),听课的收益在于获取更优秀的解题方案,因此前半段笔记我写了很多内容,但是我后来发现,我真的学习了太久,有些题目我花了好几个小时自己先解出来,然后听课学习课上的思想再解一遍,来来去去,最多6倍时长,最少2倍时长,我的收益是什么呢?

所以如果我提出了一个方案,那么就没有必要写出来,因为我的方案一定是可行的。但是课上的思维也往往是值得学习的,那么如果我写了自己的想法和代码,我只是练习了我会的东西,我的收益不多,如果课上的代码也值得学习,我去写一遍,那么我至少是2倍时长,我勉强能够接受。

可是我真的有那么多时间学习吗?

我觉得没有必要继续那么认真的学习了,对此我修改接下来的方针,首先概念部分,能抄就抄,其次题目除非我能在课上学到新的有价值的方案,否则我就不写代码,

  • 只对于自己感兴趣的,
  • 只对于自己有不一样的想法并且绝对更为优秀的,
  • 只对于自己觉得自己写不出来的

只对于这些,我写代码,其余的我只写思路,这样大概只需要1.5~3倍时间就能学习完毕。
我的之前的学习方式是先自己解题获得思路并用代码成功证明思路,再上课学习课上的想法如果更好就再用课上的想法写一遍代码。这样我需要通常的3到4倍的时间每一题,而且我还需要写笔记。

我接下来的方式是,先自己解题获得思路,再判断自己会不会,如果会就不写代码,再上课学习课上的想法如果更好就再用课上的想法写一遍代码。这样我少一遍自己先摸索的过程,可以节约1.5倍左右的时长(因为课上也存在摸索但是我可以跳过这部分时间)

对我来说,我关键要解决的问题是:我学了会忘记,而不是我学不会。

我学习理解能力一流,但是没有什么记忆力,所以很擅长做整理,但是我的时间精力有限,还是要优化和改良。
下一次加油。┗( ▔, ▔ )┛

你可能感兴趣的:(算法研习,java,编程语言)