简化路径
算法标签:栈、字符串
给我们一个路径,要求把文件路径化简,给定的路径一定是合法的 Linux 路径,一个合法的 Linux 路径一般从 / 开始,/ 表示根目录,有很多的子目录 home、y,"." 表示在当前目录(一个 "." 没有任何变化), ".." 表示返回上一级目录
如果一个路径是 / home / a / .. / b,根目录 → home → a → b,化简其实就是把所有的 "." 和 ".." 去掉,当前案例去掉就是 / home / b
注意特殊情况:斜杠 " / " 可能会有连续多个,需要把连续多个斜杆换成一个斜杆,如果在根目录返回上一级目录的话,由于根目录已经是最靠上的一个目录了,不能再返回,如果从根目录往上再返回还是根目录,保证输入的目录一定是合法的目录,所以不需要考虑不合法的情况
注意输出的时候,如果不是根目录,最后的斜杆不用加,如果是根目录就返回一个斜杆
参考
res表示当前的路径,name表示遇到两个'/'之间的字符
1.遇到". .",回到上一个目录,即将res最后一个以"/"开始往后的字符全部删去
2.遇到"."或者多余的"/"不做处理
3.其他表示可以延伸新的路径,将"/" + name字符串加入到res后面
模拟样例
path = "/a/./b/../../c/"
实现的时候类似一个栈,所有的操作要么是从当前目录往下走一格,要么就是从当前目录返回上一格,整个数据结构只会发生在栈顶的位置,要么向栈顶插入一个元素,要么从栈顶弹出一个元素,实现的时候不需要模拟栈,直接使用字符串就可以了,用 string 表示当前的目录,根据当前目录的格式进行操作,如果是 "." 的话不用变化,如果是 ".." 返回上一级目录,如果不是以上两种情况就往下走一格,运用栈的思想
时间复杂度分析
整个字符串只会扫描一遍,每一个目录最多只会进栈出栈一次,所以时间复杂度是线性的
class Solution {
public:
string simplifyPath(string path) {
//定义最终答案路径 记录当前文件名
string res, name;
//统一格式 如果 path 最后不是 "/" 就补上一个 "/" 在读取每一个文件名的时候是读到 "/" 截止 保证最后一个文件名也用类似的方式去读
if (path.back() != '/') path += '/';
//从前往后扫描每一个字符
for (auto c : path) {
//如果当前字符不是 "/" 说明当前文件名还没有完 需要把它加到文件名中
if (c != '/') name += c;
else {
//否则说明当前字符是 "/" 表示已经读取出完整的文件名 文件名放到name里面
//处理特殊情况 如果是 ".." 返回上一级目录
if (name == "..") {
//res不为空并且res最后一个字符不是 "/" 就弹出最后一个字符
while (res.size() && res.back() != '/') res.pop_back();
//res不为空把 "/" 去掉
if (res.size()) res.pop_back();
}
//如果 name 是 "." 或者是空 "//" 不用处理
else if (name != "." && name != "") {
//表示可以延伸新的路径,将"/" + 当前文件名插到路径最后
res += '/' + name;
}
//把name清空
name.clear();
}
}
//空字符串表示在根目录的位置
if (res.empty()) res = "/";
return res;
}
};
编辑距离
算法标签:字符串、动态规划
给出两个单词 word1 和 word2,要将 word1 转换成 word2,可以对任何一个单词进行 3 种操作,可以在任意位置插入一个字符,可以将任意一个字符删除,可以将任意一个字符替换成另外一个字符,问最少需要进行多少次操作可以将 word1 变成 word2?
模拟样例
经典 dp 问题
两个字符串用两维来表示
①所有的操作方案有很多种,不会出现先插入一个字符,再把这个字符删除的操作,因为这样相当于无缘无故多了两次操作,一定不是最优解
不会出现在原字符串 "abc" 上加上 "b",再把 "b" 去掉
②操作的顺序不会影响答案,例如 字符串abc,先把 a 删除再在 b 和 c 之间加一个 x 与 先在 b 和 c 之间加一个 x 再把 a 删除的效果其实是一样的
总共的操作有很多种,但是我们在考虑的时候,只需要考虑其中的一小部分就可以了,这一小部分首先没有多余操作,不会出现加一个删一个,也不会出现把一个字母变两次
第二要注意的是在考虑的时候可以不考虑顺序,可以人为规定一个顺序,操作的顺序对答案的个数是没有影响的,可以固定一个顺序,固定操作顺序是从前往后进行的
在以上的分析基础上再去考虑这个 dp 问题
状态表示 f[ i ][ j ] 要从两个角度来考虑,第一个角度是要考虑的是 f[ i ][ j ] 表示哪个集合,第二个要考虑的是 f[ i ][ j ] 表示的是这个集合的哪个属性
我们只需要考虑操作是从前往后进行的方案,不需要考虑操作顺序会变化的情况
f[ i ][ j ] 表示所有操作方案里面操作步数的最小值
集合划分的方式求最小值
看 f[ i ][ j ] 这个集合可以分成哪些子集,然后分别求每个子集的最小值,这些最小值取一个 min 就是答案
划分其实就是找最后一个不同点,把 a[ 1,j ] 变成 b[ 1, j ] 最后一步一共有多少种操作方案,然后我们按照最后一步把它分成若干种
由于我们是考虑按顺序进行的所有方案,所以我们只需要考虑在最后一步进行的操作就可以了,不需要考虑在中间存在的情况
最后一个操作有多少种呢?
首先每一个字符串都有三种操作,两个字符串就有 6 种操作,所以我们一共有 6 种不同的操作
先看第一种情况,先看只操作第一个字符串的情况,只操作第一个字符串有三种操作方案,第一种操作方案,删除 a 的最后一个字符,删除后 a[ 1,i ] 变成 b[ 1,j ],也就是去掉 ai 之后,第一个字符串等于第二个字符串,也就意味着这个操作方案的前面若干步,已经可以把 a[ 1,i - 1 ] 变成 b[ 1,j ],去掉 ai 之前,a[ 1,i - 1 ] 已经等于 b[ 1,j ],所有把 a[ 1,i - 1 ] 已经变成 b[ 1,j ] 的最小步数加 1,也就是 f(i - 1,j) + 1
第二种情况在 a 的后面加上一个字符,加上之后使得第一个字符串和第二个字符串相等,加上的那个字符必然是 bj,加之前已经有 a(1,i) 等于 b(1,j - 1),在 ai 后面补上一个 bj,这样两个字符串就相等了,如果在 a 后面加上一个字符的话,总共的最小值应该是 f(i,j - 1) + 1
第三种情况,修改 ai,把 ai 变成 bj,修改之后 ai 和 bj 相等,修改之前,a 的前 i - 1 个字符和 b 的前 i - 1 个字符相等,如果 a[ i ] 和 b[ j ] 相等的话也可以不用改,如果不相等就需要加上 1
对于 a 的操作有三种情况,对于 b 的操作也有类似的三种情况,如果删除 b 的最后一个字符,在删除之前就已经有 a 的第 ai 个字符和 b 的第 j - 1 个字符相等
由上面的推导,一共有 6 种情况
但是仔细观察,我们可以发现只有 3 大类,状态数量为 n^2 级别,转移数量只有 3 个,整个的计算量就是 3n^2,也就是 n^2
class Solution {
public:
int minDistance(string a, string b) {
//先求两个字符串长度
int n = a.size(),m =b.size();
//为了方便 让两个字符串的下标都是从 1 开始 先给两个字符串的前面补上一个空格
a = ' ' + a,b = ' ' + b;
//定义状态数组
vector<vector<int>> f(n + 1,vector<int>(m + 1));
//初始化边界 当第一个字符串长度是 0 的时候,另一个字符串长度是多少就需要操作多少次
for(int i = 1;i <= n;i++ ) f[i][0] = i;
//a、b两个字符串的边界都需要初始化
for(int i = 1;i <= m;i++ ) f[0][i] = i;
//求所有状态的值
for(int i = 1;i <= n;i++ )
for(int j = 1;j <= m;j++ ) {
//两个状态
f[i][j] = min(f[i - 1][j],f[i][j - 1]) + 1;
//最后一个状态 用t来表示加的是1还是0 如果a[i]不等于b[j]表示要加的是1 如果相等表示不需要修改要加的是0
int t = a[i] != b[j];
f[i][j] = min(f[i][j],f[i - 1][j - 1] + t);
}
return f[n][m];
}
};
矩阵置零
算法标签:数组、哈希表、矩阵
给我们一个 n × m 的矩阵,里面有一些是 0、有一些是 1 或者其他数字,需要修改矩阵里面的数值,只要有一个元素是 0,就需要将这个元素的所在的行和列都初始化成 0
要求使用原地算法解决,不可以额外开数组解决这个问题
本来需要考虑 0 需要将某些位置赋值成 0
现在考虑矩阵某一个位置什么情况下会被赋成 0?
只要这个位置所在的行或列出现 0,这个位置就要赋成 0,如果这个位置所在的行和列都不是 0 就不是 0
如果我们简化一下这个问题,某一个位置上是 0 只将某一行变成 0,这个问题就非常简单啦!
只需要枚举每一行,看这一行有没有 0,如果这一行出现 0,就把这一行变成 0,如果没有出现 0 就不用管
如果只有列的话也很简单,只需要分别看一下每一列,每一列可以用一个 bool 变量,最后判断这一列有没有 0 就可以了
如果行和列交织就不太好做了,因为我们在修改行的时候,其实也会把列修改掉
行和列是相互影响的,在做行变化的时候不能做列变化,在做列变化的时候也不能做行变化
接下来考虑一下怎么去做这个问题?
我们一定会用到一个数组来记录每一行每一列的状态,这个题目只是让我们不要开额外的新数组,所以我们其实可以利用原数组,可以用原数组第一行的数来表示每一列里面有没有 0,用原数组第一列的数来表示这个矩阵每一行有没有 0,当然 (0,0) 位置上的元素是没有用的,这样就可以表示划线部分的 子矩阵 的所有状态了
框起来的矩阵里面的所有情况都记录清楚了,但是还有第一行和第一列的情况没有记录清楚,可以开两个变量记录就可以了,用 r0 记录第一行有没有 0,用 c0 记录第一列有没有 0
综上,我们就只需要用额外的两个变量,以及原矩阵的第一行和第一列,就可以把整个矩阵每一行每一列有没有 0 全部记录清楚了
最后,先看第一列有没有 0 如果第一列有 0 就把第一列全部变成 0,再看第二列有没有 0,如果第 2 列有 0 就把第二列全部变成 0,先把每一列扫描一遍,再看每一行,如果这一行有 0 就把这一行全部变成 0,再扫描一遍赋值就可以了
时间复杂度分析
整个矩阵会扫描很多遍,但是只回扫描常数遍,所以整个算法的时间复杂度是 4 × n^2,用到的空间只有两个变量,所以是 O( 1 )
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
//矩阵为空返回 false
if(matrix.empty() || matrix[0].empty()) return;
//n 表示行数 m 表示列数
int n = matrix.size(),m = matrix[0].size();
/* 记录 */
//求r0第一行和c0第一列 1表示没有0 0表示有0
int r0 = 1,c0 = 1;
//看第一行有没有0 如果有0的话 r0就是0,如果全是1的话,r0就是1
for(int i = 0;i < m;i++ ) if(!matrix[0][i]) r0 = 0;
//看第一列有没有0
for(int i = 0;i < n;i++ ) if(!matrix[i][0]) c0 = 0;
/* 记录 */
//看第一列一直到第 n - 1 列
for(int i = 1;i < m;i++ )
//对于第 i 列来说,j 从 0 开始,matrix[0][i] 表示第 i 列有没有 0
for(int j = 0;j < n;j++ )
//处理每一列
if(!matrix[j][i]) matrix[0][i] = 0;
//处理每一行 matrix[i][0] 表示第 i 行有没有 0
for(int i = 1;i < n;i++ )
for(int j = 0;j < m;j++ )
if(!matrix[i][j]) matrix[i][0] = 0;
/* 修改 */
//枚举除了第一列之外的每一列
for(int i = 1;i < m;i++ )
//如果matrix[0][i]=0 表示第 i 列需要全部赋值成 0
if(!matrix[0][i])
for(int j = 0;j < n;j++ )
matrix[j][i] = 0;
//看一下每一行是不是应该赋值成 0
for(int i = 1;i < n;i++ )
//如果matrix[i][0]=0 表示第 i 行需要全部赋值成 0
if(!matrix[i][0])
for(int j = 0;j < m;j++ )
matrix[i][j] = 0;
//看第一行是不是应该是 0
if(!r0) for(int i = 0;i < m;i++ ) matrix[0][i] = 0;
//看第一列是不是应该是 0
if(!c0) for(int i = 0;i < n;i++ ) matrix[i][0] = 0;
}
};
搜索二维矩阵
算法标签:数组、二分查找、矩阵
题目给出一个矩阵,要求在矩阵中找一个值,先考虑一维的情况,如果给我们一个数组,数组是有序的,让我们在数组中找一个值是不是存在,可以怎么做呢?
由于数组是有序的,所以可以用二分来解决,相当于数组里面的值是递增的,要找一个目标值,每次看中点这个值,如果中点这个值小于等于当前值,说明目标在右侧,把区间缩小到右边就可以了,如果中点的值大于当前值,说明答案在左侧,所以可以二分出来
题目虽然给的是一个矩阵,本质上也是一维的情况
这个矩阵满足每一行从左到右是递增的,同时保证每一行的第一个数大于上一行的最后一个数
求解的时候可以先把矩阵展开成一维,展开成一维的情况是递增的,然后二分就可以了
需要做一个映射,把 n × m 的矩阵的下标想象成从 0 - n × m - 1,二分的 mid 是在 0 - n × m - 1 这个坐标系里面,需要把这个坐标系的里面的某一个下标变成二维的下标,把展开之后的一维数组的下标变回原来二维数组的下标
任取一个下标 t,它在原二维矩阵中的下标是多少呢?
一行有 m 个数,下标从 0 开始,所以是 t 除以 m 行,模以 m 就可以得到列数,这样就得到在原数组里面的下标
其实就是一个一维二分,注意坐标的变换就可以了
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
//为空返回 false
if(matrix.empty() || matrix[0].empty()) return false;
//n 表示行数 m 表示列数
int n = matrix.size(),m = matrix[0].size();
//二分
int l = 0,r = m *n - 1;
while(l < r)
{
int mid = (l+ r) /2;
if(matrix[mid / m][mid % m] >= target) r = mid;
else l = mid + 1;
}
if(matrix[r/ m][r % m] == target) return true;
return false;
}
};
颜色分类
算法标签:数组、双指针、排序
给我们一个数组,数组里面只有 0、1、2 这 3 个数,需要将数组排序,有两个要求:只扫描一次,仅使用常数空间
本质是一个双指针算法,在扫描的时候,用 i 下标来扫描,i 从左往右扫描,j 也是从左往右扫描,再维护一个 k,k 从右往左扫描,扫描的时候保证从 0 ~ j - 1 这一段全部是 0,从 j ~ i - 1 这一段全部是 1,从 k + 1 ~ n - 1 最后一个字符,这一段全是 2,就可以保证它排好序了
i 每次往后移动一位,移动 i 的时候维护 j 和 k
当 i 超过 k 的时候就可以满足以上条件
接下来看一下如何维护这一点
一开始的时候,i 和 j 都是在起点的位置,k 在最后一个位置
一开始从 0 ~ j - 1 是不存在的,此时 j 为 0,这一段是空的,满足要求,从 j ~ i 是空的,满足要求,最后一段也是空的,满足要求
在扫描的过程中,每次根据 i 的位置来调整就可以了,ai 有三种情况,如果 ai 等于 0,j 指向的位置是 1,把 j 指向的位置的那个 1 变成 0,把 1 移到后面的位置,最后把 j 往后移动一位,此时从 0 ~ j - 1 都是 0,j 移到了下一个 1 的位置,从 j ~ i - 1 都是 1
如果 ai 等于 2,ai 应该放到 k 这个位置,然后 k- -,因为 k 的位置已经是 2,i 不需要变化,因为 i 不一定是 1,有可能是 0
如果 ai 等于 1,i+ + 即可
每次要么是 i ++,要么是 k - -,i 和 k 之间的距离每次都会减 1,最终循环 n 次之后就会结束,由于只扫描了一遍,所以整个算法的时间复杂度是 O( n ),扫描之后 i = k + 1
用三个变量 a、b、c 统计 0、1、2 有多少个,扫描一遍,然后从前往后输出 a 个 0,b 个 1,c 个 2
class Solution {
public:
void sortColors(vector<int>& nums) {
for(int i = 0,j = 0,k = nums.size() - 1;i <= k;) {
if (nums[i] == 0) swap(nums[i ++ ], nums[j++ ]);
else if(nums[i] == 2) swap(nums[i], nums[k -- ]);
else i ++;
}
}
};