前言
比较常规的二叉树的实现方式是[结构体/对象+指针],看紫书的时候,里面给出了几种树的实现方式,基本上是比较适合在比赛中使用的。
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.是否有内存碎片无法利用。根据题目特点,是否需要频繁插入节点,是否会对树进行删除等等,选择合适的实现方式。