链表

算法模式:回溯

算法模式:回溯

D瓜哥
在上一篇文章 算法模式:变治法 介绍一种有魔力的,可以将复杂问题化繁为简,化腐朽为神奇的算法模式:变治法。本篇文章,介绍一种“一步三回头”、“落棋有悔”的算法模式:回溯。 回溯 “回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。 “回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。 “全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 \$N!\$ 这么多个。 使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。 说明: 每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现; 这些变量的不同的值,也称之为“状态”; 使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样; 因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”; 深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。 深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。 解决一个回溯问题,实际上就是一个决策树的遍历过程。只需要思考 3 个问题: 路径:也就是已经做出的选择。 选择列表:也就是你当前可以做的选择。 结束条件:也就是到达决策树底层,无法再做选择的条件。 这三个问题也就对应回溯三部曲: 定义递归函数以及参数 确定递归终止条件 思考递归单层搜索逻辑 代码方面,回溯算法的框架: result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择 其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。 必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 \$O(N!)\$,因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。 玩回溯,一定要画出递归调用树。这样可以帮助我们更深入地理解整个回溯的过程,方便进一步剪枝优化。 回溯优化,重要的是,要学会剪枝! LeetCode 46. 全排列 LeetCode - 46. 全排列 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
算法模式:变治法

算法模式:变治法

D瓜哥
在上一篇文章 算法模式:分治法 介绍一种可用于处理节点前后顺序的算法模式:拓扑排序。本篇文章,介绍一种有魔力的,可以将复杂问题化繁为简,化腐朽为神奇的算法模式:变治法。 变治法 D瓜哥最早知道变治法也是在 《算法设计与分析基础》 中。这里也直接引用该书的介绍。 变治法,就是基于变换的一种思想方法,首先把问题的实例变得容易求解,然后进行求解。 变治法的工作可以分成两个阶段:首先把问题变得更容易求解,然后对实例进行求解。根据我们对问题实例的变换方式,变治思想有3种主要的类型: 实例化简(Instance simplification) — 指将原问题变换为同样问题的一个更简单或者更方便的实例。一个典型的案例是:去重时,先排序, 列表预排序 检验数组中元素的唯一性 模式计算 查找问题 高斯消元法 系数矩阵的LU分解(LU decomposition) 计算矩阵的逆 计算矩阵的行列式 AVL 树 改变表现(Representation Change) — 指将原问题变换为同样实例的不同表现。经典的栗子:霍纳法则。 多路平衡查找树(最简单的情况:2-3树) 求多项式的霍纳法则 两种二进制幂算法 堆排序 问题化简(Problem reduction) — 指把一个给定的问题变换为另一个可以用已知算法求解的问题。(归化思想)转换的难题在于如何找到一个变换的目标算法。典型案例是背包问题,背包问题的本质是线性规划。了解了线性规划的本质后,才能更好地解决高维的背包问题。 求最小公倍数 计算图中的路径数量 最优化问题(最大化问题(maximization problem)、最小化问题(minimization problem)) 线性规划(单纯形法、0/1背包问题) 简化为图问题 LeetCode 474. 一和零 LeetCode - 474. 一和零 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
算法模式:分治法

算法模式:分治法

D瓜哥
在上一篇文章 算法模式:减治法 介绍一种有魔力的,可以将复杂问题化繁为简,化腐朽为神奇的算法模式:减治法。本篇文章,继续介绍一种有魔力的,可以将复杂问题化繁为简,化腐朽为神奇的算法模式:分治法。 分治法 关于分治法的内容,这里继续参考 《算法设计与分析基础》 中的内容。 分治法是按照以下方案工作的。 将一个问题划分为同一类型的若干子问题,子问题最好规模相同。 对这些子问题求解(一般使用递归方法,但在问题规模足够小时,有时也会利用另一个算法)。 有必要的话,合并这些子问题的解,以得到原始问题的答案。 图 1. 分治法 从字面上分析就可以看到有哪些步骤: 分-分解-将问题分解为规模更小的子问题,子问题最好相同或相似; 治-求解-将这些规模更小的子问题逐个击破; 合-合并-将已解决的子问题合并,最终得出原问题的解; 从上述步骤中我们可以看出,分治算法一般适用满足以下条件的场景: 问题规模缩小到一定的程度就可以很容易解决; 问题可以分解为若干个规模较小的相同问题; 问题分解出的若干子问题的解可以合并为该问题的解; 每个子问题都是独立的,相互之间没有交集。(这是区别分治法与减) 在“分”的过程中,我们尽可能让分解出的子问题与原始问题相似,而规模更小。这刚好符合递归的特性。因此,分治法往往与递归联系在一起。 在分治法最典型的运用中,问题规模为 n 的实例被划分为两个规模为 n/2 的实例。更一般的情况下,一个规模为 n 的实例可以划分为 b 个规模为 n/b 的实例,其中 a 个实例需要求解(这里,a 和 b 是常量,a≥1,b>1)。 \$T(n) = aT(n/b) + f(n)\$ 其中,\$f(n)\$ 是一个函数,表示将问题分解为小问题和将结果合并起来所消耗的时间 分治法的典型案例如下: 归并排序 快速排序 二叉树的经典遍历算法和其他类似的算法都需要递归处理左右两棵子树 Strassen 算法 最近对问题 凸包问题 分治法对分治出的部分需要分别处理,进行分开的单独计算,而减治法则利用了"一个问题给定实例的解和同样问题较小实例的解之间的关系",只针对部分子问题求解,减治掉的那部分就不需要了。 减常因子的减治法也可以看做是分治的变种。 LeetCode 148. 排序链表 LeetCode - 148. 排序链表 给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
算法模式:减治法

算法模式:减治法

D瓜哥
在上一篇文章 算法模式:拓扑排序 介绍一种可用于处理节点前后顺序的算法模式:拓扑排序。本篇文章,介绍一种有魔力的,可以将复杂问题化繁为简,化腐朽为神奇的算法模式:减治法。 减治法 D瓜哥最早知道减治法是在 《算法设计与分析基础》 中。这里也直接引用该书的介绍。 减治(decrease-and-conquer)技术利用了一个问题给定实例的解和同样问题较小实例的解之间的某种关系。自底向上版本往往是迭代实现的,从求解问题的一个较小实例开始,该方法有时也称为增量法(Incremental Approach)。 减治法有3种主要的变化形式: 减去一个常量。在减常量(decrease-by-a-constant)变化形式中,每次算法迭代总是从实例中减去一个相同的常量。 插入排序 减去一个常量因子。减常因子(decrease-by-a-constant-factor)技术意味着在算法的每次迭代中,总是从实例的规模中减去一个相同的常数因子。在大多数应用中,这样的常数因子等于2,其实就是减半。 二分查找 减去的规模是可变的。在减治法的减可变规模(variable-size-decrease)变化形式中,算法在每次迭代时,规模减小的模式都是不同的。 计算最大公约数的欧几里得算法是这种情况的一个很好的例子。 \$gcd(m, n)=gcd(n,m mod n)\$ LeetCode 50. Pow(x, n) LeetCode - 50. Pow(x, n) 实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,xn)。 示例 1: 输入:x = 2.00000, n = 10 输出:1024.00000 示例 2: 输入:x = 2.10000, n = 3 输出:9.26100 示例 3: 输入:x = 2.00000, n = -2 输出:0.25000 解释:2-2 = 1/22 = 1/4 = 0.25 提示:
算法模式:滑动窗口

算法模式:滑动窗口

D瓜哥
在上一篇文章 算法模式:双指针 介绍了双指针的算法模式。本篇文章,介绍一种类似双指针的算法模式:滑动窗口。 滑动窗口 滑动窗口类型的题目经常是用来执行数组或是链表上某个区间(窗口)上的操作。比如找最长的全为1的子数组长度。滑动窗口一般从第一个元素开始,一直往右边一个一个元素挪动。当然了,根据题目要求,我们可能有固定窗口大小的情况,也有窗口的大小变化的情况。 图 1. 滑动窗口 滑动窗口大概思路如下: // 向前滑动窗口 while (right < array.lenght) { // 扩大窗口,将元素放入窗口 right++; while (缩小窗口条件) { // 处理窗口内的元素 // 缩小窗口,将元素丢出窗口 left++; } } 下面是一些我们用来判断我们可能需要上滑动窗口策略的方法: 问题的输入是一些线性结构:比如链表,数组,字符串之类的 让你去求最长/最短子字符串或是某些特定的长度要求 LeetCode 3. 无重复字符的最长子串 LeetCode - 3. 无重复字符的最长子串 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 示例 2: 输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 示例 3: 输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。