• LeetCode进阶之路(三)算法题的核心套路


    一、框架思维

    1.数据结构存储方式

      数据结构底层存储方式有两种:数组和链表(即顺序存储和链式存储)。

    2.树的遍历框架

    //二叉树遍历框架
    class
    TreeNode{ int val; TreeNode left,right; } public void traverse(TreeNode root){ //前序遍历 traverse(root.left); //中序遍历 traverse(root.right); //后序遍历 }
    //n叉树比遍历框架
    class TreeNode{
    int val;
    TreeNode[] children;
    }
    
    void traverse(TreeNode root){
    for(TreeNode child:root.children){
    traverse(child);
      }
    }

    注:涉及递归的问题,基本都是树的问题。

    二、动态规划解题框架

    (一)概述

    1.动态规划通常是求最值的问题。核心问题是穷举。

    2.重叠子问题:使用备忘录或DPtable优化穷举。

    3.最优子结构:通过子问题的最值得到原始问题的最值。

    4.状态转移方程(分段函数):状态、选择、dp的定义。(思考最简单情况、问题的状态有什么、对每个状态可以进行什么操作得到什么新的状态、如何定义dp数组或函数来表现“状态”和“选择”。

    框架如下:

    //初始化base case
    dp[0][0][···]=base case
    //进行状态转移
    for 状态1 in 状态1的所有取值:
        for 状态2 in 状态2的所有取值:
            for ···
                dp[状态1][状态2][···]=求最值(选择1,选择2,···)

    (二) 具体方法:

    1.暴力递归

      递归算法的时间复杂度:子问题个数乘以解决单个子问题需要的时间。

    2.带备忘录的递归解法

      一般用一个数组充当备忘录,也可以使用哈希表(字典),将子问题的答案记录在备忘录内,需要时直接取出来,就不用再耗时计算了。

    3.dp数组的迭代解法

      用一个独立的数组表示备忘录。如果当前状态之和前几个状态有关,可以用多个变量表示dpTable——状态压缩。

    三、回溯算法解题套路框架(DFS深度优先搜索)

      回溯方法即穷举,解决回溯问题就是决策树遍历的问题。具体包括:

      1.路径:已经做出的选择。

      2.选择列表:当前可以做的选择。

      3.结束条件:到达决策树底层无法再做选择的条件。

      回溯算法是动态规划的暴力求解阶段。

      回溯算法是一个多叉树遍历的问题,关键是前序遍历和后序遍历位置的操作。写Backtrack函数时,需要维护走过的“路径”和当前可以做的“选择列表”,当触发“结束条件”时,将“路径”计入结果集。

    回溯算法的况下如下:

    result=[]
    def backtrack(路径,选择列表);
        if 满足结束条件
            result.add(路径);
            return;
        
        for 选择 in 选择列表:
            做选择
            backtrack(路径,选择列表)
            撤销选择

    for循环中的递归在条用之前做选择,在调用递归之后撤销选择。

      维护节点的选择列表和路径的方法:在递归之前做出选择,在递归之后撤销刚才的选择。

    四、BFS广度优先搜索算法框架

      核心思想:把问题想象成图,从一个点开始向四周扩散。每次将一个节点周围的所有节点加入队列。

      BFS的特点:BFS找到的路径是最短的,但空间复杂度比DFS大得多。

      应用场景:在一个图中,找到从起点到终点的最短距离。

      算法框架如下所示:

    //计算从起点到终点的最短距离
    int BFS(Node start,Node target){
            Queue<Node> q; //核心数据结构
            Set<Node> visited; //避免走回头路
    
            q.offer(start); //将起点加入队列
            visited.add(start);
            int step=0; //记录扩散的步数
    
            while(q not empty){
            int sz=q.size();
    //        将队列中的所有节点向四周扩散
            for(int i=0;i<sz;i++){
            Node cur=q.pool();
    //            这里判断是否到达终点
            if(cur is target)
            return step;
    //            将cur的相邻接点加入到队列,cur.adj()指cur相邻的节点
            for(Node X:cur.adj()){
            if(x not in visited){
            q.offer(x);
            visited.add(x);  //visited是防止走回头路,一般的二叉树没有子节点到父节点的指针,不需要visited
            }
            }
            }
    //        在这里更新步数
            step++;
            }
            }

    BFS和DFS的关系

    1.寻找最短路径时,广度优先相当于面,深度优先相当于线,深度优先也可以找到最短路径。

    2.DFS空间复杂度小,时间复杂度大BFS空间复杂度大,时间复杂度小。

    五、双指针框架

    (一)快慢指针

      1.定义:初始化两个指针指向链表头部节点head,fast指针在前,slow指针在后。

      2.应用场景:

      ①判断链表中是否有环

      用双指针,如果链表无环,快的会遇到null,如果链表有环,快的最终会超过慢的一圈和慢的相遇。

    boolean hasCycle(ListNode head){
        ListNode fast,slow;
        //初始化快、慢指针指向头节点
        fast=slow=head;
        while(fast!=null&&fast.next!=null){
            //快指针每次前进两步
            fast=fast.next.next;
            //慢指针每次前进一步
            slow=slow.next;
            //如果有环,快慢指针必然相遇
            if(fast==slow) return true;
        }
    }

      ②已知链表有环,返回环的起始位置。

       方法:把快慢指针中的其中任意一个重新指向head,然后两个指针同速前进,再次相遇的位置就是环的起点。

      原因:假设首次相遇是,slow走的长度是k,那么fast就走了2k(fast比slow多走了一圈,所以环的周长也是k)。假设此时的位置距离环的起点距离为m,那么环的起始位置可以用k-m表示。此时首次相遇的位置距离环的起点距离同样是k-m。因此重置一个指针,再次相遇即可找到环的起点。

    具体方法如下:

        ListNode detectCycle(ListNode head) {
            ListNode fast, slow;
            //初始化快、慢指针指向头节点
            fast = slow = head;
            while (fast != null && fast.next != null) {
                //快指针每次前进两步
                fast = fast.next.next;
                //慢指针每次前进一步
                slow = slow.next;
                //如果有环,快慢指针必然相遇
                if (fast == slow) break;
            }
            //以上代码类似hasCycle函数,接下来先把指针重新指向head
            slow = head;
            while (slow != fast) {
    //            两个指针同速前进
                fast = fast.next;
                slow = slow.next;
            }
            return slow;
        }

      ③寻找无环单链表的中点。

      暴力方法:先遍历一遍链表,算出长度N。再走n/2步,这样就到了链表的中点。

      更优雅的方法:快慢两个指针,快指针到终点时候慢指针的位置就是中点的位置。 

    while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            }
            return slow;    //slow就在中间位置了

      链表中点的一个重要作用:用链表进行归并排序。递归的把数组分成两部分,然后对两部分分别排序,最后合并两个有序数组。

      ④寻找单链表中倒数第K个元素

       让快指针先走k步,然后快慢指针同速前进。当快指针到达末尾null时,慢指针的位置就是倒数第K个节点。

      框架如下:

      

    ListNode slow, fast;
    slow=fast=head;
        while(k-->0)
        fast=fast.next;
        while(fast!=null){
        slow=slow.next;
        fast=fast.next;
        }
        return slow;

    (二)左右指针

      1.定义:左右指针用在数组问题中,实际是两个索引值,通常定义

    left=0;
    right=len(nums)-1;

      2.应用场景:

      ①二分搜索

      ②两数之和

      ③反转数组

      ④滑动窗口算法

      以上内容下文详细介绍。滑动窗口算法是快慢指针在数组上的应用,解决字符串匹配问题,下文单独介绍。

    六、二分搜索算法

    二分法注意问题:
    确定好搜索区间,定位闭区。
    while条件带等号。
    if条件相等了就返回。
    mid要加减1,因为是闭区间。
    while结束就返回-1。
    最后用if条件确保索引不出边界。
    尽量用else if把条件写清楚了,不用else

    1.寻找一个数

      计算mid时防止溢出。使用left+(right-left)/2与(left+right)/2的计算结果是一样的,但前者更不容易出现整数溢出的问题。

    寻找一个整数的方法如下:

    int binarySearch(int[] nums,int target){
        int left=0,right=nums.lentth-1;
        while(left<=right){
            int mid=left+(right-left)/2;
            if(nums[mid]==target){
                return mid;
            }else if(nums[mid]<target){
                left=mid+1;
            }else if(nums[mid]>target){
                right=mid-1;
            }
        }
        return -1;
    }

    2.寻找边界:数组中连续出现的多个相同的数值,找左边界或右边界。

      方法与查找某个数基本一致,不同的是,当min和目标相等时不出结果,找哪个边界就往哪个边收。再加一步检查边界。

    ①左边界

    //当nums[mid]=target时,右边界往左收
    right=mid-1;
    //检查出界情况(原return -1的位置)
    if(left>=nums.length||nums[left]!=target){
        return -1;
            }
    return left;

    ②右边界

    //当nums[mid]=target时
    left=mid+1
    //检查边界
    if(right<0||nums[right]!=target){
        return -1;
            }
    return right;

    七、滑动窗口

       维护一个窗口不断滑动,然后更新答案。通常用于解决子字符串的问题。

    基本框架如下:①额和②表示需要更新的数据。

        void slidingWindow(string s, string t) {
            unordered_map<char, int> need, window;
            for (char c : t) need[c]++;
    
            int left = 0, right = 0;
            int valid = 0;
            while (right < s.size()) {
                //c是将移入窗口的字符
                char c = s[roght];
                //右移窗口
                right++;
                //将窗口内的一系列数据更新
    //        ①...
                prindf("window:[%d,%d]\n", left, right);
    
                //判断左窗口是否需要收缩
                while (window needs shrink){
                    //d是将移出窗口的字符
                    char d = s[left];
                    //左移窗口
                    left++;
                    //进行窗口内数据的一系列更新
    //            ②...
                }
            }
        }

    常见的几个问题:

    ①最小覆盖子串

    需要考虑以下四个问题:当right扩大窗口,加入字符时,应该更新哪些数据;

               什么条件下窗口停止扩大,开始移动left缩小窗口。

               移动left缩小窗口时,应该更新哪些数据。

                  最终结果是在扩大窗口还是缩小窗口时候更新。

    对框架的修改通常为

     void minWindow(string s, string t) {
            unordered_map<char, int> need, window;
            for (char c : t) need[c]++;
    
            int left = 0, right = 0;
            int valid = 0;
            while (right < s.size()) {
                //c是将移入窗口的字符
                char c = s[roght];
                //右移窗口
                right++;
                //将窗口内的一系列数据更新
                if (need.count(c)) {
                    window[c]++;
                    if (window[c] = need[c]) {
                        valid++;
                    }
                }
                prindf("window:[%d,%d]\n", left, right);
    
                //判断左窗口是否需要收缩
                while (window needs shrink){
                    //d是将移出窗口的字符
                    char d = s[left];
                    //左移窗口
                    left++;
                    //进行窗口内数据的一系列更新
                    if (need.count(d)) {
                        if (window[d] == need[d])
                            walid__;
                        window[d]--;
                    }
                }
            }
            return len == INT_MAX ?
                    "" : s.substr(start, len);
        }

    ②字符串排列

      方法和①几乎一致,注意返回值的不同即可。

    ③找所有字母异位词

      同样,差异在于返回值的不同。

    ④最长无重复子串

      去掉need和valid,更新窗口数据也只需要window即可。

  • 相关阅读:
    Java框架-mybatis02基本的crud操作
    Java框架-mybatis01查询单个数据
    MAC常用命令
    性能测试工具Jmeter13-Jmeter跨线程组调用token
    性能测试工具Jmeter12-Jmeter连接配置带跳板机(SSH)的mysql服务器
    Java基础29-子父类中的成员变量
    Java基础28-继承
    Java基础27-单例设计模式
    启动项目时报spawn cmd ENOENT
    npm安装教程
  • 原文地址:https://www.cnblogs.com/StarZhai/p/15993586.html
Copyright © 2020-2023  润新知