LC71 如何从线性思维脱离出来解决简化UNIX路径问题|刷题打卡

188 阅读2分钟

本系列使用IDEA+LEETCODE EDITOR插件,题目描述统一英文题目链接
一、题目描述:

//Given a string path, which is an absolute path (starting with a slash '/') to 
//a file or directory in a Unix-style file system, convert it to the simplified ca
//nonical path. 
//
// In a Unix-style file system, a period '.' refers to the current directory, a 
//double period '..' refers to the directory up a level, and any multiple consecut
//ive slashes (i.e. '//') are treated as a single slash '/'. For this problem, any
// other format of periods such as '...' are treated as file/directory names. 
//
// The canonical path should have the following format: 
//
// 
// The path starts with a single slash '/'. 
// Any two directories are separated by a single slash '/'. 
// The path does not end with a trailing '/'. 
// The path only contains the directories on the path from the root directory to
// the target file or directory (i.e., no period '.' or double period '..') 
// 
//
// Return the simplified canonical path. 
//
// 
// Example 1: 
//
// 
//Input: path = "/home/"
//Output: "/home"
//Explanation: Note that there is no trailing slash after the last directory nam
//e.
// 
//
// Example 2: 
//
// 
//Input: path = "/../"
//Output: "/"
//Explanation: Going one level up from the root directory is a no-op, as the roo
//t level is the highest level you can go.
// 
//
// Example 3: 
//
// 
//Input: path = "/home//foo/"
//Output: "/home/foo"
//Explanation: In the canonical path, multiple consecutive slashes are replaced 
//by a single one.
// 
//
// Example 4: 
//
// 
//Input: path = "/a/./b/../../c/"
//Output: "/c"
// 
//
// 
// Constraints: 
//
// 
// 1 <= path.length <= 3000 
// path consists of English letters, digits, period '.', slash '/' or '_'. 
// path is a valid absolute Unix path. 

二、思路分析:
首先做这道题先要了解题意中的路径知识,UNIX系统中路径可以用一行命令遍历一个文件的上下级和同级。上级..,同级.,下级/,多个//认为是一个。要求输出最后的结果,注意最后的结果不能为空,至少为/。 看起来还可以,但是别忘了这是道M的题,虽然也是比较容易的,但是考的是你的思维模式。你是傻傻的暴力解,线性思维,还是优雅的打破思维定式,都会被面试官看在眼里,所以如何从线性思维脱离出来,这个才是这期的重点内容

三、AC 代码:
首先展示下未优化版,可以AC,但是代码行数多,判断条件多,通常当你代码写到这种时候,你自己就会发现容易出BUG,不好DEBUG,那么这多数情况下意味着,你的思路出了问题,过于线性,看不到近路/捷径。切记行数和逻辑判断的数目可以很大程度上反应代码的优劣 线性思维的思路很简单,就是一个个字符读取,和已读取的一起判断,遇到/就判断是要放弃(同级),出栈(去到上级,出栈前务必判空)还是入栈。

class Solution {
    public String simplifyPath(String path) {
        // 题目描述中已经说了是有效路径,省去了异常判断的麻烦
        // 选择数据结构,这种题类似计算器,有效的括号等,使用栈即可
        Stack<String> stack = new Stack<>();
        // 每次要入栈的内容
        String insert = "";
        for (int i = 0; i < path.length(); i++) {
            // 只需要判断当前字符串是哪种可能,
            if (path.charAt(i) == '/') {
                // 连续的/无效,只保存一个
                if ("/".equals(insert)) {
                    continue;
                }
                //如果之前没有内容,加入
                if ("".equals(insert)) {
                    insert += "/";
                    continue;
                }
                //如果之前存储了2个点了
                if ("/..".equals(insert)) {
                    // pop前记得检查
                    if(!stack.isEmpty()){
                        stack.pop();
                    }
                } else if ("/.".equals(insert)) {

                } else {
                    // 判断之前要入栈的值是否有效,有效则可以入栈了
                    stack.add(insert);
                    // 清空之前的要入栈的值,切记此处要加上此时的/
                }
                insert = "/";
            } else if (path.charAt(i) == '.') {
                insert += ".";
            } else {
                // 正常拼接即可,无需处理
                insert += path.charAt(i);
            }

        }
        //处理最后一个
        if (!"".equals(insert) && !"/".equals(insert)) {
            if("/.".equals(insert)){

            }else  if("/..".equals(insert)){
                // pop前记得检查
                if(!stack.isEmpty()){
                    stack.pop();
                }
            }else{
                stack.add(insert);
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()) {
            sb.insert(0, stack.pop());
        }
        // 特殊情况处理,如果栈里没有值返回根目录/而不是空
        return "".equals(sb.toString()) ? "/" : sb.toString();
    }
}

此时执行结果:

		解答成功:
			执行耗时:12 ms,击败了11.08% 的Java用户
			内存消耗:38.5 MB,击败了59.09% 的Java用户

并不令人满意,现在思考如何优化,打破上文一个个字符串判断的线性思维。 思考一下,我们真的需要一个个字符去读取么?我们可以看到规律,要处理的只是2个//之间的内容,比如/../,/A/B/。那么何必一个个判断直接split岂不是就可以取到了? 取到之后的逻辑就跟上面一样判断3种场景即可,AC代码

    public String simplifyPath(String path) {
        String[] pathItems = path.split("/");
        Stack<String> stack = new Stack<>();
        for (String pathItem : pathItems) {
            if ("".equals(pathItem) || ".".equals(pathItem)) {
                continue;
            }
            if ("..".equals(pathItem)) {
                // pop前记得检查
                if (!stack.isEmpty()) {
                    stack.pop();
                }
                continue;
            }
            stack.add("/" + pathItem);
        }
        StringBuilder sb = new StringBuilder();
        while (!stack.isEmpty()) {
            sb.insert(0, stack.pop());
        }
        // 特殊情况处理,如果栈里没有值返回根目录/而不是空
        return "".equals(sb.toString()) ? "/" : sb.toString();
    }
		解答成功:
			执行耗时:6 ms,击败了50.25% 的Java用户
			内存消耗:38.4 MB,击败了77.40% 的Java用户

四、总结:

  1. 代码行数和逻辑判断的次数是衡量一个代码优劣的重要标准。
  2. 打破线性思维看本质。
  3. 优雅的代码不应该大量逻辑嵌套,逻辑嵌套处可改写成continue/return的方式优化代码。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情