【LeetCode】第一次总结。

计划对LeetCode刷题,顺便总结一下,四个阶段:

  1. 找出典型中等、困难题目,走读题目根据自己还记得的算法尝试回忆一遍
  2. 对照答案,识别自己的算法弱项,巩固算法后练习一遍典型习题
  3. 尝试找出同类型题目,巩固练习一遍
  4. 找出算法短板,重复1/2/3

算法总结

题目我的算法典型算法
收集树上所有苹果的最少时间对树做剪枝,剪枝剩下的,就是要遍历的节点,剪枝的策略是DFS,复杂度On2解法一:同化节点,如果当前节点是苹果,那么同化其父节点也作为苹果,从最下层节点开始遍历;算出所有同化节点后,乘2即可得到结果;
解法二:DFS,从苹果节点出发找根节点,记录路径,访问过的visited节点都标记,当访问到visited或根节点时就退出不做累计;
解法三:和我的思路相同
安排电影院座位逐行基于阈值进行判断,计算累加结果,算法复杂度 On解法一:算出每一行的二进制表达值,也是分三种情况,分别通过11110000/11000011/00001111三种可能做或预算,大体思路差不多,只是用了位运算,代码效率会高一些
通知所有员工所需时间递归,算法复杂度 On2解法一:暴力向上查找,从每个员工遍历,求出该员工向上查找到根节点的通知时间,记录并和max对比,最终求出max;
解法二:三种搜索遍历找出最大通知时间
1、DFS自底向上,遍历每个节点,从当前节点一致通过manager查找上级,如果不为空则标记visited,同时将informTime累加到toal,遍历每个节点时如果visited,则continue下一个节点
2、DFS自顶向下,从headID节点开始计算出每一个节点对应的下级节点,从根节点开始遍历,dfs每个子节点,当子节点为空时返回max;(这里的DFS实际上是 基于每个节点进行的DFS,感觉不太对,复杂度比较高
3、BFS,计算每个节点对应的子节点,将根节点放入queue,然后遍历queue,直到queue.empty()是true,过程中如果queue中当前节点存在子节点,则push到queue中,push的过程中就顺便求一下max
爱生气的书店老板滑动窗口,算法复杂度 On2滑动窗口,但是有个细节可以优化,即在不释放技能的情况下,满意度其实是固定的,只是我们要找到在哪个index位置释放技能的补偿最大,所以滑动过程的比对标准要变成补偿最大
盛最多水的容器暴力破解,基于每个柱子进行循环遍历,算法复杂度 On2解法一:双指针,左板右板分别设置为指针,每次寻找较小的一个移动,记录对应的容积,最后得出最大值并返回;
解法二:找规律,左板和右板肯定具备一个特征,即升序,比如左板向右移动,如果变小了,那容积肯定变小,肯定不行;同理右板也是,通过这种方式其实就是对暴力破解进行了剪枝,缩小查找范围。
下一个排列暴力破解,从右侧左后一个元素开始找可调整的位置,然后进行调整,On审题错误,比如:
[1,2,3]
[1,3,2]
[2,1,3]
[2,3,1]
[3,1,2]
[3,2,1]
类似这样的序列,思路其实没问题,想找到两个非降序的元素互换,如果找不到则说明最大了;但是不够细致,认为一定是连续的两个非降序元素,其实有可能是非连续的。
寻找重复数暴力破解,数组先算一遍sum,然后再放入set里算一遍sum,前后两个的差就是我们要找的数字,算法复杂度 On解法一:哈希表判重,解法类似,用hashMap判重;
解法二:排序后找相等的相邻,复杂度高于解法一,但空间占用小;
解法三:二分查找,每次寻找序列的中位数,小于中位数的个数如果偏多,说明该元素在中位数左侧分区;反之在右侧分区;
解法四:快慢指针,将线性序列做一个映射,n->f(n),类似
0->1
1->3
2->4
3->2
4->2
这样,那么在2-4之间就会产生环路,问题就是要找到这个环路的入口2
在这里插入图片描述
摆动排序感觉是经典算法,不懂,先mark解法一:排序后间隔混合插入,排序后先分为两个子序列 ,子序列间隔插入,形成摆动序列,不算啥固定算法,且时间空间复杂度不满足题目要求
解法二:找到中位数对应的数值 + 三路划分(荷兰器)+ 交叉合并,这个是正经解法
划分字母区间暴力破解,计算每个字符的连个属性,start、end,这样就变成了基于字符的start、end计算交集、并集的问题了所以我用的是贪心算法,和官方比较类似,不过一种比较好的方法是只记录每个字符最远位置,然后从index=0开始进行遍历,当 index增加的过程中,如果恰好遍历到了当前最远切割点maxPos,则进行一次切割并记录下来,这样遍历一次就能获得结果了;

整体看题目难度,比较困难的是:摆动排序、寻找重复数、下一个排序,这三个问题解法比较固定,且寻找重复数这个题,对时间、空间复杂度要求比较高

涉及到算法:二分查找、滑动窗口、快慢指针、双指针、BFS、DFS、位运算、贪心算法

收集树上所有苹果的最少时间

https://leetcode-cn.com/problems/minimum-time-to-collect-all-apples-in-a-tree/

给你一棵有 n 个节点的无向树,节点编号为 0 到 n-1 ,它们中有一些节点有苹果。通过树上的一条边,需要花费 1 秒钟。你从 节点 0 出发,请你返回最少需要多少秒,可以收集到所有苹果,并回到节点 0 。
无向树的边由 edges 给出,其中 edges[i] = [fromi, toi] ,表示有一条边连接 from 和 toi 。除此以外,还有一个布尔数组 hasApple ,其中 hasApple[i] = true 代表节点 i 有一个苹果,否则,节点 i 没有苹果。
示例 1:

在这里插入图片描述

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false]
输出:8
解释:上图展示了给定的树,其中红色节点表示有苹果。一个能收集到所有苹果的最优方案由绿色箭头表示。

思路:对树做剪枝,剪枝剩下的,就是要遍历的节点,剪枝的策略是DFS,复杂度On2
1、如果某个节点的子树不包含苹果,那么这个节点及其子树都不用经过
2、排除所有满足1的子树,剩下的节点都是需要遍历的
3、遍历的最小时间 = ( 节点数 - 1) * 2

解题过程:
在这里插入图片描述

   /**
     * 从苹果节点出发,向根节点DFS,过程中记录节点是否已经被访问,被访问的节点路径和不做累加
     * 1、声明一个visited[],长度和hasApple一样,初始值0;定义一个minPath = 0;
     * 2、遍历hasApple
     * 3、dfs(currentNode)
     * 4、visited[currentNode] = 1;如果currentNode == 0,返回0
     * 5、如果currentNode != 0,从edges找出currentNode的父节点,dfs(currentNode的父节点)
     * 6、如果currentNode的父节点已经被访问过,则返回0;
     */
    public static int minPath = 0;
    public static int[] visied;

    public static int minTime(int n, int[][] edges, List<Boolean> hasApple) {
        visied = new int[hasApple.size()];
        Arrays.fill(visied, 0);
        // TODO 忘记初始化
        minPath = 0;

        for (int i = 0; i < hasApple.size(); i++) {
            if (hasApple.get(i)) {
                dfs(i, edges);
            }
        }
        return minPath;
    }

    public static void dfs(int currentNode, int[][] edges) {
        // 说明已经访问过了
        if (visied[currentNode] == 1) {
            return;
        }

        // 标记已访问,及这个点和这个点之上的都算过了
        visied[currentNode] = 1;

        // 已经到了根节点了
        if(currentNode == 0){
            return;
        }

        for (int i = 0; i < edges.length; i++) {
            // 序号小的永远为父节点
            int fatherNode = edges[i][0] < edges[i][1]? edges[i][0]:edges[i][1];
            int childNode = edges[i][0] > edges[i][1]? edges[i][0]:edges[i][1];
            if(childNode == currentNode){
                //计算父节点
                minPath += 2;
                dfs(fatherNode,edges);
            }
        }
    }

//
    /**
     * 同化节点,如果当前节点是苹果,那么父节点也是苹果,最后统计所有苹果节点的个数,乘2后就能得到结果
     * 1、声明一个isApple[]数组,用来表示第i个节点是否是苹果
     * 2、遍历hasApple,用递归的方法表示其父节点也是苹果
     * 3、在遍历父节点的过程中,标识isApple
     */
    public static int[] isApple;

    public static int minTime(int n, int[][] edges, List<Boolean> hasApple) {
        isApple = new int[hasApple.size()];
        Arrays.fill(isApple, 0);
        for (int i = 0; i < hasApple.size(); i++) {
            if (hasApple.get(i)) {
                markApple(i, edges);
            }
        }

        int sum=0;
        for (int i= 0;i<isApple.length;i++){
            sum+=isApple[i];
        }

        return sum==0?0:(sum-1)*2;
    }

    public static void markApple(int currentNode, int[][] edges) {
        // 说明已经访问过了
        isApple[currentNode] = 1;

        for (int i = 0; i < edges.length; i++) {
            // 序号小的永远为父节点
            int fatherNode = edges[i][0] < edges[i][1]? edges[i][0]:edges[i][1];
            int childNode = edges[i][0] > edges[i][1]? edges[i][0]:edges[i][1];
            if(childNode == currentNode){
                //计算父节点
                markApple(fatherNode,edges);
            }
        }
    }

安排电影院座位

链接:https://leetcode-cn.com/problems/cinema-seat-allocation

电影院的观影厅中有 n 行座位,行编号从 1 到 n ,且每一行内总共有 10 个座位,列编号从 1 到 10 。

给你数组 reservedSeats ,包含所有已经被预约了的座位。比如说,researvedSeats[i]=[3,8] ,它表示第 3 行第 8 个座位被预约了。

请你返回 最多能安排多少个 4 人家庭 。4 人家庭要占据 同一行内连续 的 4 个座位。隔着过道的座位(比方说 [3,3] 和 [3,4])不是连续的座位,但是如果你可以将 4 人家庭拆成过道两边各坐 2 人,这样子是允许的。

在这里插入图片描述

思路:逐行基于阈值进行判断,计算累加结果,算法复杂度 On
1、能够安排四人连坐,每一排最多也就能安排两个
2、对一行座位进行判断,是否满足三个场景
a. [2/3/4/5]空闲: + 1
b. [4/5/6/7]空闲: + 1
c. [6/7/8/9]空闲: + 1
经过上述三个判断,可以计算出一行能够安排的座位数
3、在2可以做一个优化,如果a为真,则直接到c的判断;如果b为真,直接返回1;如果c为真,基于a的结果进行计算

解题过程:

   /**
     * 思路:逐行基于阈值进行判断,计算累加结果,算法复杂度 On
     * 1、能够安排四人连坐,每一排最多也就能安排两个
     * 2、对一行座位进行判断,是否满足三个场景
     * a. [2/3/4/5]空闲: + 1
     * b. [4/5/6/7]空闲: + 1
     * c. [6/7/8/9]空闲: + 1
     * 经过上述三个判断,可以计算出一行能够安排的座位数
     * 3、在2可以做一个优化,如果a为真,则直接到c的判断;如果b为真,直接返回1;如果c为真,基于a的结果进行计算
     */
    public static int maxNumberOfFamilies(int n, int[][] reservedSeats) {
        int totalCouples = 0;
        Map<Integer, List> seetCondition = new HashMap<>();
        for (int i = 0; i < reservedSeats.length; i++) {
            if (seetCondition.get(reservedSeats[i][0]) == null) {
                List list = new ArrayList();
                list.add(reservedSeats[i][1]);
                seetCondition.put(reservedSeats[i][0], list);
            } else {
                seetCondition.get(reservedSeats[i][0]).add(reservedSeats[i][1]);
            }
        }
        // 得到seetCondition,每行座位分别那些位置被占用
        for(Map.Entry<Integer, List> rowCondition : seetCondition.entrySet()){
            List placed = rowCondition.getValue();
            if (!placed.contains(2) && !placed.contains(3) && !placed.contains(4) && !placed.contains(5)) {
                totalCouples++;
                if (!placed.contains(6) && !placed.contains(7) && !placed.contains(8) && !placed.contains(9)) {
                    totalCouples++;
                }
                continue;
            }

            if (!placed.contains(4) && !placed.contains(5) && !placed.contains(6) && !placed.contains(7)) {
                totalCouples++;
                continue;
            }

            if (!placed.contains(6) && !placed.contains(7) && !placed.contains(8) && !placed.contains(9)) {
                totalCouples++;
            }
        }

        return totalCouples + (n - seetCondition.size()) * 2;
    }

通知所有员工所需时间

链接:https://leetcode-cn.com/problems/time-needed-to-inform-all-employees/

公司里有 n 名员工,每个员工的 ID 都是独一无二的,编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。

在 manager 数组中,每个员工都有一个直属负责人,其中 manager[i] 是第 i 名员工的直属负责人。对于总负责人,manager[headID] = -1。题目保证从属关系可以用树结构显示。

公司总负责人想要向公司所有员工通告一条紧急消息。他将会首先通知他的直属下属们,然后由这些下属通知他们的下属,直到所有的员工都得知这条紧急消息。

第 i 名员工需要 informTime[i] 分钟来通知它的所有直属下属(也就是说在 informTime[i] 分钟后,他的所有直属下属都可以开始传播这一消息)。

返回通知所有员工这一紧急消息所需要的 分钟数 。

在这里插入图片描述

输入:n = 6, headID = 2, manager = [2,2,-1,2,2,2], informTime = [0,0,1,0,0,0]
输出:1
解释:id = 2 的员工是公司的总负责人,也是其他所有员工的直属负责人,他需要 1 分钟来通知所有员工。
上图显示了公司员工的树结构。

思路:递归,算法复杂度 On2
1、总耗时 = 每个员工被通知的时间之和
2、员工被通知的时间 = 上级通知他的时间 + 上级被通知的时间
3、如果上级是总负责人,则上级被通知的时间 = 0
4、基于2/3进行递归
上述这种思路其实是一个DFS的过程,从最下级开始向上找,找到耗时最长的一段

DFS的实现:

    /**
     * 思路:自下向上的DFS
     * 1、从每个底层员工出发,计算最长被通知的时间
     * 2、每个底层员工就是informTime中等于0的员工,因为他没有下级
     * 3、记录一个最长通知时间maxTime
     * 4、DFS遍历每一个inforTime = 0的员工
     * 5、DFS的逻辑是:
     * a.如果当前员工的上级是-1,说明到根节点,返回0
     * b.如果当前员工的上级不是-1,那么返回inforTime[上级索引] + dfs(上级)
     * c.过程中记录下cache,cache.put(当前节点索引,inforTime[上级索引] + dfs(上级))
     */
    static Map cache;

    public static int numOfMinutes(int n, int headID, int[] manager, int[] informTime) {
        int maxTime = Integer.MIN_VALUE;
        cache = new HashMap();
        for (int i = 0; i < informTime.length; i++) {
            // 底层员工
            if (informTime[i] == 0) {
                int notcieTime = dfs(manager, informTime, i);
                maxTime = maxTime > notcieTime ? maxTime : notcieTime;
            }
        }

        return maxTime;
    }

    public static int dfs(int[] manager, int[] informTime, int currentEmployee) {
        // 当前员工上级是-1,说明当前员工是经理
        if (manager[currentEmployee] == -1) {
            return 0;
        }

        // 如果在缓存中找到,说明在计算其他底层员工的时候经过这个员工,不用重复计算
        if (cache.get(currentEmployee) != null) {
            return (int) cache.get(currentEmployee);
        }

        int currentManager = manager[currentEmployee];
        // 耗费时间等于上级通知currentEmployee的时间加上上级被通知的时间
        int currentNoticeTime = informTime[currentManager] + dfs(manager, informTime, currentManager);
        // 放入缓存
        cache.put(currentEmployee, currentNoticeTime);
        return currentNoticeTime;
    }

也可以自上向下的进行DFS,两种思路:
1、从根节点开始,选取下级节点中耗时最长的一个,加上当前的耗时;这种DFS需要有返回值。
2、从根节点开始,一致DFS到叶子节点 ,过程中将informTime的值作为参数累加下去,叶子结点进行和全局变量maxTime比对(不同叶子可能值不同)

两种其实都差不多,我选择1:

   /**
     * 思路:自上向下的DFS
     * 1、从head出发,也就是经理节点,定义一个maxTime = 0
     * 2、从manager中找出经理的下级节点
     * 3、遍历下级节点,最大通知时间等于所有下级节点中需要通知时间最长的那个
     * 4、DFS下级每个节点
     * a. 如果当前节点在infoTime中是0,说明当前节点是底层,返回0
     * b. 如果当前节点在infoTime中不是0,说明有下级,则遍历所有下级节点 ,返回耗时最大的下级
     */
    static List<Integer>[] relations;
//    static int maxTime = 0;

    public static int numOfMinutes(int n, int headID, int[] manager, int[] informTime) {
        initMap(manager, n);
//        maxTime = 0;
//        dfs(headID, maxTime,informTime);
//        return maxTime;
        return dfs(headID, manager, informTime);
    }

    public static void initMap(int[] managers, int n) {
        relations = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            int manager = managers[i];
            if (manager == -1) {
                continue;
            }
            if (relations[manager] == null) {
                relations[manager] = new ArrayList<>();
            }
            relations[manager].add(i);
        }
    }

    //    public static void dfs(int currentEmployee, int sum, int[] infoTime) {
    public static int dfs(int currentEmployee, int[] manager, int[] informTime) {
        // 如果当前员工通知成本是0,说明没有下级了,返回当前员工被通知的耗时
//        if (relations[currentEmployee] == null) {
//            maxTime = Math.max(sum, maxTime);
//            return currentEmployee;
//        }
        if (informTime[currentEmployee] == 0) {
            return 0;
        }

//        List downEmployees = relations[currentEmployee];
//        for (Object downEmployee : downEmployees) {
//            dfs((Integer) downEmployee, sum + infoTime[currentEmployee], infoTime);
//        }

        // todo 这里比较耗时,来来回回循环
        // TODO 加个数据预处理,用map表示m的下级集合n
        // TODO 还是不对
        int maxTime = Integer.MIN_VALUE;
        List downEmployees = relations[currentEmployee];
        for (Object downEmployee : downEmployees) {
            // downEmployee是currentEmployment下级
            int noticeTime = dfs((Integer) downEmployee, manager, informTime) + informTime[currentEmployee];
            maxTime = maxTime > noticeTime ? maxTime : noticeTime;
        }

        // TODO 刚开始这种写法确实会增加遍历的次数,找的优化点是对的,降低这部分的循环次数
        // TODO 但是优化方法不是最好的,通过一个map进行了On2的复杂度尝试化,反反复复超时,不是算法问题,而是数据初始化超时了。。。
//        int maxTime = Integer.MIN_VALUE;
//        for (int i = 0; i < manager.length; i++) {
//            if (manager[i] == currentEmployee) {
//                // i是currentEmployment下级
//                int noticeTime = dfs(i, manager, informTime) + informTime[currentEmployee];
//                maxTime = maxTime > noticeTime ? maxTime : noticeTime;
//            }
//        }

        return maxTime;
    }

    // TODO
    /**
     *     这是有问题的初始化算法,两次遍历
     *     public void initMap(int[] manager, int n) {
     *         for (int i = 0; i < n; i++) {
     *             for (int j = 0; j < manager.length; j++) {
     *                 // j 是 i的下属
     *                 if (manager[j] == i) {
     *                     if (relations.get(i) == null) {
     *                         List list = new ArrayList();
     *                         list.add(j);
     *                         relations.put(i, list);
     *                     } else {
     *                         relations.get(i).add(j);
     *                     }
     *                 }
     *             }
     *         }
     *     }
     *
     * */

一开始超时了,虽然找到了优化点(每次遍历往下找的时候,其实没必要遍历所有节点查询是否是当前节点的子节点,通过数据预处理可以减少遍历的次数),但是数据预处理的效率太低了,On2的时间复杂度,来来回回看算法,实在没找到优化空间,原来果然是数据预处理的问题。

爱生气的书店老板

链接:https://leetcode-cn.com/problems/grumpy-bookstore-owner

今天,书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客(customers[i])会进入书店,所有这些顾客都会在那一分钟结束后离开。

在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。

书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X 分钟不生气,但却只能使用一次。

请你返回这一天营业下来,最多有多少客户能够感到满意的数量。

示例:

输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3
输出:16
解释:
书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.

思路:滑动窗口,算法复杂度 On2
1、基于Customers计算从0 ~ customer.length这段时间,每分钟的顾客数
2、从index = 0 开始遍历,基于每分钟的顾客数量,以及grumy值,包括但钱分钟如果使用特技的情况下带来的增益,计算出当前index+特技的顾客满意度
3、遍历到customer.length - x分钟,得出满意度最大的index

练习了一下,其实没必要太死板的按照滑动窗口去搞,滑动窗口问题特征明显,主要要考虑的就是如何
简化滑动的触发逻辑,以及简化条件触发后,滑动的逻辑。

简化后思路就是:
1、最大满意度 = 不用技能的满意度 + 技能带来的最大补偿
2、技能带来的最大补充 = 按照X的窗口大小过一遍,结合grumpy值记录下最大值

    /**
     * 思路:滑动窗口
     * tips:
     * 1、顾客只呆1分钟
     * 2、其实就是假设从第一分钟开始老板就使用不生气,然后滑动向右,求出在不生气X区间内顾客数量最大满意度
     * <p>
     * 感觉好像很简单。
     * 1、将customer和grumpy相乘,计算出每分钟损失的信誉度;同时计算出在不适用技能情况下满意度
     * 2、以X位窗口,计算能避免的最大损失
     * 3、最大满意度 = 不适用技能情况下满意度 + 技能最大挽回
     */
    public static int maxSatisfied(int[] customers, int[] grumpy, int X) {
        int maxSatisfied = 0;
        int[] angryValue = new int[customers.length];
        for (int i = 0; i < customers.length; i++) {
            angryValue[i] = customers[i] * grumpy[i];
            maxSatisfied += customers[i] * (1 - grumpy[i]);
        }

        int maxSaved = 0;
        int saved = 0;
        for (int left = 0, right = 0; right < angryValue.length; ) {
            if (right - left <= X - 1) {
                saved += angryValue[right];
                maxSaved = Math.max(saved, maxSaved);
                right++;
                continue;
            }

            // TODO 我靠,这里写死了X = 3?
            saved = saved - angryValue[left] + angryValue[right];
            maxSaved = Math.max(saved, maxSaved);

            right++;
            left++;
        }

        return maxSatisfied + maxSaved;
    }

第一次出错的原因也很蛋疼,一直看着用例,就假设X=3了,这里有个优化点值得注意,saved值不用每次向后找X去算,只需要掐头去尾就好。

盛最多水的容器

链接:https://leetcode-cn.com/problems/container-with-most-water

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。

示例 1:
在这里插入图片描述
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
思路,暴力破解,基于每个柱子进行循环遍历,算法复杂度 On2
1、第一层循环,计算以每个柱子作为左边的池子容积
2、第二层循环,计算以选定左边柱子后每个柱子作为右边的池子容积
3、如果比max大,那么就更新记录容积
4、输出最大容积max

用双指针,没啥难度,主要是理解模型和双指针算法能匹配上。

    /**
     * 双指针
     * 1、定义left = 0
     * 2、定义right = height.length - 1
     * 3、对比left 和 right 大小
     * height[left] > height[right] , right--
     * height[left] < height[right] , left++
     * 4、直到left == right
     * 5、过程中记录最大area的大小
     */
    public static int maxArea(int[] height) {
        int maxArea = 0;
        for (int left = 0, right = height.length - 1; left < right; ) {
            maxArea = Math.max(maxArea, (right - left) * Math.min(height[left], height[right]));
            if (height[left] > height[right]) {
                right--;
                continue;
            }

            if (height[left] <= height[right]) {
                left++;
                continue;
            }
        }
        return maxArea;
    }

下一个排列

链接:https://leetcode-cn.com/problems/next-permutation

实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须 原地 修改,只允许使用额外常数空间。

示例 1:

输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:

输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:

输入:nums = [1,1,5]
输出:[1,5,1]
示例 4:

输入:nums = [1]
输出:[1]

思路:暴力破解,从右侧左后一个元素开始找可调整的位置,然后进行调整,On
1、找出下一个更大的序列,那么就要从右侧最低位优先调整
2、从右侧index位置,判断nums[index] 是否小于 nums[index - 1],如果满足,则进行调整并输出
3、如果2中index == 0,说明不存在更大序列,此时直接进行倒排输出
4、所谓的原地修改我理解就是不要拷贝复制新的数组

解题过程:
写起来还是挺麻烦的,这种没有固定套路的题往往靠考虑很多场景。
思路还是从低位开始找降序的两个元素,并且对两个元素进行交换,编写中出现了2个问题:
1、交换完两个元素后,还没完,因为交换完只是代表找到了调整数字的最小化选择,其他数字还需要做降序排序,才能确保得到下一个最大的数列
2、开始定义left、right,right从右向左逐个数字进行遍历,一旦发现left比right小,说明找到了,这种思路有个缺陷就是,当left–已经到很高位时,此时交换产生的新数字并不是最小的,最小的数字可能是在下个right遍历就能发现(发生在低位交换),因此,查找的本质并不是left和right对比,而是left和left左边的整个序列对比,如果发现存在降序,就找到那个满足条件的最低位进行交换

    /**
     * 从右边找第一个降序的两个元素,然后交换他们
     */
    public static void nextPermutation(int[] nums) {
        int left = nums.length - 1;
        int maxRightIndex = nums.length - 1;
        while (left >= 0) {
            if (nums[left] < nums[maxRightIndex]) {
                // 这里代表一定出现了降序,但最小降序不一定发生在maxRightIndex这个位置。
                // 从left开始找出降序差最小的位置(位数越往后优先级越高)
                for (int i = left + 1; i < nums.length; i++) {
                    if (nums[i] > nums[left]) {
                        maxRightIndex = i;
                    }
                }

                int temp = nums[maxRightIndex];
                nums[maxRightIndex] = nums[left];
                nums[left] = temp;
                break;
            }

            if (nums[left] > nums[maxRightIndex]) {
                maxRightIndex = left;
            }

            left--;
        }

        if (left < 0) {
            Arrays.sort(nums);
        } else {
            int[] tempArr = new int[nums.length - left - 1];
            for (int i = left + 1, index = 0; i < nums.length; i++, index++) {
                tempArr[index] = nums[i];
            }
            Arrays.sort(tempArr);
            for (int i = left + 1, index = 0; i < nums.length; i++, index++) {
                nums[i] = tempArr[index];
            }
        }
    }

寻找重复数

链接:https://leetcode-cn.com/problems/find-the-duplicate-number

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: [1,3,4,2,2]
输出: 2
示例 2:

输入: [3,1,3,4,2]
输出: 3
思路:暴力破解,数组先算一遍sum,然后再放入set里算一遍sum,前后两个的差就是我们要找的数字,算法复杂度 On

尝试了好几种方法:
1、用hashmap判断是否已经存在
2、排序,然后找相邻相等的元素
3、二分查找,通过中位数判断(写的有问题)
4、快慢指针,效率最高
其中,快慢指针理解如下这张图:
在这里插入图片描述
大体的思路就是:
1、通过fast、slow最终肯定能有一个汇合点,找到汇合点
2、从头出发,和slow(当前在和fast汇合点)一起出发,一定可以汇合,且汇合点就是入环点

    /**
     * 快慢指针是找出是否存在回路的一种方法,在超出是否存在回路的同时,也能找出入环点
     * 1、对于序列{1, 3, 4, 2, 2}存在这样一个映射
     * F(0) = 1
     * F(1) = 3
     * F(2) = 4
     * F(3) = 2
     * F(4) = 2
     * 当F(3)==F(4)这个点即可看出存在回路
     * <p>
     * 1、 定义fast、slow都是从0开始
     * 2、 slow的移动 = slow = nums[i]
     * 3、 fast的移动 = fast = nums[nums[i]]
     * 4、当fast == slow时即两者汇合
     * 5、根据快慢指针找回路的原理,此时从0开始遍历数组,即可找到入环点
     * 6、此时定义一个ptr指针从头部开始,和slow同时向后移动,当两者相遇是,即找到了入环点
     */
    public static int findDuplicate(int[] nums) {
        int fast = 0, slow = 0;
        while ((fast != slow) || (fast == 0 && slow == 0)) {
            slow = nums[slow];
            fast = nums[nums[fast]];
        }
        int ptr = 0;
        while (ptr != slow) {
            ptr = nums[ptr];
            slow = nums[slow];
        }
        return ptr;
    }

    public static void main(String[] args) {
        int[] nums = {1, 3, 4, 2, 2};
        System.out.println(findDuplicate(nums));

        int[] nums1 = {3, 1, 3, 4, 2};
        System.out.println(findDuplicate(nums1));
    }

摆动排序

链接:https://leetcode-cn.com/problems/wiggle-sort-ii

给定一个无序的数组 nums,将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]… 的顺序。

示例 1:

输入: nums = [1, 5, 1, 1, 6, 4]
输出: 一个可能的答案是 [1, 4, 1, 5, 1, 6]
示例 2:

输入: nums = [1, 3, 2, 2, 3, 1]
输出: 一个可能的答案是 [2, 3, 1, 3, 1, 2]
思路:感觉是经典算法,不懂,先mark
荷兰旗算法还没来得及看 ,尝试用间隔插入的方式做另一边,发现也挺麻烦的。主要在于审题的仔细程度
题目中虽然说明是摇摆排序,但其实对序列是有大小摇摆的要求的:nums[0] < nums[1] > nums[2] < nums[3]
也就是说,开头的的元素必须是较小的元素;在第二次提交是遇到了一个{4,5,5,6}的序列,应该输出的是{5,6,5,4},但是基于简单的从small开水逐个插入的方法就会得到{4,5,5,6},因此此时的small={4,5},big={5,6},因此自然会得到错误结果;所以这是插入的时候,应该从small/big的最右边开始插入,确保较大的元素被优先使用。

    /**
     * 1、将数组排序,从小到大
     * 2、将数组分为两个,最终拼成一个
     * 3、分成两个有两种情况,奇数或偶数长度
     * a. 如果是偶数长度,那就可以直接除2,比如长度4,分为两个2的数组
     * b. 如果是奇数长度,比如长度5,那就得分为2/3两个长度的数组
     * 4、拼接数组,如果上述a的情况,直接拼接;如果是上述b的情况,需要优先拼接长度大的。
     */
    public static void wiggleSort(int[] nums) {
        Arrays.sort(nums);
        int[] small = Arrays.copyOfRange(nums, 0, nums.length % 2 == 0 ? nums.length / 2 : nums.length / 2 + 1);
        int[] big = Arrays.copyOfRange(nums, nums.length % 2 == 0 ? nums.length / 2 : nums.length / 2 + 1, nums.length);
        boolean changeSmall = true;
        for (int i = 0, smallIndex = small.length - 1, bigIndex = big.length - 1; i < nums.length; i++) {
            if (changeSmall) {
                nums[i] = small[smallIndex--];
            } else {
                nums[i] = big[bigIndex--];
            }
            changeSmall = !changeSmall;
        }
    }

划分字母区间

链接:https://leetcode-cn.com/problems/partition-labels

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:

输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。
思路:暴力破解,计算每个字符的连个属性,start、end,这样就变成了基于字符的start、end计算交集、并集的问题了
1、计算每个字符的start、end属性,得到a=[0/9],b=[2/4],c=[5/7],d=[10/16],e=[11/17],f=[12/12],j=[…]
2、这样就变成了如下图的计算问题了

在这里插入图片描述

3、遍历计算每个字符
a.如果当前字符end大于下一个字符的end,则continue
b.如果当前字符end等于下一个字符的start,则截取start到end的字符串并计算长度,同时将当前的end赋值给start,下一个字符的end赋值给当前的end
c.如果当前字符end小于下一个字符的end并且当前字符start小于下一个字符的start并且当前字符的end大于下一个字符的start,说明有交集,此时将当前字符的end赋值为下一个字符的end(字符串变大)
4、经过以上循环,得出所有字符串长度

基于上述思路提交了一版:

   /**
     * 遍历计算每个字符
     * a.如果当前字符end大于下一个字符的end,则continue
     * b.如果当前字符end等于下一个字符的start,**则截取start到end的字符串并计算长度**,同时将当前的end赋值给start,下一个字符的end赋值给当前的end
     * c.如果当前字符end小于下一个字符的end并且当前字符start小于下一个字符的start并且当前字符的end大于下一个字符的start,说明有交集,此时将当前字符的end赋值为下一个字符的end(字符串变大)
     * 经过以上循环,得出所有字符串长度
     */
    public static List<Integer> partitionLabels(String S) {
        char[] arrays = S.toCharArray();
        Map<Character, Map> locationInfo = initMapInfo(arrays);

        List result = new ArrayList();
        int lastEnd = (int) locationInfo.get(arrays[0]).get("end");
        int lastStart = (int) locationInfo.get(arrays[0]).get("start");
        for (int index = 0; index < arrays.length; index++) {
            int currentEnd = (int) locationInfo.get(arrays[index]).get("end");
            int currentStart = (int) locationInfo.get(arrays[index]).get("start");
            // 当前的字符被上一个包着
            if (currentEnd <= lastEnd) {
                continue;
            }

            // 当前字符的start等于上一个的end,开启一个新串,标记
            if (currentStart == lastEnd + 1) {
                result.add(lastEnd - lastStart + 1);
                lastEnd = currentEnd;
                lastStart = currentStart;
                continue;
            }

            // 当前字符被包着,但是end很靠后,所以lastEnd需要延长
            if (currentStart < lastEnd) {
                lastEnd = currentEnd;
            }
        }
        result.add(lastEnd - lastStart + 1);

        return result;
    }

    public static Map<Character, Map> initMapInfo(char[] arrays) {
        Map<Character, Map> locationInfo = new HashMap();
        for (int i = 0; i < arrays.length; i++) {
            if (locationInfo.get(arrays[i]) == null) {
                Map info = new HashMap();
                info.put("start", i);
                info.put("end", i);
                locationInfo.put(arrays[i], info);
            } else {
                locationInfo.get(arrays[i]).put("end", i);
            }
        }
        return locationInfo;
    }

在这里插入图片描述
性能太烂了,考虑优化:少new一些对象,能快很多。
在这里插入图片描述

    /**
     * 遍历计算每个字符
     * a.如果当前字符end大于下一个字符的end,则continue
     * b.如果当前字符end等于下一个字符的start,**则截取start到end的字符串并计算长度**,同时将当前的end赋值给start,下一个字符的end赋值给当前的end
     * c.如果当前字符end小于下一个字符的end并且当前字符start小于下一个字符的start并且当前字符的end大于下一个字符的start,说明有交集,此时将当前字符的end赋值为下一个字符的end(字符串变大)
     * 经过以上循环,得出所有字符串长度
     */
    public static List<Integer> partitionLabels(String S) {
        char[] arrays = S.toCharArray();
        int[][] locationInfo = initMapInfo(arrays);

        List result = new ArrayList();
        int lastEnd = locationInfo[arrays[0] - 'a'][1];
        int lastStart = locationInfo[arrays[0] - 'a'][0];
        for (int index = 0; index < arrays.length; index++) {
            int currentEnd = locationInfo[arrays[index] - 'a'][1];
            int currentStart = locationInfo[arrays[index] - 'a'][0];
            // 当前的字符被上一个包着
            if (currentEnd <= lastEnd) {
                continue;
            }

            // 当前字符的start等于上一个的end,开启一个新串,标记
            if (currentStart == lastEnd + 1) {
                result.add(lastEnd - lastStart + 1);
                lastEnd = currentEnd;
                lastStart = currentStart;
                continue;
            }

            // 当前字符被包着,但是end很靠后,所以lastEnd需要延长
            if (currentStart < lastEnd) {
                lastEnd = currentEnd;
            }
        }
        result.add(lastEnd - lastStart + 1);

        return result;
    }

    public static int[][] initMapInfo(char[] arrays) {
        int[][] locationInfo = new int[26][2];
        for (int i = 0; i < 26; i++) {
            Arrays.fill(locationInfo[i], -1);
        }

        for (int i = 0; i < arrays.length; i++) {
            if (locationInfo[arrays[i] - 'a'][0] == -1) {
                locationInfo[arrays[i] - 'a'][0] = i;
                locationInfo[arrays[i] - 'a'][1] = i;
            } else {
                locationInfo[arrays[i] - 'a'][1] = i;
            }
        }
        return locationInfo;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值