工作总结|文件系统模型的三种实现

最近在做关于压缩文件预览的需求,涉及到对于文件系统的目录结构进行简单的模拟。在实现过程中,尝试了三种实现方式,下文对三种实现方式做详细说明。

传统目录结构

使用递归构建文件二叉树结构

  • 最初的思路是:每次从输入流中读入一个结点,判断根据结点的路径名判断,如果是其子节点,则放入结点的右子树。如果是其同级结点,则放入其左子树中。(与上学时学的数据结构中多叉树转二叉树的区别在于:书上实现要求左孩子,右兄弟,我的实现是左兄弟,右孩子)
  • 类模型代码
public class CompressedFileInfo {
    public String filePath;
    public String parentPath;
    private String name;
    public boolean isDirectory;
    private int levels;
    private CompressedFileInfo leftFriendInfo;
    private CompressedFileInfo rightChildInfo;
}
  • 递归创建二叉树结构代码
  public void buildFileInfo(CompressedFileInfo root) {
          if (root == null) {
              return;
          }
          if (root.levels == 0) {
              if (root.rightChildInfo == null) {
                  root.rightChildInfo = this;
                  return;
              } else {
                  root = root.rightChildInfo;
              }
          }
          if (root.levels < levels) {
              if (filePath.startsWith(root.filePath)) {
                  if (root.rightChildInfo == null) {
                      root.rightChildInfo = this;
                  } else {
                      buildFileInfo(root.rightChildInfo);
                  }
              } else {
                  buildFileInfo(root.leftFriendInfo);
              }
          } else if (root.levels == levels) {
              if (parentPath == null || filePath.startsWith(root.filePath)) {
                  if (root.leftFriendInfo == null) {
                      root.leftFriendInfo = this;
                  } else {
                      buildFileInfo(root.leftFriendInfo);
                  }
              } else {
                  Log.d("raytest", "Build Error");
              }
          } else {
              Log.e("raytest", "Build Error");
          }
  }
  • 采用二叉树构建的文件结构图如下所示:
二叉树目录结构
  • 代码总结与分析
    在构建中,根据节点初始化的levels来对结点的层级数进行记录,在递归过程中,注意要结合父一级路径进行遍历。否则,可能会出现结点组织错误的问题。
    二叉树作为一种存储方式是一种很优雅的选择。但是对于我们项目中的需求来说,此种构建方式严重依赖于外部结点输入,必须保证从根节点,深度优先遍历,提供输入结点信息。否则并不能保证结点组织的准确性。同时二叉树递归遍历,在实际项目使用中,代码阅读性差,使用效率不高。所以,考虑到这些问题,我们决定采用多叉树型结构来实现文件结构

使用递归构建文件树形结构

  • 采用树形结构存储,为了摆脱对外部结点输入的依赖,引入一个缓存Map来缓存所有输入结点,在结点输入完毕后,通过递归,构建树形结构,在摆脱外部输入的基础上,提高了程序的可读性。

  • 类模型代码

public class DecompressFileItem extends FileItem {
    private String mParentPath;
    private List<FileItem> mChildList;
}

此处的FileItem可以理解为File对象属性的一些映射,包含一些获取文件名,获取文件路径之类的常用属性和方法。

  • 递归创建
public void buildFileSys(Map<String, FileItem> nodeMap, DecompressFileItem paramInfo, boolean isRoot) {
        Map<String, FileItem> tempMap = new HashMap<>();
        tempMap.putAll(nodeMap);
        if (paramInfo == null || TextUtils.isEmpty(paramInfo.mData)) {
            return;
        }
        if (nodeMap == null || nodeMap.size() < 1) {
            return;
        }
        String paramPathKey = paramInfo.mData;
        if (isRoot) {
            paramPathKey = paramPathKey.substring(0, paramPathKey.lastIndexOf(File.separator));
            tempMap.remove(paramPathKey);
        } else {
            tempMap.remove(paramInfo.mData);
        }
        for (Map.Entry<String, FileItem> entry : tempMap.entrySet()) {
            DecompressFileItem item = (DecompressFileItem) entry.getValue();
            if (paramPathKey.equals(item.getParentPath())) {
                if (paramInfo.mChildList == null) {
                    paramInfo.mChildList = new ArrayList<>();
                }
                paramInfo.mChildList.add(item);
                item.buildFileSys(tempMap, item, false);
            }
        }
    }
  • 代码总结与分析
    在构建中,每次都将所有结点放置于一个暂存map中,将当前结点移除map中,并遍历剩余结点,将其子节点加入list中,递归遍历子节点,自此完成构建。
    此种实现,较为简单清晰,但是在实现过程中,由于实现路径写入错误,极有可能导致堆栈溢出。而且涉及文件结构处理,不可避免会涉及复杂结构,内存压力和堆栈溢出的风险这两个问题都需要考虑。所以,在老大Review完代码后,提出改写为迭代的方式进行创建。

使用迭代创建文件树形结构

  • 在树形结构的基础上,我们对构建方法改造为使用迭代方式进行实现。

  • 迭代构建代码

/**
 * called by root to create the file struct
 * @param nodeMap
 */
public void buildFileSys(Map<String, FileItem> nodeMap) {
    if (nodeMap == null || nodeMap.size() < 1) {
        return;
    }
    for (Map.Entry<String, FileItem> entrySource : nodeMap.entrySet()) {
        String pathKey = entrySource.getKey();
        if (TextUtils.isEmpty(pathKey)) {
            continue;
        }
        DecompressFileItem pathItem = (DecompressFileItem) entrySource.getValue();
        if (pathItem == null) {
            continue;
        }
        for (Map.Entry<String, FileItem> entryCompare : nodeMap.entrySet()) {
            DecompressFileItem compareItem = (DecompressFileItem) entryCompare.getValue();
            if (compareItem == null) {
                continue;
            }
            boolean isRootNode = compareItem.mData.equals(mData) && compareItem.mFileName.equals(mFileName);
            if (pathKey.equals(compareItem.getParentPath()) && !isRootNode) {
                if (pathItem.mChildList == null) {
                    pathItem.mChildList = new ArrayList<FileItem>();
                }
                pathItem.mChildList.add(compareItem);
                // build file count
                if (pathItem.mIsDir) {
                    pathItem.mFileCount++;
                }
            }
        }
    }
}
  • 代码总结与分析
    此种构建方式相比于方式嵌套了两层循环,增加了代码的阅读难度,但是相对来说,还是比较简单,同时时间复杂度也只有O(n * n),相对来说还是一种不错的选择。
    同时在遍历的过程中,尝试去将一个结点所有子节点添加完毕后,就移除这个结点,可以减少时间复杂度,但是对于无序输入的结点,这样做,可能会出现,某个子节点已经被删除了,但它还未添加到其父结点中。╮(╯▽╰)╭
    如果大家对于构建类似的结构还有其他更好的建议,请大家不吝赐教(∩_∩)

最后,附上银魂的一句吐槽:压力是导致秃顶的原因,所以请注意不要压力太大,但这样一来反而容易堆积压力,所以归根到底我们无能为力」

推荐阅读更多精彩内容