Let's assume you know the size of the tree (N) up-front. Maybe it's the first line in the file. If not, it's easy to adapt this to dynamically re-size the index vector. Note that this is semi-pseudocode:
// Only needed while parsing the file
std::vector<Node*> index(N, NULL);
// We can always create the root node.
// This simplifies the while loop below.
index[0] = createNode(0);
while (!in.eof()) {
int nodeID = -1, leftID = -1, rightID = -1;
parseNode(in, &nodeID, &leftID, &rightID);
// Guaranteed to be non-NULL
Node* node = index[nodeID];
// if leftID or rightID is -1, createNode()
// will simply return NULL.
index[leftID] = createNode(leftID);
index[rightID] = createNode(rightID);
node->setLeftChild(index[leftID]);
node->setRightChild(index[rightID]);
}
The reason node = index[nodeID]
is guaranteed to be non-NULL is because of the pre-order indexing/write/read. We pre-created the root node at the start, and all other nodes are created by the parent as a left or right child.
EDIT: I just realized we don't need a full master index. We only need to store potential right-child nodes to expand in a stack. Here's that version of the algorithm:
// Candidate right-child nodes to expand
std::stack<Node*> rightNodes;
// Pre-create the root node as "left child"
Node* left = createNode(0);
while (!in.eof()) {
// We already know the next node. It is the previous
// node's left child (or root), or the nearest
// parent's right child.
Node* node;
if (left != NULL) {
node = left;
}
else {
node = rightNodes.top();
rightNodes.pop();
}
parseLine(in, &nodeID, &leftID, &rightID);
assert(node->ID() == nodeID);
// if leftID or rightID is -1, createNode()
// will simply return NULL.
left = createNode(leftID);
Node* right = createNode(rightID);
node->setLeftChild(left);
node->setRigthChild(right);
if (right != NULL) {
rightNodes.push(right);
}
}