#二叉树(binary)
二叉树就是节点的度不大于2的树,即树中每个节点的子节点最多只有两个。每个节点的子节点分为左子节点和右子节点,并且左右子节点的顺序不能改变。
1. 二叉树分类
二叉树分为满二叉树、完全二叉树和完美二叉树。
1.1满二叉树(full binary tree)
满二叉树是除了叶子节点外,每个节点都有两个子节点的树。
如下图所示的两棵树,左边的树是满二叉树,因为除了叶子节点以外,每个节点都有两个子节点。但是右边的不是满二叉树,因为除了叶子节点以外,第二层的第一个节点只有一个子节点。
1.2 完全二叉树(complete binary tree)
完全二叉树除了最后一层节点以外,其余的节点形成一个满二叉树,并且最后一层节点从左往右依次填满,没有空出来的节点。
1.3 完美二叉树(perfect binary tree)
完美二叉树的叶子节点在同一层,并且所有的内部节点都有两个子节点。
使用下面的图更好的区分这几个概念
2. 二叉树的存储
同堆栈一样,二叉树的实现也可以采用数组和链表形式实现。
2.1 数组形式
二叉树的数组存储形式其实就是层级遍历的结果:从一层逐层往下遍历,每层从左往右遍历。假设一个节点在数组中的下标为i,则其左子节点的下标为2*i, 右子节点的下标为2*i+1,其父节点的下标为\lfloor\frac{i}{2}} \rfloor N
假设有如下二叉树,则其数组存储结果为:
2.2 链式形式
二叉树的链式存储结构中,将每个节点封装成一个结构体,存储节点的值以及两个分别指向左右子节点的指针。叶子节点的左右子节点指针都为空指针。
typedef struct node
{
int val;
struct node *left;
struct node *right;
}Node;
3. 二叉树查找树及其基本操作
下面使用链式存储形式说明二叉树的相关操作。
通常我们建立一棵树,自然是希望这棵树能够表示一定的意义方便找到需要的元素或进行相关操作,所以建立的二叉树都不是无意义的。现在建立一棵树使得每个节点左边节点的值都小于它,右边的节点的值都大于它。(有时候可能需要记录每个数据元素,为了保留相同的元素有两种方法:1. 约定每个节点的左边节点的值小于等于这个节点的值,右边节点的值都大于这个节点的值。这种方法十分直观,但是会造成空间浪费; 2. 约定每个节点左边节点的值都小于它,右边的节点的值都大于它,使用一个计数器记录有多少个与当前节点值相同的元素。这里只考虑最简单的情况,即没有重复的元素)这样得到的二叉树叫做二叉查找树(Binary Search Tree,也叫作二叉搜索树)。
3.1 查找
查找的过程如下:
1. 如果二叉树为空树,查找失败,返回NULL
2. 如果key等于根节点的值,则查找成功,返回root
3. 如果key小于根节点的值,递归查找左子树
4. 如果key大于根节点的值,递归查找右子树
//recursive find a target node
Node* Find (Node* root, int key)
{
//skip loop if empty tree(root = NULL), or not find key(root = NULL), or find key (root->val = key)
if (root == NULL || key == root->val) return root;
if (key < root->left)
return Find (root->left);
else
return Find (root->right);
}
//*********************************************//
//non-recursive find a target node
Node* Find (Node* root, int key)
{
Node *temp = root;
//skip loop if empty tree(root = NULL), or not find key(temp = NULL), or find key (temp->val = key)
while(temp != NULL && temp->val != key)
{
if (key < temp->val)
temp = temp->left;
else
temp = temp->right;
}
return temp;
}
3.2 查找最大值&最小值
由于二叉查找树中,每个节点的左节点都小于它,所以最小值一定是树中最左边的节点;同样的,最大值一定是树中最右边的节点。
//find the maximum element
Node* FindMin(Node* root)
{
if (root == NULL) return NULL;
Node* temp = root;
while (temp->left != NULL)
temp = temp->left;
return temp;
}
//find the minimum element
Node *FindMax(Node *root)
{
if (root == NULL) return NULL;
Node* temp = root;
while (temp->right != NULL)
temp = temp->right;
return temp;
}
3.3 查找Floor和Ceiling
Floor即查找比key小的最大值,Ceiling即查找比key大的最小值。
先考虑查找Floor(比key小的最大值),过程如下:
1. 将key与root的值比较
2. 如果key比root的值小,那么目标节点肯定在root的左子树上
3. 如果key比root的值大,那么目标节点有是root,也有可能是在root的右子树上的某个节点。
4. 接着第3步,继续查找root的右子树。如果没有找到目标节点,则说明root右子树上的所有节点都大于key,因此目标节点就是root;否则就在右子树上找到了目标节点。
//find the maximum element that smaller than key
Node* Floor(Node* root, int key)
{
if (root == NULL) return NULL;
if (key < root->val)
return Floor(root->left, key);
else
{
Node* temp = Floor(root->right, key);
if (temp == NULL) return root;
else return temp;
}
}
Ceiling的过程其实跟Floor的一样,代码实现如下:
//find the minimum element that bigger than key
Node* Ceiling(Node* root, int key)
{
if (root == NULL) return NULL;
if (key > root->val)
return Floor(root->right, key);
else
{
Node* temp = Floor(root->left, key);
if (temp == NULL) return root;
else return temp;
}
}
3.4 插入
插入的过程与查找类似:
1. 首先查找新节点需要插入的位置
2. 插入新节点
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("space error!\n");
return NULL;
}
newNode->val = val;
newNode->left = newNode->right = NULL;
return newNode;
}
// recursive insert new value
void Insert (Node *root, int val) {
if (root == NULL) root = CreateNode(val);
if (val < root->val)
root->left = Insert (root->left, val);
else
root->right = Insert (root->right, val);
}
//non-recursive Insert new value
void Insert (Node *root, int val)
{
if (root == NULL) root = CreateNode(val);
//find target location
Node *pre = NULL, *cur = root;
while (cur != NULL) {
pre = cur;
if (val < cur->val) cur = cur->left;
if (val > cur->val) cur = cur->right;
}
//Insert value
if (val < pre->val) pre->left = CreateNode(val);
else pre->right = CreateNode(val);
}
3.5 删除
删除操作比较复杂,需要考虑多种情况
- 如果要删除的是叶子节点,则可以直接删除
如果要删除的节点有一个子节点,则将其父节点指向被删除节点的子节点即可。
如下图所示,加入要删除节点(4),则将节点(2)的右指针指向节点(4)的左子树。
如果要删除的节点有两个子节点,需要进行一下步骤
1> 由二叉搜索树的特点可知,每个节点(T)的左子树中最右边的节点(R)是左子树中最大的节点,R比左子树中所有节点大,同时比T的右子树中所有节点的值都小
2>可以将R中的值替换为T中的值,这并没有破坏二叉树搜索树的性质
3> 删除掉R节点(此时R中的值为原来T的值)
4> 当然也可以使用右子树中最左边的节点替换被删除的节点
void Delete(Node *root, int key)
{
if (root == NULL) return; //not find key
if (key < root->val)
return Delete(root->left, key);
else if (key > root>val)
return Delete(root->right, key);
else {
Node *toDeleteNode = root;
if (toDeleteNode->left == NULL){ //no left child or no children
root = root->right;
free(toDeleteNode);
}
else if (toDeleteNode->right == NULL) { //no right child
root = root->left;
free(toDeleteNode);
}
else //has two children
{
//find the min element in right subtree
Node *toDeleteNode = root->right;
while (toDeleteNode->left != NULL) toDeleteNode = toDeleteNode->left;
root->val = toDeleteNode->val;
Delete(toDeleteNode, toDeleteNode->val);
}
}
}
复杂度分析
二叉查找树的时间复杂度与树的形状有关,最好情况下二叉查找树是完全平衡的,从各节点到叶子节点的深度为O(log(N)),最坏情况下二叉树形成一条链,导致复杂度增加到O(N)。为了解决最坏情况,可以使用平衡二叉树、红黑树等存储结构。