树的基础知识
1.1 定义
树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。
在任意一颗非空树中:
1)有且仅有一个特定的称为根(Root)的结点;
2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…、Tn,其中每一个集合本身又是一棵树,并且称为根的子树
1.2 节点的度
结点拥有的子树数目称为结点的度。
度为0的节点称为叶节点或终端节点,度不为零的节点称为非终端节点或分支节点。
树的度是树内各节点的度的最大值。
1.3 树的深度
树中结点的最大层次数称为树的深度或高度。上图所示树的深度为4。
2.1 二叉树的定义
二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成。
下图展示了一棵普通二叉树:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
2.2 二叉树的特点
由二叉树定义以及图示分析得出二叉树有以下特点:
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
2.3 二叉树的性质
1)在二叉树的第i层上最多有2^i-1 个节点 。(i>=1)
2)二叉树中如果深度为k,那么最多有2^k-1个节点。(k>=1)
3)n0=n2+1 n0表示度数为0的节点数,n2表示度数为2的节点数。
4)在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]是向下取整。
5)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点有如下特性:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
2.4 特殊的二叉树
2.4.1 满二叉树
满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。 2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多
2.4.2 完全二叉树
完全二叉树:对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
特点:
1)叶子结点只能出现在最下层和次下层。
2)最下层的叶子结点集中在树的左部。
3)倒数第二层若存在叶子结点,一定在右部连续位置。
4)如果结点度为1,则该结点只有左孩子,即没有右子树。
5)同样结点数目的二叉树,完全二叉树深度最小。
注:满二叉树一定是完全二叉树,但反过来不一定成立。
2.5.线索二叉树
2.6.二叉搜索(排序)树
2.5.1定义
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),也称二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
2.5.2 查找
用递归实现查找:
如果树是空的,则查找结束,无匹配。
如果被查找的值和根结点的值相等,查找成功。否则就在子树中继续查找。如果被查找的值小于根结点的值就选择左子树,大于根结点的值就选择右子树。
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
/* 递归查找二叉排序树T中是否存在key, */
/* 指针f指向T的双亲,其初始调用值为NULL */
/* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */
/* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */
Status SearchBST(BiTree t, int key, BiTree f, BiTree *p)
{
//以下if语句用来判断当前二叉树是否到叶子节点。
if (!t) /* 查找不成功 */
{
*p = f;
return FALSE;
}
else if (key == t->data) /* 查找成功 */
{
*p = t;
return TRUE;
}
else if (key < t->data)
return SearchBST(t->lchild, key, t, p); /* 在左子树中继续查找 */
else
return SearchBST(t->rchild, key, t, p); /* 在右子子树中继续查找*/
}
2.5.3 删除
删除的时候需要考虑以下几种情况:
1)删除结点为叶子结点;
2)删除的结点只有左子树;
3)删除的结点只有右子树
4)删除的结点既有左子树又有右子树。
考虑前三种情况,处理方式比较简单。
例如:若要删除图2.8中的结点93,则直接删除该结点即可。
若要删除的结点为结点35,结点35只有右子树,只需删除结点35,将右子树37结点替代结点35即可。删除后的二叉排序树如图所示:
删除只有左子树的结点与此情况类似。
情况4相对比较复杂,对于待删除结点既有左子树又有右子树的情形,最佳办法是在剩余的序列中找到最为接近的结点来代替删除结点。
可以采用中序遍历的方式来得到删除结点的前驱和后继结点。选取前驱结点或者后继结点代替删除结点即可.。
例如:待删除的结点为47,图2.8中二叉排序树的中序遍历序列为35 37 47 51 59 60 61 73 87 93 98。则结点47的前驱结点为37,则直接将37结点替代47结点即可。替换后的二叉排序树如图所示
对于有左右子树的节点,如下图,删除节点47,将要删除的节点p赋值给临时的边量q,在将p的左孩子p->child
赋值给临时变量s,此时q指向47节点,s指向35节点,如下图所示:
然后循环找到左子树的右节点,只到右侧尽头,即让q指向35,s指向37这个没有右子树的节点,
让
此时让要删除的结点p的位置的数据被赋值为s->data,既让p->data=37;如下图所示
如果p和q的指向不同,则将s->lchild赋值给q->rchild,否则就是将s->lchild赋值给q->lchild。这里面不等,所以将s->lchild指向的36赋值给q->rchild,也就是让q->rchild指向36结点,如下图所示:
最后free(s),将37结点删除即可。
删除代码如下:
/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, */
/* 并返回TRUE;否则返回FALSE。 */
Status DeleteBST(BiTree *T,int key)
{
if(!*T) /* 不存在关键字等于key的数据元素 */
return FALSE;
else
{
if (key==(*T)->data) /* 找到关键字等于key的数据元素 */
return Delete(T);
else if (key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(*T)->rchild,key);
}
}
/* 从二叉排序树中删除结点p,并重接它的左或右子树。 */
Status Delete(BiTree *p)
{
BiTree q,s;
if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */
{
q=*p; *p=(*p)->lchild; free(q);
}
else if((*p)->lchild==NULL) /* 只需重接它的右子树 */
{
q=*p; *p=(*p)->rchild; free(q);
}
else /* 左右子树均不空 */
{
q=*p; s=(*p)->lchild;
while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */
{
q=s;
s=s->rchild;
}
(*p)->data=s->data; /* s指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值) */
//这里p==q的情况说明待删除结点的左子树没有右节点,上面while循环没有执行,直接将s的左子树加在q->lchild
if(q!=*p)
q->rchild=s->lchild; /* 重接q的右子树 */
else
q->lchild=s->lchild; /* 重接q的左子树 */
free(s);
}
return TRUE;
}
题目:leetcode450删除二叉树中的节点
解题思路:
解法一:
即采用上面分析的思路,待删除的结点分为三种情况:
1.为叶子节点;
2.仅有左或者右子树的结点;
3.左右子树都有的结点;;;这里的思路是用前驱替代,然后删除前驱结点。
代码实现
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(root==nullptr) return nullptr;
if(root->val>key){
root->left=deleteNode(root->left,key);
}else if(root->val<key){
root->right=deleteNode(root->right,key);
}else{
if(root->left==nullptr) return root->right;
if(root->right==nullptr) return root->left;
TreeNode *q=root,*s=root->left;
while(s->right){
q=s;
s=s->right;
}
root->val=s->val;
if(q!=root){
q->right=s->left;
}else{
q->left=s->left;
}
delete s;
}
return root;
}
};
解法二:
1、要删除的节点为叶子节点,可以直接删除。
2、要删除的几点不是叶子节点且拥有右节点,则该节点可以由该节点的后继节点进行替代,该后继节点位于右子树中较低的位置。然后可以从后继节点的位置递归向下操作以删除后继节点。
3、要删除的节点不是叶子节点,且没有右节点但是有左节点。这意味着它的后继节点在它的上面,但是我们并不想返回。我们可以使用它的前驱节点进行替代,然后再递归的向下删除前驱节点
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//查找后继结点,当前结点的右子树,然后向左子树查找到尽头
int successor(TreeNode *root){
root=root->right;
while(root->left) root=root->left;
return root->val;
}
//找到前驱:当前结点的左子树,然后右到尽头
int predecessor(TreeNode *root){
root=root->left;
while(root->right) root=root->right;
return root->val;
}
TreeNode* deleteNode(TreeNode* root, int key) {
if(root==nullptr) return nullptr;
if(key>root->val) {
root->right=deleteNode(root->right,key);
}else if(key<root->val){
root->left=deleteNode(root->left,key);
}else{
//为叶子节点
if(root->left==nullptr&&root->right==nullptr) root=nullptr;
//不是叶子但有右子树,用后继代替,然后递归删除后驱结点
else if(root->right){
root->val=successor(root);
root->right=deleteNode(root->right,root->val);
}//不是叶子,只有有左子树,用前驱代替,然后递归删除前驱结点
else{
root->val=predecessor(root);
root->left=deleteNode(root->left,root->val);
}
}
return root;
}
};
2.5.4 插入
二叉排序的插入是建立在二叉排序的查找之上的,插入一个结点,就是通过查找发现该结点合适插入位置,把结点直接放进去:
若查找的key已经有在树中,则p指向该数据结点。
若查找的key没有在树中,则p指向查找路径上最后一个结点。
struct BiTree {
int data;
BiTree *lchild;
BiTree *rchild;
};
//在二叉排序树中插入查找关键字key
BiTree* InsertBST(BiTree *t,int key)
{
//当树为空时,直接插入
if (t == NULL)
{
t = new BiTree();
t->lchild = t->rchild = NULL;
t->data = key;
return t;
}
if (key < t->data)
t->lchild = InsertBST(t->lchild, key);
else
t->rchild = InsertBST(t->rchild, key);
return t;
}
//n个数据在数组d中,tree为二叉排序树根
int d[10]={61,24,5,27,20,29,26,21,4,15};
BiTree* CreateBiTree(BiTree *tree, int d[], int n)
{
for (int i = 0; i < 10; i++)
tree = InsertBST(tree, d[i]);
}
递归版本:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if(root==nullptr){
TreeNode *t=new TreeNode(val);
return t;
}
if(val<root->val){
root->left=insertIntoBST(root->left,val);
}else{
root->right=insertIntoBST(root->right,val);
}
return root;
}
};
非递归版本:
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr) {
return new TreeNode(val);
}
TreeNode* pos = root;
while (pos != nullptr) {
//小于应该插入到左子树,
if (val < pos->val) {
//如果左子树为空,直接插入,否者继续向左查
if (pos->left == nullptr) {
pos->left = new TreeNode(val);
break;
} else {
pos = pos->left;
}
} else {
if (pos->right == nullptr) {
pos->right = new TreeNode(val);
break;
} else {
pos = pos->right;
}
}
}
return root;
}
};
2.7 平衡二叉树(AVL树)
2.7.1定义
是一种二叉排序树,一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
平衡二叉树大部分操作和二叉查找树类似,主要不同在于插入、删除的时候平衡二叉树的平衡可能被改变,并且只有从那些插入点到根结点的路径上的结点的平衡性可能被改变,因为只有这些结点的子树可能变化。
2.7.2特点
①非叶子节点最多拥有两个子节点。
②非叶子节点值大于左边子节点、小于右边子节点。
③树的左右两边的层级数相差不会大于1。
④没有值相等重复的节点。
2.8 红黑树
为什么有了平衡树还需要红黑树?
虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
包含n个内部节点的红黑树的高度是 O(log(n))
C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树实现的。