经典算法总结(持续完善中……)

目录

二分查找 

双指针

二叉树:深度优先搜索

二叉树:广度优先搜索

图:深度优先搜索 

图:广度优先搜索 

常用数据结构 

技巧

性能

动态规划

动态规划的应用场景:

我们解决动态规划问题一般分为四步:

自顶向下的备忘录法模板:

自底向上法模板:

动态规划的经典模型

分治算法

分治基本思想及策略:

分治法使用场景:

分治法的基本步骤及模板

分治法的复杂性分析(选读)


基本参考自leetcode官方写法,希望能写出简洁、高效、标准的模式

二分查找 

    public int search(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        while (low <= high) {
            int mid = (high - low) / 2 + low;
            int num = nums[mid];
            if (num == target) {
                return mid;
            } else if (num > target) {
                high = mid - 1;
            } else {
                low = mid + 1;
            }
        }
        return -1;
    }

参考:. - 力扣(LeetCode)

双指针

回文串判断: 

    //while形式
    public boolean isPalindrome(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) {
                return false;
            }
            ++left;
            --right;
        }
        return true;
    }

    //for形式
    public boolean isPalindrome(String s) {
        for(int i =0, j=s.length()-1; i < j; i++, j--){
            if(s.charAt(i) != s.charAt(j)){
                return false;
            }
        }
        return true;
    }

参考:125. 验证回文串

二叉树:深度优先搜索

    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        } else {
            int leftHeight = maxDepth(root.left);
            int rightHeight = maxDepth(root.right);
            return Math.max(leftHeight, rightHeight) + 1;
        }
    }

参考:    104. 二叉树的最大深度. - 力扣(LeetCode)

二叉树:广度优先搜索

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null) {
            return ret;
        }

        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            List<Integer> level = new ArrayList<Integer>();
            int currentLevelSize = queue.size();
            for (int i = 1; i <= currentLevelSize; ++i) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            ret.add(level);
        }
        
        return ret;
    }

 参考:102. 二叉树的层序遍历. - 力扣(LeetCode)

图:深度优先搜索 

    void dfs(char[][] grid, int r, int c) {
        int nr = grid.length;
        int nc = grid[0].length;

        if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
            return;
        }

        grid[r][c] = '0';
        dfs(grid, r - 1, c);
        dfs(grid, r + 1, c);
        dfs(grid, r, c - 1);
        dfs(grid, r, c + 1);
    }

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int nr = grid.length;
        int nc = grid[0].length;
        int num_islands = 0;
        for (int r = 0; r < nr; ++r) {
            for (int c = 0; c < nc; ++c) {
                if (grid[r][c] == '1') {
                    ++num_islands;
                    dfs(grid, r, c);
                }
            }
        }

        return num_islands;
    }

 参考: 200. 岛屿数量 : . - 力扣(LeetCode)

图:广度优先搜索 

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }

        int nr = grid.length;
        int nc = grid[0].length;
        int num_islands = 0;

        for (int r = 0; r < nr; ++r) {
            for (int c = 0; c < nc; ++c) {
                if (grid[r][c] == '1') {
                    ++num_islands;
                    grid[r][c] = '0';
                    Queue<Integer> neighbors = new LinkedList<>();
                    neighbors.add(r * nc + c);
                    while (!neighbors.isEmpty()) {
                        int id = neighbors.remove();
                        int row = id / nc;
                        int col = id % nc;
                        if (row - 1 >= 0 && grid[row-1][col] == '1') {
                            neighbors.add((row-1) * nc + col);
                            grid[row-1][col] = '0';
                        }
                        if (row + 1 < nr && grid[row+1][col] == '1') {
                            neighbors.add((row+1) * nc + col);
                            grid[row+1][col] = '0';
                        }
                        if (col - 1 >= 0 && grid[row][col-1] == '1') {
                            neighbors.add(row * nc + col-1);
                            grid[row][col-1] = '0';
                        }
                        if (col + 1 < nc && grid[row][col+1] == '1') {
                            neighbors.add(row * nc + col+1);
                            grid[row][col+1] = '0';
                        }
                    }
                }
            }
        }

        return num_islands;
    }

 参考:    200. 岛屿数量 : . - 力扣(LeetCode)

常用数据结构 

栈:可用LinkedList实现,常用方法pop、push、peek、isEmpty

Deque<T> stack = new LinkedList<T>();

参考练习:20. 有效的括号

队列:

Queue<T> queue = new LinkedList<T>();

 参考练习:387. 字符串中的第一个唯一字符

技巧

x、y的最大公约数:

    public int gcd(int x, int y) {
        return y > 0 ? gcd(y, x % y) : x;
    }

参考自 189. 轮转数组  的官方解法二: . - 力扣(LeetCode)     

性能

【防止超时的方式:

1.缓存,通常使用哈希表

2.分治(动态规划)

3.当可以使用for或while循环时,排序+双指针,时间复杂度通常优于回溯,双指针可以使n层循环减少为n-1层循环。    参考练习:18. 四数之和  ,该参考练习中,也使用了比较有效的剪枝操作。

4.剪枝操作,即不满足功能要求时提前break/continue掉for/while循环,或者下标 i++不用进行逻辑处理(尤其在合适的用例下 i 能不停的向后移动一大段会加速很快)】

动态规划

算法导论这本书是这样介绍这个算法的,动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。再来了解一下什么是分治方法,以及这两者之间的差别,分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。而动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作()。

动态规划算法的核心就是记住已经解决过的子问题的解

记住求解的方式有两种:①自顶向下的备忘录法 ②自底向上。(递归法比较简单,不讲了,且其步骤被包含于自顶向下的备忘录法中)

动态规划的应用场景:

1.最优子结构

用动态规划求解最优化问题的第一步就是刻画最优解的结构,如果一个问题的解结构包含其子问题的最优解,就称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个很好的线索。使用动态规划算法时,用子问题的最优解来构造原问题的最优解。因此必须考查最优解中用到的所有子问题

2.重叠子问题

在斐波拉契数列和钢条切割结构图中,可以看到大量的重叠子问题,比如说在求fib(6)的时候,fib(2)被调用了5次,在求cut(4)的时候cut(0)被调用了4次。如果使用递归算法的时候会反复的求解相同的子问题,不停的调用函数,而不是生成新的子问题如果递归算法反复求解相同的子问题,就称为具有重叠子问题(overlapping subproblems)性质。在动态规划算法中使用数组来保存子问题的解,这样子问题多次求解的时候可以直接查表不用调用函数递归。(动态规划帮助我们解决一类递归问题,这些问题可以分解成一个个高度重叠的子问题。“高度重叠”指的是子问题一遍遍地重复出现。相反地,像归并排序这种递归算法,会先独立地排好列表的一半,再把结果合并。这种把问题分解成子问题,并且产生的子问题不会重叠的算法,就是分治法)

 (后边这个描述我认为不够好: 动态规划方法一般用来求解最优化问题。这类问题可以有很多可行解,每个解都有一个值,我们希望找到具有最优值的解,我们称这样的解为问题的一个最优解,而不是最优解,因为可能有多个解都达到最优值。)

我们解决动态规划问题一般分为四步:

1、定义一个状态,这是一个最优解的结构特征

2、进行状态递推,得到递推公式

3、进行初始化

4、返回结果

自顶向下的备忘录法模板:

1. 定义备忘录 (视情况看是否进行备忘录初始化)

2. 调用带备忘录的dpMethod

3.  dpMethod方法结构:备忘录有值直接返回备忘录的值;否则用递归终止条件和递归递推公式分情况为备忘录赋值,并返回。

public static int dpMethod(int n)
{            

              int []Memo=new int[n+1];           //定义备忘录
              for(int i=0;i<=n;i++)     //视情况看是否需要初始化备忘录, 不显式初
                Memo[i]=-1;    //始化则备忘录各元素为默认值,显式初始化会额外耗时
              return dpMethodWithMem(n, Memo);

}
       public static int dpMethodWithMem(int n,int []Memo)
       {
              if(Memo[n]!=-1) //如果已经求出了dpMethodWithMem(n)的值直接返回(不为初始值则为已求出)
                     return Memo[n];
              //否则将求出的值保存在Memo备忘录中。
              if(n<=2)
                 Memo[n]=1;//递归的终止条件,当dpMethodWithMem的入参n为1或2时,Memo[1] Memo[2]没被初始化,进入此分支将Memo[1] Memo[2]赋值为1,并在下边return
              else
                 Memo[n]=dpMethodWithMem(n-1,Memo)+dpMethodWithMem(n-2,Memo);  

              return Memo[n];
       }

备忘录法还是利用了递归,上面算法不管怎样,计算fib(6)的时候最后还是要计算出fib(1),fib(2),fib(3)…,那么何不先计算出fib(1),fib(2),fib(3)…,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。由此得到:

自底向上法模板:

1.定义dp数组

2.初始化dp数组的前1个或2个元素的值(也即递归时递归的终止条件对应的值,有时可用定义数组时的默认值,与问题相关,具体问题具体分析)

3.采用for循环,依次从前往后根据递推公式赋值dp数组的每个元素,直到n(数组后边的元素求解依赖前边的元素值,n为要求解的规模,通常为问题入参。)

具体示例1:

public static int dpMethod(int n)
{
              if(n<=0)
                 return n;
              int []Memo=new int[n+1];
              Memo[0]=0;
              Memo[1]=1;
              for(int i=2;i<=n;i++)
              {
                   Memo[i]=Memo[i-1]+Memo[i-2]; //根据递推公式自底向上往后计算Memo值,不同问题,递推公式不同
              }            

              return Memo[n];
}

观察参与循环的只有 ii-1 , i-2三项,因此该方法的空间可以进一步的压缩如下。

public static int dpMethod(int n)
{
       if(n<=1)
          return n;             

       int Memo_i_2=0;
       int Memo_i_1=1;
       int Memo_i=1;
       for(int i=2;i<=n;i++)
       {
              Memo_i=Memo_i_2+Memo_i_1;
              Memo_i_2=Memo_i_1;
              Memo_i_1=Memo_i;
       }            

       return Memo_i;
}

具体示例2:

public static int buttom_up_cut(int []p)
{
     int []r=new int[p.length+1];
     for(int i=1;i<=p.length;i++)
     {
          int q=-1;
          //① 根据递推公式自底向上往后计算r[i]值,此处递推公式是带for循环的范围内的求最值
          for(int j=1;j<=i;j++)
              q=Math.max(q, p[j-1]+r[i-j]);

          r[i]=q;
     }

     return r[p.length];
}

以上2示例均来自:https://blog.csdn.net/u013309870/article/details/75193592

由于备忘录方式的动态规划方法使用了递归,递归的时候会产生额外的开销,使用自底向上的动态规划方法要比备忘录方法好。

动态规划的经典模型

线性模型

线性模型的是动态规划中最常用的模型,上文讲到的钢条切割问题就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。

区间模型

区间模型的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。

【例题2】给定一个长度为n(n <= 1000)的字符串A,求插入最少多少个字符使得它变成一个回文串。

典型的区间模型,回文串拥有很明显的子结构特征,即当字符串X是一个回文串时,在X两边各添加一个字符’a’后,aXa仍然是一个回文串,我们用d[i][j]来表示A[i…j]这个子串变成回文串所需要添加的最少的字符数,那么对于A[i] == A[j]的情况,很明显有 d[i][j] = d[i+1][j-1] (这里需要明确一点,当i+1 > j-1时也是有意义的,它代表的是空串,空串也是一个回文串,所以这种情况下d[i+1][j-1] = 0);当A[i] != A[j]时,我们将它变成更小的子问题求解,我们有两种决策:

1、在A[j]后面添加一个字符A[i];

2、在A[i]前面添加一个字符A[j];        

根据两种决策列出状态转移方程为:

d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1; (每次状态转移,区间长度增加1)

背包模型

背包问题是动态规划中一个最典型的问题之一。(注:暂未看用动态规划解背包问题的解决方案代码,我收藏中的一片博文中观点,是用回溯解的背包问题。 除本参考文章https://blog.csdn.net/u013309870/article/details/75193592 描述背包问题属于背包模型,暂未看到过其他背包模型的动态规划算法题,线性模型和区间模型的动态规划算法题居多。)

分治算法

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序归并排序),傅立叶变换(快速傅立叶变换)

分治基本思想及策略:

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

  如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

分治法使用场景:

分治法所能解决的问题一般具有以下几个特征:

  1) 该问题的规模缩小到一定的程度就可以容易地解决

  2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

  3) 利用该问题分解出的子问题的解可以合并为该问题的解

  4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

第四条特征涉及到分治法的效率如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

分治法的基本步骤及模板

  分治法在每一层递归上都有三个步骤

  step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

  step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

  step3 合并:将各个子问题的解合并为原问题的解。

它的一般的算法模板如下

Divide-and-Conquer(P)

  1. if |P|≤n0

  2. then return(ADHOC(P))

  3. 将P分解为较小的子问题 P1 ,P2 ,…,Pk

  4. for i←1 to k

  5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi

  6. T ← MERGE(y1,y2,…,yk) △ 合并子问题

  7. return(T)

分治法的复杂性分析(选读)

  一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:

  T(n)= k T(n/m)+f(n)

  通过迭代法求得方程的解:

  递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。

分治法参考:五大常用算法:分治算法_可以使用分治法求解的有-CSDN博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值