猿最近在做关于压缩文件预览的需求,涉及到对于文件系统的目录结构进行简单的模拟。在实现过程中,尝试了三种实现方式,文章将对三种实现方式进行详细说明。
使用递归构建文件二叉树结构
- 最初的思路是:每次从输入流中读入一个结点,判断根据结点的路径名判断,如果是其子节点,则放入结点的右子树。如果是其同级结点,则放入其左子树中。(与上学时学的数据结构中多叉树转二叉树的区别在于:书上实现要求左孩子,右兄弟,我的实现是左兄弟,右孩子)
- 类模型代码
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完代码后,提出改写为迭代的方式进行创建。
使用迭代创建文件树形结构
在树形结构的基础上,我们对构建方法改造为使用迭代方式进行实现。
迭代构建代码
public void buildFileSys(Map
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),相对来说还是一种不错的选择。
同时在遍历的过程中,尝试去将一个结点所有子节点添加完毕后,就移除这个结点,可以减少时间复杂度,但是对于无序输入的结点,这样做,可能会出现,某个子节点已经被删除了,但它还未添加到其父结点中。╮(╯▽╰)╭
如果大家对于构建类似的结构还有其他更好的建议,请大家不吝赐教(∩_∩)
最后,附上银魂的一句吐槽:压力是导致秃顶的原因,所以请注意不要压力太大,但这样一来反而容易堆积压力,所以归根到底我们无能为力」