首先给出个人对于二叉树的理解:一个有限的节点集合,集合可以为空,或者仅含有根节点,又或者由一个根节点r和称为左右子树的两个不想交的二叉树构成。在这里,对于二叉树的理解用了递归的方法。
在上面的基础上,就很容易理解什么叫做完全二叉树了:如果二叉树中每个内部节点均有两个左右子节点,且所有叶子具有同样的深度,那么这棵二叉树可以称之为完全二叉树。
和前面所分享的大顶堆一样,首先给出二叉树的数组实现方式,其实现基于这样一个基本事实:对于节点i,若左右子节点均存在,其左子节点为2i+1,右子节点为2i+2。在下面给出的C++实现中,我们用简单的int类型举例,并且用-1标记不存在的节点。
二叉树的数组实现方式:
/*二叉树的数组实现*/
/*
1:对于节点i,若左右子节点均存在,其左子节点为2i+1,右子节点为2i+2
2:我们用-1来标记不存在的子节点
*/
class BinaryTree
{
public:
BinaryTree(int size, int *pRoot); //创建树
~BinaryTree(); //销毁树
int* searchNode(int nodeIndex); //由索引查找值
bool addNode(int nodeIndex, char direction, int *pNode);//添加nodeIndex的孩子节点
bool deleteNode(int nodeIndex, int *pNode); //删除指定节点
void treeTraverse(); //遍历
int getTreeSize(); //返回树的大小
private:
int *m_pTree; //指向树的指针
int arraySize; //数组大小
int treeSize; //树的大小
};
BinaryTree::BinaryTree(int size, int *pRoot) //size表示树的最大节点数
{
arraySize = size;
treeSize = 1; //初始化时指定了树根
m_pTree = new int[size];
/*初始化树,表示当前树无任何节点*/
if(m_pTree!=NULL) //不为空表示分配内存成功
for (int i = 0; i < size; i++)
{
m_pTree[i] = -1;
}
m_pTree[0] = *pRoot; //指定树根
}
BinaryTree::~BinaryTree()
{
delete[]m_pTree;
m_pTree = NULL;
}
int* BinaryTree::searchNode(int nodeIndex)
{
if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
return NULL;
if (m_pTree[nodeIndex] == -1)//索引节点不存在
return NULL;
return &m_pTree[nodeIndex];
}
bool BinaryTree::addNode(int nodeIndex, char direction, int *pNode)
{
if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
return false;
if (m_pTree[nodeIndex] == -1)//索引节点不存在
return false;
/*插入到左孩子节点*/
if (direction == 'l')
{
if ((2 * nodeIndex + 1) < arraySize && m_pTree[2 * nodeIndex + 1] == -1)
{
m_pTree[2 * nodeIndex + 1] = *pNode;
treeSize++;
return true;
}
else
return false;
}
/*插入到右孩子节点*/
else if (direction == 'r')
{
if ((2 * nodeIndex + 2) < arraySize && m_pTree[2 * nodeIndex + 2] == -1)
{
m_pTree[2 * nodeIndex + 2] = *pNode;
treeSize++;
return true;
}
else
return false;
}
else
return false;
}
bool BinaryTree::deleteNode(int nodeIndex, int *pNode)
{
if (nodeIndex < 0 || nodeIndex >= arraySize)//索引不合法
return false;
if (m_pTree[nodeIndex] == -1)//索引节点不存在
return false;
*pNode = m_pTree[nodeIndex];
m_pTree[nodeIndex] = -1;
treeSize--;
return true;
}
void BinaryTree::treeTraverse() //遍历数组即可
{
for (int i = 0;i < arraySize;i++)
{
if (m_pTree[i] != -1)
cout << m_pTree[i] << " ";
}
}
int BinaryTree::getTreeSize()
{
return treeSize;
}
基于数组的实现方式比较简单(时间代价较优),但是我们设想这样的两种情况:
- 由于存储二叉树的数组大小从初始化后便固定不变,假设在后期动态维护此二叉树过程中,删除了大量的节点,如此势必造成数组中大量位置的空缺,使得很多存储空间被白白浪费,空间利用率不高;
- 当因为某种情况需要增加大量节点时,若节点总数超出数组大小,则需要复制整棵树到一个更大的数组空间中,当数据量巨大时,这种操作是耗时耗力的。
为了避免上述在空间利用方面的问题,下面给出基于链表实现的二叉树方式:
同样,对于节点的数据以int这种简单的数据类型为例,在应用中也完全可以很方便的扩展到更加复杂的数据类型。
二叉树的链表实现:
/*节点类*/
class Node
{
public:
int index; //节点索引
int data; //节点数据,这里以简单类型int为例
Node *p_lChild, *p_rChild, *p_Parent; //左孩子指针,右孩子指针,父指针
Node();
~Node();
Node* searchNode(int nodeIndex); //搜索指定节点,被BinaryTree类中同名方法调用
void deleteNode(); //删除当前结点,递归删除其子树
void preorderTraversal(); //前序遍历在当前结点的操作
void inorderTraversal(); //中序遍历在当前结点的操作
void postorderTraversal(); //后序遍历在当前结点的操作
};
Node::Node()
{
index = 0;
data = 0;
p_lChild = NULL;
p_rChild = NULL;
p_Parent = NULL;
}
Node::~Node()
{
p_lChild = NULL;
p_rChild = NULL;
p_Parent = NULL;
}
Node* Node::searchNode(int nodeIndex)
{
if (this->index == nodeIndex) //当前结点为搜索节点
return this;
Node *temp = NULL;
if (this->p_lChild != NULL) //比较当前结点左孩子和待搜索节点
{
if (this->p_lChild->index == nodeIndex)
return this->p_lChild;
else
{
temp = this->p_lChild->searchNode(nodeIndex);
if (temp != NULL)
return temp;
}
}
if (this->p_rChild != NULL) //比较当前结点右孩子和待搜索节点
{
if (this->p_rChild->index == nodeIndex)
return this->p_rChild;
else
{
temp = this->p_rChild->searchNode(nodeIndex);
return temp;
}
}
return NULL;
}
void Node::deleteNode()
{
if (this->p_lChild != NULL) //当前结点存在左孩子,则递归删除左子树
this->p_lChild->deleteNode();
if (this->p_rChild != NULL) //当前结点存在右孩子,则递归删除右子树
this->p_rChild->deleteNode();
if (this->p_Parent != NULL) //当前结点不为根
{
if (this->p_Parent->p_lChild == this) //当前结点为其父节点的左孩子
this->p_Parent->p_lChild = NULL;
if (this->p_Parent->p_rChild == this) //当前结点为其父节点的右孩子
this->p_Parent->p_rChild = NULL;
}
delete this; //释放当前结点
}
void Node::preorderTraversal()
{
cout << this->data << " "; //当前结点
if (this->p_lChild != NULL) //递归遍历左子树
this->p_lChild->preorderTraversal();
if (this->p_rChild != NULL) //递归遍历右子树
this->p_rChild->preorderTraversal();
}
void Node::inorderTraversal()
{
if (this->p_lChild != NULL) //递归遍历左子树
this->p_lChild->inorderTraversal();
cout << this->data << " "; //当前结点
if (this->p_rChild != NULL) //递归遍历右子树
this->p_rChild->inorderTraversal();
}
void Node::postorderTraversal()
{
if (this->p_lChild != NULL) //递归遍历左子树
this->p_lChild->postorderTraversal();
if (this->p_rChild != NULL) //递归遍历右子树
this->p_rChild->postorderTraversal();
cout << this->data << " "; //当前结点
}
/*二叉树类*/
class BinaryTree
{
public:
BinaryTree(); //创建树
~BinaryTree(); //销毁树
Node* searchNode(int nodeIndex); //根据索引搜索节点
bool addNode(int nodeIndex, char direction, Node *pNode); //增加节点
bool deleteNode(int nodeIndex, Node *pNode); //删除指定索引节点,放入pNode
void preorderTraversal(); //前序遍历
void inorderTraversal(); //中序遍历
void postorderTraversal(); //后序遍历
private:
Node *m_pRoot; //根指针,根节点不存放数据
};
BinaryTree::BinaryTree()
{
m_pRoot = new Node();
}
BinaryTree::~BinaryTree()
{
deleteNode(0, NULL); //根节点索引为0
//m_pRoot->deleteNode();
delete m_pRoot;
}
Node* BinaryTree::searchNode(int nodeIndex)
{
return m_pRoot->searchNode(nodeIndex);
}
bool BinaryTree::addNode(int nodeIndex, char direction, Node *pNode)
{
Node *temp = searchNode(nodeIndex); //此处调用BinaryTree类的方法
if (temp == NULL) //拟挂载节点不存在
return false;
Node *node = new Node(); //读取拟添加节点的数据
if (node == NULL) //申请内存失败
return false;
node->index = pNode->index;
node->data = pNode->data;
if (direction == 'l') //挂载到nodeIndex节点的左边
{
temp->p_lChild = node;
node->p_Parent = temp;
}
if (direction == 'r') //挂载到nodeIndex节点的右边
{
temp->p_rChild = node;
node->p_Parent = temp;
}
temp = NULL;
return true;
}
bool BinaryTree::deleteNode(int nodeIndex, Node *pNode)
{
Node *temp = searchNode(nodeIndex); //此处调用BinaryTree类的方法
if (temp == NULL) //拟删除节点不存在
return false;
if(pNode!=NULL) //将待删除节点数据复制到pNode中
pNode->data = temp->data;
temp->deleteNode();
temp = NULL;
return true;
}
void BinaryTree::preorderTraversal()
{
m_pRoot->preorderTraversal();
}
void BinaryTree::inorderTraversal()
{
m_pRoot->inorderTraversal();
}
void BinaryTree::postorderTraversal()
{
m_pRoot->postorderTraversal();
}
不同于数组实现方式,这里我们要定义节点类型(class Node):其主要包括节点索引(这样可以更快的查找到此节点)、节点数据项、左右孩子指针和父指针。三个指针分别用于指向当前结点的左右孩子节点和其父亲节点。
另外,值得注意的三点是:
- 这里在实现的时候,根节点将不用于存储数据项。
- 三种遍历方式、搜索指定节点、删除指定节点等操作均以递归函数实现(和树的递归定义有一定关系)。
- 实现过程中,,因为存在很多指针的情况,小伙伴们一定要特别注意防止内存泄漏问题,不要把头弄晕了哦。
关于递归操作的封装,在实现时会发现,如果在类BinaryTree对象上进行操作,会变得无从下手,而在某一个节点上进行上述递归操作是很方便的,故而我们把上述操作下移,在Node类中封装其实现逻辑,而在BinaryTree类对象中只是简单的调用Node类封装好的相关方法即可。
小结: 对比数组实现方式和链表实现方式,可以发现:数组实现方式在时间代价上是更加优化的,而链表实现方式在空间利用上更加灵活。实际应用中应根据实际情况进行选择。
点击这里下载完整源码,包括最小生成树等方法的封装。