目錄樹
看到這個問題,我們是一頭霧水啊,這講了個啥?別急,讓我們用測試樣例模擬一遍。
樣例模擬
首先我們先考慮存儲數據的方式,根據觀察和我們對文件夾的理解,對於一個文件夾而言,與其他文件或文件夾只會有 2 種關系——和我在同一目錄、在我的目錄,也就是只有同級和下級兩種關系。因此我們就很自然地想到孩子兄弟表示法,因為在孩子兄弟對於一個結點也只會有 2 種關系——孩子和兄弟,那我們就用孩子表示在下級目錄,兄弟表示在同級目錄。
首先我們擁有一個根目錄 root,讀取第一行數據,表示根目錄有個名為 b 的文件,是 root 的孩子,因此根據孩子兄弟表示法,b 應該是 root 的左分支結點。
讀取第二行數據,在根目錄中有個 c 目錄。因此 c 目錄是 root 的孩子,並且與 b 文件同級,也就是互為兄弟關系,進行結點的加入。
讀取第三行數據,在根目錄中有個 ab 目錄,並在這個目錄下有個 cd 文件。因此 ab 目錄是 root 的孩子,並且與 c 目錄同級,也就是互為兄弟關系,同時 ab 目錄有個左分支 cd,進行結點的加入。
讀取第四行數據,在根目錄中有個 a 目錄,並在這個目錄下有個 bc 文件。因此 a 目錄是 root 的孩子,並且與 ab 目錄同級,也就是互為兄弟關系,同時 a 目錄有個左分支 bc,進行結點的加入。
讀取第五行數據,在 ab 目錄下有個 d 文件。由於 ab 已經存在,因此 d 與 cd 互為兄弟關系,修改 ab 的孩子為 d,d 的兄弟為 cd,進行結點的加入。
重復上述操作完成建樹。
把這棵樹整理成二叉樹的形式。
我們按照前序遍歷的順序讀一下這課樹,發現在忽略縮進的情況下,讀取的順序和樣例的輸出數據是一模一樣啊,也就是說,只要把這棵樹建出來,這個情景我們就解決了。在應用的時候,我們應當積極地考慮使用孩子兄弟表示法建樹,因為這種方法建立的是二叉樹,我們就可以用二叉樹的基操來操作這棵樹。
結點結構體定義
typedef struct CSNode
{
string data; //數據域
struct CSNode* firstchild; //指向對應長子結點的指針域
struct CSNode* rightsib; //指向對應右兄弟結點的指針域
int flag_file; //判斷是文件還是目錄的 flag
}CSNode, * CSTree;
- 此處為什么用 int 類型來當 flag 而不是 bool 類型呢?這是因為改為 int 類型,相當於直接賦予優先級,判斷插入位置時直接在同優先級的結點進行查找即可。
建樹算法
字符串切片算法
切片算法可以用字符數組實現,也可以用 string 類實現,此處我用字符數組描述。由於我們讀入的數據是字符串,因此我們需要先把各個目錄分離開來。需要注意的是,雖然文件是特殊數據,但是文件只會出現在字符串串尾,因此只需要一個分支結構單獨處理即可。
偽代碼
需要強調的是,string 類用來判斷字典序和復制操作都可以直接用運算符實現,更為方便。
代碼實現
調試結果
偽代碼
建樹算法只需要再字符串切片算法進行改動即可,把輸出語句改為調用結點插入函數即可實現。
代碼實現
void createTree(CSTree pre, string str)
{
int idx = 0;
getline(cin, str);
for (int i = 0; i < str.size(); i++)
{
if (str[i] == '\\') //注意用反義字符,不然會報錯
{ //只要不在串尾,只會是目錄
pre = insertNode(pre, str.substr(idx, i - idx), 1);
idx = i + 1; //移動字符串到下一個目錄,即 '\' 之后
}
}
if (idx < str.size()) //文件只出現在字符串尾
{
pre = insertNode(pre, str.substr(idx, str.size() - idx), 0);
}
}
結點插入算法
該算法的目的是向一個樹結構中,在正確的位置插入新結點,是解決這個問題的核心。這里要強調一下返回值的重要性,如果不設置返回值,而是把目錄引用進去,那么回到調用函數的時候需要自行將指針移動到當前目錄,更為繁瑣,好的解法是將插入后的接點作為所在的目錄,以此為返回值返回函數調用的位置。容易出錯的地方是若插入位置是目錄的長子結點的話,直接通過前驅指針的后繼來操作會插在錯誤位置導致斷鏈,因此需要設置當前位置指針和前驅指針,就可以規避這個問題。
偽代碼
代碼實現
CSTree insertNode(CSTree t, string str, int flag) //核心在此
{
CSTree a_node = new CSNode;
CSTree pre = t, ptr;
a_node->data = str; //初始化新結點
a_node->firstchild = a_node->rightsib = NULL;
a_node->flag_file = flag;
if (t->firstchild == NULL) //所在目錄沒孩子,直接插入結點
{
t->firstchild = a_node;
return t->firstchild;
}
ptr = t->firstchild; //由於根結點本身插入時,是插在長子位,因此另外設置 pre 當前驅結點,ptr 當 pre 的后繼,比較好寫
while (ptr != NULL && ((ptr->flag_file > a_node->flag_file) || (ptr->flag_file == a_node->flag_file && str > ptr->data)))
{
pre = ptr;
ptr = ptr->rightsib;
}
//要先判空,不然有段錯誤
if (ptr == NULL) //無處可插入,插在鏈尾
{
a_node->rightsib = pre->rightsib;
pre->rightsib = a_node;
return a_node; //接下來以 a_node 為根目錄操作
}
else if (ptr->data == a_node->data && ptr->flag_file == a_node->flag_file) //目錄或文件已存在(第三版就是因為這個出問題)
{
delete a_node; //把申請的新結點打掉
return ptr; //接下來在已有的 ptr 目錄下操作
}
else //找到了應該插入的位置
{
if (pre->data == t->data) //插在根目錄的長子位
{
a_node->rightsib = pre->firstchild;
pre->firstchild = a_node;
}
else //正常插入
{
a_node->rightsib = pre->rightsib;
pre->rightsib = a_node;
}
return a_node; //接下來以 a_node 為根目錄操作
}
}
打印目錄樹
我們已經知道輸出結點的順序就是先序遍歷二叉樹的順序,因此我們只需要添加個縮進的機制就能實現。
void PreOrderTraverse(CSTree T, int space) //從博客上拿來的遍歷函數
{ //因為要輸出空格,稍微改裝下
if (T == NULL)
return;
for (int i = 0; i < space; i++)
{
cout << " ";
}
cout << T->data << endl; //前序遍歷
PreOrderTraverse(T->firstchild,space + 2); //下一層多兩個空格
//cout << T->data << " " ; //中序遍歷
PreOrderTraverse(T->rightsib,space); //兄弟結點不需要多空格
//cout << T->data << " " ; //后序遍歷
}
主函數
測試樣例
輸入樣例
15
b
c\
ab\cd
a\bc
ab\d
a\d\a
a\d\z\
b\
c
ab\cd\e
a\bc\f
ab\d\g
a\d\a\h
a\d\z
輸出樣例
root
a
bc
f
d
a
h
z
a
z
bc
ab
cd
e
d
g
cd
d
b
c
b
c