转载:
http://blog.jobbole.com/108184/
https://blog.csdn.net/zhanggonglalala/article/details/79738213
https://blog.csdn.net/heyanxi0101/article/details/79593542
一、概述
平衡树:所有结点左右子树深度差≤1
排序树:所有结点“左小右大
字典树:由字符串构成的二叉排序树
判定树:分支查找树(例如12个球如何只称3次便分出轻重)
带权树:路径带权值(例如长度)
最优树:是带权路径长度最短的树,又称 Huffman树,用途之一是通信中的压缩编码。
二、二叉树的定义
二叉树通常以结构体的形式定义,如下,结构体内容包括三部分:本节点所存储的值、左孩子节点的指针、右孩子节点的指针。这里需要注意,子节点必须使用指针,就像我们定义结构体链表一样,下一个节点必须使用地址的方式存在在结构体当中。
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
当然,我们也可以为我们的的树节点结构体重新定义一下名字,使用C语言中的typedef方法就可以了。
typedef struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
} BiNode, *BiTree;
三、二叉树的创建
二叉树的操作通常使用递归方法。二叉树的操作可以分为两类,一类是需要改变二叉树的结构的,比如二叉树的创建、节点删除等等,这类操作,传入的二叉树的节点参数为二叉树指针的地址,这种参入传入,便于更改二叉树结构体的指针(即地址)。这里稍微有一点点绕,可能需要多思考一下。
如下是二叉数创建的函数,这里我们规定,节点值必须为大于0的数值,如果不是大于0的数,则表示结束继续往下创建子节点的操作。然后我们使用递归的方法以此创建左子树和右子树。
int CreateTree(struct TreeNode** root) {
int val;
scanf_s("%d", &val);
if (val <= 0) {
*root = NULL;
return 0;
}
*root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
if (!root) {
printf("创建失败\n");
}
if (val > 0) {
(*root)->val = val;
CreateTree(&((*root)->left));
CreateTree(&((*root)->right));
}
return 0;
}
四、二叉树的遍历
二叉树的遍历有四种方式:前序遍历、中序遍历、后序遍历、层序遍历。
1.1、前序遍历
先访问根节点,再分别前序遍历左、右两棵子树。
//前序遍历
void PreShow(Tree T)
{
if (!T)
{
return;
}
printf("%c ", T->id);
PreShow(T->left);
PreShow(T->right);
}
1.2、中序遍历
先中序遍历左子树,然后访问根节点,最后中序遍历右子树。
//中序遍历
void MidShow(Tree T)
{
if (!T)
{
return;
}
MidShow(T->left);
printf("%c ", T->id);
MidShow(T->right);
}
1.3、后序遍历
先后序遍历左子树,再后序遍历右子树,最后访问根节点。
//后序遍历
void BackShow(Tree T)
{
if (!T)
{
return;
}
BackShow(T->left);
BackShow(T->right);
printf("%c ", T->id);
}
1.4、层序遍历
从根节点开始,逐层访问各子节点。
前、中、后序遍历都采用递归的方式,实际上也就是用到了栈。
而层序遍历则用队列的方式实现,具体的步骤如下:
(1)初始化一个队列,二叉树的根节点进队,即插入队尾。
(2)队首的树节点出队,访问这个树节点。先看它有没有左孩子,如果有,就让左孩子进队。
再看它有没有右孩子,如果有,就让右孩子也进队。
(3)重复过程(2),直到队列清空为止。
//层序遍历
void LevelShow(Tree T)
{
if (!T)
{
return;
}
Queue m_queue;
Queue *Q;
QueueNode *p, *q;
Q = &m_queue;
InitQueue(Q); //队列初始化
p = (QueueNode *)malloc(sizeof(QueueNode));
p->next = NULL;
p->treenode = T;
InQueue(Q, p); //根节点进队
while (!IsQueueEmpty(Q))
{
OutQueue(Q, &q); //当前队首节点出队
printf("%c ", q->treenode->id);
if (q->treenode->left) //左孩子非空,则左孩子进队
{
p = (QueueNode *)malloc(sizeof(QueueNode));
p->next = NULL;
p->treenode = q->treenode->left;
InQueue(Q, p);
}
if (q->treenode->right) //右孩子非空,则右孩子进队
{
p = (QueueNode *)malloc(sizeof(QueueNode));
p->next = NULL;
p->treenode = q->treenode->right;
InQueue(Q, p);
}
}
}
五、二叉树的删除
先删除左子树,再删除右子树,最后删除根节点,采用递归实现。
//清除二叉树
void ClearTree(Tree *T)
{
if (!*T)
{
return;
}
ClearTree(&(*T)->left);
ClearTree(&(*T)->right);
free(*T);
*T = NULL;
}
六、获得二叉树的高度
如果二叉树只有根节点,那么它的高度就为1。否则二叉树的高度等于1+左、右子树高度的较大者。按这种方法递归实现。
//获得二叉树的高度
int GetHeight(Tree T)
{
if (T)
{
return MaxOfTwo(GetHeight(T->left), GetHeight(T->right)) + 1;
}
else
{
return 0;
}
}
//两数较大值
int MaxOfTwo(int a, int b)
{
if (a >= b)
{
return a;
}
else
{
return b;
}
}
七、获得二叉树的节点数
二叉树的节点数 = 左子树节点数 + 右子树的节点数 + 1。递归实现即可。
//获得二叉树的节点数
int GetNodeNumber(Tree T)
{
if (T)
{
return GetNodeNumber(T->left) + GetNodeNumber(T->right) + 1;
}
else
{
return 0;
}
}
八、二叉树叶子节点的数量
int LeafNodeNum(struct TreeNode* root) {
if (root == NULL) {
return 0;
}
if (root->left == NULL&&root->right == NULL) {
return 1;
}
else {
return LeafNodeNum(root->left) + LeafNodeNum(root->right);
}
}
九、插入二叉树节点
public void insert(int id, double dd) {
Node newNode = new Node(); // 创建要添加的节点
newNode.iData = id;
newData.dData = dd;
if (root == null) // 如果该树在插入之前没有其他的子节点
root = newNode; // 直接使用根节点指向新的节点
else {
Node current = root; // 维护一个当前节点的变量
Node parent; // 维护一个父节点的变量
while (true) {
parent = current; // 首先让父节点保存当前节点的状态
if (id < current.iData) { // 如果要插的数据 比当前节点的值小
current = current.leftChild; // 向左走
if (current == null) { // 走到当前的节点为空时 着说明其父节点应该为一个子叶节点 可直接插入
parent.leftChild = newNode; // 使用当前节点的父节点 让其左子叶的引用指向 要插入的节点
return; // 插入后返回
}
} else { // 和上面类似
current = current.rightChild;
if (current == null) {
parent.rightChile = newNode;
return;
}
}
}
}
}
十、删除二叉树节点
在删除之前需要找到要删的那个节点,这个节点找到之后,可能会有三种情况出现在你面前:
- 该节点是叶节点、没有子节点
- 该节点有一个子节点
- 该节点有两个子节点
对于情况1:
找到要删除的节点,此节点没有子叶节点 ,直接将该节点的父节点对其的引用置为null。
public boolean delete(int key) {
// 查找要删除的节点
Node current = root; // 记录当前的节点
Node parent = root; // 记录当前节点的父节点
boolean isLeftChild = true; // 是否为左子节点的标志位
while (current.iData != key) {
parent = current;
if (key < current.iData) {
isLeftChid = true;
current = current.leftChild;
} else {
isLeftChid = false;
current = current.rightChild;
}
if (current = null)
return false;
}
// 如果没有子节点
if (current.leftChild == null && current.rightChild == null) {
if (current == root) // 如果是根节点
root = null;
else if (isLeftChild) // 如果这个节点在左边
parent.leftChild = null; // 将其父节点的左节点的索引置为null
else
parent.rightChild = null; // 否则将其父节点的右节点的索引置为null
}
}
对于情况2:
由于要删除的节点还存在有一个子叶节点,所以直接将父节对其的引用,变到其的子叶节点。
(简单讲就是让其的一个子节点代替他)
// 连接上面的 delete 方法
else if(current.rightChild == null) { // 如果这个节点的右边为空,左边还有相应的连接
if (current == root) // 如果这个节点为 根节点
root = current.lefeChild; // 那么直接将根节点替换为 原根节点的左子节点
else if (isLeftChild) // 如果这个节点为一个左子节点
parent.leftChild = current.leftChild; // 让其父节点对于左子节点的连接直接连向要删除节点的左子节点
else // 这是一个右子节点
parent.rightChild = current.leftChild; // 让其父节点对于右子节点的连接直接连向要伤处节点的左子节点
} else if (current.leftChild == null) { // 如果这个节点的左边为空,右边还有相应的连接
if (current == root) // -----> 同上
root = current.rightChild;
else if (isLeftChild)
parent.leftChild = current.rightChild;
else
parent.rightChild = current.rightChild;
}
对于情况3:
对于要删除的节点有两个子叶节点的,这个删除的过程中,比较复杂的过程就是寻找代替要删除节点的后继节点,这个节点具有的特征就是,这个节点要比被删除的节点大,但是在比被删除节点大的节点中,这个节点应该是最小的。在寻找后继节点分过程中,涉及到一种算法,也可以说是一种思维方式,首先,找到被删除节点的右子节点(绝对可以找到,因为这个节点是由两个子节点的),这个顺理成章,因为右子节点和其下可能存在的右子树的所有值,绝对比要删除的节点的值大,但是要在这些所有比较大的值中找到最小的那个,所以继续向被删除节点的右子节点的左子节点查找,一直找左子节点,知道没有左子节点的时候,那么那个节点就是我们要找的后继节点,但是这时候还是有两种情况,第一种是:被删除的节点的右子节点直接是后继节点,因为它没有字节的左子节点;还有一种就是,我们在要删除的节点的右子节点的子树上找到那棵树上最小的值;针对这两种不同的情况,要采取不同的方式去应对。
// 连接上面的delete代码
else {
Node successor = getSuccessor(current); // 在这获取到后继节点
if (current == root) // 如果要删除的节点为 根,那么直接让后继节点代替根
root = successor;
else if (isLeftChild) // 如果要删除的节点在 当前节点的左边 那么让其父节点对左子节点的引用 直接指向后继节点
parent.leftChild = successor;
else // 否则 父节点应该将对右子节点的引用 直接指向后继节点
parent.rightChild = successor;
successor.leftChild = current.leftChild; // 统一处理后继节点的左边子树:让后继节点的左引用 直接与删除节点的左子树相连
}
return true;
}
// 获取后继节点的方法
private Node getSuccessor (Node delNode) {
// 后继节点的父节点
Node successorParent = delNode;
// 后继节点
Node successor = delNode;
// 当前节点
Node current = delNode.rightChild;
while (current != null) {
// 后继节点父节点首先保存后继节点的状态
successorParent = successor;
// 后继节点 不断的向左更新
successor = current;
current = current.leftChild;
}
// 假如我们找到的后继节点不直接是 要删除节点的右节点 而是在其右节点那条子树上面最小的一个节点
if (successor != delNode.rightChild) {
// 后继节点的父节点断开其与后继节点左边的引用,重新连接上后继节点的右子节点(因为后继节点是没有左子节点的,锁以要保存之前树的状态,还要把后继节点的右子节点处理一下,不管 其存在不存在)
successorParent.leftChild = successor.rightChild;
// 这时候后继节点的右边已经空了 上一条语句已经将其给了自己父节点的左子节点 然后让后继节点的右边 连接要删除节点的右子树
successor.rightChild = delNode.rightChild;
}
return successor;
}