前言
比較常規的二叉樹的實現方式是[結構體/物件+指標],看紫書的時候,裡面給出了幾種樹的實現方式,基本上是比較適合在比賽中使用的。
1.結構體+指標
struct Node {
bool p;
int v;
Node * left;
Node * right;
Node(): p(0), left(NULL), right(NULL) {}
};
Node* new_node()
{
return new Node();
}
void remove_tree(Node * u)
{
if(u == NULL) return;
remove_tree(u->left);
remove_tree(u->right);
delete u;
}
最常規的實現方式,結構體中p用來標識是否存在/被賦值過。這一方式為動態分配記憶體,刪除樹或某一子樹時採用遞迴delete釋放記憶體。
2.陣列+ID
const int max_n = 1000, rootID = 1;
int val[max_n], left[max_n], right[max_n], nodeID;
bool present[max_n];
void new_tree()
{
left[rootID] = right[rootID] = 0; present[rootID] = 0;
nodeID = rootID;
}
int new_node()
{
int u = ++nodeID;
left[u] = right[u] = 0; present[u] = 0;
return u;
}
由於動態分配記憶體是非常耗時的操作,因此我們想用靜態方式來替代。每一個節點擁有獨自的nodeID,根節點rootID為常量1,left[i],right[i]陣列是第i節點的子節點ID,相當於指標。
分配新節點只需要初始化++nodeID節點的各項值就好,省去了動態分配記憶體的時間。而刪除節點,若刪除節點i的左孩子,只需要left[i] = 0就行,免去了釋放記憶體的麻煩。分配新樹只需要將nodeID重置就行。
然而這種方法存在記憶體碎片無法利用的問題,由於nodeID是一直遞增的,若對樹的刪除操作較多,會導致陣列中很多部分不能再利用,或者說樹節點陣列規模難以確定。
3.結構體陣列+ID
struct Node {
bool p;
int v;
Node * left;
Node * right;
Node(): p(0), left(NULL), right(NULL) {}
};
const int max_n = 1000, rootID = 1;
int nodeID;
Node node[max_n];
void new_tree(Node *u)
{
Node* u = &node[rootID];
u->left = u->right = NULL; u->p = 0;
nodeID = rootID;
}
Node* new_node()
{
Node* u = &node[++nodeID];
u->left = u->right = NULL; u->p = 0;
return u;
}
指標訪問會比陣列下標快一些,但使用結構體更主要的原因還是因為能更好地將各項屬性組織起來,優於陣列的表達效果。
思路上與[陣列+ID]相差不同,同樣也有記憶體碎片無法利用的問題。
4.結構體陣列+記憶體池
struct Node {
bool p;
int v;
Node * left;
Node * right;
Node(): p(0), left(NULL), right(NULL) {}
};
const int max_n = 1000;
queue<Node*> node_pool;
Node node[max_n];
void init()
{
for(int i = 0; i < max_n; i++)
node_pool.push(&node[i]);
}
Node* new_node()
{
Node* u = node_pool.front(); node_pool.pop();
u->left = u->right == NULL; u->p = 0;
return u;
}
void delete_node(Node* u)
{
node_pool.push(u);
}
為了解決記憶體碎片無法利用的問題,可以採用記憶體池管理。建立一個Node*的佇列,初始化時將Node陣列中所有項的指標入隊。分配新節點時,從佇列中出隊取指標;刪除節點時,將節點重新入隊即可。
總結
以上幾種實現方式,主要區別在於:1.記憶體分配是動態還是靜態;2.是否有記憶體碎片無法利用。根據題目特點,是否需要頻繁插入節點,是否會對樹進行刪除等等,選擇合適的實現方式。