1. 双指针

算法中的双指针使用,有时候会觉得很巧妙,解决了很多的问题。首先双指针也是个很宽泛的概念,它类似于遍历中的ij。但区别是,这两个指针是同时移动的

1.1 题型归纳

这里将题型归纳为三种,分别如下:

  • 快慢指针(前后按不同步调的两个指针)

  • 前后双端指针(一个在首,一个在尾部,向中间靠拢)

  • 固定间隔的指针(以ii + k的间距的两个指针)

1.2 比较版本号

给出两个版本号version1version2,请比较它们的大小

版本号由一个或多个修订号组成,各修订号由一个'.'连接。每个修订号由多位数字组成,可能包含前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从0开始,最左边的修订号下标为0,下一个修订号下标为1,以此类推。例如:2.5.330.1都是有效的版本号

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值 。也就是说,修订号1和修订号001相等。如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如:版本1.0小于版本1.1,因为它们下标为0的修订号相同,而下标为1的修订号分别为01,即:0 < 1

返回规则如下:

  • 如果version1 > version2返回1

  • 如果version1 < version2返回-1

  • 除此之外返回0

示例1:

  1. 输入:version1 = "1.01", version2 = "1.001"
  2. 输出:0
  • 忽略前导零,01001都表示相同的整数1

示例2:

  1. 输入:version1 = "1.0", version2 = "1.0.0"
  2. 输出:0
  • version1没有指定下标为2的修订号,即视为0

示例3:

  1. 输入:version1 = "0.1", version2 = "1.1"
  2. 输出:-1
  • version1中下标为0的修订号是0version2中下标为0的修订号是1 。由于0 < 1,所以version1 < version2

解题思路:

使用双指针,i负责记录遍历v1字符串的索引,j负责记录遍历v2字符串的索引。由它们控制外层循环的停止条件

内部实现两个内循环,分别遍历v1v2中的字符。使用x = x * 10 + c算法,将字符格式的版本号变为真正的数字,遇到.停止本次内循环

当两个内循环停止后,比较本次的两个版本号。如果不相等,直接返回最终结果。如果相等,继续执行外层循环,由此开启下一轮的内循环,完成后续版本号的对比

如果版本号一直相等,只有当v1v2中全部字符遍历结束时,外层循环才会停止,并返回结果相等

代码实现:

  1. class Solution {
  2. func compareVersion(_ version1: String, _ version2: String) -> Int {
  3. var i = 0;
  4. var j = 0;
  5. while(i < version1.count || j < version2.count) {
  6. var x = 0;
  7. while(i < version1.count){
  8. let c = version1[i];
  9. i += 1;
  10. if(c == "."){
  11. break;
  12. }
  13. x = x * 10 + Int(c)!;
  14. }
  15. var y = 0;
  16. while(j < version2.count){
  17. let c = version2[j];
  18. j += 1;
  19. if(c == "."){
  20. break;
  21. }
  22. y = y * 10 + Int(c)!;
  23. }
  24. if(x > y){
  25. return 1;
  26. }
  27. if(x < y){
  28. return -1;
  29. }
  30. }
  31. return 0;
  32. }
  33. }
  • 时间复杂度:O(n + m)

  • 空间复杂度:O(1)

2. 动态规划

动态规划(DP):Dynamic Programming,实际上是一种思想,通过将优化问题分解为更简单的⼦问题,并利⽤整体问题的最优解取决于其⼦问题的最优解,这⼀事实来解决优化问题的算法技术

动态规划的两个关键点:

  • 重叠⼦问题

  • 最优⼦结构

2.1 解决问题的⽅式

动态规划提供了两种解决问题的⽅法:

  • ⾃顶向下的记忆化

    • 在这种⽅法中,尝试通过递归找到较⼩⼦问题的解决⽅案,从而解决较⼤的问题

    • 每当我们解决⼀个⼦问题时,我们都会缓存它的结果,这样如果它被多次调⽤,我们就不会重复解决它

    • 相反,我们可以只返回保存的结果。这种存储已解决⼦问题结果的技术称为记忆化

  • ⾃底向上的制表法

    • 制表与⾃上⽽下的⽅法相反,避免了递归。在这种⽅法中,我们“⾃下⽽上”的解决问题,即:⾸先解决所有相关的⼦问题

    • 通常会填写⼀个n维表来完成。根据表中的结果,然后计算顶部(原始问题)的解决⽅案

使⽤动态规划解决的问题有个明显的特点:

  • ⼀旦⼀个⼦问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做⽆后效性

  • 动态规划只解决每个⼦问题⼀次,具有天然剪枝的功能,从⽽减少计算量

动态规划解题思路分为两步:

  • 状态定义:定义动态规划过程中涉及的变量状态

  • 转移⽅程:归纳和抽象出⼦问题对应的数学⽅程

2.2 最长公共子序列

给出两个字符串text1text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列 ,返回0

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串

例如:”ace“ 是 “abcde“ 的子序列,但 “aec“ 不是 “abcde“ 的子序列。两个字符串的公共子序列是这两个字符串所共同拥有的子序列

示例1:

  1. 输入:text1 = "abcde", text2 = "ace"
  2. 输出:3
  • 最长公共子序列是”ace“ ,它的长度为3

示例2:

  1. 输入:text1 = "abc", text2 = "abc"
  2. 输出:3
  • 最长公共子序列是”abc“ ,它的长度为3

示例3:

  1. 输入:text1 = "abc", text2 = "def"
  2. 输出:0
  • 两个字符串没有公共子序列,返回0

2.2.1 思路分析

我们将两个字符串中的字符进行逐一对比,就可以得到最终的结果。而字符对比的结果,只存在两种情况,相等与不对

如果字符相等:

  • 将相同子序列的长度+1

  • 分别获取t1t2的下一个字符,进行下一轮对比

当字符不相等:

  • 使用t1当前字符,与t2中后续的字符依次对比。遇到相等字符,触发上面的相对逻辑。否则,从t1中下一个字符,循环往复,对比完t1t2的所有字符,得到按t1顺序排列的相同子序列长度

  • t2同样需要这个过程,逐一获取t2中字符与t1进行对比,得到按t2顺序排列的相同子序列长度

  • t1t2中相同子序列长度,返回较大值即可

我们按照动态规划解题思路进行分析,首先找到子问题,即:字符对比。然后进行以下两个步骤:

【步骤一】找到状态定义:

  • 相等

  • 不等

【步骤二】找到转移⽅程:定义getResult方法,传入的ij分别表示两个字符串中,即将对比的字符索引位置,该方法最终返回对比结果

  • t1[i] == t2[j],则1 + getResult(i + 1, j + 1)

  • t1[i] != t2[j],则Max(getResult(i, j + 1), getResult(i + 1, j))

2.2.2 递归解法

递归解法,从所需结果出发不断回溯前一运算,直到回到初值再递推得到所需结果。属于自顶向下的解决方案,从未知到已知

代码实现:

  1. class Solution {
  2. fileprivate var _t1 : String?;
  3. fileprivate var _t2 : String?;
  4. func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {
  5. _t1 = text1;
  6. _t2 = text2;
  7. return getResult(i: 0, j: 0);
  8. }
  9. fileprivate func getResult(i : Int, j : Int) -> Int {
  10. if(i == _t1!.count || j == _t2!.count){
  11. // 所有字符都不匹配
  12. // 相同子序列长度+0
  13. return 0;
  14. }
  15. if(_t1![i] == _t2![j]){
  16. // 将相同子序列的长度+1
  17. // 分别获取t1和t2的下一个字符,进行下一轮对比
  18. return 1 + getResult(i: i + 1, j: j + 1);
  19. }
  20. // 使用t1当前字符,与t2中后续的字符依次对比
  21. let result1 = getResult(i: i, j: j + 1);
  22. // 使用t2当前字符,与t1中后续的字符依次对比
  23. let result2 = getResult(i: i + 1, j: j);
  24. // 将t1和t2中相同子序列长度,返回较大值即可
  25. return result1 > result2 ? result1 : result2;
  26. }
  27. }

如果m == n,则时间复杂度:O(2 ^ n)
image.png

  • 求解过程中,会出现一些重复子问题。而这些子问题每次都会被重复计算,所以算法的执行效率非常低下

2.2.3 缓存优化

算法时间复杂度的优化,可以使⽤⼀个数组来存储⼦问题结果。当想要⼀个⼦问题的解决⽅案时,⾸先查看数组中是否已经存在解决⽅案。如果有,取出。否则,执⾏计算并存储结果

代码实现:

  1. class Solution {
  2. fileprivate var _t1 : String?;
  3. fileprivate var _t2 : String?;
  4. fileprivate var _arr : [[Int]]?;
  5. func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {
  6. _t1 = text1;
  7. _t2 = text2;
  8. _arr = [[Int]].init(repeating: [Int].init(repeating: -1, count: _t2!.count + 1), count: _t1!.count + 1);
  9. let result = getResult(i: 0, j: 0);
  10. return result;
  11. }
  12. fileprivate func getResult(i : Int, j : Int) -> Int {
  13. // ⾸先查看数组中是否已经存在解决⽅案
  14. if(_arr![i][j] == -1) {
  15. // 未知结果,计算并存储结果
  16. if(i == _t1!.count || j == _t2!.count){
  17. // 所有字符都不匹配
  18. // 相同子序列长度+0
  19. _arr![i][j] = 0;
  20. }
  21. else if(_t1![i] == _t2![j]){
  22. // 将相同子序列的长度+1
  23. // 分别获取t1和t2的下一个字符,进行下一轮对比
  24. _arr![i][j] = 1 + getResult(i: i + 1, j: j + 1);
  25. }
  26. else {
  27. // 使用t1当前字符,与t2中后续的字符依次对比
  28. let result1 = getResult(i: i, j: j + 1);
  29. // 使用t2当前字符,与t1中后续的字符依次对比
  30. let result2 = getResult(i: i + 1, j: j);
  31. _arr![i][j] = result1 > result2 ? result1 : result2;
  32. }
  33. }
  34. // 已知结果,直接返回
  35. return _arr![i][j];
  36. }
  37. }
  • 时间复杂度:O(m * n)

  • 空间复杂度:O(m * n)

2.2.4 递推解法

递推解法:从初值出发反复进行某一运算得到所需结果。属于自底向上的解决方案,从已知到未知

创建二维数组的行、列长度时,在两个字符串原有长度基础上各自+1。多创建出来的行与列,元素用0填充。这样做的目的:当遇到字符串中最后一个字符的对比时,不会因为i + 1j + 1导致数组越界,统一逻辑的处理

代码实现:

  1. class Solution {
  2. fileprivate var _t1 : String?;
  3. fileprivate var _t2 : String?;
  4. fileprivate var _arr : [[Int]]?;
  5. func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {
  6. _t1 = text1;
  7. _t2 = text2;
  8. // 两个字符串原有长度基础上各自+1,元素用0填充
  9. _arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: _t2!.count + 1), count: _t1!.count + 1);
  10. let result = getResult();
  11. return result;
  12. }
  13. fileprivate func getResult() -> Int {
  14. // 从t1与t2字符串的结尾字符开始对比
  15. for i in (0..<_t1!.count).reversed() {
  16. for j in (0..<_t2!.count).reversed() {
  17. if(_t1![i] == _t2![j]){
  18. // 字符相等,在上一次结果上+1
  19. // 因为虚拟行列,不用担心数组越界
  20. _arr![i][j] = 1 + _arr![i + 1][j + 1];
  21. continue;
  22. }
  23. // 字符不相等,获取[i][j + 1]和[i + 1][j]的结果
  24. let result1 = _arr![i][j + 1];
  25. let result2 = _arr![i + 1][j];
  26. // 选择较大的值,记录在[i][j]中
  27. _arr![i][j] = result1 > result2 ? result1 : result2;
  28. }
  29. }
  30. // 返回最终的计算结果
  31. return _arr![0][0];
  32. }
  33. }
  • 时间复杂度:O(m * n)

  • 空间复杂度:O(m * n)

示例:

  1. let text1 = "nematode knowledge";
  2. let text2 = "empty bottle";
  3. let solution = Solution();
  4. print("\(solution.longestCommonSubsequence(text1, text2))");

使用递推法求解后,会生成以下矩阵:
image.png

上述代码是从字符串结尾处开始对比,如果从字符串的起始字符开始,需要进行简单的修改:

  1. class Solution {
  2. fileprivate func getResult() -> Int {
  3. for i in 1..._t1!.count {
  4. for j in 1..._t2!.count {
  5. if(_t1![i - 1] == _t2![j - 1]){
  6. _arr![i][j] = 1 + _arr![i - 1][j - 1];
  7. continue;
  8. }
  9. let result1 = _arr![i][j - 1];
  10. let result2 = _arr![i - 1][j];
  11. _arr![i][j] = result1 > result2 ? result1 : result2;
  12. }
  13. }
  14. return _arr![_t1!.count][_t2!.count];
  15. }
  16. }
  • 索引1位置开始遍历字符串,将索引0当成虚拟行列使用

  • 对比字符时,则需要t1[i - 1] == t2[j - 1],通过-1进行索引值的修正

  • 当前[i][j]位置的值,使用i - 1j - 1配合计算

  • 最终计算结果,在数组的[t1.count][t2.count]中存储

2.2.5 空间优化

以上算法的空间复杂度为O(n * m),但通过代码可以得知,只要有上一行的结果,即可推算出当前行的结果。所以我们可以定义两个一维数组,分别存储上一行与当前行的结果

按照这个思路,我们将长度较短的字符串作为列,可以让数组所需的空间更小,从而进一步优化空间

  1. class Solution {
  2. fileprivate var _row : String?;
  3. fileprivate var _col : String?;
  4. // 当前行
  5. fileprivate var _currRow : [Int]?;
  6. // 上一行
  7. fileprivate var _prevRow : [Int]?;
  8. func longestCommonSubsequence(_ text1: String, _ text2: String) -> Int {
  9. // 较长字符串作为行
  10. _row = text1.count >= text2.count ? text1 : text2;
  11. // 较短字符串作为列
  12. _col = text1.count >= text2.count ? text2 : text1;
  13. _currRow = [Int].init(repeating: 0, count: _col!.count + 1);
  14. _prevRow = [Int].init(repeating: 0, count: _col!.count + 1);
  15. let result = getResult();
  16. return result;
  17. }
  18. fileprivate func getResult() -> Int {
  19. for i in 1..._row!.count {
  20. for j in 1..._col!.count {
  21. if(_row![i - 1] == _col![j - 1]){
  22. // _prevRow为上一行,j - 1为上一列,等同于二维数组的arr[i - 1][j - 1]
  23. _currRow![j] = 1 + _prevRow![j - 1];
  24. continue;
  25. }
  26. //等同于二维数组的arr[i - 1][j]
  27. let result1 = _prevRow![j];
  28. //等同于二维数组的arr[i][j - 1]
  29. let result2 = _currRow![j - 1];
  30. // 将较大的结果,更新当前行的j列上
  31. _currRow![j] = result1 > result2 ? result1 : result2;
  32. }
  33. // 将当前行的结果赋值给上一行,准备下一轮的循环使用
  34. _prevRow = _currRow;
  35. }
  36. return _currRow![_col!.count];
  37. }
  38. }
  • 时间复杂度:O(m * n)

  • 空间复杂的:O(2 * min(m, n))

2.2.6 公共子序列

通过矩阵,反推出两个字符串的公共子序列内容:

  1. class Solution {
  2. fileprivate var _t1 : String?;
  3. fileprivate var _t2 : String?;
  4. fileprivate var _arr : [[Int]]?;
  5. func commonSubsequence(_ text1: String, _ text2: String) -> [String]? {
  6. _t1 = text1;
  7. _t2 = text2;
  8. _arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: _t2!.count + 1), count: _t1!.count + 1);
  9. let result = getResult();
  10. return result;
  11. }
  12. fileprivate func getResult() -> [String]? {
  13. // 得到公共子序列的最大长度
  14. var k = get_lcs_lenght();
  15. if(k == 0){
  16. return nil;
  17. }
  18. // 按最大长度分配结果数组的空间
  19. var result = [String].init(repeating: "", count: k);
  20. // 从最终的结果,向初始位置推算
  21. var i = _t1!.count;
  22. var j = _t2!.count;
  23. k -= 1;
  24. while(k >= 0){
  25. if(_t1![i - 1] == _t2![j - 1]){
  26. result[k] = _t1![i - 1];
  27. k -= 1;
  28. i -= 1;
  29. j -= 1;
  30. continue;
  31. }
  32. if(_arr![i][j - 1] > _arr![i - 1][j]){
  33. j -= 1;
  34. }
  35. else{
  36. i -= 1;
  37. }
  38. }
  39. return result;
  40. }
  41. fileprivate func get_lcs_lenght() -> Int {
  42. for i in 1..._t1!.count {
  43. for j in 1..._t2!.count {
  44. if(_t1![i - 1] == _t2![j - 1]){
  45. _arr![i][j] = 1 + _arr![i - 1][j - 1];
  46. continue;
  47. }
  48. let result1 = _arr![i][j - 1];
  49. let result2 = _arr![i - 1][j];
  50. _arr![i][j] = result1 > result2 ? result1 : result2;
  51. }
  52. }
  53. return _arr![_t1!.count][_t2!.count];
  54. }
  55. }

2.3 背包问题

给定一个背包容量target,再给定一个物品数组nums,能否按一定方式选取nums中的元素得到target,每个元素选一次/每个元素选多次/选元素进行排列组合

根据上述需求,可以分为四种背包问题:

  • 0/1背包问题:每个元素最多选取一次

  • 完全背包问题:每个元素乐意重复选择

  • 组合背包问题:背包中的物品要考虑顺序

  • 分组背包问题:不止一个背包,需要遍历每个背包

2.3.1 0/1背包

以0/1背包问题为例,使用动态规划的思想解决

给定n个物品的重量和价值,从中选择物品放在容量为w的背包中(表示选择物品重量不超过w),同时每个物品最多允许选择1个,使得背包中物品价值之和最高

输入:

  • int Values[n]:物品的价值

  • int Weights[n]:物品的重量

  • int LimitWeight:背包容量剩余大小

    输出:

  • MaxValue:背包里的物品最大价值

2.3.2 思路分析

拆解为子问题:物品的选取。每个物品只能选一个,在不超过背包容量的情况下,保证背包中物品总价值最高

状态定义:

  • 选取物品

  • 不选取物品

转移⽅程:

  • 选取物品:value + getResult(index: index + 1, limit: limit - weight)

    • 当前物品价值 + 即将获取的后续物品价值,预先将背包剩余重量 - 当前物品重量
  • 不选取物品:getResult(index: index + 1, limit: limit)

    • 跳过本次物品,直接获取的后续物品价值

2.3.3 递归解法

  1. class Solution {
  2. fileprivate var _weights: [Int]?;
  3. fileprivate var _values: [Int]?;
  4. func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {
  5. _weights = weights;
  6. _values = values;
  7. return getResult(index: 0, limit: limit);
  8. }
  9. fileprivate func getResult(index : Int, limit : Int) -> Int {
  10. var max = 0;
  11. if(limit <= 0 || index >= weights.count){
  12. // 背包没有空间,或者物品已经取完,本次选取的物品价值返回0
  13. return max;
  14. }
  15. // 获取当前物品重量
  16. let weight = _weights![index];
  17. // 获取当前物品价值
  18. let value = _values![index];
  19. if(weight > limit){
  20. // 当前物品重量,超过背包剩余重量,放弃本次物品,继续选取下一个物品
  21. max = getResult(index: index + 1, limit: limit);
  22. // 返回可选取的后续物品的总价值
  23. return max;
  24. }
  25. // 选取本次物品,背包中的物品价值增加
  26. var selectedValue = value;
  27. // 选取本次物品,背包中的剩余重量减少
  28. let selectedWeight = limit - weight;
  29. // 继续选取下一个物品
  30. selectedValue += getResult(index: index + 1, limit: selectedWeight);
  31. // 放弃本次物品,直接选取下一个物品
  32. let skipValue = getResult(index: index + 1, limit: limit);
  33. // 比较两种选取方式中,可获得更大的物品价值
  34. max = selectedValue > skipValue ? selectedValue : skipValue;
  35. // 返回更大的物品价值
  36. return max;
  37. }
  38. }
  • 时间复杂度:O(m ^ n)

2.3.4 缓存优化

  1. class Solution {
  2. fileprivate var _weights: [Int]?;
  3. fileprivate var _values: [Int]?;
  4. fileprivate var _arr: [[Int]]?;
  5. func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {
  6. _weights = weights;
  7. _values = values;
  8. // 创建二维数组作为缓存,i和j分别为物品数量和背包限制的总重量
  9. _arr = [[Int]].init(repeating: [Int].init(repeating: -1, count: limit + 1), count: weights.count + 1);
  10. return getResult(index: 0, limit: limit);
  11. }
  12. fileprivate func getResult(index : Int, limit : Int) -> Int {
  13. if(limit <= 0 || index >= weights.count){
  14. // 背包没有空间,或者物品已经取完,本次选取的物品价值返回0
  15. return 0;
  16. }
  17. if(_arr![index][limit] == -1) {
  18. // 获取当前物品重量
  19. let weight = _weights![index];
  20. // 获取当前物品价值
  21. let value = _values![index];
  22. if(weight > limit){
  23. // 当前物品重量,超过背包剩余重量,放弃本次物品,继续选取下一个物品
  24. _arr![index][limit] = getResult(index: index + 1, limit: limit);
  25. // 返回可选取的后续物品的总价值
  26. return _arr![index][limit];
  27. }
  28. // 选取本次物品,背包中的物品价值增加
  29. var selectedValue = value;
  30. // 选取本次物品,背包中的剩余重量减少
  31. let selectedWeight = limit - weight;
  32. // 继续选取下一个物品
  33. selectedValue += getResult(index: index + 1, limit: selectedWeight);
  34. // 放弃本次物品,直接选取下一个物品
  35. let skipValue = getResult(index: index + 1, limit: limit);
  36. // 比较两种选取方式中,可获得更大的物品价值
  37. _arr![index][limit] = selectedValue > skipValue ? selectedValue : skipValue;
  38. // 返回更大的物品价值
  39. return _arr![index][limit];
  40. }
  41. // 命中缓存
  42. return _arr![index][limit];
  43. }
  44. }
  • 时间复杂度:O(m * n)

  • 空间复杂度:O(m * n)

2.3.5 递推解法

  1. class Solution {
  2. fileprivate var _weights: [Int]?;
  3. fileprivate var _values: [Int]?;
  4. fileprivate var _arr: [[Int]]?;
  5. func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {
  6. _weights = weights;
  7. _values = values;
  8. // 创建二维数组作为缓存,i和j分别为物品数量和背包限制的总重量
  9. _arr = [[Int]].init(repeating: [Int].init(repeating: 0, count: limit + 1), count: weights.count + 1);
  10. return getResult(limit: limit);
  11. }
  12. fileprivate func getResult(limit : Int) -> Int {
  13. for i in 1..._weights!.count {
  14. for j in 1...limit{
  15. // 获取当前物品重量
  16. let weight = _weights![i - 1];
  17. // 获取当前物品价值
  18. let value = _values![i - 1];
  19. if(weight > j){
  20. // 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充
  21. _arr![i][j] = _arr![i - 1][j];
  22. continue;
  23. }
  24. // 选取本次物品,背包中的物品价值增加
  25. var selectedValue = value;
  26. // 选取本次物品,背包中的剩余重量减少
  27. let selectedWeight = j - weight;
  28. // 在剩余重量相同的条件下,加入上一次物品选择后的总价值
  29. selectedValue += _arr![i - 1][selectedWeight];
  30. // 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值
  31. let skipValue = _arr![i - 1][j];
  32. // 比较两种选取方式,保存更大的物品价值
  33. _arr![i][j] = selectedValue > skipValue ? selectedValue : skipValue;
  34. }
  35. }
  36. // 返回最终物品选取的总价值
  37. return _arr![_weights!.count][limit];
  38. }
  39. }
  • 时间复杂度:O(m * n)

  • 空间复杂度:O(m * n)

2.2.6 空间优化

  1. class Solution {
  2. fileprivate var _weights: [Int]?;
  3. fileprivate var _values: [Int]?;
  4. fileprivate var _curRow: [Int]?;
  5. fileprivate var _preRow: [Int]?;
  6. func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {
  7. _weights = weights;
  8. _values = values;
  9. // 初始化当前行
  10. _curRow = [Int].init(repeating: 0, count: limit + 1);
  11. // 初始化上一行
  12. _preRow = [Int].init(repeating: 0, count: limit + 1);
  13. return getResult(limit: limit);
  14. }
  15. fileprivate func getResult(limit : Int) -> Int {
  16. for i in 0..<_weights!.count {
  17. // 获取当前物品重量
  18. let weight = _weights![i];
  19. // 获取当前物品价值
  20. let value = _values![i];
  21. for j in 1...limit{
  22. if(weight > j){
  23. // 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充
  24. _curRow![j] = _preRow![j];
  25. continue;
  26. }
  27. // 选取本次物品,背包中的物品价值增加
  28. var selectedValue = value;
  29. // 选取本次物品,背包中的剩余重量减少
  30. let selectedWeight = j - weight;
  31. // 在剩余重量相同的条件下,加入上一次物品选择后的总价值
  32. selectedValue += _preRow![selectedWeight];
  33. // 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值
  34. let skipValue = _preRow![j];
  35. // 比较两种选取方式,保存更大的物品价值
  36. _curRow![j] = selectedValue > skipValue ? selectedValue : skipValue;
  37. }
  38. for j in 1...limit{
  39. print("\(_curRow![j]),");
  40. }
  41. print("\n");
  42. // 将当前行的结果赋值给上一行,准备下一轮的循环使用
  43. _preRow = _curRow;
  44. }
  45. // 返回最终物品选取的总价值
  46. return _curRow![limit];
  47. }
  48. }
  • 时间复杂度:O(m * n)

  • 空间复杂的:O(2n)

2.2.6 空间再优化

  1. class Solution {
  2. fileprivate var _weights: [Int]?;
  3. fileprivate var _values: [Int]?;
  4. // 当前行,同时也作为上一行的数据
  5. fileprivate var _curRow: [Int]?;
  6. func knapsack(_ weights: [Int], _ values: [Int], _ limit: Int) -> Int {
  7. _weights = weights;
  8. _values = values;
  9. // 初始化当前行
  10. _curRow = [Int].init(repeating: 0, count: limit + 1);
  11. return getResult(limit: limit);
  12. }
  13. fileprivate func getResult(limit : Int) -> Int {
  14. for i in 0..<_weights!.count {
  15. // 获取当前物品重量
  16. let weight = _weights![i];
  17. // 获取当前物品价值
  18. let value = _values![i];
  19. // 由于当前总价值,依赖于上一行的总价值,所以剩余空间需要从大到小遍历,避免上一行的数据提前被覆盖为当前行
  20. for j in (1...limit).reversed() {
  21. if(weight > j){
  22. // 当前物品重量,超过背包剩余重量,放弃本次物品,使用上一次的物品总价值填充
  23. // 使用curRow存储当前总价值,同时它也作为上一行的总价值在使用
  24. // 所以不去修改curRow[j]的值,相当于使用了上一次的物品总价值
  25. continue;
  26. }
  27. // 选取本次物品,背包中的物品价值增加
  28. var selectedValue = value;
  29. // 选取本次物品,背包中的剩余重量减少
  30. let selectedWeight = j - weight;
  31. // 在剩余重量相同的条件下,加入上一次物品选择后的总价值
  32. selectedValue += _curRow![selectedWeight];
  33. // 放弃本次物品,在相同重量的条件下,得到上一次物品选择后的总价值
  34. // 只要curRow[j]没有提前被覆盖,拿到的就是上一次的物品总价值
  35. let skipValue = _curRow![j];
  36. // 比较两种选取方式,保存更大的物品价值
  37. _curRow![j] = selectedValue > skipValue ? selectedValue : skipValue;
  38. }
  39. }
  40. // 返回最终物品选取的总价值
  41. return _curRow![limit];
  42. }
  43. }
  • 时间复杂度:O(m * n)

  • 空间复杂的:O(n)

总结

双指针题型归纳:

  • 快慢指针(前后按不同步调的两个指针)

  • 前后双端指针(一个在首,一个在尾部,向中间靠拢)

  • 固定间隔的指针(以ii + k的间距的两个指针)

动态规划:

  • 动态规划(DP):Dynamic Programming,实际上是一种思想,通过将优化问题分解为更简单的⼦问题,并利⽤整体问题的最优解取决于其⼦问题的最优解,这⼀事实来解决优化问题的算法技术

  • 动态规划的两个关键点:

    • 重叠⼦问题

    • 最优⼦结构

  • 动态规划提供了两种解决问题的⽅法:

    • ⾃顶向下的记忆化

    • ⾃底向上的制表法

  • 使⽤动态规划解决的问题有个明显的特点:

    • ⼀旦⼀个⼦问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做⽆后效性

    • 动态规划只解决每个⼦问题⼀次,具有天然剪枝的功能,从⽽减少计算量

  • 动态规划解题思路分为两步:

    • 状态定义:定义动态规划过程中涉及的变量状态

    • 转移⽅程:归纳和抽象出⼦问题对应的数学⽅程