首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >吊打面试官03:如何设计一个内存文件系统(多级目录)

吊打面试官03:如何设计一个内存文件系统(多级目录)

作者头像
早起的鸟儿有虫吃
修改2025-08-15 16:21:06
修改2025-08-15 16:21:06
15100
代码可运行
举报
文章被收录于专栏:吊打面试官吊打面试官
运行总次数:0
代码可运行

在reddit看到到一个帖子:如何把一个文件系统目录树存储到数据库,

感觉特别有意思,因此串联成一个系统设计面题,

希望帮助你从青铜晋升到王者

大纲如下

一、青铜:前端开发展示一个多级菜单(目录)

unsetunset1.1 题目描述unsetunset

树状结构或层次结构的数据在企业应用里非常常见, 例如公司的组织架构、文档库的目录结构、 仓库的库位组织以及物件的分类等等

如图:

请设计一个表结构,存储多级菜单(目录)

unsetunset1.2 采用什么数据结构表示?(数据库设计)unsetunset

什么是邻接列表

邻接列表模型的特点是简单。 表中的每条记录都包含对其父级的引用,形成父子关系。 这通常是通过使用存储父节点标识符的列来实现的。 没有父节点的根节点通常由父列中的 NULL 值表示。

优点:

  • 简单直观,容易设计
  • 插入、更新、删除单节点方便

问题(在 SQL 查询中暴露):

  • 难以一次性查询出整棵树
  • 查询所有祖先或所有子孙时需要递归,而标准 SQL 不支持递归(旧版本)
  • 需要数据库支持 WITH RECURSIVE 或者通过程序端构造

例如:

category_id

parent_id

category_name

1

NULL

Electronics

2

1

Computers

3

2

Laptops

4

2

Desktops

5

1

Televisions

6

3

Gaming Laptops

7

3

Business Laptop

代码语言:javascript
代码运行次数:0
运行
复制
//表结构
CREATETABLE categories (
  category_id INT PRIMARY KEY,
  parent_id   INTNULL,
  category_name TEXTNOTNULL,
FOREIGNKEY (parent_id) REFERENCES categories(category_id)
);
CREATEINDEX idx_categories_parent ON categories(parent_id);

//插入记录
INSERTINTO categories (category_id, category_name, parent_id) VALUES
(1, 'Electronics', NULL),
(2, 'Computers', 1),
(3, 'Laptops', 2),
(4, 'Desktops', 2),
(5, 'Smartphones', 1);

邻接列表优点

📌 为什么使用邻接表模型存储多级目录?
  1. 简单直观,开发与维护友好
  2. 插入、更新、删除操作高效
  3. 查询常见访问路径高效

个人感受:✔️ 为什么一般用 parent_id 而非 child_id

  • 多级目录或分类结构中,每个节点通常只有一个父节点,但可以有多个子节点
  • parent_id 的邻接表模型符合关系型数据库范式。若用 child_id,需定义多个子字段,容易违反第一范式(1NF),难以扩展,字段冗余严重

数据库设计三大范式

(1)简单归纳:   第一范式(1NF):字段不可分;   第二范式(2NF):有主键,非主键字段依赖主键;   第三范式(3NF):非主键字段不能相互依赖。 (2)解释:   1NF:原子性。 字段不可再分,否则就不是关系数据库;;   2NF:唯一性 。一个表只说明一个事物;   3NF:每列都与主键有直接关系,不存在传递依赖。

银行的面试官问了个简单的问题,满足第二范式,但是不满足第三范式的例子

第三范式定义:所有的非主属性(非主键)都直接由其它表的主属性(主键)推导生成,而不需要传递依赖

重点:1)满足第二范式。2)不能传递依赖,非主属性不能部分或者传递依赖于码。

表: 学号, 姓名, 年龄, 学院名称, 学院电话

因为存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) 。

正确做法:

学生:(学号, 姓名, 年龄, 所在学院);

学院:(学院, 电话)。

邻接列表缺点

1. 深度遍历效率较差

要查询某个节点的所有后代(deep descendants)或完整路径,通常需要使用递归查询(如 CTE)或多次自连接。对于树深度较大时,查询非常复杂且性能低下。

“Finding all descendants of a node requires recursive queries or multiple joins”

2. 路径查询不便

想要查询某个节点从根节点到它的完整路径,需要多层连接并提前知道树深度,否则无法构造固定数量的自关联 JOIN。

“Before being able to see the full path … we have to know the level at which it resides.”

1.3 还有其他存储方式吗?

请参考:

  • SQL反模式 第三章节 单 纯 的 树
  • SQL Antipatterns. Naive Trees

二、白银:后端开发 请设计表示多级目录的类和相关接口

[!NOTE] 数据库的表要读取内存中去吧 如何在内存中表示

2.1 方式1 直接用指针表示

代码语言:javascript
代码运行次数:0
运行
复制
#include <vector>
#include <memory>

class CategoryNode {
public:
    int category_id;            // 节点ID(主键)
    std::string category_name; // 分类名称
    CategoryNode* parent;      // 指向父节点的指针(弱引用,避免循环强引用)
    std::vector<std::unique_ptr<CategoryNode>> children; // 子节点列表

    // 构造函数
    CategoryNode(int id, conststd::string& name, CategoryNode* parent_ptr = nullptr)
        : category_id(id), category_name(name), parent(parent_ptr) {}
};

class CategoryTree {
private:
    std::unique_ptr<CategoryNode> root; // 根节点
    std::unordered_map<int, CategoryNode*> node_map; // ID到节点的映射(加速查找)

public:
    // 添加节点(自动关联父子关系)
    void add_node(int id, const std::string& name, int parent_id) {
        CategoryNode* parent_ptr = (parent_id == -1) ? nullptr : node_map.at(parent_id);
        auto new_node = std::make_unique<CategoryNode>(id, name, parent_ptr);
        
        if (parent_ptr) {
            parent_ptr->children.push_back(std::move(new_node));
        } else { // 根节点
            root = std::move(new_node);
        }
        node_map[id] = node_map.empty() ? root.get() : parent_ptr->children.back().get();
    }
};

// 获取从根节点到当前节点的路径
std::vector<CategoryNode*> get_path(int id) {
    std::vector<CategoryNode*> path;
    CategoryNode* current = get_node(id);
    while (current) {
        path.push_back(current);
        current = current->parent;
    }
    std::reverse(path.begin(), path.end()); // 从根到当前
    return path;
}

// 查找所有后代节点(广度优先)
std::vector<CategoryNode*> get_descendants(int id) {
    std::vector<CategoryNode*> result;
    std::queue<CategoryNode*> queue;
    queue.push(get_node(id));
    while (!queue.empty()) {
        auto current = queue.front();
        queue.pop();
        for (constauto& child : current->children) {
            result.push_back(child.get());
            queue.push(child.get());
        }
    }
    return result;
}

2.2 从demo到产品化(CephFS)

🧩 CephFS 的目录树结构 🔹 Metadata Server(MDS)

  • MDS 负责管理文件和目录元数据
  • 每个目录/文件是一个 inode,使用类似文件系统的树形组织
  • 目录之间通过 parentchild 关系形成层级
代码语言:javascript
代码运行次数:0
运行
复制
https://github.com/ceph/ceph/blob/main/src/mds/CInode.h
//MDS内部数据结构
class CInode {
inodeno_t ino;
snapid_t snapid;
std::map<std::string, CDentry*> dentries; // children
  CInode* parent;         // pointer to parent directory
};
//OpenFileTable 结构加速恢复:记录打开文件的路径信息,避免全量加载
CInode (根目录) 
│
└── CDir (根目录内容)
    ├── CDentry ("a") ── CInode (目录"a")
    │               │
    │               └── CDir (目录"a"内容)
    │                   └── CDentry ("b.txt") ── CInode (文件"b.txt")
    │
    └── CDentry ("c") ── CInode (文件"c")

Ceph 文件系统由 MDS(Metadata Server)管理目录树结构,其内存模型核心由三种结构组成:

  • CInode:表示每个文件或目录的
  • CDentry:关联 inode 与其名称(目录项),支持同一个 inode 多个路径(硬链接);
  • CDir:仅用于目录 inode,将该目录下所有 CDentry 组织起来。当目录被碎片化时,一个 CInode 可对应多个 CDir
📌 结构特点
  • 使用 C++ 类结构:父指针 + 子集合 【基本功能】
  • 更新目录、插入文件、递归遍历都在树结构上进行

数据结构

角色

关键作用

CInode

元数据实体

存储文件/目录属性,指向数据块位置。

CDentry

名称到实体的映射

链接文件名与 CInode,构建路径层级。

CDir

目录内容容器

管理子项,支持分片以优化大规模目录性能。

三、如何体现左右子树(不体现)

没有左右子树的字段,因为目录结构是多叉树

没有左右子树的字段,因为目录结构是多叉树

没有左右子树的字段,因为目录结构是多叉树

四、Leetcode 588题

设计一个内存文件系统,模拟以下功能:

实现文件系统类:

  • FileSystem() 初始化系统对象
  • List<String> ls(String path)
  • void mkdir(String path) 根据给定的路径创建一个新目录。给定的目录路径不存在。如果路径中的中间目录不存在,您也应该创建它们。
  • void addContentToFile(String filePath, String content)
  • String readContentFromFile(String filePath) 返回 filePath下的文件内容。
代码语言:javascript
代码运行次数:0
运行
复制
FileSystem fileSystem = new FileSystem();
fileSystem.ls("/");                         // 返回 []
fileSystem.mkdir("/a/b/c");
fileSystem.addContentToFile("/a/b/c/d", "hello");
fileSystem.ls("/");                         // 返回 ["a"]
fileSystem.readContentFromFile("/a/b/c/d"); // 返回 "hello"
代码语言:javascript
代码运行次数:0
运行
复制
#include <vector>
#include <string>
#include <unordered_map>
#include <set>
#include <sstream>
#include <algorithm>
usingnamespacestd;

class FileSystem {
private:
    // 文件系统节点结构
    struct FileNode {
        bool isFile = false;          // 是否为文件
        string content;                // 文件内容
        unordered_map<string, FileNode*> children; // 子节点映射
    };

    FileNode* root; // 根节点指针

    // 分割路径为组成部分
    vector<string> splitPath(const string& path) {
        vector<string> parts;
        if (path.empty() || path == "/") return parts;
        
        istringstream iss(path);
        string part;
        while (getline(iss, part, '/')) {
            if (!part.empty()) parts.push_back(part);
        }
        //
        return parts;
    }

    // 根据路径查找节点
    FileNode* traverse(const string& path) {
        if (path == "/") return root;
        
        vector<string> parts = splitPath(path);
        FileNode* current = root;
        for (conststring& part : parts) {
            if (!current->children.count(part)) returnnullptr;
            current = current->children[part];
        }
        return current;
    }

public:
    FileSystem() {
        root = new FileNode(); // 初始化根节点
    }

    vector<string> ls(const string& path) {
        FileNode* node = traverse(path);
        if (!node) return {};
        
        // 文件路径:返回文件名
        if (node->isFile) {
            int idx = path.find_last_of('/');
            return {path.substr(idx + 1)};
        }
        
        // 目录路径:返回排序后的子项名称
        vector<string> result;
        for (constauto& child : node->children) {
            result.push_back(child.first);
        }
        sort(result.begin(), result.end());
        return result;
    }

    void mkdir(string path) {
        vector<string> parts = splitPath(path);
        FileNode* current = root;
        
        for (conststring& part : parts) {
            if (!current->children.count(part)) {
                current->children[part] = new FileNode();
            }
            current = current->children[part];
        }
    }

    void addContentToFile(string filePath, string content) {
        vector<string> parts = splitPath(filePath);
        FileNode* current = root;
        
        // 确保目录路径存在
        for (int i = 0; i < parts.size() - 1; ++i) {
            if (!current->children.count(parts[i])) {
                current->children[parts[i]] = new FileNode();
            }
            current = current->children[parts[i]];
        }
        
        // 处理文件名
        string fileName = parts.back();
        if (!current->children.count(fileName)) {
            current->children[fileName] = new FileNode();
            current->children[fileName]->isFile = true;
        }
        current->children[fileName]->content += content;
    }

    string readContentFromFile(string filePath) {
        FileNode* node = traverse(filePath);
        if (node && node->isFile) {
            return node->content;
        }
        return"";
    }
};
  • 标准库中的 std::getline() 是一个用于从输入流中读取一行数据的函数。它的函数签名如下:
代码语言:javascript
代码运行次数:0
运行
复制
std::istream& getline(std::istream& is, std::string& str, char delim);

其中,is 是输入流对象,str 是存储读取数据的字符串,delim 是指定的行分隔符(默认为换行符 \n)。

unsetunset参考文章unsetunset

  • 如何在关系型数据库中存储树形结构
  • What is Third Normal Form (3NF)?

unsetunset参考数据unsetunset

  • SQL反模式 第三章 单 纯 的 树P29
  • SQL Antipatterns. Naive Trees P34
SQL反模式
SQL反模式
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、青铜:前端开发展示一个多级菜单(目录)
    • unsetunset1.1 题目描述unsetunset
    • unsetunset1.2 采用什么数据结构表示?(数据库设计)unsetunset
      • 什么是邻接列表
      • 邻接列表优点
      • 数据库设计三大范式
      • 邻接列表缺点
      • 1.3 还有其他存储方式吗?
  • 二、白银:后端开发 请设计表示多级目录的类和相关接口
    • 2.1 方式1 直接用指针表示
    • 2.2 从demo到产品化(CephFS)
  • 三、如何体现左右子树(不体现)
  • 四、Leetcode 588题
    • unsetunset参考文章unsetunset
    • unsetunset参考数据unsetunset
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档