牛客101刷题笔记

文章目录

  • 1、BM1 反转链表
  • 2、BM2 链表内指定区间反转
  • 3、BM3 链表中的节点每k个一组翻转
  • 4、BM4 合并两个排序的链表
  • 5、BM5 合并k个已排序的链表
  • 6、BM6 判断链表中是否有环
  • 7、BM7 链表中环的入口结点
  • 8、BM8 链表中倒数最后k个结点
  • 9、BM9 删除链表的倒数第n个节点
  • 10、BM10 两个链表的第一个公共结点
  • 11、BM11 链表相加(二)
  • 12、BM12 单链表的排序
  • 13、BM13 判断一个链表是否为回文结构
  • 14、BM14 链表的奇偶重排
  • 15、BM15 删除有序链表中重复的元素-I
  • 16、BM16 删除有序链表中重复的元素-II
  • 17、BM17 二分查找-I
  • 18、BM18 二维数组中的查找
  • 19、BM19 寻找峰值
  • 20、BM20 数组中的逆序对
  • 21、BM21 旋转数组的最小数字
  • 22、BM22 比较版本号
  • 23、BM23 二叉树的前序遍历
  • 24、BM24 二叉树的中序遍历
  • 25、BM25 二叉树的后序遍历
  • 26、BM26 求二叉树的层序遍历
  • 27、BM27 按之字形顺序打印二叉树
  • 28、BM28 二叉树的最大深度
  • 29、BM29 二叉树中和为某一值的路径(一)
  • 30、BM30 二叉搜索树与双向链表
  • 31、BM31 对称的二叉树
  • 32、BM32 合并二叉树
  • 33、BM33 二叉树的镜像
  • 34、BM34 判断是不是二叉搜索树
  • 35、BM35 判断是不是完全二叉树
  • 36、BM36 判断是不是平衡二叉树
  • 37、BM37 二叉搜索树的最近公共祖先
  • 38、BM38 在二叉树中找到两个节点的最近公共祖先
  • 39、BM39 序列化二叉树
  • 40、BM40 重建二叉树
  • 41、BM41 输出二叉树的右视图
  • 42、BM42 用两个栈实现队列
  • 43、BM43 包含min函数的栈
  • 44、BM44 有效括号序列
  • 45、BM45 滑动窗口的最大值
  • 46、BM46 最小的K个数
  • 47、BM47 寻找第K大
  • 48、BM48 数据流中的中位数
  • 49、BM49 表达式求值
  • 50、BM50 两数之和
  • 51、BM51 数组中出现次数超过一半的数字
  • 52、BM52 数组中只出现一次的两个数字
  • 53、BM53 缺失的第一个正整数
  • 54、BM54 三数之和
  • 55、BM55 没有重复项数字的全排列
  • 56、BM56 有重复项数字的全排列
  • 57、BM57 岛屿数量
  • 58、BM58 字符串的排列
  • 59、BM59 N皇后问题
  • 60、BM60 括号生成
  • 61、BM61 矩阵最长递增路径
  • 62、BM62 斐波那契数列
  • 63、BM63 跳台阶
  • 64、BM64 最小花费爬楼梯
  • 65、BM65 最长公共子序列(二)
  • 66、BM66 最长公共子串
  • 67、BM67 不同路径的数目(一)
  • 68、BM68 矩阵的最小路径和
  • 69、BM69 把数字翻译成字符串
  • 70、BM70 兑换零钱(一)
  • 71、BM71 最长上升子序列(一)
  • 72、BM72 连续子数组的最大和
  • 73、BM73 最长回文子串
  • 74、BM74 数字字符串转化成IP地址
  • 75、BM75 编辑距离(一)
  • 76、BM76 正则表达式匹配
  • 77、BM77 最长的括号子串
  • 78、BM78 打家劫舍(一)
  • 79、BM79 打家劫舍(二)
  • 80、BM80 买卖股票的最好时机(一)
  • 81、BM81 买卖股票的最好时机(二)
  • 82、BM82 买卖股票的最好时机(三)
  • 83、BM83 字符串变形
  • 84、BM84 最长公共前缀
  • 85、BM85 验证IP地址
  • 86、BM86 大数加法
  • 87、BM87 合并两个有序的数组
  • 88、BM88 判断是否为回文字符串
  • 89、BM89 合并区间
  • 90、BM90 最小覆盖子串
  • 91、BM91 反转字符串
  • 92、BM92 最长无重复子数组
  • 93、BM93 盛水最多的容器
  • 94、BM94 接雨水问题
  • 95、BM95 分糖果问题
  • 96、BM96 主持人调度(二)
  • 97、BM97 旋转数组
  • 98、BM98 螺旋矩阵
  • 99、BM99 顺时针旋转矩阵
  • 100、BM100 设计LRU缓存结构
  • 101、BM101 设计LFU缓存结构

1、BM1 反转链表

在这里插入图片描述
题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

思路步骤:

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

牛客101刷题笔记_第1张图片

public class Solution {

    public ListNode ReverseList(ListNode head) {
        //pre指针:用来指向反转后的节点,初始化为null
        ListNode pre = null;
        //当前节点指针
        ListNode cur = head;
        //循环迭代
        while(cur!=null){
            //temp节点,永远指向当前节点cur的下一个节点
            ListNode temp = cur.next;
            //反转的关键:当前的节点指向其前一个节点(注意这不是双向链表,没有前驱指针)
            cur.next = pre;
            //更新pre
            pre = cur;
            //更新当前节点指针
            cur = temp ;
        }
        //为什么返回pre?因为pre是反转之后的头节点
        return pre;
    }
}

2、BM2 链表内指定区间反转

在这里插入图片描述
思路步骤:

  1. 构建一个虚拟结点,让它指向原链表的头结点。

  2. 设置两个指针,pre 指针指向以虚拟头结点为链表的头部位置,cur 指针指向原链表的头部位置。

  3. 让着两个指针向前移动,直到 pre 指向了第一个要反转的结点的前面那个结点,而 cur 指向了翻转区域里面的第一个结点。

  4. 开始指向翻转操作

  • 设置临时变量 temp,temp 是 cur 的 next 位置,保存当前需要翻转结点的后面的结点,我们需要交换 temp 和 cur
  • 让 cur 的 next 位置变成 temp 的下一个结点

3)、让 temp 的 next 位置变成 cur
4)、让 pre 的 next 位置变成 temp

public class Solution {

    public ListNode reverseBetween (ListNode head, int m, int n) {
        if(head == null || head.next == null){
            return head;
        }
        //设置虚拟头节点
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        //走m-1步到 m的前一个节点
        ListNode pre = dummy;
        for(int i =0; i < m - 1; ++i){
            pre = pre.next;
        }
        //走n-m+1步到 n的节点
        ListNode rightNode = pre;
        for(int i = 0; i < n-m+1; ++i){
            rightNode = rightNode.next;
        }
        //截取出子链表
        ListNode leftNode  = pre.next;
        ListNode cur = rightNode.next;
        //断开链表
        pre.next = null;
        rightNode.next = null;
        //反转中间的链表
        reverseListNode(leftNode);  

        //拼接链表(这时已经反转过来了哦)
        pre.next = rightNode;
        leftNode.next = cur;

        return dummy.next;
    }
    private void reverseListNode(ListNode head){
        ListNode pre = null;
        ListNode cur = head;
        while(cur!=null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
    }
}

3、BM3 链表中的节点每k个一组翻转

在这里插入图片描述

思路步骤:

现在我们想一想,如果拿到一个链表,想要像上述一样分组翻转应该做些什么?首先肯定是分段吧,至少我们要先分成一组一组,才能够在组内翻转,之后就是组内翻转,最后是将反转后的分组连接。

但是连接的时候遇到问题了:首先如果能够翻转,链表第一个元素一定是第一组,它翻转之后就跑到后面去了,而第一组的末尾元素才是新的链表首,我们要返回的也是这个元素,而原本的链表首要连接下一组翻转后的头部,即翻转前的尾部,如果不建立新的链表,看起来就会非常难。但是如果我们从最后的一个组开始翻转,得到了最后一个组的链表首,是不是可以直接连在倒数第二个组翻转后的尾(即翻转前的头)后面,这样从后往前是不是看起来就容易多了。

怎样从后往前呢?我们这时候可以用到自上而下再自下而上的递归或者说栈。接下来我们说说为什么能用递归?如果这个链表有n个分组可以反转,我们首先对第一个分组反转,那么是不是接下来将剩余n−1个分组反转后的结果接在第一组后面就行了,那这剩余的n−1组就是一个子问题。我们来看看递归的三段式模版:

  • 终止条件: 当进行到最后一个分组,即不足k次遍历到链表尾(0次也算),就将剩余的部分直接返回。
  • 返回值: 每一级要返回的就是翻转后的这一分组的头,以及连接好它后面所有翻转好的分组链表。
  • 本级任务: 对于每个子问题,先遍历k次,找到该组结尾在哪里,然后从这一组开头遍历到结尾,依次翻转,结尾就可以作为下一个分组的开头,而先前指向开头的元素已经跑到了这一分组的最后,可以用它来连接它后面的子问题,即后面分组的头。

具体做法:

  • step 1:每次从进入函数的头节点优先遍历链表k次,分出一组,若是后续不足k个节点,不用反转直接返回头。
  • step 2:从进入函数的头节点开始,依次反转接下来的一组链表
  • step 3:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    public ListNode reverseKGroup (ListNode head, int k) {
        // write code here
        //找到每次要反转的尾部;
        ListNode tail = head;
        //遍历k次到尾部
        for(int i = 0; i < k; ++i){
            //basecase
            if(tail == null){
                return head;
            }
            tail = tail.next;
        }
        //定义一个前驱节点和当前节点
        ListNode pre = null;
        ListNode cur = head;
        //开始反转
        while(cur != tail){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        //当前尾指向下一个要反转的链表
        head.next = reverseKGroup(tail,k);

        //返回pre
        return pre;
    }
}

4、BM4 合并两个排序的链表

在这里插入图片描述

思路步骤:

  • 从头结点开始考虑,比较两表头结点的值,值较小的list的头结点后面接merge好的链表(进入递归了);
  • 若两链表有一个为空,返回非空链表,递归结束;
  • 当前层不考虑下一层的细节,当前层较小的结点接上该结点的next与另一结点merge好的表头就ok了;
  • 每层返回选定的较小结点就ok;

重新整理一下:

  • 终止条件:两链表其中一个为空时,返回另一个链表;
  • 当前递归内容:若list1.val <= list2.val 将较小的list1.next与merge后的表头连接,即list1.next = Merge(list1.next,list2); list2.val较大时同理;
  • 每次的返回值:排序好的链表头;

复杂度:O(m+n) O(m+n)

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param list1 ListNode类 
     * @param list2 ListNode类 
     * @return ListNode类
     */
    public ListNode Merge (ListNode list1, ListNode list2) {
        // write code here
        //basecase
        //要是list1为空,直接返回list2,同理
        if(list1 == null){
            return list2;
        }else if(list2 == null){
            return list1;
        }
        //递归
        if(list1.val < list2.val){
            list1.next = Merge(list1.next,list2);
            return list1;
        }else{
            list2.next = Merge(list1,list2.next);
            return list2;
        }
    }
}

5、BM5 合并k个已排序的链表

在这里插入图片描述

思路步骤:
1、将两个链表合并。(递归方法合并)

2、分而治之!求一个mid,将mid左边的合并,右边的合并,最后将左右两边的链表合并。

3、重复这一过程,直到获取最终的有序链表。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param lists ListNode类ArrayList 
     * @return ListNode类
     */
    public ListNode mergeKLists (ArrayList<ListNode> lists) {
        // write code here
        //递归
        return mergeList(lists, 0 ,lists.size() - 1);

    }
    private ListNode mergeList(ArrayList<ListNode> lists, int left, int right) {
        //basecase
        if(left == right){
            return lists.get(left);
        }
        if(left > right){
            return null;
        }
        int mid = left + ((right - left)>>1);

        return merge(mergeList(lists, left, mid),mergeList(lists, mid + 1,right));
    }
    //合并两个有序链表
    private ListNode merge(ListNode list1, ListNode list2){
        //basecase
        if(list1 == null){
            return list2;
        }if(list2 == null){
            return list1;
        }
        if(list1.val < list2.val){
            list1.next = merge(list1.next, list2);
            return list1;
        }else{
            list2.next = merge(list1, list2.next);
            return list2;
        }   
    }
}

6、BM6 判断链表中是否有环

在这里插入图片描述

思路步骤:

我们都知道链表不像二叉树,每个节点只有一个val值和一个next指针,也就是说一个节点只能有一个指针指向下一个节点,不能有两个指针,那这时我们就可以说一个性质:环形链表的环一定在末尾,末尾没有NULL了。为什么这样说呢?仔细看上图,在环2,0,-4中,没有任何一个节点可以指针指出环,它们只能在环内不断循环,因此环后面不可能还有一条尾巴。如果是普通线形链表末尾一定有NULL,那我们可以根据链表中是否有NULL判断是不是有环。

但是,环形链表遍历过程中会不断循环,线形链表遍历到NULL结束了,但是环形链表何时能结束呢?我们可以用双指针技巧,同向访问的双指针,速度是快慢的,只要有环,二者就会在环内不断循环,且因为有速度差异,二者一定会相遇。

具体做法:

  • step 1:设置快慢两个指针,初始都指向链表头。
  • step 2:遍历链表,快指针每次走两步,慢指针每次走一步。
  • step 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。
  • step 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。
public class Solution {
    public boolean hasCycle(ListNode head) {
        /**
        快慢指针
         */
        //先判断链表为空的情况
        if(head == null){
            return false;
        }
         //快慢双指针
         ListNode fast = head;
         ListNode slow = head;
         //如果没环快指针会先到链表尾
         while(fast != null && fast.next != null){
            //快指针移动两步
            fast = fast.next.next;

            slow = slow.next;
            //相遇则有环
            if(fast == slow){
                return true;
            }
            
         }
         //到末尾则没有环
         return false;
    }
}

7、BM7 链表中环的入口结点

在这里插入图片描述

题目描述
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

思路步骤:

  1. 这题我们可以采用双指针解法,一快一慢指针。快指针每次跑两个element,慢指针每次跑一个。如果存在一个圈,总有一天,快指针是能追上慢指针的。
  2. 如下图所示,我们先找到快慢指针相遇的点,p。我们再假设,环的入口在点q,从头节点到点q距离为A,q p两点间距离为B,p q两点间距离为C。
  3. 因为快指针是慢指针的两倍速,且他们在p点相遇,则我们可以得到等式 2(A+B) = A+B+C+B. (感谢评论区大佬们的改正,此处应为:如果环前面的链表很长,而环短,那么快指针进入环以后可能转了好几圈(假设为n圈)才和慢指针相遇。但无论如何,慢指针在进入环的第一圈的时候就会和快的相遇。等式应更正为 2(A+B)= A+ nB + (n-1)C)
  4. 由3的等式,我们可得,C = A。
  5. 这时,因为我们的slow指针已经在p,我们可以新建一个另外的指针,slow2,让他从头节点开始走,每次只走下一个,原slow指针继续保持原来的走法,和slow2同样,每次只走下一个。
  6. 我们期待着slow2和原slow指针的相遇,因为我们知道A=C,所以当他们相遇的点,一定是q了。
  7. 我们返回slow2或者slow任意一个节点即可,因为此刻他们指向的是同一个节点,即环的起始点,q。

在这里插入图片描述

public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        /**
        快慢指针
         */
        //basecase
        if(pHead == null && pHead.next == null){
            return pHead;
        }
        //定义两个指针
        ListNode fast = pHead;
        ListNode slow = pHead;
        
        while (fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                ListNode slow2 = pHead;
                while(slow2 != slow){
                    slow2 = slow2.next;
                    slow = slow.next;
                }
                return slow;
            }
        }
        return null;
    }
}

8、BM8 链表中倒数最后k个结点

在这里插入图片描述

思路步骤:

这题要求链表的倒数第k个节点,最简单的方式就是使用两个指针

第一个指针先移动k步,然后第二个指针再从头开始,这个时候这两个指针同时移动,当第一个指针到链表的末尾的时候,返回第二个指针即可

注意,如果第一个指针还没走k步的时候链表就为空了,我们直接返回null即可。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    public ListNode FindKthToTail (ListNode pHead, int k) {
        // write code here
        //双指针
        //basecase
        if(pHead == null){
            return pHead;
        }
        //首先定义两个指针
        ListNode first = pHead;
        ListNode second = pHead;
        //让first指针先走k步
        while(k > 0){
            //如果first还没走到k步链表就为空,直接返回null
            if(first == null){
                return null;
            }
            first = first.next;
            k--;
        }
        //然后两个指针一起走
        while(first != null){
            first = first.next;
            second = second.next;
        }
        //返回second节点
        return second;
    }
}

9、BM9 删除链表的倒数第n个节点

在这里插入图片描述

思路步骤:

我们可以使用两个指针,一个指针fast先走n步,然后另一个指针slow从头结点开始,找到要删除结点的前一个结点,这样就可以完成结点的删除了。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @param n int整型 
     * @return ListNode类
     */
    public ListNode removeNthFromEnd (ListNode head, int n) {
        // 双指针解法
        // 首先定义两个指针
        ListNode fast = head;
        ListNode slow = head;
        //fast要移动n步slow才能移动
        for(int i = 0; i < n; i++){
            fast = fast.next;
        }
        //basecase
        if(fast == null){
            return head.next;
        }
        //开始走啦
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        //现在slow走到了n的前一个节点,直接跳过n节点,链接到下一个节点
        slow.next = slow.next.next;

        //返回head
        return head;
    }
}

10、BM10 两个链表的第一个公共结点

在这里插入图片描述

思路步骤:

我们准备两个指针分别从两个链表头同时出发,每次都往后一步,遇到末尾就连到另一个链表的头部,这样相当于每个指针都遍历了这个交叉链表的所有结点,那么它们相遇的地方一定是交叉的地方,即第一个公共结点。

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        //首先定义两个指针
        ListNode first = pHead1;
        ListNode second = pHead2;
        //当两个指针不相遇的时候就一直遍历
        while(first != second){
            first = (first == null )? pHead2 : first.next;
            second = (second == null)? pHead1 : second.next;
        }
        //返回任意一个
        return first;
    }
}

11、BM11 链表相加(二)

在这里插入图片描述

思路步骤:

既然链表每个节点表示数字的每一位,那相加的时候自然可以按照加法法则,从后往前依次相加。但是,链表是没有办法逆序访问的,这是我们要面对第一只拦路虎。解决它也很简单,既然从后往前不行,那从前往后总是可行的吧,将两个链表反转一 下,即可得到个十百千……各个数字从前往后的排列,相加结果也是个位在前,怎么办?再次反转,结果不就正常了。

具体做法:

  • step 1:任意一个链表为空,返回另一个链表就行了,因为链表为空相当于0,0加任何数为0,包括另一个加数为0的情况。
  • step 2:相继反转两个待相加的链表。
  • step 3:设置返回链表的链表头,设置进位carry=0.
  • step 4:从头开始遍历两个链表,直到两个链表节点都为空且carry也不为1. 每次取出不为空的链表节点值,为空就设置为0,将两个数字与carry相加,然后查看是否进位,将进位后的结果(对10取模)加入新的链表节点,连接在返回链表后面,并继续往后遍历。
  • step 5:返回前将结果链表再反转回来。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param head1 ListNode类
     * @param head2 ListNode类
     * @return ListNode类
     */

    public ListNode addInList (ListNode head1, ListNode head2) {
        //basecase
        //任意一个链表为空,直接返回另外一个
        if (head1 == null)
            return head2;
        if (head2 == null)
            return head1;
        //反转两个链表
        head1 = reverseList(head1);
        head2 = reverseList(head2);
        //添加一个表头
        ListNode dummy = new ListNode(-1);
        ListNode head = dummy;
        //定义一个变量用来存放是否要进位
        int carry = 0;
        //只要某个链表还有或者进位还有
        while (head1 != null || head2 != null || carry != 0) {
            //链表不为空则取其值
            int val1 = head1 == null ? 0 : head1.val;
            int val2 = head2 == null ? 0 : head2.val;
            //相加
            int temp = val1 + val2 + carry;
            //获取进位
            carry = temp / 10;
            //获取结果值
            temp %= 10;
            //添加元素
            head.next = new ListNode(temp);
            head = head.next;
            //移动下一个
            if (head1 != null) {
                head1 = head1.next;
            }
            if (head2 != null) {
                head2 = head2.next;
            }

        }
        //结果反转回来
        return reverseList(dummy.next);

    }
    //反转两个链表
    public ListNode reverseList(ListNode head) {
        //basecase
        if (head == null) {
            return null;
        }
        //定义两个节点
        ListNode pre = null;
        ListNode cur = head;
        //反转链表
        while (cur != null) {
            //断开链表,要记录后续一个
            ListNode temp = cur.next;
            //当前的next指向前一个
            cur.next = pre;
            //前一个更新为当前
            pre = cur;
            //当前记录为刚刚记录的后一个
            cur = temp;
        }
        return pre;
    }
}

12、BM12 单链表的排序

在这里插入图片描述

思路步骤:

堆排序应该是最简单直观的,且时间复杂度和空间复杂度都符合题目要求。注意点就是最后一个节点的next指针要设置为null,否则可能会出现环形链表的情况

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 the head node
     * @return ListNode类
     */
    public ListNode sortInList (ListNode head) {
        // 堆排序
        PriorityQueue<ListNode> heap = new PriorityQueue<>((n1, n2) -> n1.val - n2.val);
        while(head != null){
            //将元素全部丢到堆里
            heap.add(head);
            //链表后移
            head = head.next;
        }
        //定义一个虚拟头节点
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        //将堆里面的数据全部拿出来
        while(!heap.isEmpty()){
            cur.next = heap.poll();
            cur = cur.next;
        }
        cur.next = null;
        return dummy.next;
    }
}

13、BM13 判断一个链表是否为回文结构

在这里插入图片描述

思路步骤:

这题是让判断链表是否是回文链表,所谓的回文链表就是以链表中间为中心点两边对称。我们常见的有判断一个字符串是否是回文字符串,这个比较简单,可以使用两个指针,一个最左边一个最右边,两个指针同时往中间靠,判断所指的字符是否相等

但这题判断的是链表,因为这里是单向链表,只能从前往后访问,不能从后往前访问,所以使用判断字符串的那种方式是行不通的。但我们可以通过找到链表的中间节点然后把链表后半部分反转最后再用后半部分反转的链表和前半部分一个个比较即可

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 the head
     * @return bool布尔型
     */
    public boolean isPail (ListNode head) {
        //快慢指针
        //首先定义两个指针
        ListNode fast =  head;
        ListNode slow =  head;
        //接着走,直到快指针为null,并且快指针的下一个为null
        while(fast != null && fast.next != null){
            //快指针走两步,满指针走一步
            fast = fast.next.next;
            slow = slow.next;
        }
        //链表是奇数的情况下,将慢指针再往后走一步
        if(fast != null){
            slow = slow.next;
        }
        //然后反转慢指针所在的往后的链表
        slow = reverseList(slow);
        //将快指针指向头节点
        fast = head;
        //将快指针和慢指针对应的值进行比较
        while(slow != null){
            //若是值不相等,返回false
            if(fast.val != slow.val){
                return false;
            }
            //快慢指针各走一步
            fast = fast.next;
            slow = slow.next;
        }
        //返回true
        return true;

    }
    //反转链表
    private ListNode reverseList(ListNode head){
        //要是这个链表为空,直接返回这个链表
        if(head == null){
            return head;
        }
        //然后定义两个指针开始反转
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        //返回pre
        return pre;
    } 
}

14、BM14 链表的奇偶重排

在这里插入图片描述

思路步骤:

如下图所示,第一个节点是奇数位,第二个节点是偶数,第二个节点后又是奇数位,因此可以断掉节点1和节点2之间的连接,指向节点2的后面即节点3,如红色箭头。如果此时我们将第一个节点指向第三个节点,就可以得到那么第三个节点后为偶数节点,因此我们又可以断掉节点2到节点3之间的连接,指向节点3后一个节点即节点4,如蓝色箭头。那么我们再将第二个节点指向第四个节点,又回到刚刚到情况了。

//odd连接even的后一个,即奇数位
odd.next = even.next; 
//odd进入后一个奇数位
odd = odd.next; 
//even连接后一个奇数的后一位,即偶数位
even.next = odd.next; 
//even进入后一个偶数位
even = even.next; 

这样我们就可以使用了两个同方向访问指针遍历解决这道题。

alt

具体做法:

  • step 1:判断空链表的情况,如果链表为空,不用重排。
  • step 2:使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。
  • step 3:上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。
  • step 4:将偶数节点头接在奇数最后一个节点后,再返回头部。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @return ListNode类
     */
    public ListNode oddEvenList (ListNode head) {
        // 判断空链表的情况,如果链表为空,直接返回
        if(head == null){
            return head;
        }
        //使用双指针odd和even分别遍历奇数节点和偶数节点
        //首次定义两个指针
        ListNode odd = head;
        ListNode even = head.next;
        //给偶数节点链表一个头
        ListNode evenhead = even;
        //开始遍历
        while(even != null && even.next != null){
            //odd连接even的后一个,即奇数位
            odd.next = even.next;
            //odd向后走一位
            odd = odd.next;
            //even连接odd的后一个,即偶数位
            even.next = odd.next;
            //even向后走一位
            even = even.next;

        }
        //even整体接在odd后面
        odd.next = evenhead;
        return head;
    }
}

15、BM15 删除有序链表中重复的元素-I

在这里插入图片描述

思路步骤:

既然连续相同的元素只留下一个,我们留下哪一个最好呢?当然是遇到的第一个元素了!

if(cur.val == cur.next.val) 
    cur.next = cur.next.next;

因为第一个元素直接就与前面的链表节点连接好了,前面就不用管了,只需要跳过后面重复的元素,连接第一个不重复的元素就可以了,在链表中连接后面的元素总比连接前面的元素更方便嘛,因为不能逆序访问。

具体做法:

  • step 1:判断链表是否为空链表,空链表不处理直接返回。
  • step 2:使用一个指针遍历链表,如果指针当前节点与下一个节点的值相同,我们就跳过下一个节点,当前节点直接连接下个节点的后一位。
  • step 3:如果当前节点与下一个节点值不同,继续往后遍历。
  • step 4:循环过程中每次用到了两个节点值,要检查连续两个节点是否为空。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @return ListNode类
     */
    public ListNode deleteDuplicates (ListNode head) {
        // write code here
        /**
        *   当发现有相同的结点则删除,cur.next = cur.next.next
        *   其他情况下,继续循环:cur = cur.next
         */
        //basecase
        if(head == null){
            return null;
        }
        //遍历指针
        ListNode cur = head;
        //遍历链表
        while(cur != null && cur.next != null){
            
            if(cur.next != null && cur.val == cur.next.val){
                //跳过那个节点
                cur.next = cur.next.next;
            }else{
                //否则指针正常遍历
                cur = cur.next;
            }
        }
        return head;
    }
}

16、BM16 删除有序链表中重复的元素-II

在这里插入图片描述

题目描述:

  • 在一个非降序的链表中,存在重复的节点,删除该链表中重复的节点
  • 重复的节点一个元素也不保留

思路步骤:

这是一个升序链表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。

//遇到相邻两个节点值相同
if(cur.next.val == cur.next.next.val){ 
    int temp = cur.next.val;
    //将所有相同的都跳过
    while (cur.next != null && cur.next.val == temp) 
        cur.next = cur.next.next;
}

具体做法:

  • step 1:给链表前加上表头,方便可能的话删除第一个节点。
ListNode res = new ListNode(0);
//在链表前加一个表头
res.next = head; 
  • step 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。
  • step 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
  • step 4:返回时去掉添加的表头。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @return ListNode类
     */
    public ListNode deleteDuplicates (ListNode head) {
        // write code here
        //空链表
        if(head == null){
            return null;
        }
        ListNode res = new ListNode(0);
        //在链表前加一个表头
        res.next = head;

        ListNode cur = res;
        //开始遍历
        while(cur.next != null && cur.next.next != null){
            //遇到相邻两个节点值相同
            if(cur.next.val == cur.next.next.val){
                //将cur.next.val存到一个临时变量
                int temp = cur.next.val;
                //将所有相同的都跳过
                while(cur.next != null && cur.next.val == temp){
                    cur.next = cur.next.next;
                }
            }else{
                //指针往后
                cur = cur.next;
            }
        }
        //返回时去掉表头
        return res.next;

    }
}

17、BM17 二分查找-I

在这里插入图片描述

思路步骤:

  • step 1:从数组首尾开始,每次取中点值。
  • step 2:如果中间值等于目标即找到了,可返回下标,如果中点值大于目标,说明中点以后的都大于目标,因此目标在中点左半区间,如果中点值小于目标,则相反。
  • step 3:根据比较进入对应的区间,直到区间左右端相遇,意味着没有找到。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @param target int整型 
     * @return int整型
     */
    public int search (int[] nums, int target) {
        // 二分查找
        int l = 0;
        int r = nums.length - 1;
        //从数组首尾开始,直到二者相遇
        while(l <= r){
            //每次检查中点的值
            int m = (l + r) / 2;
            if(nums[m] == target){
                return m;
            }
            //进入左的区间
            if(nums[m] > target){
                r = m - 1;
            }else{
                l = m + 1;
            }
        }
        //未找到
        return -1;
    }
}

18、BM18 二维数组中的查找

在这里插入图片描述

思路步骤:

似乎我们可以直接从上到下遍历矩阵,再从左到右遍历矩阵每一行,然后检验目标值是否是遇到的元素。

//两层循环,遍历二维数组
for(int i = 0; i < n; i++)  
    for(int j = 0; j < m; j++)
        //找到target
        if(array[i][j] == target)  
            return true;

但是我们这样就没有利用到矩阵内部的行列都是有序这个性质,我们再来找找规律:

首先看四个角,左上与右下必定为最小值与最大值,而左下与右上就有规律了:左下元素大于它上方的元素,小于它右方的元素,右上元素与之相反。既然左下角元素有这么一种规律,相当于将要查找的部分分成了一个大区间和小区间,每次与左下角元素比较,我们就知道目标值应该在哪部分中,于是可以利用分治思维来做。

具体做法:

  • step 1:首先获取矩阵的两个边长,判断特殊情况。
  • step 2:首先以左下角为起点,若是它小于目标元素,则往右移动去找大的,若是他大于目标元素,则往上移动去找小的。
  • step 3:若是移动到了矩阵边界也没找到,说明矩阵中不存在目标值。

图示:

图片说明

public class Solution {

    public boolean Find (int target, int[][] array) {
        // 二分查找
        //basecase
        //首先判断矩阵的两个边长
        if(array.length == 0){
            return false;
        }
        if(array[0].length == 0){
            return false;
        }
        //定义两个变量
        int m = array.length;
        int n = array[0].length;
        
        //从最左下角开始往右或者往上找
        for(int i = 0, j = n -1; i < m && j >= 0;){
            //小于目标值,开始往右找
            if(array[i][j] < target){
                i++;
            }else if(array[i][j] > target){
                j--;
            }else{
                return true;
            }
        }
        return false;  
    }
}

19、BM19 寻找峰值

在这里插入图片描述

思路:

因为题目将数组边界看成最小值,而我们只需要找到其中一个波峰,因此只要不断地往高处走,一定会有波峰。那我们可以每次找一个标杆元素,将数组分成两个区间,每次就较高的一边走,因此也可以用分治来解决,而标杆元素可以选择区间中点。

//右边是往下,不一定有坡峰
if(nums[mid] > nums[mid + 1])
    right = mid;
//右边是往上,一定能找到波峰
else
    left = mid + 1;

具体做法:

  • step 1:二分查找首先从数组首尾开始,每次取中间值,直到首尾相遇。
  • step 2:如果中间值的元素大于它右边的元素,说明往右是向下,我们不一定会遇到波峰,但是那就往左收缩区间。
  • step 3:如果中间值大于右边的元素,说明此时往右是向上,向上一定能有波峰,那我们往右收缩区间。
  • step 4:最后区间收尾相遇的点一定就是波峰。

图示:

alt

public class Solution {

    public int findPeakElement (int[] nums) {
        //关键思想:下坡的时候可能找到波峰,但是可能找不到,一直向下走的
        //上坡的时候一定能找到波峰,因为题目给出的是nums[-1] = nums[n] = -∞
        int l = 0;
        int r = nums.length - 1;
        while(l < r){
             //int mid = l + ((l - r)>>1);
             int mid = (l + r) / 2;
             //右边是往下,不一定有波峰
             if(nums[mid] > nums[mid + 1]){
                r = mid;
             }else{
                l = mid + 1;
             }   
        }
        return r; 
    }
}

20、BM20 数组中的逆序对

在这里插入图片描述

思路步骤

那么,我们先来说说归并算法吧,归并算法讲究一个先分后并!

先分:分呢,就是将数组分为两个子数组,两个子数组分为四个子数组,依次向下分,直到数组不能再分为止!

后并:并呢,就是从最小的数组按照顺序合并,从小到大或从大到小,依次向上合并,最后得到合并完的顺序数组!

介绍完归并排序,我们来说说归并统计法,我们要在哪个步骤去进行统计呢?

归并统计法,关键点在于合并环节,在合并数组的时候,当发现右边的小于左边的时候,此时可以直接求出当前产生的逆序对的个数。

举个例子:

在合并 {4 ,5} {1 , 2} 的时候,首先我们判断 1 < 4,我们即可统计出逆序对为2,为什么呢?这利用了数组的部分有序性。因为我们知道 {4 ,5} 这个数组必然是有序的,因为是合并上来的。此时当 1比4小的时候,证明4以后的数也都比1大,此时就构成了从4开始到 {4,5}这个数组结束,这么多个逆序对(2个),此时利用一个临时数组,将1存放起来,接着比较2和4的大小,同样可以得到有2个逆序对,于是将2也放进临时数组中,此时右边数组已经完全没有元素了,则将左边剩余的元素全部放进临时元素中,最后将临时数组中的元素放进原数组对应的位置。

最后接着向上合并~

可以看到下面这张图~

image-20210623223031128

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param nums int整型一维数组
     * @return int整型
     */
    int count = 0;
    public int InversePairs (int[] nums) {
        // 归并排序
        //长度小于2则无逆序对
        if (nums == null || nums.length < 2) {
            return 0;
        }
        //将数组分为左右两部分进行排序
        mergeSort(nums, 0, nums.length - 1);
        return count;

    }
    private void mergeSort(int[] arr, int L, int R) {
        //basecase
        //如果L下标和R下标重合,那也说明有序
        if (L == R) {
            return;
        }
        //找分割点
        int mid = L + ((R - L) >> 1);
        //将 左边部分排好序
        mergeSort(arr, L, mid);
        //将 右边部分排好序
        mergeSort(arr, mid + 1, R);
        //merge,将两部分排好序的数组进行整体排序
        merge(arr, L, mid, R);

    }
    public void merge(int[] arr, int L, int mid, int R) {
        //首先定义一个help数组,长度为此时两个子数组加起来的长度
        int[] help = new int[R - L + 1];
        //临时数组的下标起点
        int i = 0;
        //定义两个指针

        int p1 = L;

        int p2 = mid + 1;

        //如果这两个数组都没有越界
        while (p1 <= mid && p2 <= R) {
            // 当左子数组的当前元素小的时候,跳过,无逆序对
            if (arr[p1] <= arr[p2]) {
                // 放入临时数组
                help[i++] = arr[p1++];
            } else { // 否则,此时存在逆序对
                // 放入临时数组
                help[i++] = arr[p2++];
                // 逆序对的个数为    左子数组的终点- 当前左子数组的当前指针
                count += mid - p1 + 1;
                count %= 1000000007;
            }
            //help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        //如果左边的数组没有越界
        while (p1 <= mid) {
            //将左边数组的值直接拷贝到hele数组里面
            help[i++] = arr[p1++];
        }
        //如果右边的数组没有越界
        while (p2 <= R) {
            //将右边数组的值直接拷贝到hele数组里面
            help[i++] = arr[p2++];
        }
        //经过上面的步骤,help数组有序啦,拷贝到arr数组就完成啦
        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }
}

21、BM21 旋转数组的最小数字

在这里插入图片描述

解题思路:

排序数组的查找问题首先考虑使用 二分法 解决,其可将 遍历法 的 线性级别 时间复杂度降低至 对数级别

算法流程:

1、初始化: 声明 i, j 双指针分别指向 array 数组左右两端

2、循环二分: 设 m = (i + j) / 2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m1、当 array[m] > array[j] 时: m 一定在 左排序数组 中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1

3、当 array[m] < array[j] 时: m 一定在 右排序数组 中,即旋转点 x 一定在[i, m]闭区间内,因此执行 j = m

4、当 array[m] = array[j] 时: 无法判断 mm 在哪个排序数组中,即无法判断旋转点 x 在 [i, m] 还是 [m + 1, j] 区间中。

​ 解决方案: 执行 j = j - 1 缩小判断范围

5、返回值: 当 i = j 时跳出二分循环,并返回 旋转点的值 array[i] 即可。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int minNumberInRotateArray (int[] nums) {
        // basecase
        if(nums.length == 0){
            return 0;
        }
        //左右指针
        int l = 0 , r = nums.length - 1;

        //循环
        while(l < r){
            //找到数组中的重点m
            int m = l + (r - l)/2;
            // m在左排序数组中,旋转点在 [m+1, r] 中
            if(nums[m] > nums[r]){
                l = m + 1;
            // m 在右排序数组中,旋转点在 [l, m]中    
            }else if(nums[m] < nums[r]){
                r = m;
            }else{
                // 缩小范围继续判断
                r--;
            }

        }
        // 返回旋转点
        return nums[l];
    }
}

22、BM22 比较版本号

在这里插入图片描述

思路:

既然是比较两个字符串每个点之间的数字是否相同,就直接同时遍历字符串比较,因此我们需要使用两个同向访问的指针各自访问一个字符串。

比较的时候,数字前导零不便于我们比较,因为我们不知道后面会出现多少前导零,因此应该将点之间的部分转化为数字再比较才方便。

while(i < n1 && version1[i] != '.'){ 
    num1 = num1 * 10 + (version1[i] - '0');
    i++;
}

具体做法:

  • step 1:利用两个指针表示字符串的下标,分别遍历两个字符串。
  • step 2:每次截取点之前的数字字符组成数字,即在遇到一个点之前,直接取数字,加在前面数字乘10的后面。(因为int会溢出,这里采用long记录数字)
  • step 3:然后比较两个数字大小,根据大小关系返回1或者-1,如果全部比较完都无法比较出大小关系,则返回0
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 比较版本号
     * @param version1 string字符串 
     * @param version2 string字符串 
     * @return int整型
     */
    public int compare (String version1, String version2) {
        // write code here
        int n1 = version1.length();
        int n2 = version2.length();

        int i = 0, j = 0;
        //遍历直到某个字符串结束
        while(i < n1 || j < n2){
            long num1 = 0;
            //从下一个点前截取数字
            while(i < n1 && version1.charAt(i) != '.'){
                num1 = num1 * 10 + (version1.charAt(i) - '0');
                i++;
            }
            //跳过点
            i++;
            long num2 = 0;
            //从下一个点前截取数字
            while(j < n2 && version2.charAt(j) != '.'){
                num2 = num2 * 10 + (version2.charAt(j) - '0');
                j++;
            }
            //跳过点
            j++;
            //比较数字大小
            if(num1 > num2){
                return 1;
            }
            if(num1 < num2){
                return -1;
            }
            
        }
        //版本号相同
        return 0;
    }
}

23、BM23 二叉树的前序遍历

在这里插入图片描述

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型一维数组
     */
    public int[] preorderTraversal (TreeNode root) {
        //前序遍历  遵循根--->左--->右
        //创建一个集合来存储遍历的结点值
        List<Integer> list = new ArrayList<>();
        helper(root, list);

        int[] res = new int[list.size()];
        for (int i = 0; i < res.length; i++) {
            //将集合中的元素转存到数组中
            res[i] = list.get(i);
        }
        //将数组返回
        return res;
    }
    private void helper(TreeNode node, List<Integer> list) {
        if (node == null) {
            return;
        }
        //存入结点
        list.add(node.val);
        //遍历左树
        helper(node.left, list);
        //遍历右树
        helper(node.right, list);
    }
}

24、BM24 二叉树的中序遍历

在这里插入图片描述

思路:

什么是二叉树的中序遍历,简单来说就是“左根右”,展开来说就是对于一棵二叉树,我们优先访问它的左子树,等到左子树全部节点都访问完毕,再访问根节点,最后访问右子树。同时访问子树的时候,顺序也与访问整棵树相同。

从上述对于中序遍历的解释中,我们不难发现它存在递归的子问题,根节点的左右子树访问方式与原本的树相同,可以看成一颗树进行中序遍历,因此可以用递归处理:

  • 终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。
  • 返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。
  • 本级任务: 每个子问题优先访问左子树的子问题,等到左子树的结果返回后,再访问自己的根节点,然后进入右子树。

具体做法:

  • step 1:准备数组用来记录遍历到的节点值,Java可以用List,C++可以直接用vector。
  • step 2:从根节点开始进入递归,遇到空节点就返回,否则优先进入左子树进行递归访问。
  • step 3:左子树访问完毕再回到根节点访问。
  • step 4:最后进入根节点的右子树进行递归。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型一维数组
     */
    public int[] inorderTraversal (TreeNode root) {
        // write code here
        /**
        递归法
        */
        //中序遍历  遵循左--->根--->右
        //创建一个集合来存储遍历的结点值
        List<Integer> list = new ArrayList<>();
        helper(root, list);

        int[] res = new int[list.size()];
        for (int i = 0; i < res.length; i++) {
            //将集合中的元素转存到数组中
            res[i] = list.get(i);
        }
        //将数组返回
        return res;
    }
    private void helper(TreeNode node, List<Integer> list) {
        if (node == null) {
            return;
        }
        //遍历左树
        helper(node.left, list);
        //存入结点
        list.add(node.val);
        //遍历右树
        helper(node.right, list);
    }
}

25、BM25 二叉树的后序遍历

在这里插入图片描述

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型一维数组
     */
    public int[] postorderTraversal (TreeNode root) {
        // write code here
        //后序遍历  遵循左--->右--->根
        //创建一个集合来存储遍历的结点值
        List<Integer> list = new ArrayList<>();
        helper(root, list);

        int[] res = new int[list.size()];
        for (int i = 0; i < res.length; i++) {
            //将集合中的元素转存到数组中
            res[i] = list.get(i);
        }
        //将数组返回
        return res;
    }
    private void helper(TreeNode node, List<Integer> list) {
        //basecase
        if (node == null) {
            return;
        }

        helper(node.left, list);
        helper(node.right, list);
        list.add(node.val);

    }
}

26、BM26 求二叉树的层序遍历

在这里插入图片描述

BFS广度优先遍历

思路:

二叉树的层次遍历就是按照从上到下每行,然后每行中从左到右依次遍历,得到的二叉树的元素值。对于层次遍历,我们通常会使用队列来辅助:

因为队列是一种先进先出的数据结构,我们依照它的性质,如果从左到右访问完一行节点,并在访问的时候依次把它们的子节点加入队列,那么它们的子节点也是从左到右的次序,且排在本行节点的后面,因此队列中出现的顺序正好也是从左到右,正好符合层次遍历的特点。

具体做法:

  • step 1:首先判断二叉树是否为空,空树没有遍历结果。
  • step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面。
  • step 3:每次进入一层,统计队列中元素的个数。因为每当访问完一层,下一层作为这一层的子节点,一定都加入队列,而再下一层还没有加入,因此此时队列中的元素个数就是这一层的元素个数。
  • step 4:每次遍历这一层这么多的节点数,将其依次从队列中弹出,然后加入这一行的一维数组中,如果它们有子节点,依次加入队列排队等待访问。
  • step 5:访问完这一层的元素后,将这个一维数组加入二维数组中,再访问下一层。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型ArrayList>
     */
    public ArrayList<ArrayList<Integer>> levelOrder (TreeNode root) {
        //BFS 广度优先遍历
        //通常与队列一起配合
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        //basecase
        if (root == null) {
            return res;
        }
        //定义一个队列
        Queue<TreeNode> q = new LinkedList<>();
        //将root 先丢到队列里
        q.add(root);
        //开启新的层
        while (q.size() > 0) {
            //维护一个size用来记录队列中元素的数量
            //每次循环都更新size
            int size = q.size();
            //定义一个List用来记录二叉树的某一行
            ArrayList<Integer> list = new ArrayList<>();
            //当前层的元素
            while (size > 0) {
                //将队列中的元素取出来
                TreeNode  cur = q.poll();
                //存到List
                list.add(cur.val);
                //判断取出的这个元素有没有左右孩子
                if (cur.left != null) {
                    //将左孩子加到队列
                    q.add(cur.left);
                }
                if (cur.right != null) {
                    q.add(cur.right);
                }
                size--;
            }
            res.add(new ArrayList<>(list));
        }
        return res;
    }
}

DFS深度优先遍历

思路:

既然二叉树的前序、中序、后序遍历都可以轻松用递归实现,树型结构本来就是递归喜欢的形式,那我们的层次遍历是不是也可以尝试用递归来试试呢?

按行遍历的关键是每一行的深度对应了它输出在二维数组中的深度,即深度可以与二维数组的下标对应,那我们可以在递归的访问每个节点的时候记录深度:

dfs(root, res, 0);

进入子节点则深度加1:

//递归左右时深度记得加1
dfs(node.left, res, level + 1);
dfs(node.right, res, level + 1);

每个节点值放入二维数组相应行。

res.get(level).add(node.val);

因此可以用递归实现:

  • 终止条件: 遍历到了空节点,就不再继续,返回。
  • 返回值: 将加入的输出数组中的结果往上返回。
  • 本级任务: 处理按照上述思路处理非空节点,并进入该节点的子节点作为子问题。

具体做法:

  • step 1:首先判断二叉树是否为空,空树没有遍历结果。
  • step 2:使用递归进行层次遍历输出,每次递归记录当前二叉树的深度,每当遍历到一个节点,如果为空直接返回。
  • step 3:如果遍历的节点不为空,输出二维数组中一维数组的个数(即代表了输出的行数)小于深度,说明这个节点应该是新的一层,我们在二维数组中增加一个一维数组,然后再加入二叉树元素。
  • step 4:如果不是step 3的情况说明这个深度我们已经有了数组,直接根据深度作为下标取出数组,将元素加在最后就可以了。
  • step 5:处理完这个节点,再依次递归进入左右节点,同时深度增加。因为我们进入递归的时候是先左后右,那么遍历的时候也是先左后右,正好是层次遍历的顺序。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型ArrayList>
     */
    public ArrayList<ArrayList<Integer>> levelOrder (TreeNode root) {
        //DFS 深度优先遍历
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        //basecase
        if (root == null) {
            return res;
        }
        dfs(root, res, 0);
        return res;

    }
    private void dfs(TreeNode node, ArrayList<ArrayList<Integer>> res, int level) {
        //递归退出的条件
        if (node == null) {
            return;
        }
        //当前层级大于数组大小
        if (level > res.size() - 1) {
            //就需要加一个空的数组,才能存放元素
            res.add(new ArrayList<>());
        }
        res.get(level).add(node.val);

        if (node.left != null) {
            dfs(node.left, res, level + 1);
        }
        if (node.right != null) {
            dfs(node.right, res, level + 1);
        }
    }
}

27、BM27 按之字形顺序打印二叉树

在这里插入图片描述

思路:

按照层次遍历按层打印二叉树的方式,每层分开打印,然后对于每一层利用flag标记,第一层为false,之后每到一层取反一次,如果该层的flag为true,则记录的数组整个反转即可。

//奇数行反转,偶数行不反转
if(flag) 
    reverse(row.begin(), row.end());

但是难点在于如何每层分开存储,从哪里知晓分开的时机?在层次遍历的时候,我们通常会借助队列(queue),事实上,队列中的值大有玄机,让我们一起来看看:当根节点进入队列时,队列长度为1,第一层节点数也为1;若是根节点有两个子节点,push进队列后,队列长度为2,第二层节点数也为2;若是根节点一个子节点,push进队列后,队列长度为为1,第二层节点数也为1。由此,我们可知,每层的节点数等于进入该层时队列长度,因为刚进入该层时,这一层每个节点都会push进队列,而上一层的节点都出去了。

int n = temp.size();
for(int i = 0; i < n; i++){
    //访问一层
}

具体做法:

  • step 1:首先判断二叉树是否为空,空树没有打印结果。
  • step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面,初始化flag变量。
  • step 3:每次进入一层,统计队列中元素的个数,更改flag变量的值。因为每当访问完一层,下一层作为这一层的子节点,一定都加入队列,而再下一层还没有加入,因此此时队列中的元素个数就是这一层的元素个数。
  • step 4:每次遍历这一层这么多的节点数,将其依次从队列中弹出,然后加入这一行的一维数组中,如果它们有子节点,依次加入队列排队等待访问。
  • step 5:访问完这一层的元素后,根据flag变量决定将这个一维数组直接加入二维数组中还是反转后再加入,然后再访问下一层。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pRoot TreeNode类 
     * @return int整型ArrayList>
     */
    public ArrayList<ArrayList<Integer>> Print (TreeNode pRoot) {
        // write code here
        TreeNode head  = pRoot;
        ArrayList<ArrayList<Integer>> res =  new ArrayList<ArrayList<Integer>>();
        if(head == null){
            //返回空list
            return res;
        }
        //队列存储,进行层次遍历
        Queue<TreeNode> temp = new LinkedList<TreeNode>();
        //先把根节点加到队列里面
        temp.offer(head);
        //定义一个变量存放队列中取出来节点
        TreeNode p;
        //定义一个flag变量
        boolean flag = true;
        while(!temp.isEmpty()){
            //定义一个数组记录二叉树的某一行
            ArrayList<Integer> row = new ArrayList<>();
            //维护一个变量记录队列元素的个数
            int n = temp.size();
            //奇数行反转,偶数行不反转
            flag = !flag;
            //因为首先进入的都是根节点,所以每层节点多少,队列大小就是多少
            for(int i = 0; i < n; i++){
                //将队列中的节点取出来
                p = temp.poll();
                //加到存放行的列表
                row.add(p.val);
                //若是左右孩子存在,则存入左右孩子作为下一个层次
                if(p.left != null){
                    temp.offer(p.left);
                }
                if(p.right != null){
                    temp.offer(p.right);
                }

            }
            //奇数行反转,偶数行不反转
            if(flag){
                Collections.reverse(row);
            }
            res.add(row);
        }
        return res;
    }
}

28、BM28 二叉树的最大深度

在这里插入图片描述

方法一:递归

二叉树的递归,则是将某个节点的左子树、右子树看成一颗完整的树,那么对于子树的访问或者操作就是对于原树的访问或者操作的子问题,因此可以自我调用函数不断进入子树。

思路:

最大深度是所有叶子节点的深度的最大值,深度是指树的根节点到任一叶子节点路径上节点的数量,因此从根节点每次往下一层深度就会加1。因此二叉树的深度就等于根节点这个1层加上左子树和右子树深度的最大值,而每个子树我们都可以看成一个根节点,继续用上述方法求的深度,于是我们可以对这个问题划为子问题,利用递归来解决:

  • 终止条件: 当进入叶子节点后,再进入子节点,即为空,没有深度可言,返回0.
  • 返回值: 每一级按照上述公式,返回两边子树深度的最大值加上本级的深度,即加1.
  • 本级任务: 每一级的任务就是进入左右子树,求左右子树的深度。

具体做法:

  • step 1:对于每个节点,若是不为空才能累计一次深度,若是为空,返回深度为0.
  • step 2:递归分别计算左子树与右子树的深度。
  • step 3:当前深度为两个子树深度较大值再加1。
import java.util.*;
public class Solution {
    public int maxDepth (TreeNode root) {
        //空节点没有深度
        if(root == null) 
            return 0;
        //返回子树深度+1
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; 
    }
}

方法二:队列

队列是一种仅支持在表尾进行插入操作、在表头进行删除操作的线性表,插入端称为队尾,删除端称为队首,因整体类似排队的队伍而得名。它满足先进先出的性质,元素入队即将新元素加在队列的尾,元素出队即将队首元素取出,它后一个作为新的队首。

思路:

既然是统计二叉树的最大深度,除了根据路径到达从根节点到达最远的叶子节点以外,我们还可以分层统计。对于一棵二叉树而言,必然是一层一层的,那一层就是一个深度,有的层可能会很多节点,有的层如根节点或者最远的叶子节点,只有一个节点,但是不管多少个节点,它们都是一层。因此我们可以使用层次遍历,二叉树的层次遍历就是从上到下按层遍历,每层从左到右,我们只要每层统计层数即是深度。

具体做法:

  • step 1:既然是层次遍历,我们遍历完一层要怎么进入下一层,可以用队列记录这一层中节点的子节点。队列类似栈,只不过是一个先进先出的数据结构,可以理解为我们平时的食堂打饭的排队。因为每层都是按照从左到右开始访问的,那自然记录的子节点也是从左到右,那我们从队列出来的时候也是从左到右,完美契合。
  • step 2:在刚刚进入某一层的时候,队列中的元素个数就是当前层的节点数。比如第一层,根节点先入队,队列中只有一个节点,对应第一层只有一个节点,第一层访问结束后,它的子节点刚好都加入了队列,此时队列中的元素个数就是下一层的节点数。因此遍历的时候,每层开始统计该层个数,然后遍历相应节点数,精准进入下一层。
  • step 3:遍历完一层就可以节点深度就可以加1,直到遍历结束,即可得到最大深度。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @return int整型
     */
    public int maxDepth (TreeNode root) {
        //BFS
        // write code here
        if (root == null) {
            return 0;
        }
        //创建一个队列
        Queue<TreeNode> q = new LinkedList<>();
        //将根节点加入到队列
        q.offer(root);
        //维护一个res记录深度
        int res = 0;
        //如果这个队列不等于空
        while (!q.isEmpty()) {
            //每一层的个数
            int size = q.size();
            while (size-- > 0) {
                //如果这一层的个数大于0,就取出来
                TreeNode cur = q.poll();
                //取出来之后看这个节点是否有左右孩子
                if (cur.left != null)
                    //加入到队列
                    q.offer(cur.left);
                if (cur.right != null)
                    q.offer(cur.right);
            }
            //每一层的个数++
            res++;
        }
        return res;
    }
}

29、BM29 二叉树中和为某一值的路径(一)

在这里插入图片描述

思路步骤:

既然是检查从根到叶子有没有一条等于目标值的路径,那肯定需要从根节点遍历到叶子,我们可以在根节点每次往下一层的时候,将sum减去节点值,最后检查是否完整等于0. 而遍历的方法我们可以选取二叉树常用的递归前序遍历,因为每次进入一个子节点,更新sum值以后,相当于对子树查找有没有等于新目标值的路径,因此这就是子问题,递归的三段式为:

  • 终止条件: 每当遇到节点为空,意味着过了叶子节点,返回。每当检查到某个节点没有子节点,它就是叶子节点,此时sum减去叶子节点值刚好为0,说明找到了路径。
  • 返回值: 将子问题中是否有符合新目标值的路径层层往上返回。
  • 本级任务: 每一级需要检查是否到了叶子节点,如果没有则递归地进入子节点,同时更新sum值减掉本层的节点值。

具体做法:

  • step 1:每次检查遍历到的节点是否为空节点,空节点就没有路径。
  • step 2:再检查遍历到是否为叶子节点,且当前sum值等于节点值,说明可以刚好找到。
  • step 3:检查左右子节点是否可以有完成路径的,如果任意一条路径可以都返回true,因此这里选用两个子节点递归的或。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param root TreeNode类
     * @param sum int整型
     * @return bool布尔型
     */
    public boolean hasPathSum (TreeNode root, int sum) {
        // write code here
        //如果根节点为空,或者叶子节点也遍历完了也没找到这样的结果,就返回false
        if (root == null)
            return false;
        //如果到叶子节点了,并且剩余值等于叶子节点的值,说明找到了这样的结果,直接返回true
        if (root.left == null && root.right == null && sum - root.val == 0)
            return true;
        //分别沿着左右子节点走下去,然后顺便把当前节点的值减掉,左右子节点只要有一个返回true,
        //说明存在这样的结果
        return hasPathSum(root.left, sum - root.val) ||
               hasPathSum(root.right, sum - root.val);
    }
}

30、BM30 二叉搜索树与双向链表

在这里插入图片描述

思路:

二叉搜索树最左端的元素一定最小,最右端的元素一定最大,符合“左中右”的特性,因此二叉搜索树的中序遍历就是一个递增序列,我们只要对它中序遍历就可以组装称为递增双向链表。

具体做法:

  • step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre)。
  • step 2:首先递归到最左,初始化head与pre。
  • step 3:然后处理中间根节点,依次连接pre与当前节点,连接后更新pre为当前节点。
  • step 4:最后递归进入右子树,继续处理。
  • step 5:递归出口即是节点为空则返回。
public class Solution {
    //返回的第一个指针,即为最小值,先定为null
    TreeNode head = null;
    //中序遍历当前值的上一位,初值为最小值,先定为null
    TreeNode pre = null;

    public TreeNode Convert(TreeNode pRootOfTree) {

        if (pRootOfTree == null) {
            return null;
        }
        //首先递归到最左最小值
        Convert(pRootOfTree.left);
        //找到最小值,然后初始化head和pre
        if (pre == null) {
            head = pRootOfTree;
            pre = pRootOfTree;
        }
        //当前节点与上一节点建立连接,将pre设置为当前值
        else {
            pre.right = pRootOfTree;
            pRootOfTree.left = pre;
            pre = pRootOfTree;
        }
        Convert(pRootOfTree.right);
        return head;
    }
}

31、BM31 对称的二叉树

在这里插入图片描述

思路:

前序遍历的时候我们采用的是“根左右”的遍历次序,如果这棵二叉树是对称的,即相应的左右节点交换位置完全没有问题,那我们是不是可以尝试“根右左”遍历,按照轴对称图像的性质,这两种次序的遍历结果应该是一样的。

不同的方式遍历两次,将结果拿出来比较看起来是一种可行的方法,但也仅仅可行,太过于麻烦。我们不如在遍历的过程就结果比较了。而遍历方式依据前序递归可以使用递归:

  • 终止条件: 当进入子问题的两个节点都为空,说明都到了叶子节点,且是同步的,因此结束本次子问题,返回true;当进入子问题的两个节点只有一个为空,或是元素值不相等,说明这里的对称不匹配,同样结束本次子问题,返回false。
  • 返回值: 每一级将子问题是否匹配的结果往上传递。
  • 本级任务: 每个子问题,需要按照上述思路,“根左右”走左边的时候“根右左”走右边,“根左右”走右边的时候“根右左”走左边,一起进入子问题,需要两边都是匹配才能对称。

具体做法:

  • step 1:两种方向的前序遍历,同步过程中的当前两个节点,同为空,属于对称的范畴。
  • step 2:当前两个节点只有一个为空或者节点值不相等,已经不是对称的二叉树了。
  • step 3:第一个节点的左子树与第二个节点的右子树同步递归对比,第一个节点的右子树与第二个节点的左子树同步递归比较。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param pRoot TreeNode类
     * @return bool布尔型
     */
    public boolean isSymmetrical (TreeNode pRoot) {
        // write code here

        return recursion(pRoot, pRoot);
    }
    public boolean recursion(TreeNode root1, TreeNode root2) {
        //可以两个都为空,
        if (root1 == null && root2 == null) {
            return true;
        }
        //只有一个为空或者节点值不同,一定不对称
        if (root1 == null || root2 == null || root1.val != root2.val) {
            return false;
        }
        //每层对应的节点进入递归比较
        return recursion(root1.left, root2.right) && recursion(root1.right, root2.left);
    }
}

32、BM32 合并二叉树

在这里插入图片描述

思路:

要将一棵二叉树的节点与另一棵二叉树相加合并,肯定需要遍历两棵二叉树,那我们可以考虑同步遍历两棵二叉树,这样就可以将每次遍历到的值相加在一起。遍历的方式有多种,这里推荐前序递归遍历。

具体做法:

  • step 1:首先判断t1与t2是否为空,若为则用另一个代替,若都为空,返回的值也是空。
  • step 2:然后依据前序遍历的特点,优先访问根节点,将两个根点的值相加创建到新树中。
  • step 3:两棵树再依次同步进入左子树和右子树。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param t1 TreeNode类
     * @param t2 TreeNode类
     * @return TreeNode类
     */
    public TreeNode mergeTrees (TreeNode t1, TreeNode t2) {
        // write code here
        //若只有一个节点返回另一个,两个都为null自然返回null
        if(t1 == null){
            return t2;
        }
        if(t2 == null){
            return t1;
        }
        //根左右的方式递归
        TreeNode head = new TreeNode(t1.val + t2.val);
        head.left = mergeTrees(t1.left, t2.left);
        head.right = mergeTrees(t1.right,t2.right);
        return head;
    }
}

33、BM33 二叉树的镜像

在这里插入图片描述

解题思路:

根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。

解题步骤:

1、特判:如果pRoot为空,返回空

2、交换左右子树
3、把pRoot的左子树放到Mirror中镜像一下
4、把pRoot的右子树放到Mirror中镜像一下
5、返回根节点root

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pRoot TreeNode类 
     * @return TreeNode类
     */
    public TreeNode Mirror (TreeNode pRoot) {
        // write code here
        if(pRoot == null){
            return pRoot;
        }
        //左右子树交换
        TreeNode temp = pRoot.left;
        pRoot.left = pRoot.right;
        pRoot.right = temp;
        //递归左右子树
        Mirror(pRoot.left);
        Mirror(pRoot.right);

        return pRoot;
    }
}

34、BM34 判断是不是二叉搜索树

在这里插入图片描述

思路:

二叉搜索树的特性就是中序遍历是递增序。既然是判断是否是二叉搜索树,那我们可以使用中序递归遍历。只要之前的节点是二叉树搜索树,那么如果当前的节点小于上一个节点值那么就可以向下判断。只不过在过程中我们要求反退出。比如一个链表1->2->3->4,只要for循环遍历如果中间有不是递增的直接返回false即可。

if(root.val < pre)
    return false;

具体做法:

  • step 1:首先递归到最左,初始化maxLeft与pre。
  • step 2:然后往后遍历整棵树,依次连接pre与当前节点,并更新pre。
  • step 3:左子树如果不是二叉搜索树返回false。
  • step 4:判断当前节点是不是小于前置节点,更新前置节点。
  • step 5:最后由右子树的后面节点决定。
public class Solution {
     
    int pre = Integer.MIN_VALUE;
    public boolean isValidBST (TreeNode root) {
        // write code here
        //中序遍历
        if(root == null){
            return true;
        }
        //先进入左子树
        if(!isValidBST(root.left)){
            return false;
        }
        if(root.val < pre){
            return false;
        }
        //更新最值
        pre = root.val;
        //再进入右子树
        return isValidBST(root.right);
    }
}

35、BM35 判断是不是完全二叉树

在这里插入图片描述

思路:

对完全二叉树最重要的定义就是叶子节点只能出现在最下层和次下层,所以我们想到可以使用队列辅助进行层次遍历——从上到下遍历所有层,每层从左到右,只有次下层和最下层才有叶子节点,其他层出现叶子节点就意味着不是完全二叉树。

具体做法:

  • step 1:先判断空树一定是完全二叉树。
  • step 2:初始化一个队列辅助层次遍历,将根节点加入。
  • step 3:逐渐从队列中弹出元素访问节点,如果遇到某个节点为空,进行标记,代表到了完全二叉树的最下层,若是后续还有访问,则说明提前出现了叶子节点,不符合完全二叉树的性质。
  • step 4:否则,继续加入左右子节点进入队列排队,等待访问。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param root TreeNode类 
     * @return bool布尔型
     */
    public boolean isCompleteTree (TreeNode root) {
        // write code here
        //空树一定是完全二叉树
        if(root == null){
            return true;
        }
        //辅助队列
        Queue<TreeNode> q = new LinkedList<>();
        //根节点加入队列
        q.offer(root);

        //从队列中取出的节点
        TreeNode cur;
        //定义一个首次出现的标记位
        boolean notComplete = false;
        while(!q.isEmpty()){
            cur = q.poll();
            //标记第一次遇到空节点
            if(cur == null){
                notComplete = true;
                //退出
                continue;
            }
            //后续访问已经遇到空节点了
            if(notComplete){
                return false;
            }
            q.offer(cur.left);
            q.offer(cur.right);
        }
        return true;
    }
}

36、BM36 判断是不是平衡二叉树

在这里插入图片描述

思路:

从题中给出的有效信息:

  • 左右两个子树的高度差的绝对值不超过1
  • 左右两个子树都是一棵平衡二叉树

故此 首先想到的方法是使用递归的方式判断子节点的状态

具体做法:
如果一个节点的左右子节点都是平衡的,并且左右子节点的深度差不超过 1,则可以确定这个节点就是一颗平衡二叉树。

public class Solution {

    public boolean IsBalanced_Solution (TreeNode pRoot) {
        // write code here
        //递归
        if(pRoot == null){
            return true;
        }
        //判断左右子树是否符合规则,且深度不能超过2
        return IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right) && Math.abs(deep(pRoot.left) - deep(pRoot.right))< 2;
    }
    public int deep(TreeNode root){
        if(root == null){
            return 0;
        }
        return Math.max(deep(root.left), deep(root.right)) + 1;
    }
}

37、BM37 二叉搜索树的最近公共祖先

在这里插入图片描述

思路:

我们可以利用二叉搜索树的性质:对于某一个节点若是p与q都小于等于这个这个节点值,说明p、q都在这个节点的左子树,而最近的公共祖先也一定在这个节点的左子树;若是p与q都大于等于这个节点,说明p、q都在这个节点的右子树,而最近的公共祖先也一定在这个节点的右子树。而若是对于某个节点,p与q的值一个大于等于节点值,一个小于等于节点值,说明它们分布在该节点的两边,而这个节点就是最近的公共祖先,因此从上到下的其他祖先都将这个两个节点放到同一子树,只有最近公共祖先会将它们放入不同的子树,每次进入一个子树又回到刚刚的问题,因此可以使用递归。

具体做法:

  • step 1:首先检查空节点,空树没有公共祖先。
  • step 2:对于某个节点,比较与p、q的大小,若p、q在该节点两边说明这就是最近公共祖先。
  • step 3:如果p、q都在该节点的左边,则递归进入左子树。
  • step 4:如果p、q都在该节点的右边,则递归进入右子树。
public class Solution {

    public int lowestCommonAncestor (TreeNode root, int p, int q) {
        // write code here
        //递归
        //空树找不到公共祖先
        if(root == null){
            return -1;
        }
        //pq在该节点两边说明该节点就是最近公共祖先
        if((p >= root.val && q <= root.val) || (p <= root.val && q >= root.val)){
            return root.val;
        }
        //pq都在该节点的左边
        else if(p <= root.val && q <= root.val){
            //进入左子树
            return lowestCommonAncestor(root.left, p , q);
        }//pq都在该节点的右侧
        else{
            //进入右子树
            return lowestCommonAncestor(root.right, p, q);
        }
    }
}

38、BM38 在二叉树中找到两个节点的最近公共祖先

在这里插入图片描述

思路:

我们可以讨论几种情况:

  • step 1:如果o1和o2中的任一个和root匹配,那么root就是最近公共祖先。
  • step 2:如果都不匹配,则分别递归左、右子树。
  • step 3:如果有一个节点出现在左子树,并且另一个节点出现在右子树,则root就是最近公共祖先.
  • step 4:如果两个节点都出现在左子树,则说明最低公共祖先在左子树中,否则在右子树。
  • step 5:继续递归左、右子树,直到遇到step1或者step3的情况。
public class Solution {

    public int lowestCommonAncestor (TreeNode root, int o1, int o2) {
        // write code here
        //该子树为空,返回-1
        if(root == null){
            return -1;
        }
        //该节点是其中某一个节点
        if(root.val == o1 || root.val == o2){
            return root.val;
        }
        //左子树寻找公共祖先
        int left = lowestCommonAncestor(root.left, o1, o2);
        //右子树寻找公共祖先
        int right = lowestCommonAncestor(root.right, o1, o2);
        //左子树中没找到,则在右子树中
        if(left == -1){
            return right;
        }
        //右子树没有找到,则在左子树中
        if(right == -1){
            return left;
        }
        //否则是当前节点
        return root.val;
    }
}

39、BM39 序列化二叉树

在这里插入图片描述

思路:

序列化即将二叉树的节点值取出,放入一个字符串中,我们可以按照前序遍历的思路,遍历二叉树每个节点,并将节点值存储在字符串中,我们用‘#’表示空节点,用‘!'表示节点与节点之间的分割。

反序列化即根据给定的字符串,将二叉树重建,因为字符串中的顺序是前序遍历,因此我们重建的时候也是前序遍历,即可还原。

具体做法:

  • step 1:优先处理序列化,首先空树直接返回“#”,然后调用SerializeFunction函数前序递归遍历二叉树。
SerializeFunction(root, res);
  • step 2:SerializeFunction函数负责前序递归,根据“根左右”的访问次序,优先访问根节点,遇到空节点在字符串中添加‘#’,遇到非空节点,添加相应节点数字和‘!’,然后依次递归进入左子树,右子树。
//根节点
str.append(root.val).append('!');
//左子树
SerializeFunction(root.left, str); 
//右子树
SerializeFunction(root.right, str);
  • step 3:创建全局变量index表示序列中的下标(C++中直接指针完成)。
  • step 4:再处理反序列化,读入字符串,如果字符串直接为"#",就是空树,否则还是调用DeserializeFunction函数前序递归建树。
TreeNode res = DeserializeFunction(str);
  • step 5:DeserializeFunction函数负责前序递归构建树,遇到‘#’则是空节点,遇到数字则根据感叹号分割,将字符串转换为数字后加入新创建的节点中,依据“根左右”,创建完根节点,然后依次递归进入左子树、右子树创建新节点。
TreeNode root = new TreeNode(val);
......
//反序列化与序列化一致,都是前序
root.left = DeserializeFunction(str);  
root.right = DeserializeFunction(str);
import java.util.*;
public class Solution {
    //序列的下标
    public int index = 0;
    //处理序列化的功能函数(递归)
    private void SerializeFunction(TreeNode root, StringBuilder str) {
        //如果节点为空,表示左子节点或右子节点为空,用#表示
        if (root == null) {
            str.append('#');
            return;
        }
        //根节点
        str.append(root.val).append('!');
        //左子树
        SerializeFunction(root.left, str);
        //右子树
        SerializeFunction(root.right, str);
    }

    public String Serialize(TreeNode root) {
        //处理空树
        if (root == null)
            return "#";
        StringBuilder res = new StringBuilder();
        SerializeFunction(root, res);
        //把str转换成char
        return res.toString();
    }
    //处理反序列化的功能函数(递归)
    private TreeNode DeserializeFunction(String str) {
        //到达叶节点时,构建完毕,返回继续构建父节点
        //空节点
        if (str.charAt(index) == '#') {
            index++;
            return null;
        }
        //数字转换
        int val = 0;
        //遇到分隔符或者结尾
        while (str.charAt(index) != '!' && index != str.length()) {
            val = val * 10 + ((str.charAt(index)) - '0');
            index++;
        }
        TreeNode root = new TreeNode(val);
        //序列到底了,构建完成
        if (index == str.length())
            return root;
        else
            index++;
        //反序列化与序列化一致,都是前序
        root.left = DeserializeFunction(str);
        root.right = DeserializeFunction(str);
        return root;
    }

    public TreeNode Deserialize(String str) {
        //空序列对应空树
        if (str == "#")
            return null;
        TreeNode res = DeserializeFunction(str);
        return res;
    }
}

40、BM40 重建二叉树

在这里插入图片描述

思路:

对于二叉树的前序遍历,我们知道序列的第一个元素必定是根节点的值,因为序列没有重复的元素,因此中序遍历中可以找到相同的这个元素,而我们又知道中序遍历中根节点将二叉树分成了左右子树两个部分

具体做法:

  • step 1:先根据前序遍历第一个点建立根节点。
  • step 2:然后遍历中序遍历找到根节点在数组中的位置。
  • step 3:再按照子树的节点数将两个遍历的序列分割成子数组,将子数组送入函数建立子树。
  • step 4:直到子树的序列长度为0,结束递归。
public class Solution {
    public TreeNode reConstructBinaryTree (int[] pre, int[] vin) {
        // write code here
        int n = pre.length;
        int m = vin.length;
        //每个遍历都不能为0
        if (n == 0 || m == 0) {
            return null;
        }
        //构建根节点
        TreeNode root = new TreeNode(pre[0]);
        for (int i = 0; i < m; i++) {
            //找到中序遍历中的前序第一个元素
            if (pre[0] == vin[i]) {
                //构建左子树
                root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1),
                                                  Arrays.copyOfRange(vin, 0, i));
                //构建右子树
                root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length),
                                                   Arrays.copyOfRange(vin, i + 1, vin.length));
                break;
            }
        }
        return root;
    }
}

41、BM41 输出二叉树的右视图

在这里插入图片描述

思路:

首先呢根据上一题的建树思路,拿到树;然后采用层序遍历的方式 + 辅助队列,最后返回结果

具体做法:

  • step 1:首先检查树是否为空,为空就不打印。

  • step 2:建立队列辅助层次遍历,根节点先进队。

  • step 3:用一个size变量,每次进入一层的时候记录当前队列大小,等到size为0时,便到了最右边,记录下该节点元素。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 求二叉树的右视图
     * @param preOrder int整型一维数组 先序遍历
     * @param inOrder int整型一维数组 中序遍历
     * @return int整型一维数组
     */
    public int[] solve (int[] preOrder, int[] inOrder) {
        // write code here
        //层次遍历
        //先拿到重建的二叉树
        TreeNode tree = reConstructBinaryTree(preOrder, inOrder);
        if (tree == null) {
            return new int[] {};
        }

        Queue<TreeNode> q = new LinkedList<>();
        q.add(tree);

        List<Integer> res = new ArrayList<>();

        while (!q.isEmpty()) {
            int size = q.size();

            for (int i = 0; i < size; ++i) {
                //取出来de节点
                TreeNode  cur = q.poll();
                //加入到结果集中
                //这里是关键,将每一层的最后一个元素加入到res中
                if (i == size - 1) {
                    res.add(cur.val);
                }

                //看当前节点有没有左右孩子,有的话加入到队列
                if (cur.left != null) {
                    q.offer((cur.left));
                }
                if (cur.right != null) {
                    q.offer(cur.right);
                }

            }

        }
        //封装右视图
        int[] result = new int[res.size()];
        for (int i = 0; i < res.size(); i++) {
            result[i] = res.get(i);
        }
        return result;


    }
    //重建二叉树
    public TreeNode reConstructBinaryTree (int[] pre, int[] vin) {
        // write code here
        int n = pre.length;
        int m = vin.length;
        //每个遍历都不能为0
        if (n == 0 || m == 0) {
            return null;
        }
        //构建根节点
        TreeNode root = new TreeNode(pre[0]);
        for (int i = 0; i < m; i++) {
            //找到中序遍历中的前序第一个元素
            if (pre[0] == vin[i]) {
                //构建左子树
                root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1),
                                                  Arrays.copyOfRange(vin, 0, i));
                //构建右子树
                root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length),
                                                   Arrays.copyOfRange(vin, i + 1, vin.length));
                break;
            }
        }
        return root;
    }
}

42、BM42 用两个栈实现队列

牛客101刷题笔记_第2张图片

思路:

元素进栈以后,只能优先弹出末尾元素,但是队列每次弹出的却是最先进去的元素,如果能够将栈中元素全部取出来,才能访问到最前面的元素,此时,可以用另一个栈来辅助取出。

具体做法:

  • step 1:push操作就正常push到第一个栈末尾。
  • step 2:pop操作时,优先将第一个栈的元素弹出,并依次进入第二个栈中。
//将第一个栈中内容弹出放入第二个栈中
while(!stack1.isEmpty()) 
    stack2.push(stack1.pop()); 
  • step 3:第一个栈中最后取出的元素也就是最后进入第二个栈的元素就是队列首部元素,要弹出,此时在第二个栈中可以直接弹出。
  • step 4:再将第二个中保存的内容,依次弹出,依次进入第一个栈中,这样第一个栈中虽然取出了最里面的元素,但是顺序并没有变。
//再将第二个栈的元素放回第一个栈
while(!stack2.isEmpty()) 
    stack1.push(stack2.pop());
public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        //将所有元素入栈
        stack1.push(node);
        
    }
    
    public int pop() {
        //将第一个栈中内容弹出放入第二个栈中
        while(!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
        //第二个栈栈顶就是最先进来的元素 
        int res = stack2.pop();
        //再将第二个栈的元素放回第一个栈
        while(!stack2.isEmpty()){
            stack1.push(stack2.pop());
        }
        return res;
    
    }
}

43、BM43 包含min函数的栈

牛客101刷题笔记_第3张图片

思路:

我们都知道栈结构的push、pop、top操作都是O(1),但是min函数做不到,于是想到在push的时候就将最小值记录下来,由于栈先进后出的特殊性,我们可以构造一个单调栈,保证栈内元素都是递增的,栈顶元素就是当前最小的元素。

具体做法:

  • step 1:使用一个栈记录进入栈的元素,正常进行push、pop、top操作。
  • step 2:使用另一个栈记录每次push进入的最小值。
  • step 3:每次push元素的时候与第二个栈的栈顶元素比较,若是较小,则进入第二个栈,若是较大,则第二个栈的栈顶元素再次入栈,因为即便加了一个元素,它依然是最小值。于是,每次访问最小值即访问第二个栈的栈顶。
//空或者新元素较小,则入栈
if(s2.isEmpty() || s2.peek() > node)  
    s2.push(node);
else
    //重复加入栈顶
    s2.push(s2.peek());
public class Solution {
    //用于栈的push和pop
    Stack<Integer> s1 = new Stack<>();
    //用于存储最小值
    Stack<Integer> s2 = new Stack<>();
    
    public void push(int node) {
        s1.push(node);
        //空或者新元素较小,则入栈
        if(s2.isEmpty() || s2.peek() > node){
            s2.push(node);
        }else{
            //重复加入栈顶
            s2.push(s2.peek());
        }
        
    }
    
    public void pop() {
        s1.pop();
        s2.pop();
        
    }
    
    public int top() {
        return s1.peek();
    }
    
    public int min() {
        return s2.peek();
        
    }
}

44、BM44 有效括号序列

牛客101刷题笔记_第4张图片

思路:

括号的匹配规则应该符合先进后出原理:最外层的括号即最早出现的左括号,也对应最晚出现的右括号,即先进后出,因此可以使用同样先进后出的栈:遇到左括号就将相应匹配的右括号加入栈中,后续如果是合法的,右括号来的顺序就是栈中弹出的顺序。

具体做法:

  • step 1:创建辅助栈,遍历字符串。
  • step 2:每次遇到小括号的左括号、中括号的左括号、大括号的左括号,就将其对应的呦括号加入栈中,期待在后续遇到。
  • step 3:如果没有遇到左括号但是栈为空,说明直接遇到了右括号,不合法。
  • step 4:其他情况下,如果遇到右括号,刚好会与栈顶元素相同,弹出栈顶元素继续遍历。
  • step 5:理论上,只要括号是匹配的,栈中元素最后是为空的,因此检查栈是否为空即可最后判断是否合法。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return bool布尔型
     */
    public boolean isValid (String s) {
        // write code here
        //辅助栈
        Stack<Character> st = new Stack<>();
        //遍历字符串
        for(int i = 0; i < s.length(); i++){
            //遇到左括号就将对应的右括号入栈
            if(s.charAt(i) == '('){
                st.push(')');
            }else if(s.charAt(i) == '['){
                st.push(']');
            }else if(s.charAt(i) == '{'){
                st.push('}');
            //开始比较,如果栈为空或者栈中弹出的元素与当前元素不相同
            }else if(st.isEmpty() || st.pop() != s.charAt(i)){
                return false;
            }
        }
        //最终栈为空,返回true;
        return st.isEmpty();
    }
}

45、BM45 滑动窗口的最大值

牛客101刷题笔记_第5张图片

思路:

我们都知道,若是一个数字A进入窗口后,若是比窗口内其他数字都大,那么这个数字之前的数字都没用了,因为它们必定会比A早离开窗口,在A离开之前都争不过A,所以A在进入时依次从尾部排除掉之前的小值再进入,而每次窗口移动要弹出窗口最前面值,因此队首也需要弹出,所以我们选择双向队列。

具体做法:

  • step 1:维护一个双向队列,用来存储数列的下标。
  • step 2:首先检查窗口大小与数组大小。
  • step 3:先遍历第一个窗口,如果即将进入队列的下标的值大于队列后方的值,依次将小于的值拿出来去掉,再加入,保证队列是递增序。
//先遍历一个窗口
for(int i = 0; i < size; i++){
    //去掉比自己先进队列的小于自己的值
    while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
        dq.pollLast();
    dq.add(i);
}
  • step 4:遍历后续窗口,每次取出队首就是最大值,如果某个下标已经过了窗口,则从队列前方将其弹出。
  • step 5:对于之后的窗口,重复step 3,直到数组结束。
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {

        ArrayList<Integer> res = new ArrayList<Integer>();
        //窗口大于数组长度的时候,返回空
        if(size > num.length){
            return res;
        }
        if(size <= num.length && size != 0){
            //双向队列
            ArrayDeque <Integer> dq = new ArrayDeque<>();  
            //先遍历一个窗口
            for(int i = 0; i < size; i++){
                //去掉比自己先进队列的小于自己的值
                while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
                     dq.pollLast();
                dq.add(i);
            }
            //遍历后续数组元素
            for(int i = size; i < num.length; i++){
                //取窗口内的最大值加入到结果集
                res.add(num[dq.peekFirst()]);
                while(!dq.isEmpty() && dq.peekFirst() < (i - size + 1))
                    //弹出窗口移走后的值
                    dq.pollFirst(); 
                //加入新的值前,去掉比自己先进队列的小于自己的值
                while(!dq.isEmpty() && num[dq.peekLast()] < num[i])
                    dq.pollLast();
                dq.add(i);
            }
            res.add(num[dq.pollFirst()]);
        }     
        return res;
    }
}

46、BM46 最小的K个数

牛客101刷题笔记_第6张图片

知识点:优先队列

优先队列即PriorityQueue,是一种内置的机遇堆排序的容器,分为大顶堆与小顶堆,大顶堆的堆顶为最大元素,其余更小的元素在堆下方,小顶堆与其刚好相反。且因为容器内部的次序基于堆排序,因此每次插入元素时间复杂度都是O*(*logn),而每次取出堆顶元素都是直接取出。

思路:

要找到最小的k个元素,只需要准备k个数字,之后每次遇到一个数字能够快速的与这k个数字中最大的值比较,每次将最大的值替换掉,那么最后剩余的就是k个最小的数字了。

如何快速比较k个数字的最大值,并每次替换成较小的新数字呢?我们可以考虑使用优先队列(大根堆),只要限制堆的大小为k,那么堆顶就是k个数字的中最大值,如果需要替换,将这个最大值拿出,加入新的元素就好了。

//较小元素入堆
if(q.peek() > input[i]){  
    q.poll();
    q.offer(input[i]);
}

具体做法:

  • step 1:利用input数组中前k个元素,构建一个大小为k的大顶堆,堆顶为这k个元素的最大值。
  • step 2:对于后续的元素,依次比较其与堆顶的大小,若是比堆顶小,则堆顶弹出,再将新数加入堆中,直至数组结束,保证堆中的k个最小。
  • step 3:最后将堆顶依次弹出即是最小的k个数。
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution (int[] input, int k) {
        // write code here
        //大顶堆
        ArrayList<Integer> res = new ArrayList<>();
        //basecase
        if(k == 0 || input.length == 0){
            return res;
        }
        //大根堆
        PriorityQueue<Integer> q = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));
        //构建一个k个大小的堆
        for(int i = 0; i < k; i++){
            q.offer(input[i]);
        }
        for(int i = k; i < input.length; i++){
            //较小元素入堆
            if(q.peek() > input[i]){
                q.poll();
                q.offer(input[i]);
            }
        }
        //堆中的元素取出加入数组
        for(int i = 0; i < k; i++){
            res.add(q.poll());
        }
        return res;
    }
}

47、BM47 寻找第K大

牛客101刷题笔记_第7张图片
随机快速排序(改进的快速排序)

1)在数组范围中,等概率随机选一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧<划分值、中间==划分值、右侧>划分值
2)对左侧范围和右侧范围,递归执行
3)时间复杂度为O(NlogN)

public class Solution {

    public int findKth (int[] a, int n, int K) {
        // write code here
        return  quickSort(a, 0, a.length - 1, K);
    }
    public int quickSort(int[] nums, int l, int r, int k) {
        int index = randomParition(nums, l, r);
        if (index == k - 1) {
            return nums[index];
        } else {
            return index > k - 1 ? quickSort(nums, l, index - 1, k) :
                   quickSort(nums, index + 1, r, k);
        }
    }

    public int randomParition(int[] nums, int l, int r) {
        int i = (int) (Math.random() * (r - l)) + l;
        swap(nums, i, r);
        return partition(nums, l, r);
    }

    public int partition(int[] nums, int l, int r) {
        int pivot = nums[r];
        int rightmost = r;
        while (l <= r) {
            while ( l <= r && nums[l] > pivot ) {
                l++;
            }
            while ( l <= r && nums[r] <= pivot ) {
                r--;
            }
            if (l <= r) {
                swap(nums, l, r);
            }
        }

        swap(nums, l, rightmost);
        return l;
    }
    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

48、BM48 数据流中的中位数

牛客101刷题笔记_第8张图片

知识点:插入排序

插入排序是排序中的一种方式,一旦一个无序数组开始排序,它前面部分就是已经排好的有序数组(一开始长度为0),而其后半部分则是需要排序的无序数组,插入排序的做法就是遍历后续需要排序的无序部分,对于每个元素,插入到前半部分有序数组中属于它的位置——即最后一个小于它的元素后。

思路:

传统的寻找中位数的方法便是排序之后,取中间值或者中间两位的平均即可。但是这道题因为数组在不断增长,每增长一位便需要排一次,很浪费时间,于是可以考虑在增加数据的同时将其有序化,这个过程就让我们想到了插入排序:对于每个输入的元素,遍历已经有序的数组,将其插入到属于它的位置。

int i = 0;
//遍历找到插入点
for(; i < val.size(); i++){
    if(num <= val.get(i))
        break;
}
//插入相应位置
val.add(i, num);

具体做法:

  • step 1:用一数组存储输入的数据流。
  • step 2:Insert函数在插入的同时,遍历之前存储在数组中的数据,按照递增顺序依次插入,如此一来,加入的数据流便是有序的。
  • step 3:GetMedian函数可以根据下标直接访问中位数,分为数组为奇数个元素和偶数个元素两种情况。记得需要类型转换为double。
public class Solution {
    //new一个集合存储输入的数据流
    ArrayList<Integer> arr = new ArrayList<>();

    public void Insert(Integer num) {
        //arr中没有数据,直接加入
        if(arr.isEmpty()){
            arr.add(num);
        }
        //arr中有数据,需要插入排序
        else{
            int i = 0;
            //遍历找到插入点
            for(; i < arr.size(); ++i){
                if(num < arr.get(i))
                break;
            }
            //插入相应的位置
            arr.add(i, num);
        }
    }
    public Double GetMedian() {
        int n = arr.size();
        //奇数个数字
        if(n % 2 == 1){
            //类型转换
            return (double)arr.get(n / 2);
        }else{
            //偶数个数字
            double a = arr.get(n / 2);
            double b = arr.get(n / 2 - 1);
            return (a + b) / 2;
        }   
    }
}

49、BM49 表达式求值

牛客101刷题笔记_第9张图片

【双栈思路】
情况1:是数,直接压nums栈;
情况2:是 ‘(’ ,直接压opts栈;
情况3:是 ‘)’ ,先计算opts栈中 ‘(’ 前的操作符,然后将 '('弹出;
情况4:是 ‘±*’ ,先计算opts栈中 ‘(’ 前的、优先级>=它的操作符,然后将它压栈;

import java.util.*;

public class Solution {
    public static int solve (String s) {
        Map<Character,Integer> map = new HashMap<>();    //存优先级的map
        map.put('-', 1);
        map.put('+', 1);
        map.put('*', 2);
        Deque<Integer> nums = new ArrayDeque<>();    // 数字栈
        Deque<Character> opts = new ArrayDeque<>();    // 操作符栈
        s.replaceAll(" ","");    // 去空格
        char[] chs = s.toCharArray();
        int n = chs.length;

        for(int i = 0; i < n; i++){
            char c = chs[i];
            if(isNumber(c)){    // 情况1
                int num = 0;
                int j = i;
                // 读取连续数字符号
                while(j < n && isNumber(chs[j])){
                    num = 10*num + chs[j++] - '0';    
                }
                nums.push(num);
                i = j - 1;
            }else if (c == '('){    // 情况2
                opts.push(c);
            }else if (c == ')' ){    // 情况3
                while(opts.peek() != '('){
                    cal(nums, opts);
                }
                opts.pop();
            }else{    // 情况4
                while(!opts.isEmpty() && opts.peek()!='(' && map.get(opts.peek()) >= map.get(c)){
                    cal(nums, opts);
                }
                opts.push(c);
            }
        }
        while(!opts.isEmpty()) {    // 计算栈中剩余操作符
            cal(nums, opts);
        }
        return nums.pop();
    }
    
    public static boolean isNumber(Character c){
        return Character.isDigit(c);
    }
    public static void cal(Deque<Integer> nums, Deque<Character> opts){
        int num2 = nums.pop();
        int num1 = nums.pop();
        char opt = opts.pop();
        if(opt == '+'){
            nums.push(num1 + num2);
        }else if(opt == '-'){
            nums.push(num1 - num2);
        }else if(opt == '*'){
            nums.push(num1 * num2);
        }
    }
}

50、BM50 两数之和

牛客101刷题笔记_第10张图片

思路:

我们能想到最直观的解法,可能就是两层遍历,将数组所有的二元组合枚举一遍,看看是否是和为目标值,但是这样太费时间了,既然加法这么复杂,我们是不是可以尝试一下减法:对于数组中出现的一个数a,如果目标值减去a的值已经出现过了,那这不就是我们要找的一对元组吗?这种时候,快速找到已经出现过的某个值,可以考虑使用哈希表快速检验某个元素是否出现过这一功能。

public class Solution {
    public int[] twoSum (int[] numbers, int target) {
        int[] res = new int[0];
        //创建哈希表,两元组分别表示值、下标
        HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
        //在哈希表中查找target-numbers[i]
        for (int i = 0; i < numbers.length; i++) {
            int differ = target - numbers[i];
            //若是没找到,将此信息计入哈希表
            if (!map.containsKey(differ)) {
                map.put(numbers[i], i);
            }
            //否则返回两个下标+1
            else{
               return new int[] {map.get(differ) + 1, i + 1};
            }
        }
        return res;
    }
}

51、BM51 数组中出现次数超过一半的数字

牛客101刷题笔记_第11张图片

思路:

首先我们分析一下,数组某个元素出现次数超过了数组长度的一半,那它肯定出现最多,而且只要超过了一半,其他数字不可能超过一半了,必定是它。

如果给定的数组是有序的,那我们在连续的相同数字中找到出现次数最多即可,但是题目没有要求有序,一种方法是对数组排序后解决,但是时间复杂度就上去了。那我们可以考虑遍历一次数组统计各个元素出现的次数,找到出现次数大于数组长度一半的那个数字。

具体做法:

  • step 1:构建一个哈希表,统计数组元素各自出现了多少次,即key值为数组元素,value值为其出现次数。
  • step 2:遍历数组,每遇到一个元素就把哈希表中相应key值的value值加1,用来统计出现次数。
  • step 3:本来可以统计完了之后统一遍历哈希表找到频次大于数组长度一半的key值,但是根据我们上面加粗的点,只要它出现超过了一半,不管后面还有没有,必定就是这个元素了,因此每次统计后,我们都可以检查value值是否大于数组长度的一半,如果大于则找到了。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param numbers int整型一维数组
     * @return int整型
     */
    public int MoreThanHalfNum_Solution (int[] nums) {
        //哈希表统计每个数字出现的次数
        HashMap<Integer, Integer> map = new HashMap<>();

        for (int num : nums) {
            if (!map.containsKey(num)) {
                map.put(num, 0);
            }
            map.put(num, map.get(num) + 1);
        }

        int half = nums.length / 2;
        
        for (int key : map.keySet()) {
            if (map.get(key) > half) {
                return key;
            }
        }
        return -1;
    }
}

52、BM52 数组中只出现一次的两个数字

牛客101刷题笔记_第12张图片

知识点:哈希表

哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

思路:

既然有两个数字只出现了一次,我们就统计每个数字的出现次数,利用哈希表的快速根据key值访问其频率值。

具体做法:

  • step 1:遍历数组,用哈希表统计每个数字出现的频率。
  • step 2:然后再遍历一次数组,对比哈希表,找到出现频率为1的两个数字。
  • step 3:最后整理次序输出。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型一维数组
     */
    public int[] FindNumsAppearOnce (int[] nums) {
        // write code here
        HashMap<Integer,Integer> map = new HashMap<>();

        ArrayList<Integer> res = new ArrayList<>();
        //遍历数组
        for(int i = 0; i < nums.length; ++i){
            //统计每个数出现的频率
            if(!map.containsKey(nums[i])){
                map.put(nums[i], 1);
            }else{
                map.put(nums[i], map.get(nums[i]) + 1);
            }
        }
        //再次遍历数组
        for(int i = 0; i < nums.length; i++){
            //找到频率为1的两个数
            if(map.get(nums[i]) == 1){
                res.add(nums[i]);
            }
        }
        //整理次序
        if(res.get(0) < res.get(1)){
            return new int[]{res.get(0),res.get(1)};
        }else{
            return new int[]{res.get(1),res.get(0)};
        }

    }
}

53、BM53 缺失的第一个正整数

牛客101刷题笔记_第13张图片

思路:

n个长度的数组,没有重复,则如果数组填满了1~n,那么缺失n+1,如果数组填不满1~n,那么缺失的就是1~n中的数字。对于这种快速查询某个元素是否出现过的问题,还是可以使用哈希表快速判断某个数字是否出现过。

具体做法:

  • step 1:构建一个哈希表,用于记录数组中出现的数字。
  • step 2:从1开始,遍历到n,查询哈希表中是否有这个数字,如果没有,说明它就是数组缺失的第一个正整数,即找到。
  • step 3:如果遍历到最后都在哈希表中出现过了,那缺失的就是n+1.
public class Solution {

    public int minNumberDisappeared (int[] nums) {
        // write code here
        //map用来存数组中的元素
        HashMap<Integer, Integer> map = new HashMap<>();
        //将数组中的元素丢进map中
        for (int i = 0; i < nums.length; ++i) {
            map.put(nums[i], 1);
        }
        int res = 1;
        //从1开始找到哈希表中第一个没有出现的正整数
        while (map.containsKey(res)){
            res++;
        }
        return res;
    }
}

54、BM54 三数之和

牛客101刷题笔记_第14张图片

知识点1:哈希表

哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。

知识点2:双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

思路:

直接找三个数字之和为某个数,太麻烦了,我们是不是可以拆分一下:如果找到了某个数a*,要找到与之对应的另外两个数,三数之和为0,那岂不是只要找到另外两个数之和为−*a?这就方便很多了。

因为三元组内部必须是有序的,因此可以优先对原数组排序,这样每次取到一个最小的数为a,只需要在后续数组中找到两个之和为−a就可以了,我们可以用双指针缩小区间,因为太后面的数字太大了,就不可能为−a,可以舍弃。

具体做法:

  • step 1:排除边界特殊情况。
  • step 2:既然三元组内部要求非降序排列,那我们先得把这个无序的数组搞有序了,使用sort函数优先对其排序。
  • step 3:得到有序数组后,遍历该数组,对于每个遍历到的元素假设它是三元组中最小的一个,那么另外两个一定在后面。
  • step 4:需要三个数相加为0,则另外两个数相加应该为上述第一个数的相反数,我们可以利用双指针在剩余的子数组中找有没有这样的数对。双指针指向剩余子数组的首尾,如果二者相加为目标值,那么可以记录,而且二者中间的数字相加可能还会有。
  • step 5:如果二者相加大于目标值,说明右指针太大了,那就将其左移缩小,相反如果二者相加小于目标值,说明左指针太小了,将其右移扩大,直到两指针相遇,剩余子数组找完了。

注:对于三个数字都要判断是否相邻有重复的情况,要去重。

import java.util.*;
class Solution {

    public ArrayList<ArrayList<Integer>> threeSum(int[] nums) {

        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
        Arrays.sort(nums);
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.length; i++) {
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
                return res;
            }

            if (i > 0 && nums[i] == nums[i - 1]) {  // 去重a
                continue;
            }

            int left = i + 1;
            int right = nums.length - 1;
            while (right > left) {
                int sum = nums[i] + nums[left] + nums[right];
                
                if (sum > 0) {
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    ArrayList<Integer> temp = new ArrayList<Integer>();
                    temp.add(nums[i]);
                    temp.add(nums[left]);
                    temp.add(nums[right]);
                    res.add(temp);
                    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    right--;
                    left++;
                }
            }
        }
        return res;
    }
}

55、BM55 没有重复项数字的全排列

牛客101刷题笔记_第15张图片

思路:

public class Solution {

    public ArrayList<ArrayList<Integer>> permute (int[] nums) {
        /**
        回溯法
        */
        //首先需要一个二维数组List来存放结果
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        //然后需要一个HashMap来判断是否该元素被使用过
        HashMap<Integer, Boolean> visited = new HashMap<>();
        //将元素丢进map里,value初始为false
        for (int num : nums) {
            visited.put(num, false);
        }
        //回溯法
        backtracking(nums, res, visited, new ArrayList<>());
        //返回结果
        return res;
    }

    //重头戏回溯法  list用来存放当前递归路径上的组合
    void backtracking(int[] nums, ArrayList<ArrayList<Integer>> result,
                      HashMap<Integer, Boolean> visited, ArrayList<Integer> list) {
        //剪枝条件
        if (list.size() == nums.length) {
            //这里直接复制,避免引用传递
            result.add(new ArrayList<>(list));
            return;
        }
        //如果没有从递归里面退出去
        for (int i = 0; i < nums.length; ++i) {
            int num = nums[i];
            //map里面的num值出现的情况下直接进入下一层循环,就不要往下走了
            //map里面的num值没有出现的情况下
            if (!visited.get(num)) {
                list.add(num);
                visited.put(num, true);
                backtracking(nums, result, visited, list);
                //将list的最后一个数删掉
                list.remove(list.size() - 1);
                //将num值在进入下一层递归之前改为false
                visited.put(num, false);
            }
        }
    }
}

56、BM56 有重复项数字的全排列

牛客101刷题笔记_第16张图片

思路:

这道题类似没有重复项数字的全排列,但是因为交换位置可能会出现相同数字交换的情况,出现的结果需要去重,因此不便于使用交换位置的方法。

我们就使用临时数组去组装一个排列的情况:每当我们选取一个数组元素以后,就确定了其位置,相当于对数组中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归

  • 终止条件: 临时数组中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
  • 返回值: 每一层给上一层返回的就是本层级在临时数组中添加的元素,递归到末尾的时候就能添加全部元素。
  • 本级任务: 每一级都需要选择一个不重复元素加入到临时数组末尾(遍历数组选择)。

回溯的思想也与没有重复项数字的全排列类似,对于数组[1,2,2,3],如果事先在临时数组中加入了1,后续子问题只能是[2,2,3]的全排列接在1后面,对于2开头的分支达不到,因此也需要回溯:将临时数组刚刚加入的数字pop掉,同时vis修改为没有加入,这样才能正常进入别的分支。

具体做法:

  • step 1:先对数组按照字典序排序,获取第一个排列情况。
  • step 2:准备一个数组暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的数字被加入了。
  • step 3:每次递归从头遍历数组,获取数字加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素num[i]与同一层的前一个元素num[i-1]相同且num[i-1]已经用,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入数组的元素,
  • step 6:临时数组长度到达原数组长度就是一种排列情况。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param num int整型一维数组
     * @return int整型ArrayList>
     */
    public ArrayList<ArrayList<Integer>> permuteUnique (int[] num) {
        // write code here
        ArrayList<ArrayList<Integer>> res = new  ArrayList<>();
        //basecase
        if (num == null || num.length == 0) {
            return res;
        }
        //先排序
        Arrays.sort(num);
        Boolean[] vis = new Boolean[num.length];
        //将布尔数组 vis 中的所有元素都设置为 false
        Arrays.fill(vis, false);
        // temp用来存放当前递归路径上的组合
        ArrayList<Integer> temp = new ArrayList<>();
        //递归
        recursion(res, num, temp, vis);
        return res;

    }
    public void recursion(ArrayList<ArrayList<Integer>> res, int[] num,
                          ArrayList<Integer> temp, Boolean[] vis) {

        //临时数组满了就加入到res,剪枝条件
        if (temp.size() == num.length) {
            res.add(new ArrayList<Integer>(temp));
            return;
        }
        //遍历所有元素选取一个加入
        for (int i = 0; i < num.length; i++) {
            //如果该元素以及被加入了就不用加入了
            if (vis[i]) {
                continue;
            }
            //当前的元素num[i]与同一层的前一个元素num[i-1]相同且num[i-1]已经用过了
            if (i > 0 && num[i] == num[i - 1] && !vis[i - 1]) {
                continue;
            }
            //标记为使用过
            vis[i] = true;
            //加入数组
            temp.add(num[i]);
            //递归
            recursion(res, num, temp, vis);
            //将temp的最后一个数删掉
            temp.remove(temp.size() - 1);
            //在进入下一层递归之前改为false
            vis[i] =  false;
        }
    }
}

57、BM57 岛屿数量

牛客101刷题笔记_第17张图片

知识点:深度优先搜索(dfs) 深度优先搜索一般用于树或者图的遍历,其他有分支的(如二维矩阵)也适用。它的原理是从初始点开始,一直沿着同一个分支遍历,直到该分支结束,然后回溯到上一级继续沿着一个分支走到底,如此往复,直到所有的节点都有被访问到。

思路:

矩阵中多处聚集着1,要想统计1聚集的堆数而不重复统计,那我们可以考虑每次找到一堆相邻的1,就将其全部改成0,而将所有相邻的1改成0的步骤又可以使用深度优先搜索(dfs):当我们遇到矩阵的某个元素为1时,首先将其置为了0,然后查看与它相邻的上下左右四个方向,如果这四个方向任意相邻元素为1,则进入该元素,进入该元素之后我们发现又回到了刚刚的子问题,又是把这一片相邻区域的1全部置为0,因此可以用递归实现。

  • 终止条件: 进入某个元素修改其值为0后,遍历四个方向发现周围都没有1,那就不用继续递归,返回即可,或者递归到矩阵边界也同样可以结束。
  • 返回值: 每一级的子问题就是把修改后的矩阵返回,因为其是函数引用,也不用管。
  • 本级任务: 对于每一级任务就是将该位置的元素置为0,然后查询与之相邻的四个方向,看看能不能进入子问题。

具体做法:

  • step 1:优先判断空矩阵等情况。
  • step 2:从上到下从左到右遍历矩阵每一个位置的元素,如果该元素值为1,统计岛屿数量。
  • step 3:接着将该位置的1改为0,然后使用dfs判断四个方向是否为1,分别进入4个分支继续修改。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 判断岛屿数量
     * @param grid char字符型二维数组
     * @return int整型
     */
    public int solve (char[][] grid) {
        /**
        DFS
        */
        if (grid == null || grid.length == 0) {
            return 0;
        }
        int result = 0;
        int row = grid.length;
        int col = grid[0].length;

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == '1') {
                    result++;
                    dfs(grid, i, j, row, col);
                }
            }
        }
        return result;
    }
    private void dfs(char[][] grid, int x, int y, int row, int col) {
        if (x < 0 || y < 0 || x >= row || y >= col || grid[x][y] == '0') {
            return;
        }
        grid[x][y] = '0';
        dfs(grid, x - 1, y, row, col);
        dfs(grid, x + 1, y, row, col);
        dfs(grid, x, y - 1, row, col);
        dfs(grid, x, y + 1, row, col);
    }
}

58、BM58 字符串的排列

牛客101刷题笔记_第18张图片

知识点:递归与回溯

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

如果是线型递归,子问题直接回到父问题不需要回溯,但是如果是树型递归,父问题有很多分支,我需要从子问题回到父问题,进入另一个子问题。因此回溯是指在递归过程中,从某一分支的子问题回到父问题进入父问题的另一子问题分支,因为有时候进入第一个子问题的时候修改过一些变量,因此回溯的时候会要求改回父问题时的样子才能进入第二子问题分支。

思路:

都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。

使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归

  • 终止条件: 临时字符串中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
  • 返回值: 每一层给上一层返回的就是本层级在临时字符串中添加的元素,递归到末尾的时候就能添加全部元素。
  • 本级任务: 每一级都需要选择一个元素加入到临时字符串末尾(遍历原字符串选择)。

递归过程也需要回溯,比如说对于字符串“abbc”,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。

具体做法:

  • step 1:先对字符串按照字典序排序,获取第一个排列情况。
  • step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
  • step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
  • step 6:临时字符串长度到达原串长度就是一种排列情况。
public class Solution {

    public ArrayList<String> Permutation (String str) {
        // write code here
        ArrayList<String> res = new ArrayList<>();
        //basecase
        if (str == null || str.length() == 0) {
            return res;
        }
        //将字符串转字符数组
        char[] charStr = str.toCharArray();
        //排序
        Arrays.sort(charStr);
        //布尔数组中的每个元素用于标记字符串中对应位置的字符是否被访问过或处理过
        boolean[] vis = new boolean[str.length()];
        //标记每个位置的字符是否被使用过,默认false
        //Arrays.fill(vis, false) 是一个数组工具类 Arrays 的静态方法,
        //用于将布尔数组 vis 中的所有元素都设置为 false。
        Arrays.fill(vis, false);
        StringBuffer temp = new StringBuffer();
        //递归获取
        recursion(charStr, res, temp, vis);
        return res;
    }
    public void recursion(char[] str, ArrayList<String> res, StringBuffer temp,
                          boolean[] vis) {
        // 剪枝条件,当temp满了就加入res
        if (temp.length() == str.length) {
            res.add(new String(temp));
            return;
        }
        //遍历所有元素选取一个加入
        for (int i = 0; i < str.length; i++) {
            //如果该元素已经被加入了就不用在加入了
            if (vis[i]) {
                continue;
            }
            //如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了
            if (i > 0 && str[i - 1] == str[i] && !vis[i - 1]) {
                //跳出本次循环
                continue;
            }
            //标记为未使用
            vis[i] = true;
            //加入到临时字符串
            temp.append(str[i]);
            //回溯
            recursion(str, res, temp, vis);
            //在进入下一层递归之前改为false
            vis[i] = false;
            //将temp的最后一个字符删掉
            temp.deleteCharAt(temp.length() - 1);
        }
    }
}

59、BM59 N皇后问题

牛客101刷题笔记_第19张图片

n个皇后,不同行不同列,那么肯定棋盘每行都会有一个皇后,每列都会有一个皇后。如果我们确定了第一个皇后的行号与列号,则相当于接下来在n−1行中查找n−1个皇后,这就是一个子问题,因此使用递归:

  • 终止条件: 当最后一行都被选择了位置,说明n个皇后位置齐了,增加一种方案数返回。
  • 返回值: 每一级要将选中的位置及方案数返回。
  • 本级任务: 每一级其实就是在该行选择一列作为该行皇后的位置,遍历所有的列选择一个符合条件的位置加入数组,然后进入下一级。

具体做法:

  • step 1:对于第一行,皇后可能出现在该行的任意一列,我们用一个数组pos记录皇后出现的位置,下标为行号,元素值为列号。
  • step 2:如果皇后出现在第一列,那么第一行的皇后位置就确定了,接下来递归地在剩余的n−1行中找 n−1个皇后的位置。
  • step 3:每个子问题检查是否符合条件,我们可以对比所有已经记录的行,对其记录的列号查看与当前行列号的关系:即是否同行、同列或是同一对角线。
public class Solution {
    private int res;
    public int Nqueen (int n) {
        // write code here
        res = 0;
        //下标为行号,元素为列号,记录皇后的位置
        int[] pos = new int[n];
        //
        Arrays.fill(pos, 0);
        //递归
        recursion(n, 0, pos);

        return res;

    }
    //递归查找皇后的种类
    private void recursion(int n, int row, int[] pos) {
        // 当最后一行都被选择了位置,说明n个皇后位置齐了,增加一种方案数返回
        if (row == n) {
            res++;
            return;
        }
        //遍历所有的列
        for (int i = 0; i < n; i++) {
            //检查该位置是否符合条件
            if (isValid(pos, row, i)) {
                //加入位置
                pos[row] = i;
                //递归继续查找
                recursion(n, row + 1, pos);
            }
        }
    }
    //判断皇后是否符合条件
    public boolean isValid(int[] pos, int row, int col) {
        //遍历所有已经记录的行
        for (int i = 0; i < row; i++) {
            //不能同行同列同一斜线
            if (row == i || col == pos[i] || Math.abs(row - i) == Math.abs(col - pos[i]))
                return false;
        }
        return true;
    }
}

60、BM60 括号生成

牛客101刷题笔记_第20张图片

思路:

相当于一共n个左括号和n个右括号,可以给我们使用,我们需要依次组装这些括号。每当我们使用一个左括号之后,就剩下n−1个左括号和n个右括号给我们使用,结果拼在使用的左括号之后就行了,因此后者就是一个子问题,可以使用递归:

  • 终止条件: 左右括号都使用了n个,将结果加入数组。
  • 返回值: 每一级向上一级返回后续组装后的字符串,即子问题中搭配出来的括号序列。
  • 本级任务: 每一级就是保证左括号还有剩余的情况下,使用一次左括号进入子问题,或者右括号还有剩余且右括号使用次数少于左括号的情况下使用一次右括号进入子问题。

但是这样递归不能保证括号一定合法,我们需要保证左括号出现的次数比右括号多时我们再使用右括号就一定能保证括号合法了,因此每次需要检查左括号和右括号的使用次数。

具体做法:

  • step 1:将空串与左右括号各自使用了0个送入递归。
  • step 2:若是左右括号都使用了n个,此时就是一种结果。
  • step 3:若是左括号数没有到达n个,可以考虑增加左括号,或者右括号数没有到达n个且左括号的使用次数多于右括号就可以增加右括号。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int整型
     * @return string字符串ArrayList
     */
    public ArrayList<String> generateParenthesis (int n) {
        /**
        * 回溯法
        l:左边括号的数量
        r:右边括号的数量
        str: 括号的样式
         */
        ArrayList<String> res = new ArrayList<>();
        //递归
        backtracking(n, res, 0, 0, "");
        return res;
    }
    void backtracking(int n, List res, int l, int r, String str) {
        //r 的数量一旦大于l,终止
        if (l < r) {
            return;
        }
        //左右括号都用完了,就加入结果
        if (l == n && r == n) {
            res.add(str);
            return;
        }
        //使用一次左括号
        if (l < n) {
            backtracking(n, res, l + 1, r, str + "(");
        }
        //使用右括号个数必须少于左括号
        if (r < l) {
            backtracking(n, res, l, r + 1, str + ")");
        }
    }
}

61、BM61 矩阵最长递增路径

牛客101刷题笔记_第21张图片

思路:

既然是查找最长的递增路径长度,那我们首先要找到这个路径的起点,起点不好直接找到,就从上到下从左到右遍历矩阵的每个元素。然后以每个元素都可以作为起点查找它能到达的最长递增路径。

如何查找以某个点为起点的最长递增路径呢?我们可以考虑深度优先搜索,因为我们查找递增路径的时候,每次选中路径一个点,然后找到与该点相邻的递增位置,相当于进入这个相邻的点,继续查找递增路径,这就是递归的子问题。因此递归过程如下:

  • 终止条件: 进入路径最后一个点后,四个方向要么是矩阵边界,要么没有递增的位置,路径不能再增长,返回上一级。
  • 返回值: 每次返回的就是本级之后的子问题中查找到的路径长度加上本级的长度。
  • 本级任务: 每次进入一级子问题,先初始化后续路径长度为0,然后遍历四个方向(可以用数组表示,下标对数组元素的加减表示去往四个方向),进入符合不是边界且在递增的邻近位置作为子问题,查找子问题中的递增路径长度。因为有四个方向,所以最多有四种递增路径情况,因此要维护当级子问题的最大值。

具体做法:

  • step 1:定义一个变量maxStep来记录最长路径的个数。
  • step 2:遍历矩阵每个位置,都可以作为起点,并维护一个最大的路径长度的值。
  • step 3:对于每个起点,使用dfs查找最长的递增路径:只要下一个位置比当前的位置数字大,就可以深入,同时累加路径长度。
public class Solution {

    int maxStep  = 0;
    public int solve (int[][] matrix) {
        // write code here
        //因为不知道起始点,所以所有的点都要试一遍
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                dfs(1, i, j, matrix, -1);
            }
        }
        return maxStep ;
    }
    private void dfs(int steps, int i, int j, int[][] matrix, int last) {
        //边界判定,以及小于上一个值的也属于走不通的
        if (i < 0 || i >= matrix.length
                || j < 0 || j >= matrix.length
                || matrix[i][j] <= last) {
            return;
        }
        //记录备份当前值
        int cur = matrix[i][j];
        //将走过的路涂黑,这里就是变成-1
        matrix[i][j] = -1;
        maxStep  = Math.max(steps, maxStep );
        //向下
        dfs(steps + 1, i + 1, j, matrix, cur);
        //向上
        dfs(steps + 1, i - 1, j, matrix, cur);
        //向右
        dfs(steps + 1, i, j + 1, matrix, cur);
        //向左
        dfs(steps + 1, i, j - 1, matrix, cur);

        //走完上面以后说明要回退了,重新给这个点赋值回去
        matrix[i][j] = cur;

    }
}

62、BM62 斐波那契数列

牛客101刷题笔记_第22张图片

知识点:动态规划

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

思路:

斐波那契数列初始化第1项与第2项都是1,则根据公式第0项为0,可以按照斐波那契公式累加到第n项。

具体做法:

  • step 1:低于2项的数列,直接返回n。
  • step 2:初始化第0项,与第1项分别为0,1.
  • step 3:从第2项开始,逐渐按照公式累加,并更新相加数始终为下一项的前两项。
public class Solution {

    public int Fibonacci (int n) {
        // write code here
        //从 0 开始,第零项是0,第一项是1
        //低于2项的数列,直接返回n。
        if(n <= 1){
            return n;
        }
        //初始化第0项,与第1项分别为0,1.
        //因n=2时也为1,初始化的时候a = 0,b = 1
        int res = 0;
        int a = 0;
        int b = 1;
        for(int i = 2; i <= n; i++){
            //第三项开始是前两项的和,然后保留最新的两项,更新数据相加
            res = (a + b);
            a = b;
            b = res;
        }
        return res;
    }
}

63、BM63 跳台阶

牛客101刷题笔记_第23张图片

思路:

一只青蛙一次可以跳1阶或2阶,直到跳到第n阶,也可以看成这只青蛙从n阶往下跳,到0阶,按照原路返回的话,两种方法事实上可以的跳法是一样的——即怎么来的,怎么回去! 当青蛙在第n阶往下跳,它可以选择跳1阶到n−1,也可以选择跳2阶到 n−2,即它后续的跳法变成了f(n−1)+f(n−2),这就变成了斐波那契数列。因此可以按照斐波那契数列的做法来做:即输入n,输出第n个斐波那契数列的值,初始化0阶有1种,1阶有1种。

具体做法:

  • step 1:低于2项的数列,直接返回n。
  • step 2:初始化第0项,与第1项分别为0,1.
  • step 3:从第2项开始,逐渐按照公式累加,并更新相加数始终为下一项的前两项。
public class Solution {
    public int jumpFloor(int target) {
        //从0开始,第0项是0,第一项是1
        if(target <= 1)   
            return 1;
        int res = 0;
        int a = 0;
        int b = 1;
        //因n=2时也为1,初始化的时候把a=0,b=1
        for(int i = 2; i <= target + 1; i++){
        //第三项开始是前两项的和,然后保留最新的两项,更新数据相加
            res = (a + b);
            a = b;
            b = res;
        }
        return res ;
    }

64、BM64 最小花费爬楼梯

牛客101刷题笔记_第24张图片

思路:

题目同样考察斐波那契数列的动态规划实现,不同的是题目要求了最小的花费,因此我们将方案统计进行递推的时候只记录最小的开销方案即可。

具体做法:

  • step 1:可以用一个数组记录每次爬到第i阶楼梯的最小花费,然后每增加一级台阶就转移一次状态,最终得到结果。
  • step 2:(初始状态) 因为可以直接从第0级或是第1级台阶开始,因此这两级的花费都直接为0.
  • step 3:(状态转移) 每次到一个台阶,只有两种情况,要么是它前一级台阶向上一步,要么是它前两级的台阶向上两步,因为在前面的台阶花费我们都得到了,因此每次更新最小值即可,转移方程为:dp[i]=min(dp[i−1]+cost[i−1],dp[i−2]+cost[i−2])
public class Solution {

    public int minCostClimbingStairs (int[] cost) {
        //dp[i]表示爬到第i阶楼梯需要的最小花费
        int[] dp = new int[cost.length + 1];
        
        for (int i = 2; i <= cost.length; i++)
            //每次选取最小的方案
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        return dp[cost.length];
    }
}

65、BM65 最长公共子序列(二)

牛客101刷题笔记_第25张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * longest common subsequence
     * @param s1 string字符串 the string
     * @param s2 string字符串 the string
     * @return string字符串
     */
    public String LCS (String s1, String s2) {
        // write code here
        //1. 确定dp数组(dp table)以及下标的含义
        //2. 确定递推公式
        //3. dp数组如何初始化
        //4. 确定遍历顺序
        //5. 举例推导dp数组
        int n = s1.length();
        int m = s2.length();
        //创建一个二维数组 dp,用于存储中间状态的计算结果。
        //dp[i][j] 表示 s1 前 i 个字符和 s2 前 j 个字符的最长公共子序列的长度
        int[][] dp = new int[n + 1][m + 1];
        for (int i = 1; i <= n ; i++) {
            for (int j = 1; j <= m; j++) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j  - 1] + 1;
                } else {
                    dp[i][j] =  Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        if (dp[n][m] == 0 ) {
            return "-1";
        }
        StringBuilder sb = new StringBuilder();
        while (n > 0 && m > 0) {
            if (s1.charAt(n - 1) == s2.charAt(m - 1)) {
                sb.append(s1.charAt(n - 1));
                n--;
                m--;
            } else {
                if (dp[n - 1][m] > dp[n][m - 1]) {
                    n--;
                } else {
                    m--;
                }
            }
        }
        return sb.reverse().toString();
    }
}

66、BM66 最长公共子串

牛客101刷题笔记_第26张图片

具体做法:

  • step 1:我们可以dp[i][j]表示在str1中以第i个字符结尾,在str2中以第j个字符结尾时的公共子串长度,
  • step 2:遍历两个字符串填充dp数组,转移方程为:如果遍历到的该位两个字符相等,则此时长度等于两个前一位长度+1,dp[i][j]=dp[i−1][j−1]+1,如果遍历到该位时两个字符不相等,则置为0,因为这是子串,必须连续相等,断开要重新开始。
  • step 3:每次更新dp[i][j]后,我们维护最大值,并更新该子串结束位置。
  • step 4:最后根据最大值结束位置即可截取出子串。
public class Solution {
    public String LCS (String str1, String str2) {
        //dp[i][j]表示到str1第i个到str2第j个为止的公共子串长度
        int n = str1.length();
        int m = str2.length();
        
        int[][] dp = new int[n + 1][m + 1]; 
        int max = 0;
        int pos = 0;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                //如果该两位相同
                if(str1.charAt(i - 1) == str2.charAt(j - 1)) 
                    //则增加长度
                    dp[i][j] = dp[i - 1][j - 1] + 1; 
                else 
                    //该位置为0
                    dp[i][j] = 0; 
                //更新最大长度
                if(dp[i][j] > max){ 
                    max = dp[i][j];
                    pos = i - 1;
                }
            }
        }
        return str1.substring(pos - max + 1, pos + 1);
    }
}

67、BM67 不同路径的数目(一)

牛客101刷题笔记_第27张图片

思路:

如果我们此时就在右下角的格子,那么能够到达该格子的路径只能是它的上方和它的左方两个格子,因此从左上角到右下角的路径数应该是从左上角到它的左边格子和上边格子的路径数之和,因此可以动态规划。

具体做法:

  • step 1:用dp[i][j]表示大小为i∗j 的矩阵的路径数量,下标从1开始。
  • step 2:(初始条件) 当i或者j为1的时候,代表矩阵只有一行或者一列,因此只有一种路径。
  • step 3:(转移方程) 每个格子的路径数只会来自它左边的格子数和上边的格子数,因此状态转移为dp[i][j]=dp[i−1][j]+dp[i][j−1]
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param m int整型 
     * @param n int整型 
     * @return int整型
     */
    public int uniquePaths (int m, int n) {
        // write code here
        int[][] dp = new int[m + 1][n + 1];
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                //只有一行的时候,只有一种路径
                if(i == 1){
                    dp[i][j] = 1;
                    continue;
                }
                //只有一列的时候,只有一种路径
                if(j == 1){
                    dp[i][j] = 1;
                    continue;
                }
                //路径的长度等于左边的格子加上上方的格子
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m][n];
    }
}

68、BM68 矩阵的最小路径和

牛客101刷题笔记_第28张图片

这题求的是从左上角到右下角,路径上的数字和最小,并且每次只能向下或向右移动。所以上面很容易想到动态规划求解。我们可以使用一个二维数组dpdp[i][j]表示的是从左上角到坐标(i,j)的最小路径和。那么走到坐标(i,j)的位置只有这两种可能,要么从上面(i-1,j)走下来,要么从左边(i,j-1)走过来,我们要选择路径和最小的再加上当前坐标的值就是到坐标(i,j)的最小路径。

所以递推公式就是

dp[i][j]=min(dp[i-1][j]+dp[i][j-1])+grid[i][j];

有了递推公式再来看一下边界条件,当在第一行的时候,因为不能从上面走下来,所以当前值就是前面的累加。同理第一列也一样,因为他不能从左边走过来,所以当前值只能是上面的累加。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param matrix int整型二维数组 the matrix
     * @return int整型
     */
    public int minPathSum (int[][] matrix) {
        // write code here
        int m = matrix.length;
        int n = matrix[0].length;

        int[][] dp = new int[m][n];
        dp[0][0] = matrix[0][0];
        
        //第一列只能从上面走下来
        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + matrix[i][0];
        }
        //第一行只能从左边走过来
        for (int i = 1; i < n; i++) {
            dp[0][i] = dp[0][i - 1] + matrix[0][i];
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //递推公式,取从上面走下来和从左边走过来的最小值+当前坐标的值
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
            }
        }
        return dp[m - 1][n - 1];
    }
}

我们看到二维数组dp和二维数组matrix的长和宽都是一样的,没必要再申请一个dp数组,完全可以使用matrix,来看下代码

public int minPathSum(int[][] matrix) {
    int m = matrix.length;
    int n = matrix[0].length;
    
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (i == 0 && j == 0)
                continue;
            if (i == 0) {
                //第一行只能从左边走过来
                matrix[i][j] += matrix[i][j - 1];
            } else if (j == 0) {
                //第一列只能从上面走下来
                matrix[i][j] += matrix[i - 1][j];
            } else {
                //递推公式,取从上面走下来和从左边走过来的最小值+当前坐标的值
                matrix[i][j] += Math.min(matrix[i - 1][j], matrix[i][j - 1]);
            }
        }
    }
    return matrix[m - 1][n - 1];
}

69、BM69 把数字翻译成字符串

牛客101刷题笔记_第29张图片

思路:

对于普通数组1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案,因此我们使用动态规划将两种方案累计。

具体做法:

  • step 1:用辅助数组dp表示前i个数的译码方法有多少种。
  • step 2:对于一个数,我们可以直接译码它,也可以将其与前面的1或者2组合起来译码:如果直接译码,则dp[i]=dp[i−1];如果组合译码,则dp[i]=dp[i−2]
  • step 3:对于只有一种译码方式的,dp[i−1],对于满足两种译码方式(10,20不能)则是dp[i−1]+dp[i−2]
  • step 4:依次相加,最后的dp[length]即为所求答案。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 解码
     * @param nums string字符串 数字串
     * @return int整型
     */
    public int solve (String nums) {
        // write code here
        //排除 0
        if (nums.equals("0")) {
            return 0;
        }
        //排除只有一种可能的10 20
        if (nums == "10" || nums == "20" ) {
            return 1;
        }
        //当0  的前面不是1 或者2时,无法编译,0 种
        for (int i = 1; i < nums.length(); i++) {
            if (nums.charAt(i) == '0') {
                if (nums.charAt(i - 1) != '1' && nums.charAt(i - 1) != '2') {
                    return 0;
                }
            }
        }
        int[] dp = new int[nums.length() + 1];
        //辅助数组初始化为1
        Arrays.fill(dp, 1);
        for (int i = 2; i <= nums.length(); i++) {
            //在11-19,21-26之间的情况
            if ((nums.charAt(i - 2) == '1' && nums.charAt(i - 1) != '0') ||
                    (nums.charAt(i - 2) == '2' && nums.charAt(i - 1) > '0' &&
                     nums.charAt(i - 1) < '7')) {
                dp[i] = dp[i - 1] + dp[i - 2];
            }

            else {
                dp[i] = dp[i - 1];
            }

        }
        return dp[nums.length()];
    }
}

70、BM70 兑换零钱(一)

牛客101刷题笔记_第30张图片

思路:

这类涉及状态转移的题目,可以考虑动态规划。

具体做法:

  • step 1:可以用dp[i]表示要凑出i元钱需要最小的货币数。
  • step 2:一开始都设置为最大值aim+1,因此货币最小1元,即货币数不会超过aim.
  • step 3:初始化dp[0]=0
  • step 4:后续遍历1元到aim元,枚举每种面值的货币都可能组成的情况,取每次的最小值即可,转移方程为dp[i]=min(dp[i],dp[i−arr[j]]+1).
  • step 5:最后比较dp[aim]的值是否超过aim,如果超过说明无解,否则返回即可。
public int minMoney (int[] arr, int aim) {
    // write code here
    //小于1的都返回0
    if (aim < 1) {
        return 0;
    }
    int[] dp = new int[aim + 1];
    //dp[i] 表示凑齐i元最少需要多少货币数
    Arrays.fill(dp, aim + 1);
    dp[0] = 0;
    //遍历1- aim元
    for (int i = 1; i <= aim; i++) {
        //每种面值的货币都要枚举
        for (int j = 0; j < arr.length; j++) {
            //如果面值不超过要凑的钱才能用
            if (arr[j] <= i)
                //维护最小值
                dp[i] = Math.min(dp[i], dp[i - arr[j]] + 1);
        }
    }
    //如果最终答案大于aim代表无解
    return dp[aim] > aim ? -1 : dp[aim];
}

71、BM71 最长上升子序列(一)

牛客101刷题笔记_第31张图片

思路:

要找到最长的递增子序列长度,每当我们找到一个位置,它是继续递增的子序列还是不是,它选择前面哪一处接着才能达到最长的递增子序列,这类有状态转移的问题常用方法是动态规划。

具体做法:

  • step 1:用dp[i]表示到元素i结尾时,最长的子序列的长度,初始化为1,因为只有数组有元素,至少有一个算是递增。
  • step 2:第一层遍历数组每个位置,得到n个长度的子数组。
  • step 3:第二层遍历相应子数组求对应到元素i结尾时的最长递增序列长度,期间维护最大值。
  • step 4:对于每一个到i结尾的子数组,如果遍历过程中遇到元素j小于结尾元素,说明以该元素结尾的子序列加上子数组末尾元素也是严格递增的,因此转移方程为dp[i]=dp[j]+1
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 给定数组的最长严格上升子序列的长度。
     * @param arr int整型一维数组 给定的数组
     * @return int整型
     */
    public int LIS (int[] arr) {
        // write code here
        if(arr.length == 1){
            return 1;
        }
        int dp[] = new int[arr.length];
        //设置数组长度大小的动态规划辅助数组
        Arrays.fill(dp, 1);
        int res = 0;
        for(int i = 1; i < arr.length; i++){
            for(int j = 0; j < i; j++){
                //可能j不是所需要的最大的,因此需要dp[i] = dp[j] + 1
                if(arr[i] > arr[j] && dp[i] < dp[j] + 1){
                    //i 点比 j 点大,理论上dp要加1
                    dp[i] = dp[j] + 1;
                    //找到最大长度
                    res = Math.max(res, dp[i]);
                }
            }
        }
        return res;
    }
}

72、BM72 连续子数组的最大和

牛客101刷题笔记_第32张图片

解题思路

方法1:连续的子数组,即数组中从i下标到j下标(0<=i<=j<数组长度)的数据,想要获得所有的子数组和,可以通过暴力法,两次循环获得,但时间复杂度为O(n^2),效率不高。

方法2:动态规划,设动态规划列表 dp,dp[i] 代表以元素 array[i] 为结尾的连续子数组最大和。
状态转移方程: dp[i] = Math.max(dp[i-1]+array[i], array[i]);
具体思路如下:

  1. 遍历数组,比较 dp[i-1] + array[i] 和 array[i]的大小;
  2. 为了保证子数组的和最大,每次比较 sum 都取两者的最大值;
  3. 用max变量记录计算过程中产生的最大的连续和dp[i];
    牛客101刷题笔记_第33张图片

方法3:我们可以简化动态规划,使用一个变量sum来表示当前连续的子数组和,以及一个变量max来表示中间出现的最大的和。

代码实现

方法1:暴力法,时间复杂度O(n^2),空间复杂度O(1)

public int FindGreatestSumOfSubArray(int[] array) {
        int max = array[0];
        int sum = 0;
        for(int i=0;i<array.length;i++){
            // 每开启新的循环,需要把sum归零
            sum = 0;
            for(int j=i;j<array.length;j++){
                // 这里是求从i到j的数值和
                sum += array[j];
                // 每次比较,保存出现的最大值
                max = Math.max(max,sum);
            }
        }
        return max;
    }

方法2:动态规划,时间复杂度O(n),空间复杂度O(n)

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param array int整型一维数组
     * @return int整型
     */
    public int FindGreatestSumOfSubArray (int[] array) {
        // write code here
        int[] dp = new int[array.length];

        dp[0] = array[0];
        
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            //状态转移方程
            dp[i] = Math.max(dp[i - 1] + array[i], array[i]);
            //更新最大值
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

方法3:优化动态规划,时间复杂度O(n),空间复杂度O(1)

public int FindGreatestSumOfSubArray(int[] array) {
        int sum = 0;
        int max = array[0];
        for(int i=0;i<array.length;i++){
            // 优化动态规划,确定sum的最大值
            sum = Math.max(sum + array[i], array[i]);
            // 每次比较,保存出现的最大值
            max = Math.max(max,sum);
        }
        return max;
}

73、BM73 最长回文子串

牛客101刷题笔记_第34张图片

1、中心扩散法

中心扩散法也很好理解,我们遍历字符串的每一个字符,然后以当前字符为中心往两边扩散,查找最长的回文子串

但是回文串的长度不一定都是奇数,也可能是偶数,比如字符串"abba"。我们来思考这样一个问题,如果是单个字符,我们可以认为他是回文子串,如果是多个字符,并且他们都是相同的,那么他们也是回文串

所以对于上面的问题,我们以当前字符为中心往两边扩散的时候,先要判断和他挨着的有没有相同的字符,如果有,则直接跳过,来看下代码

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param A string字符串
     * @return int整型
     */
    public int getLongestPalindrome (String A, int n) {
        // write code here
        if (n < 2) {
            return A.length();
        }
        //maxLen 表示最长回文串的长度
        int maxLen = 0;
        for (int i = 0; i < n; ) {
            //如果剩余子串长度小于目前查找到的最长回文子串的长度,直接终止循环
            // (因为即使他是回文子串,也不是最长的,所以直接终止循环,不再判断)
            if (n - i <= maxLen / 2)
                break;
            int left = i;
            int right = i;
            while (right < n - 1 && A.charAt(right + 1) == A.charAt(right))
                ++right; //过滤掉重复的

            //下次在判断的时候从重复的下一个字符开始判断
            i = right + 1;
            //然后往两边判断,找出回文子串的长度
            while (right < n - 1 && left > 0 
                    && A.charAt(right + 1) == A.charAt(left - 1)) {
                ++right;
                --left;
            }
            //保留最长的
            if (right - left + 1 > maxLen) {
                maxLen = right - left + 1;
            }
        }
        //截取回文子串
        return maxLen;
    }
}

2、暴力求解

暴力求解是最容易想到的,要截取字符串的所有子串,然后再判断这些子串中哪些是回文的,最后返回回文子串中最长的即可

这里我们可以使用两个变量,一个记录最长回文子串开始的位置,一个记录最长回文子串的长度,最后再截取。代码如下

    public int getLongestPalindrome(String A, int n) {
        //边界条件判断
        if (n < 2)
            return A.length();
        int maxLen = 0;
        for (int i = 0; i < n - 1; i++) {
            for (int j = i; j < n; j++) {
                //截取所有子串,然后在逐个判断是否是回文的
                if (isPalindrome(A, i, j)) {
                    if (maxLen < j - i + 1) {
                        maxLen = j - i + 1;
                    }
                }
            }
        }
        return maxLen;
    }

    //判断是否是回文串
    private boolean isPalindrome(String s, int start, int end) {
        while (start < end) {
            if (s.charAt(start++) != s.charAt(end--))
                return false;
        }
        return true;
    }

实际上面代码其实还可以优化一下,在截取的时候,如果截取的长度小于等于目前查找到的最长回文子串,我们可以直接跳过,不需要再判断了,因为即使他是回文子串,也不可能是最长的

    public int getLongestPalindrome(String A, int n) {
        //边界条件判断
        if (n < 2)
            return A.length();
        int maxLen = 0;
        for (int i = 0; i < n - 1; i++) {
            for (int j = i; j < n; j++) {
                //截取所有子串,然后在逐个判断是否是回文的
                if (isPalindrome(A, i, j)) {
                    if (maxLen < j - i + 1) {
                        maxLen = j - i + 1;
                    }
                }
            }
        }
        return maxLen;
    }

    //判断是否是回文串
    private boolean isPalindrome(String s, int start, int end) {
        while (start < end) {
            if (s.charAt(start++) != s.charAt(end--))
                return false;
        }
        return true;
    }

3、动态规划

定义二维数组dp[length][length],如果dp[left][right]为true,则表示字符串从left到right是回文子串,如果dp[left][right]为false,则表示字符串从leftright不是回文子串。

如果dp[left+1][right-1]为true,我们判断s.charAt(left)s.charAt(right)是否相等,如果相等,那么dp[left][right]肯定也是回文子串,否则dp[left][right]一定不是回文子串。

所以我们可以找出递推公式

 dp[left][right]=s.charAt(left)==s.charAt(right) && dp[left+1][right-1]

有了递推公式,还要确定边界条件:

如果s.charAt(left)!=s.charAt(right),那么字符串从left到right是不可能构成子串的,直接跳过即可。

如果s.charAt(left)==s.charAt(right),字符串从left到right能不能构成回文子串还需要进一步判断

  • 如果left==right,也就是说只有一个字符,我们认为他是回文子串。即dp[left][right]=true(left==right)
  • 如果right-left<=2,类似于"aa",或者"aba",我们认为他是回文子串。即dp[left][right]=true(right-left<=2)
  • 如果right-left>2,我们只需要判断dp[left+1][right-1]是否是回文子串,才能确定dp[left][right]是否为true还是false。即dp[left][right]=dp[left+1][right-1]

有了递推公式和边界条件,代码就很容易写了,来看下

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param A string字符串
     * @return int整型
     */
    public int getLongestPalindrome(String A) {
        int n = A.length();
        //边界条件判断
        if (n < 2)
            return n;
        //start表示最长回文串开始的位置,
        //maxLen表示最长回文串的长度
        int maxLen = 1;
        boolean[][] dp = new boolean[n][n];
        for (int right = 1; right < n; right++) {
            for (int left = 0; left <= right; left++) {
                //如果两种字符不相同,肯定不能构成回文子串
                if (A.charAt(left) != A.charAt(right))
                    continue;

                //下面是s.charAt(left)和s.charAt(right)两个
                //字符相同情况下的判断
                //如果只有一个字符,肯定是回文子串
                if (right == left) {
                    dp[left][right] = true;
                } else if (right - left <= 2) {
                    //类似于"aa"和"aba",也是回文子串
                    dp[left][right] = true;
                } else {
                    //类似于"a******a",要判断他是否是回文子串,只需要
                    //判断"******"是否是回文子串即可
                    dp[left][right] = dp[left + 1][right - 1];
                }
                //如果字符串从left到right是回文子串,只需要保存最长的即可
                if (dp[left][right] && right - left + 1 > maxLen) {
                    maxLen = right - left + 1;
                }
            }
        }
        //最长的回文子串
        return maxLen;
    }
}

74、BM74 数字字符串转化成IP地址

牛客101刷题笔记_第35张图片

思路:

对于IP地址每次取出一个数字和一个点后,对于剩余的部分可以看成是一个子问题,因此可以使用递归和回溯将点插入数字中。

具体做法:

  • step 1:使用step记录分割出的数字个数,index记录递归的下标,结束递归是指step已经为4,且下标到达字符串末尾。
  • step 2:在主体递归中,每次加入一个字符当数字,最多可以加入三个数字,剩余字符串进入递归构造下一个数字。
  • step 3:然后要检查每次的数字是否合法(不超过255且没有前导0).
  • step 4:合法IP需要将其连接,同时递归完这一轮后需要回溯。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串ArrayList
     */

    //记录分段IP数字字符串
    private String nums = "";
    
    public ArrayList<String> restoreIpAddresses (String s) {
        // write code here
        ArrayList<String> res = new ArrayList<>();
        dfs(s, res,  0, 0);
        return res;
    }
    //step 表示第几个数字,index表示字符串下标
    public void dfs(String s, ArrayList<String> res, int step, int index){
        //当前分割出的字符串
        String cur = "";
        //分割出了四个数字
        if(step == 4){
            //下标必须走到末尾
            if(index != s.length()){
                return;
            }
            res.add(nums);
        }else{
            //最长遍历三位
            for(int i = index; i < index + 3 && i < s.length(); i++){
                cur += s.charAt(i);
                //转数字比较
                int num = Integer.parseInt(cur);
                String temp = nums;
                //不能超过255且不能有前导0
                if(num <= 255 && (cur.length() == 1 || cur.charAt(0) != '0')){
                    //添加点
                    if(step - 3 != 0){
                        nums += cur + ".";
                    }else{
                        nums += cur;
                    }
                    //递归查找下一位数字
                    dfs(s, res, step + 1, i + 1);
                    //回溯
                    nums = temp;
                }
            }
        }
    }
}

75、BM75 编辑距离(一)

牛客101刷题笔记_第36张图片

思路:

把第一个字符串变成第二个字符串,我们需要逐个将第一个字符串的子串最少操作下变成第二个字符串,这就涉及了第一个字符串增加长度,状态转移,那可以考虑动态规划。用dp[i][j]表示从两个字符串首部各自到str1[i]str2[j]为止的子串需要的编辑距离,那很明显dp[str1.length][str2.length]就是我们要求的编辑距离。(下标从1开始)

具体做法:

  • step 1:初始条件: 假设第二个字符串为空,那很明显第一个字符串子串每增加一个字符,编辑距离就加1,这步操作是删除;同理,假设第一个字符串为空,那第二个字符串每增加一个字符,编剧距离就加1,这步操作是添加。
  • step 2:状态转移: 状态转移肯定是将dp矩阵填满,那就遍历第一个字符串的每个长度,对应第二个字符串的每个长度。如果遍历到str1[i]str2[j]的位置,这两个字符相同,这多出来的字符就不用操作,操作次数与两个子串的前一个相同,因此有dp[i][j]=dp[i−1][j−1];如果这两个字符不相同,那么这两个字符需要编辑,但是此时的最短的距离不一定是修改这最后一位,也有可能是删除某个字符或者增加某个字符,因此我们选取这三种情况的最小值增加一个编辑距离,即dp[i][j]=min(dp[i−1][j−1],min(dp[i−1][j],dp[i][j−1]))+1
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param str1 string字符串
     * @param str2 string字符串
     * @return int整型
     */
    public int editDistance (String str1, String str2) {
        // write code here
        int n = str1.length();
        int m = str2.length();
        //dp[i][j]
        int[][] dp = new int[n + 1][m + 1];
        //初始化边界
        for (int i = 1; i <= n; i++) {
            dp[i][0] = dp[i - 1][0] + 1;
        }
        for (int i = 1; i <= m; i++) {
            dp[0][i] = dp[0][i - 1] + 1;
        }
        //遍历第一个字符串的每个位置
        for (int i = 1; i <= n; i++) {
            //对应第二个字符串的每个位置
            for (int j = 1; j <= m; j++) {
                //若是字符相同,则不用处理
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                    //直接等于二者前一个的距离
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    //选取最小的距离加上此处的编辑距离 1
                    dp[i][j] = Math.min(dp[i - 1][j - 1],
                                Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
                }
            }
        }
        return dp[n][m];
    }
}

76、BM76 正则表达式匹配

77、BM77 最长的括号子串

78、BM78 打家劫舍(一)

牛客101刷题笔记_第37张图片

牛客101刷题笔记_第38张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int rob (int[] nums) {
        // write code here
        //只有一个元素,就返回该元素的值
        if(nums.length == 1){
            return nums[0];
        }
        //两个元素,就返回两元素其中一个最大值
        if(nums.length == 2){
            return Math.max(nums[0], nums[1]);
        }
        //定义一个数组,n-1个值和n-2个值的和
        int[] maxVal  = new int[nums.length];
        //初始化结果数组 第0 个元素和第1个元素
        maxVal[0] = nums[0];
        maxVal[1] = Math.max(nums[0],nums[1]);
        
        for(int i = 2; i < nums.length; i++){
            maxVal[i] = Math.max(maxVal[i - 1], maxVal[i - 2] + nums[i]);
        }
        return  maxVal[maxVal.length -1];
    }
}

79、BM79 打家劫舍(二)

牛客101刷题笔记_第39张图片

思路:

这道题与BM78.打家劫舍(一)比较类似,区别在于这道题是环形,第一家和最后一家是相邻的,既然如此,在原先的方案中第一家和最后一家不能同时取到。

具体做法:

  • step 1:使用原先的方案是:用dp[i]表示长度为i的数组,最多能偷取到多少钱,只要每次转移状态逐渐累加就可以得到整个数组能偷取的钱。
  • step 2:(初始状态)如果数组长度为1,只有一家人,肯定是把这家人偷了,收益最大,因此dp[1]=nums[0]
  • step 3:(状态转移) 每次对于一个人家,我们选择偷他或者不偷他,如果我们选择偷那么前一家必定不能偷,因此累加的上上级的最多收益,同理如果选择不偷他,那我们最多可以累加上一级的收益。因此转移方程为dp[i]=max(dp[i−1],nums[i−1]+dp[i−2])。这里的i在dp中为数组长度,在nums中为下标。
  • step 4:此时第一家与最后一家不能同时取到,那么我们可以分成两种情况讨论:
    • 情况1:偷第一家的钱,不偷最后一家的钱。初始状态与状态转移不变,只是遍历的时候数组最后一位不去遍历。
    • 情况2:偷最后一家的请,不偷第一家的钱。初始状态就设定了dp[1]=0,第一家就不要了,然后遍历的时候也会遍历到数组最后一位。
  • step 5:最后取两种情况的较大值即可。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param nums int整型一维数组 
     * @return int整型
     */
    public int rob (int[] nums) {
        // write code here
        //dp[i]表示长度为i的数组,最多能偷取多少钱
        int[] dp = new int[nums.length + 1];
        //选择偷了第一家
        dp[1] = nums[0];
        //最后一家不能偷
        for(int i = 2; i < nums.length; i++){
            //对于每家可以选择偷或者不偷
            dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
        }
        int res = dp[nums.length - 1];
        //清除dp数组,第二次循环
        Arrays.fill(dp, 0);
        //不偷第一家
        dp[1] = 0;
        //可以偷最后一家
        for(int i = 2; i <= nums.length; i++){
            //对于每家可以选择偷或者不偷
            dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
        }
        //选择最大值返回
        return Math.max(res, dp[nums.length]);
    }
}

80、BM80 买卖股票的最好时机(一)

牛客101刷题笔记_第40张图片

思路:

对于每天有到此为止的最大收益和是否持股两个状态,因此我们可以用动态规划。

具体做法:

  • step 1:用dp[i][0]表示第i天不持股到该天为止的最大收益,dp[i][1]表示第i天持股,到该天为止的最大收益。
  • step 2:(初始状态) 第一天不持股,则总收益为0,dp[0][0]=0;第一天持股,则总收益为买股票的花费,此时为负数,dp[0][1]=−prices[0]
  • step 3:(状态转移) 对于之后的每一天,如果当天不持股,有可能是前面的若干天中卖掉了或是还没买,因此到此为止的总收益和前一天相同,也有可能是当天才卖掉,我们选择较大的状态dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i])
  • step 4:如果当天持股,有可能是前面若干天中买了股票,当天还没卖,因此收益与前一天相同,也有可能是当天买入,此时收益为负的股价,同样是选取最大值:dp[i][1]=max(dp[i−1][1],−prices[i])
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param prices int整型一维数组 
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here
        int n = prices.length;
        //dp[i][0]表示某一天不持股到该天为止的最大收益,
        //dp[i][1]表示某天持股,到该天为止的最大收益
        int[][] dp = new int[n][2];
        //第一天不持股,总收益为0;
        dp[0][0] = 0;
        //第一天持股,总收益为减去该天的股价
        dp[0][1] = -prices[0];
        //遍历后续的每天,状态转移
        for(int i = 1; i < n; i++){
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
        }
        //最后一天不持股,到该天为止的最大收益
        return dp[n - 1][0];
    }
}

81、BM81 买卖股票的最好时机(二)

牛客101刷题笔记_第41张图片

这道题与BM80.买卖股票的最好时机(一)的区别在于可以多次买入卖出。

定义dp[i][0]表示第i+1天交易完之后手里没有股票的最大利润,dp[i][1]表示第i+1天交易完之后手里持有股票的最大利润。

当天交易完之后手里没有股票可能有两种情况,一种是当天没有进行任何交易,又因为当天手里没有股票,所以当天没有股票的利润只能取前一天手里没有股票的利润。一种是把当天手里的股票给卖了,既然能卖,说明手里是有股票的,所以这个时候当天没有股票的利润要取前一天手里有股票的利润加上当天股票能卖的价格。这两种情况我们取利润最大的即可,所以可以得到

dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);

当天交易完之后手里持有股票也有两种情况,一种是当天没有任何交易,又因为当天手里持有股票,所以当天手里持有的股票其实前一天就已经持有了。还一种是当天买入了股票,当天能卖股票,说明前一天手里肯定是没有股票的,我们取这两者的最大值,所以可以得到

dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);

动态规划的递推公式有了,那么边界条件是什么,就是第一天

如果买入:dp[0][1]=-prices[0];
如果没买:dp[0][0]=0;

有了递推公式和边界条件,代码很容易就写出来了。

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算最大收益
     * @param prices int整型一维数组 股票每一天的价格
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here

        if (prices == null || prices.length < 2) {
            return 0;
        }
        int n = prices.length;
        //dp[i][0]表示某一天不持股到该天为止的最大收益,
        //dp[i][1]表示某天持股,到该天为止的最大收益
        int[][] dp = new int[n][2];
        //初始条件
        //第一天不持股,总收益为0
        dp[0][0] = 0;
        //第一天持股,总收益为减去该天的股价
        dp[0][1] = -prices[0];
        //遍历后续每天,状态转移
        for (int i = 1; i < n; i++) {
            //递推公式
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        //最后一天肯定是手里没有股票的时候,利润才会最大,
        //只需要返回dp[length - 1][0]即可
        return dp[n - 1][0];
    }
}

上面计算的时候我们看到当天的利润只和前一天有关,没必要使用一个二维数组,只需要使用两个变量,一个记录当天交易完之后手里持有股票的最大利润,一个记录当天交易完之后手里没有股票的最大利润,来看下代码

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算最大收益
     * @param prices int整型一维数组 股票每一天的价格
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here

        if (prices == null || prices.length < 2) {
            return 0;
        }
        int n = prices.length;
        //初始条件
        //没持有股票
        int noHold = 0;
        //持有股票
        int hold = -prices[0];
        //遍历后续每天,状态转移
        for (int i = 1; i < n; i++) {
            //递推公式转化的
            noHold = Math.max(noHold, hold + prices[i]);
            hold = Math.max(hold, noHold - prices[i]);
        }
        //最后一天肯定是手里没有股票的时候,利润才会最大,
        //所以这里返回的是noHold
        return noHold;
    }
}

82、BM82 买卖股票的最好时机(三)

牛客101刷题笔记_第42张图片

思路:

这道题与BM80.买卖股票的最好时机(一)的区别在于最多可以买入卖出2次,那实际上相当于它的状态多了几个,对于每天有到此为止的最大收益和持股情况两个状态,持股情况有了5种变化,我们用:

  • dp[i][0]表示到第i天为止没有买过股票的最大收益
  • dp[i][1]表示到第i天为止买过一次股票还没有卖出的最大收益
  • dp[i][2]表示到第i天为止买过一次也卖出过一次股票的最大收益
  • dp[i][3]表示到第i天为止买过两次只卖出过一次股票的最大收益
  • dp[i][4]表示到第i天为止买过两次同时也买出过两次股票的最大收益

于是使用动态规划,有了如下的状态转移

具体做法:

  • step 1:初始状态 :与上述提到的题类似,第0天有买入了和没有买两种状态:dp[0][0]=0dp[0][1]=−prices[0]
  • step 2:状态转移:对于后续的每一天,如果当天还是状态0,则与前一天相同,没有区别;
  • step 3:如果当天状态为1,可能是之前买过了或者当天才第一次买入,选取较大值:dp[i][1]=max(dp[i−1][1],dp[i−1][0]−prices[i])
  • step 4:如果当天状态是2,那必须是在1的状态下(已经买入了一次)当天卖出第一次,或者早在之前就卖出只是还没买入第二次,选取较大值:dp[i][2]=max(dp[i−1][2],dp[i−1][1]+prices[i])
  • step 5:如果当天状态是3,那必须是在2的状态下(已经卖出了第一次)当天买入了第二次,或者早在之前就买入了第二次,只是还没卖出,选取较大值:dp[i][3]=max(dp[i−1][3],dp[i−1][2]−prices[i]);
    • step 6:如果当天是状态4,那必须是在3的状态下(已经买入了第二次)当天再卖出第二次,或者早在之前就卖出了第二次,选取较大值:dp[i][4]=max(dp[i−1][4],dp[i−1][3]+prices[i])
  • step 7:最后我们还要从0、第一次卖出、第二次卖出中选取最大值,因为有可能没有收益,也有可能只交易一次收益最大。

牛客101刷题笔记_第43张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 两次交易所能获得的最大收益
     * @param prices int整型一维数组 股票每一天的价格
     * @return int整型
     */
    public int maxProfit (int[] prices) {
        // write code here
        int n = prices.length;
        int[][] dp = new int[n][5];
        //初始化dp为最小
        Arrays.fill(dp[0], -10000);
        //第0天不持有状态
        dp[0][0] = 0;
        //第0天持有股票
        dp[0][1] = -prices[0];
        //状态转移
        for (int i = 1; i < n; i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        //选取最大值,可以只操作一次
        return Math.max(dp[n - 1][2], Math.max(0, dp[n - 1][4]));
    }
}

83、BM83 字符串变形

牛客101刷题笔记_第44张图片

思路:

将单词位置的反转,那肯定前后都是逆序,不如我们先将整个字符串反转,这样是不是单词的位置也就随之反转了。但是单词里面的成分也反转了啊,既然如此我们再将单词里面的部分反转过来就行。

具体做法:

  • step 1:遍历字符串,遇到小写字母,转换成大写,遇到大写字母,转换成小写,遇到空格正常不变。
  • step 2:第一次反转整个字符串,这样基本的单词逆序就有了,但是每个单词的字符也是逆的。
  • step 3:再次遍历字符串,以每个空间为界,将每个单词反转回正常。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param s string字符串
     * @param n int整型 -
    4,mn
     * @return string字符串
     */
    public String trans (String s, int n) {
        // write code here
        if (n == 0)
            return s;
        StringBuffer res = new StringBuffer();
        for (int i = 0; i < n; i++) {
            //大小写转换
            if (s.charAt(i) >= 'A' && s.charAt(i) <= 'Z'){
                res.append((char)(s.charAt(i) - 'A' + 'a'));
            } else if (s.charAt(i) >= 'a' && s.charAt(i) <= 'z'){
                res.append((char)(s.charAt(i) - 'a' + 'A'));
            } else{
                //空格直接复制
                res.append(s.charAt(i));
            }
        }
        //翻转整个字符串
        res = res.reverse();
        
        for (int i = 0; i < n; i++) {
            int j = i;
            //以空格为界,二次翻转
            while (j < n && res.charAt(j) != ' ')
                j++;
            String temp = res.substring(i, j);
            StringBuffer buffer = new StringBuffer(temp);
            temp = buffer.reverse().toString();
            res.replace(i, j, temp);
            i = j;
        }
        return res.toString();
    }
}

84、BM84 最长公共前缀

牛客101刷题笔记_第45张图片

纵向扫描

  • 将字符串数组看作一个二维空间,每一次从第一列开始。
  • 确定所有字符子串中第一列字符。
  • 逐层扫描后面每一列,遇到不同字符停止扫描。

牛客101刷题笔记_第46张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param strs string字符串一维数组 
     * @return string字符串
     */
    public String longestCommonPrefix (String[] strs) {
        // write code here
        //纵向扫描
        if(strs.length == 0 || strs == null){
            return "";
        }
        int rows = strs.length;
        int cols = strs[0].length();
        //开始扫描
        for(int i = 0; i < cols; i++){
            //拿到每一列的第一个字符
            char firstChar = strs[0].charAt(i);
            //遍历比较
            for(int j = 1; j < rows; j++){
                //如果遍历到的字符跟第一个字符不相等,就结束
                if(strs[j].length() == i || strs[j].charAt(i)!=firstChar){
                    return strs[0].substring(0, i);
                }
            }
        }
        return strs[0];
    }
}

85、BM85 验证IP地址

牛客101刷题笔记_第47张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 验证IP地址
     * @param IP string字符串 一个IP地址字符串
     * @return string字符串
     */
    public String solve (String IP) {
        // 判断是否为IPv4地址,是则返回"IPv4",否则继续验证是否为IPv6地址
        return validIPv4(IP) ? "IPv4" : (validIPv6(IP) ? "IPv6" : "Neither");
    }

    /**
     * 验证IPv4地址
     * @param IP string字符串 一个IP地址字符串
     * @return boolean值,表示是否为有效的IPv4地址
     */
    private boolean validIPv4(String IP) {
        // 将IP地址按照"."进行拆分,得到字符串数组
        String[] strs = IP.split("\\.", -1);
        // 如果拆分后的数组长度不为4,则说明IP地址无效
        if (strs.length != 4) {
            return false;
        }

        // 遍历每个部分进行验证
        for (String str : strs) {
            // 如果部分长度大于1且以0开头,则说明有不合法的前导零
            if (str.length() > 1 && str.startsWith("0")) {
                return false;
            }
            try {
                // 将部分转换为整数
                int val = Integer.parseInt(str);
                // 判断整数是否在有效范围内(0到255之间)
                if (!(val >= 0 && val <= 255)) {
                    return false;
                }
            } catch (NumberFormatException numberFormatException) {
                // 如果转换出现异常,则说明部分不是有效的整数
                return false;
            }
        }
        // IP地址为有效的IPv4地址
        return true;
    }

    /**
     * 验证IPv6地址
     * @param IP string字符串 一个IP地址字符串
     * @return boolean值,表示是否为有效的IPv6地址
     */
    private boolean validIPv6(String IP) {
        // 将IP地址按照":"进行拆分,得到字符串数组
        String[] strs = IP.split(":", -1);
        // 如果拆分后的数组长度不为8,则说明IP地址无效
        if (strs.length != 8) {
            return false;
        }

        // 遍历每个部分进行验证
        for (String str : strs) {
            // 如果部分长度超过4或者长度为0,则说明不是有效的IPv6地址
            if (str.length() > 4 || str.length() == 0) {
                return false;
            }
            try {
                // 将部分作为16进制数转换为整数
                int val = Integer.parseInt(str, 16);
            } catch (NumberFormatException numberFormatException) {
                // 如果转换出现异常,则说明部分不是有效的整数
                return false;
            }
        }
        // IP地址为有效的IPv6地址
        return true;
    }
}

86、BM86 大数加法

牛客101刷题笔记_第48张图片

思路:

大整数相加,就可以按照整数相加的方式,从个位开始,逐渐往上累加,换到字符串中就是从两个字符串的末尾开始相加。

具体做法:

  • step 1:若是其中一个字符串为空,直接返回另一个,不用加了。
  • step 2:交换两个字符串的位置,我们是s为较长的字符串,t为较短的字符串,结果也记录在较长的字符串中。
  • step 3:从后往前遍历字符串s,每次取出字符转数字,加上进位制,将下标转换为字符串t中从后往前相应的下标,如果下标为非负数则还需要加上字符串t中相应字符转化的数字。
  • step 4:整型除法取进位,取模算法去掉十位,将计算后的结果放入较长数组对应位置。
  • step 5:如果遍历结束,进位值还有,则需要直接在字符串s前增加一个字符‘1’。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算两个数之和
     * @param s string字符串 表示第一个整数
     * @param t string字符串 表示第二个整数
     * @return string字符串
     */
    public String solve (String s, String t) {
        // write code here
        //若是其中一个为空,返回另一个
        if (s.length() <= 0)
            return t;
        if (t.length() <= 0)
            return s;
        //让s为较长的,t为较短的
        if (s.length() < t.length()) {
            String temp = s;
            s = t;
            t = temp;
        }
        //进位标志
        int carry = 0; 
        char[] res = new char[s.length()];
        //从后往前遍历较长的字符串
        for (int i = s.length() - 1; i >= 0; i--) {
            //转数字加上进位
            int temp = s.charAt(i) - '0' + carry;
            //转较短的字符串相应的从后往前的下标
            int j = i - s.length() + t.length();
            //如果较短字符串还有
            if (j >= 0)
                //转数组相加
                temp += t.charAt(j) - '0';
            //取进位
            carry = temp / 10;
            //去十位
            temp = temp % 10;
            //修改结果
            res[i] = (char)(temp + '0');
        }
        String output = String.valueOf(res);
        //最后的进位
        if (carry == 1)
            output = '1' + output;
        return output;
    }
}

87、BM87 合并两个有序的数组

牛客101刷题笔记_第49张图片

解法一:合并后排序

思路步骤:

  • 对原数组A,B进行合并

  • 将合并后的数组A进行排序

    import java.util.*;
    public class Solution {
       public void merge(int A[], int m, int B[], int n) {
          //合并
           for(int i=0;i!=n;i++){
               A[m+i] = B[i];
           }
           //排序
           Arrays.sort(A);
       }
    }
    

解法二:双指针

思路步骤

  • 这一方法将两个数组看作队列
  • 每次从两个数组头部取出比较小的数字放到结果中。
  • 参考图解:

public class Solution {
    /**
     * 合并两个有序数组
     *
     * @param A 数组A,长度为m
     * @param m 数组A的有效元素个数
     * @param B 数组B,长度为n
     * @param n 数组B的有效元素个数
     */
    public void merge(int A[], int m, int B[], int n) {
        // 定义两个指针,分别指向数组A和数组B的起始位置
        int p1 = 0, p2 = 0;
        // 新开一个大小为m+n的数组用于存放合并后的结果
        int[] sorted = new int[m + n];
        int cur; // 用于记录选择出来的较小的元素

        // 循环选择较小的元素放入新数组
        while (p1 < m || p2 < n) {
            if (p1 == m) {
                // 如果数组A的元素已经全部选择完,则直接选择数组B的元素
                cur = B[p2++];
            } else if (p2 == n) {
                // 如果数组B的元素已经全部选择完,则直接选择数组A的元素
                cur = A[p1++];
            } else if (A[p1] < B[p2]) {
                // 如果数组A的当前元素小于数组B的当前元素,则选择数组A的元素
                cur = A[p1++];
            } else {
                // 否则选择数组B的元素
                cur = B[p2++];
            }
            sorted[p1 + p2 - 1] = cur; // 将选择的元素放入新数组中
        }

        // 将新数组中的元素复制回原数组A
        for (int i = 0; i != m + n; ++i) {
            A[i] = sorted[i];
        }
    }
}

复杂度分析:

时间复杂度: 。
指针移动单调递增,最多移动 m+n次。

空间复杂度:
需要建立长度为 m+n的中间数组sorted

88、BM88 判断是否为回文字符串

牛客101刷题笔记_第50张图片

解法一:双指针

回文字符串正向遍历与逆向遍历结果都是一样的,因此我们可以准备两个对撞指针,一个正向遍历,一个逆向遍历。

具体做法:

  • step 1:准备两个指针,一个在字符串首,一个在字符串尾。
  • step 2:在首的指针往后走,在尾的指针往前走,依次比较路过的两个字符是否相等,若是不相等则直接就不是回文。
  • step 3:直到两指针在中间相遇,都还一致就是回文。因为首指针到了后半部分,走过的正好是尾指针走过的路,二者只是交换了位置,比较相等还是一样的。
import java.util.*;
public class Solution {
    public boolean judge (String str) {
        //首指针
        int left = 0;
        //尾指针
        int right = str.length() - 1;
        //首尾往中间靠
        while (left < right) {
            //比较前后是否相同
            if (str.charAt(left) != str.charAt(right))
                return false;
            left++;
            right--;
        }
        return true;
    }
}

解法二:反转字符串比较法

思路:

既然字符串正向遍历与逆向遍历遇到字符都相等,那我们就反转字符串,看看它到底是不是正逆都一样,因为字符串支持整体比较,因此我们可以比较反转后的字符串与原串是不是相等。

具体做法:

  • step 1:使用reverse函数将字符串反转。
  • step 2:比较反转后的字符串,还是与原来的字符串相等,则是回文字符串。
import java.util.*;
public class Solution {
    public boolean judge (String str) {
        StringBuffer temp = new StringBuffer(str);
        //反转字符串
        String s = temp.reverse().toString();
        //比较字符串是否相等
        if(s.equals(str))
            return true;
        return false;
    }
}

89、BM89 合并区间

牛客101刷题笔记_第51张图片

public class Solution {
    /**
     * 合并区间
     *
     * @param intervals 区间列表,类型为ArrayList
     * @return 合并后的区间列表,类型为ArrayList
     */
    public ArrayList<Interval> merge(ArrayList<Interval> intervals) {
        // 创建一个用于存放合并结果的列表
        ArrayList<Interval> result = new ArrayList<>();

        // 如果输入为空或只有一个区间,则无需合并,直接返回原列表
        if (intervals == null || intervals.size() < 2) {
            return intervals;
        }

        // 按照区间起始位置进行排序,以保证相邻区间之间的关系
        Collections.sort(intervals, (v1, v2) -> v1.start - v2.start);

        int index = -1; // 记录结果列表中最后一个区间的索引

        // 遍历整个区间列表
        for (Interval interval : intervals) {
            if (index == -1 || interval.start > result.get(index).end) {
                // 如果当前区间和前一个区间没有重叠,则将当前区间直接添加到结果列表中
                result.add(interval);
                index++;
            } else {
                // 如果当前区间和前一个区间有重叠,则更新前一个区间的结束位置
                result.get(index).end = Math.max(interval.end, result.get(index).end);
            }
        }

        return result;
    }
}

90、BM90 最小覆盖子串

91、BM91 反转字符串

牛客101刷题笔记_第52张图片

使用StringBuilder,一行代码搞定

public String solve(String str) {
    return new StringBuilder(str).reverse().toString();
}

双指针:

把字符串转化为数组,使用两个指针,一个在最前面一个在最后面,两个指针指向的值相互交换,交换完之后两个指针在分别往中间走……,重复上面的过程,直到两指针相遇为止

public String solve(String str) {
    char[] chars = str.toCharArray();
    int left = 0;
    int right = str.length() - 1;
    while (left < right) {
        char temp = chars[left];
        chars[left] = chars[right];
        chars[right] = temp;
        left++;
        right--;
    }
    return new String(chars);
}

92、BM92 最长无重复子数组

牛客101刷题笔记_第53张图片

我们使用两个指针,一个i一个j,最开始的时候i和j指向第一个元素,然后i往后移,把扫描过的元素都放到map中,如果i扫描过的元素没有重复的就一直往后移,顺便记录一下最大值max,如果i扫描过的元素有重复的,就改变j的位置,画个图看一下(数字和字符串原理一样)
牛客101刷题笔记_第54张图片
牛客101刷题笔记_第55张图片

public class Solution {
    /**
     * 无重复字符的最长子数组长度
     *
     * @param arr int类型的一维数组
     * @return int类型,表示最长子数组的长度
     */
    public int maxLength(int[] arr) {
        // 如果输入数组为空,则最长子数组长度为0
        if (arr.length == 0) {
            return 0;
        }
        // 用于存储元素和对应的下标
        HashMap<Integer, Integer> map = new HashMap<>();
        // 记录最长子数组的长度
        int max = 0; 
        for (int i = 0, j = 0; i < arr.length; i++) {
            if (map.containsKey(arr[i])) {
                // 如果当前元素在map中已经存在,更新窗口的起始位置j
                //map.get(arr[i])拿到的是前一个元素,j应该往后一步
                j = Math.max(j, map.get(arr[i]) + 1);
            }
            // 将当前元素和下标放入map中
            map.put(arr[i], i); 
            // 更新最长子数组的长度
            max = Math.max(max, i - j + 1); 
        }
        return max;
    }
}

我们还可以用一个队列,把元素不停的加入到队列中,如果有相同的元素,就把队首的元素移除,这样我们就可以保证队列中永远都没有重复的元素
牛客101刷题笔记_第56张图片

public int maxLength(int[] arr) {
    //用链表实现队列,队列是先进先出的
    Queue<Integer> queue = new LinkedList<>();
    int res = 0;
    for (int c : arr) {
        while (queue.contains(c)) {
            //如果有重复的,队头出队
            queue.poll();
        }
        //添加到队尾
        queue.add(c);
        res = Math.max(res, queue.size());
    }
    return res;
}

93、BM93 盛水最多的容器

牛客101刷题笔记_第57张图片

题目主要信息:

  • 输入一个数组,其中每个元素代表水桶边界高度
  • 水桶容积为边界较短的一边高度乘上两边界的距离(数组下标表示距离)
  • 求在数组中选取两个边,求最大容积

方法:贪心法(建议使用)

知识点1:双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

知识点2:贪心思想

贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。

思路:

这道题利用了水桶的短板原理,较短的一边控制最大水量,因此直接用较短边长乘底部两边距离就可以得到当前情况下的容积。但是要怎么找最大值呢?

可以利用贪心思想:我们都知道容积与最短边长和底边长有关,最长的底边一定以首尾为边,但是首尾不一定够高,中间可能会出现更高但是底边更短的情况,因此我们可以使用对撞双指针向中间靠,这样底边长会缩短,因此还想要有更大容积只能是增加最短边长,此时我们每次指针移动就移动较短的一边,因为贪心思想下较长的一边比较短的一边更可能出现更大容积。

//优先舍弃较短的边
if(height[left] < height[right]) 
    left++;
else
    right--;

具体做法:

  • step 1:优先排除不能形成容器的特殊情况。
  • step 2:初始化双指针指向数组首尾,每次利用上述公式计算当前的容积,维护一个最大容积作为返回值。
  • step 3:对撞双指针向中间靠,但是依据贪心思想,每次指向较短边的指针向中间靠,另一指针不变。

图示:

牛客101刷题笔记_第58张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param height int整型一维数组 
     * @return int整型
     */
    public int maxArea (int[] height) {
        // write code here
        //首先排除不能形成容器的情况
        if(height.length < 2){
            return 0;
        }
        int res = 0;
        //双指针左右界
        int left = 0;
        int right = height.length - 1;
        //共同遍历完所有的数组
        while(left < right){
            //计算区域水容量
            int capacity = Math.min(height[left],height[right]) 
                                    * (right - left);
            //维护最大值
            res = Math.max(res, capacity);
            //优先舍弃较短的边
            if(height[left] < height[right]){
                left ++;
            }else{
                right --;
            }
        }
        return res;
    }
}

94、BM94 接雨水问题

95、BM95 分糖果问题

96、BM96 主持人调度(二)

牛客101刷题笔记_第59张图片

思路:

我们利用贪心思想,什么时候需要的主持人最少?那肯定是所有的区间没有重叠,每个区间首和上一个的区间尾都没有相交的情况,我们就可以让同一位主持人不辞辛劳,一直主持了。但是题目肯定不是这种理想的情况,那我们需要对交叉部分,判断需要增加多少位主持人。

具体做法:

  • step 1: 利用辅助数组获取单独各个活动开始的时间和结束时间,然后分别开始时间和结束时间进行排序,方便后面判断是否相交。
  • step 2: 遍历n个活动,如果某个活动开始的时间大于之前活动结束的时候,当前主持人就够了,活动结束时间往后一个。
  • step 3: 若是出现之前活动结束时间晚于当前活动开始时间的,则需要增加主持人。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 计算成功举办活动需要多少名主持人
     * @param n int整型 有n个活动
     * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
     * @return int整型
     */
    public int minmumNumberOfHost (int n, int[][] startEnd) {
        // write code here
        //利用辅助数组获取单独各个活动开始的时间和结束时间
        int[] start = new int[n];
        int[] end = new int[n];
        //分别得到活动起始时间
        for(int i = 0; i < n; i ++){
            start[i] = startEnd[i][0];
            end[i] = startEnd[i][1];
        }
        //单独排序
        Arrays.sort(start, 0, start.length);
        Arrays.sort(end, 0 ,end.length);
        int res = 0;
        int j = 0;
        for(int i = 0; i < n; i++){
            //新开始的节目大于上一轮结束的时间,主持人不变
            if(start[i] >= end[j]){
                j ++;
            }else{
                //主持人增加
                res ++;
            }
        }
        return res;
    }
}

97、BM97 旋转数组

牛客101刷题笔记_第60张图片

题目主要信息:

  • 一个长度为n的数组,将数组整体循环右移m个位置(m可能大于n
  • 循环右移即最后m个元素放在数组最前面,前nm个元素依次后移
  • 不能使用额外的数组空间

思路:

循环右移相当于从第m个位置开始,左右两部分视作整体翻转

具体做法:

  • step 1:因为m可能大于n,因此需要对n取余,因为每次长度为n的旋转数组相当于没有变化。
  • step 2:第一次将整个数组翻转,得到数组的逆序,它已经满足了右移的整体出现在了左边。
  • step 3:第二次就将左边的m*个元素单独翻转,因为它虽然移到了左边,但是逆序了。
  • step 4:第三次就将右边的nm个元素单独翻转,因此这部分也逆序了。
public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 旋转数组
     * @param n int整型 数组长度
     * @param m int整型 右移距离
     * @param a int整型一维数组 给定数组
     * @return int整型一维数组
     */
    public int[] solve (int n, int m, int[] a) {
        // write code here
        //取余,因为每次长度为n的旋转数组相当于没有变化
        m = m % n;
        //第一次逆转全部数组元素
        reverse(a, 0, n - 1);
        //第二次只逆转开头m个
        reverse(a, 0, m - 1);
        //第三次只逆转结尾m个
        reverse(a, m, n - 1);
        return a;
    }
    //反转函数
    public void reverse(int[] nums, int start, int end) {
        while (start < end) {
            swap(nums, start++, end--);
        }
    }
    //交换函数
    public void swap(int[] nums, int a, int b) {
        int temp = nums[a];
        nums[a] = nums[b];
        nums[b] = temp;
    }
}

98、BM98 螺旋矩阵

牛客101刷题笔记_第61张图片

思路:

这道题就是一个简单的模拟,我们想象有一个矩阵,从第一个元素开始,往右到底后再往下到底后再往左到底后再往上,结束这一圈,进入下一圈螺旋。

具体做法:

  • step 1:首先排除特殊情况,即矩阵为空的情况。
  • step 2:设置矩阵的四个边界值,开始准备螺旋遍历矩阵,遍历的截止点是左右边界或者上下边界重合。
  • step 3:首先对最上面一排从左到右进行遍历输出,到达最右边后第一排就输出完了,上边界相应就往下一行,要判断上下边界是否相遇相交。
  • step 4:然后输出到了右边,正好就对最右边一列从上到下输出,到底后最右边一列已经输出完了,右边界就相应往左一列,要判断左右边界是否相遇相交。
  • step 5:然后对最下面一排从右到左进行遍历输出,到达最左边后最下一排就输出完了,下边界相应就往上一行,要判断上下边界是否相遇相交。
  • step 6:然后输出到了左边,正好就对最左边一列从下到上输出,到顶后最左边一列已经输出完了,左边界就相应往右一列,要判断左右边界是否相遇相交。
  • step 7:重复上述3-6步骤直到循环结束。

牛客101刷题笔记_第62张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param matrix int整型二维数组
     * @return int整型ArrayList
     */
    public ArrayList<Integer> spiralOrder (int[][] matrix) {
        // write code here
        ArrayList<Integer> res = new ArrayList<>();
        //先排除特殊情况
        if (matrix.length == 0) {
            return res;
        }
        //左边界
        int left = 0;
        //右边界
        int right = matrix[0].length - 1;
        //上边界
        int up = 0;
        //下边界
        int down = matrix.length - 1;
        //直到边界重合
        while (left <= right && up <= down) {
            //上边界的从左到右
            for (int i = left; i <= right; i++) {
                res.add(matrix[up][i]);
            }
            //上边界向下
            up++;
            if (up > down) {
                break;
            }
            //右边界的从上到下
            for (int i = up; i <= down; i++) {
                res.add(matrix[i][right]);
            }
            //右边界向左
            right--;
            if (left > right)
                break;
            //下边界的从右到左
            for (int i = right; i >= left; i--)
                res.add(matrix[down][i]);
            //下边界向上
            down--;
            if (up > down)
                break;
            //左边界的从下到上
            for (int i = down; i >= up; i--)
                res.add(matrix[i][left]);
            //左边界向右
            left++;
            if (left > right)
                break;
        }
        return res;
    }
}

99、BM99 顺时针旋转矩阵

牛客101刷题笔记_第63张图片

牛客101刷题笔记_第64张图片

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param mat int整型二维数组
     * @param n int整型
     * @return int整型二维数组
     */
    public int[][] rotateMatrix (int[][] mat, int n) {
        // write code here
        int length = mat.length;
        //先上下交换
        for (int i = 0; i < length / 2; i++) {
            int temp[] = mat[i];
            mat[i] = mat[length - i - 1];
            mat[length - i - 1] = temp;
        }
        //在按照对角线交换
        for (int i = 0; i < length; ++i) {
            for (int j = i + 1; j < length; ++j) {
                int temp = mat[i][j];
                mat[i][j] = mat[j][i];
                mat[j][i] = temp;
            }
        }
        return mat;
    }
}

100、BM100 设计LRU缓存结构

101、BM101 设计LFU缓存结构

你可能感兴趣的:(笔记)