个人算法、数据结构复习笔记

项目地址

github项目地址

算法

1 算法分析

关于算法的时间复杂度,这里我们先简单介绍下相关的三种符号记法:

  • Big O notation,它给出了运行时间的”渐进上界“,也就是算法在最坏情况下运行时间的上限。它的定义如下:对于f(n)和g(n),若存在常数c和N0,使得对于所有n > N0,都有 |f(n)| < c * g(n),则称f(n)为O(g(n)。
  • Big Ω notation,它给出了运行时间的“渐进下界”,也就是算法在最坏情况下运行时间的下限。它的定义如下:对于f(n)和g(n),若存在常数c和N0,使得对于所有n > N0,都有|f(n)| > c * g(n),则称f(n)为Ω(g(n))。
  • Big Θ notation,它确定了运行时间的”渐进确界“。定义如下:对于f(n)和g(n),若存在常数c和N0,对于所有n> N0,都有|f(n)| = c * g(n),则称f(n)为Θ为Θ(g(n))。
  • 分别代表上界、下界和上下界约束

1.1 算法分析的一般方法

  直观地想,一个算法的运行时间也就是执行所有程序语句的耗时总和。然而在实际的分析中,我们并不需要考虑所有程序语句的运行时间,我们应该做的是集中注意力于最耗时的部分,也就是执行频率最高而且最耗时的操作。
  但是对于递归、或各分支条件不确定的算法较难分析。

1.2 递归算法分析

详细介绍见:递归算法时间复杂度分析

  • 代入法: 猜测解的形式(如 c n l g n cnlgn cnlgn),得到 T ( n ) < = c n l g n T(n)<=cnlgn T(n)<=cnlgn的猜测,通过数学归纳法等方法,对不等式进行缩放验证。
  • 递归树法: 1. 画出递归树给出程序的递归过程,以及每一个子程序的时间复杂度;2. 计算树的深度和广度。例如, T ( n ) = 3 T ( n / 4 ) T(n)=3T(n/4) T(n)=3T(n/4)的深度为 l o g 4 n log_4n log4n,广度为 3 l o g 4 n 3^{log_4n} 3log4n。3. 级数求和计算所有层次的代价。
  • 主定理法: 对于 T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)对比 n l o g b a n^{log_ba} nlogba与f(n)的复杂度,选择最大的一个。

1.3 摊还分析

2 排序算法

十大经典排序算法

  • 冒泡排序: 不断交换获得最后/最前位置的最大/最小值,重复n次。
  • 插入排序: 将当前值插入顺序列的合适位置。
  • 分治排序: 分开、合并。
  • 快速排序: 找中间值,每次把大的放右边,小的放左边,再递归排左右两个子列。详细说明
  • 堆排序: 维持一个大顶堆、小顶堆,每次取出最大值或最小值。功能包括建堆、调整。
  • 桶排序: 它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
  • 基数排序: 1. 取得数组中的最大数,并取得位数;2. 从最低位开始取每个位组成radix数组;对radix进行计数排序。

2.1 优先队列

3 分治算法

https://leetcode.cn/tag/divide-and-conquer/problemset/

  • 分治排序: 分开、合并。
  • 快速排序: 找中间值,每次把大的放右边,小的放左边,再递归排左右两个子列。
  • 最大子数组: 分治递归计算左侧最大值和右侧最大值以及中间最大值。max(左,右,中)
  • 矩阵乘法:
  • 第k大(小)数: 改进快速排序(找到第k大数之后判断下一次查找的子串),或改进堆排序。
  • 汉诺塔:

4 贪心算法

  贪心算法原理:找到最优子结构,并证明正确性。

  • 活动规划方法及证明:
    – 每次都选择最先结束的活动
    –证明:假设我们得到一个最大兼容活动子集A,其中ai是A中结束最早的活动,aj是所有活动中结束最早的活动。如果ai=aj,那集合不变。如果ai!=aj,令A‘为ai,aj替换后的集合,A‘必然为最大兼容活动子集。(也就是说只要替换了更靠前的肯定不亏)

  • 哈夫曼编码方法及证明: 哈夫曼编码方法用于压缩数据,能够根据每个字符的出现频率,构造出字符的最优二进制表示。下图为定长编码与变长编码树示例,哈夫曼编码算法维护一个最小优先队列,每次取队列中两个最小值,相加后加回到最小优先队列。在构建队列的同时构造二叉树。哈夫曼编码的贪心之处在于合并操作的频率之和等于合并操作的代价(将一个新字母代替两个旧字母)。
    在这里插入图片描述

  • dijkdstra方法及证明:

  • 顺序矩阵搜索及证明 二维矩阵从上到下,从左到右递增。对于目标值target,在左下角开始搜索,如果当前值大于目标值,说明当前行右侧全部大于目标值,可直接向上移动一行,否则向右移动一格。

  • 跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。例如[2,3,1,1,4]有解,而[3,2,1,0,4]无解。维持一个最大范围,在这一范围之内的数字都可以到达,不断向前推进下标,更新最大范围,直到无法推进或数组尾部进入范围。证明范围内任一点不会超出范围和范围外任一点无法到达。

  • 跳跃游戏最短跳数 从后往前找,每次从当前点找距离最远的起跳点。时间效率为n^2。每次跳到能跳最远的点,时间效率为n。

  • 买卖股票 相邻天之间获利就交易。

  • 买卖股票带手续费 相邻天之间获利且大于手续费就交易是错的。 应采用动态规划的思想,当前持有股票的最大收益和当前不持有股票的最大收益。参考买卖股票带冷静期。进一步,可以不采用两个数组,而是两个整数存储。其中buy记录了当前成本,sell记录了当前最大收益。每次收益大于上次交易就选择结算,结算之中相当于加了当前价减了成本价和手续费,剩余的钱写入了sell。buy每次选择降低成本,且计算中加入sell(相当于保存了当前最大收益)。

  • 摆动排序 证明任取两个偶数,位置可互换(考虑不相邻,相邻两种情况,很好证明)。因此直接先把最大数排到偶数位置就可以了。然后由于最大数在最前的偶数位置(中位数在最靠后位置),我们可以从最小数倒排,一个一个放入奇数位置。

5 动态规划

https://leetcode.cn/tag/dynamic-programming/problemset/

  • 匹配正则表达式:
    通配符匹配:给定一个字符串s和一个字符模式p,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。其中 * 可以匹配任意字符串(包括空字符串)。用 dp[i][j]dp[i][j] 表示字符串 s 的前 i 个字符和模式 p 的前 j 个字符是否能匹配。在进行状态转移时,如果当前 p j p_j pj为字母,那么dp[i][j] = 同为字母&&dp[i-1][j-1];;如果当前 p j p_j pj为问号,直接转移;;如果当前 p j p_j pj为星号,则当前状态为使用星号,和不使用星号,如果我们不使用这个星号,那么就会从 dp[i][j-1]转移而来;如果我们使用这个星号,那么就会从 dp[i-1][j]转移而来。先遍历s,再遍历p。可以想 a a a a a b 和 ∗ a a b aaaaab和*aab aaaaabaab
    正则表达式匹配:先遍历s,再遍历p。

  • 背包问题: 有容量为w的包,装价值为v的货品。设dp[j]为j容量下能装的最大价值,遍历每个商品。0-1从w到0计算dp(之所以倒序是因为修改过的dp不再参与这一轮计算),完全背包从0到w计算dp(相当于能装就装,可以装好多次的)。
    数组均等切分:求得数组和一半为背包容量。
    双重限制的0-1背包问题:设置dp二维数组。
    求数组目标和给定一组数,分配加减号,得到目标值共有多少方法。设置dp数组,记录当前和的所有可能数量,转移方程为方案数不断加上dp[和±当前数](例如和为100和102有100中方案,数列中还有1个1没有扫到,到101的方案数就是100+100=200)
    零钱兑换:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回-1。按完全背包问题,dp记录最少硬币个数,初始化为INT_MAX

  • 多重背包问题: 多重背包问题要求很简单,就是每件物品给出确定的件数,求可得到的最大价值。思路是把一堆同样的物品,转化为多堆单个商品,如7=14+12+1*1,比如最优解需要5,那么就装入一个4和一个1即可。见链接链接

  • 钢条切割: 给定一个长度为n的钢条和各长度钢条的价格表,求收益最大的切割方案。dp[n]记录n长度的最佳切割方案。

  • 矩阵链乘法: 一个矩阵序列进行相乘时,例如ABC,其中A为(p, q),B为(q, r),那么乘积AB为(p, r)矩阵,代价为pqr。如果三个矩阵为(10,100),(100,5),(5,50)那么按顺序相乘的代价为7500,先乘BC的代价为75000,后者代价为前者的10倍。dp[i][j]记录从矩阵i到矩阵j最小乘法代价,假设分割点为k,循环遍历dp[i][j]=dp[i][k]+dp[i][k+1]+k点矩阵相乘代价。选取pos[i][j]保存最优分割点位置。打印最优解: 给一个完整链,打印左括号,打印左子列,打印右子列,打印右括号。

  • 最长公共子序列: 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。dp[i][j]保存对应位置的最长公共子序列,分为当前字母相等和当前字母不等两种情况,如相等。则从i-1 j-1转移,如不等,则相当于等于i-1 j-1中最大的状态。打印最长公共子序列: 用pos数组记录方向,如果是斜向,打印字母,按照方向递归调用方法。

  • 最长回文子串: 给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度(子序列还是可以跳着的)。

  • 最长回文子序列: 给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度(子序列还是可以跳着的)。

  • 最优二叉搜索树:

  • 序列二叉搜索树数量 给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?dp记录n个节点的二叉搜索树数量,对于n,选取每个节点作为根节点进行遍历。

  • 输出序列二叉搜索树 首先可以采用分治算法的思路,选择一个根节点,生成左侧的树集合,生成右侧的树集合,遍历左侧右侧拼接。

  • 路径数量-无障碍 图中任一位置的路径数量等于其左节点和上方节点的数量之和。

  • 路径数量-有障碍 有障碍,每遇到障碍,当前点路径数变为0。

  • 单词拆分 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。这个题分为多种思路:首先是DFS:如果指针的左侧部分是单词,则对剩余子串,递归考察。如果指针的左侧部分不是单词,不用看了,回溯,考察别的分支。DFS的问题在于对于类似于aaaaaaaaaaaaaaaaaa,和aa,会导致大量递归过程。动态规划的思路是,通过dp记录长度为i的子串是否能拆分,如果字串可拆分,剩余串为单词(不要去考虑两边是否可拆分),则成功解读字符串。

  • 单词拆分方案 在单词拆分的基础上,首先对输入字符串和词典,判断能否拆分。如可以拆分,则进入递归,求所有拆分方案。在递归过程中,首先判断当前是否为单词,如果是,将这个词的作为一个序列开头加入答案集;循环当前字符串,如果一半可拆分,一半是单词,则对可拆分的一半递归调用,返回的答案集每一个方案加上另一半的单词。最终返回当前答案集。

  • 各种买卖股票(在Leetcode上搜索买卖股票):
    买卖股票1:给一个价格序列,只买卖一次,找最好的方案。维持一个min一个max即可(max是全局的,min是当前局部的)。
    买卖股票2:可交易多次,带一天冻结期。这个题的解法比较纠结:不考虑是否处于冻结期,我们用 buy[i] 和 sell[i] 分别表示第 i 天交易完成后手中持有股票(当天买入,或者前一状态买入并且当天维持)和不持有股票(当天卖出,或者前一状态卖出并且当天维持)的最大累计收益。当第 i 天交易完成后持有股票时,可能是维持前一天买入的状态,也可能在卖掉持有股票的情况下买入当前股票,注意由于卖掉股票之后有一天的冻结期,所以这种情况对应的卖操作是在第 i - 2 天完成;;;相应的,当第 i 天交易完成后不持有股票时,可能维持前一天卖出的状态,也可能在前一天买入股票的情况下今日卖出。(题解摘自链接,作者为da-shi-shuo)
    买卖股票4:有K次交易机会(买卖股票3有两次交易机会,就不详细说了)。其中深度优先搜索思路: 从初始状态开始,买入–持有–卖出,递归k次,深度优先搜索。首先主函数调用dfs,在递归过程中,如果到数组最后则返回。否则进行三种递归调用,1. 保持不动递归;2. 如果可卖则卖递归;3. 如果可买则买递归。在此基础上,记忆k次交易。可进一步优化为当交易次数k>=n/2时,我们直接用贪心计算就可以了。当k<n/2时仍然采用之前的递归实现。动态规划思路: 动态规划的常规思路为dp[天数][交易次数][状态],外层的循环遍历数组;内层的循环遍历k次交易。空间优化去除天数,

for(int i=1;i<n;++i) {
    for(int j=k;j>0;--j) {
        //处理第k次买入
        dp[j-1][1] = Math.max(dp[j-1][1], dp[j-1][0]-prices[i]);
        //处理第k次卖出
        dp[j][0] = Math.max(dp[j][0], dp[j-1][1]+prices[i]);

    }
}

作者:wang_ni_ma
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/si-chong-jie-fa-tu-jie-188mai-mai-gu-piao-de-zui-j/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

5.1 状态转移方程

  1. 刻画一个最优解的结构特征。
  2. 递归地定义最优解的值。
  3. 计算最优解的值,通常采用自底向上的方法。
  4. 利用计算出的信息构造一个最优解。

  对于确定状态转移方程就在第一步和第二步中,首先要确定问题的决策对象,接着对决策对象划分阶段并确定各个阶段的状态变量,最后建立各阶段的状态变量的转移方程。

  例如用dp[i]表示以序列中第i个数字结尾的最长递增子序列长度和最长公共子序列中用dp[i][j]表示的两个字符串中前 i、 j 个字符的最长公共子序列,我们就是通过对这两个数字量的不断求解最终得到答案的。这个数字量就被我们称为状态。状态是描述问题当前状况的一个数字量。首先,它是数字的,是可以被抽象出来保存在内存中的。其次,它可以完全的表示一个状态的特征,而不需要其他任何的辅助信息。最后,也是状态最重要的特点,状态间的转移完全依赖于各个状态本身,如最长递增子序列中,dp[x]的值由 dp[i](i < x)的值确定。若我们在分析动态规划问题的时候能够找到这样一个符合以上所有条件的状态,那么多半这个问题是可以被正确解出的。所以说,解动态规划问题的关键,就是寻找一个好的状态。

5.2 状态压缩

  对于一部分动态规划题目,直观的状态往往涉及多个维数,这些状态在一定条件下可以进行状态压缩。链接
   一般来说,如果dp[i][j]的转移只与二维数组本行的上一行有关(如dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2],斐波那契)。则可压缩第一维。

6 搜索

6.1 二分法

  1. 查找旋转排序数组:

6.2 深度优先搜索

  1. 二叉树相关经典递归思路: 判定当前点,递归调用左子树,递归调用右子树。例如,对有序数列生成高度平衡的二叉搜索树,每次取数组中点,中–左–右进行处理。有些题目用递归思路难度不会减小例如通过前序数组和中序数组构建二叉树。
  2. 二叉树相关经典迭代思路:
  3. 根据前序、中序序列构造二叉树 递归思路:记录前序数列左右点,记录中序数列当前点(根节点),根据根节点,和子树范围,构建根节点的左右子树。迭代思路:用栈储存没有构建右子树的节点,对于前序序列顺序遍历,中序数列记录当前位置。向左添加节点(同时加入栈)直到栈顶节点值等于中序数列当前位置,相当于到达了左侧底部。此时出栈,中序数列记录向前移动一格,直到栈顶节点值不等于中序数列当前位置,并将当前前序序列值加入右节点,将右节点入栈。建议用图像模拟。
  4. 二叉树中最大路径和 路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列,其中最底部的父节点可以包含两个子节点。采用递归的思路,递归调用一个点能贡献的最大值,同时全局存储一个最大路径的答案。
  5. 克隆图 在内存中完全拷贝一个图,图的每一个node包含对应的值和相邻节点的数组。通过map储存新图和旧图节点的对应关系,如果当前节点为map中存在的节点,说明这个节点已经创建过,直接返回map对应的引用。反之则新建节点,遍历当前节点(旧图)的邻接节点数组,对于每一个节点递归调用创建程序。
  6. 组合总和使用多次 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。其中每个数字可以使用多次。总体使用dfs的思路,对candidate数组进行遍历,当前值大于target则跳过,当前值等于target则返回含有当前值的list,当前值小于target,则递归调用target-当前值。优化方案:记录位置,每次递归从位置p开始遍历。
  7. 组合总和使用一次 首先对数组进行排序,每次小于target时,用while确认有n个重复值,进行n次循环,每次加入1,2,3,…,n个当前值,p设置为i+n,并对当前遍历的i设定为i+ n

6.3 广度优先搜索

  1. 按层遍历二叉树 使用队列存储树中待访问的节点,每层循环当前队列中节点的次数,循环中每次从队列移出一个节点,并加入这一节点的左右子节点。
  2. 单词接龙 给定两个单词(beginWord 和 endWord)和一个字典,找到从beginWord 到endWord 的最短转换序列的长度。转换需遵循如下规则:每次转换只能改变一个字母。转换过程中的中间单词必须是字典中的单词。BFS的解空间形如[hot–hit–hip–tip].BFS的思路为储存一个队列,代表待遍历的单词,每层循环当前队列中单词的数量,循环中每次从队列移出一个单词,并加入这一单词的所有合法接龙词。层数加一,如果加入的词有目标词,则返回当前层数+1。优化BFS是使用BOOLEAN数组储存访问过的节点,访问过就不要加入队列了(其实是贪心,先加入就的层数一定比后加入的小)。
  3. 单词接龙-双向BFS 维持两个队列和visited,从开始单词和结束单词分别开始bfs,谁的队列短就先运行谁,每运行一层(不管是谁),层数+1,当遍历到被另一个方向访问过的节点,返回层数+1.

6.4 回溯算法

回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

  1. 找到一个可能存在的正确的答案;
  2. 在尝试了所有可能的分步方法后宣告该问题没有答案。

回溯是一种更通用的算法。可以用于任何类型的结构,其中可以消除域的部分 ——无论它是否是逻辑树。深度优先搜索是与搜索树或图结构相关的特定回溯形式。它使用回溯作为其使用树的方法的一部分,但仅限于树/图结构。

  1. 全排列 每次递归维持一个output数组,遍历每次负责output i位置的所有情况,及i位置当前数字和j位置交换之后,递归i处理之后的output,每当i==数组长度的时候,将当前output存入答案集。递归调用之后,要恢复i和j位置交换的数字,以遍进一步遍历i和j+1位置互换。

7 经典图算法

8 经典字符串算法

9 其他

  • leetcode169 求大多数:每次去除一对不同数。
  • 找坏芯片:算法导论第四章课后习题,当芯片同好或同坏时,互相检测两个好,否则检测两个坏。思路同上,不断排除一好一坏的两个芯片。检测单个芯片性质时,所有测一个。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值