LeetCode+ 71 - 75

简化路径

算法标签:栈、字符串

给我们一个路径,要求把文件路径化简,给定的路径一定是合法的 Linux 路径,一个合法的 Linux 路径一般从 / 开始,/ 表示根目录,有很多的子目录 home、y,"." 表示在当前目录(一个 "." 没有任何变化), ".." 表示返回上一级目录

如果一个路径是 / home / a / .. / b,根目录 → home → a → b,化简其实就是把所有的 "." 和 ".." 去掉,当前案例去掉就是      / home / b

注意特殊情况:斜杠 " / " 可能会有连续多个,需要把连续多个斜杆换成一个斜杆,如果在根目录返回上一级目录的话,由于根目录已经是最靠上的一个目录了,不能再返回,如果从根目录往上再返回还是根目录,保证输入的目录一定是合法的目录,所以不需要考虑不合法的情况

注意输出的时候,如果不是根目录,最后的斜杆不用加,如果是根目录就返回一个斜杆

参考

LeetCode 71 简化路径 AcWing

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 ++;
        }
    }
};
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qiuqiuyaq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值