4.1 树的基本概念
树(Tree)是n(n>=0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树 T:
-
有且只有一个称之为根的结点
-
除根节点以外的其他结点可分为m(m>0)个互不相交的有限集T1, T2, T3 ...,Tm,其中每个结合本身也是一颗树,并且称之为根的子树(SubTree)
4.2 树的相关术语
1、结点的度
一个结点含有的子树的个数称为该结点的度。
2、叶子结点
度为0的结点称为叶结点,也可以叫做终端结点,叶子结点没有直接后继。
3、分支结点
度不为0的结点称为分支结点,也可以叫做非终端结点。
4、结点的层次
从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推。
5、树的度
树中所有结点的度的最大值。
6、树的高度(深度)
树中结点的最大层次。
7、森林
m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根结点,森林就变成一棵树。
8、孩子结点
一个结点的直接后继结点称为该结点的孩子结点。
9、双亲结点(父结点)
一个结点的直接前驱称为该结点的双亲结点。
10、兄弟结点
同一双亲结点的 孩子结点间互称兄弟结点。
4.3 二叉树
4.3.1 二叉树的概念
二叉树就是度不超过2的树(每个结点最多有两个子结点)。
4.3.2 二叉树的基本性质
1、在二叉树的第i层上最多有 个结点
2、深度为K的二叉树最多有 个结点
3、深度为K时最少有K个结点(每层1个结点)
4.3.3 特殊形态的二叉树
1、满二叉树
一个二叉树,每层的结点树都达到最大值,则这个二叉树就是满二叉树。深度为K的满二叉树有 个结点。
2、完全二叉树
叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
思考:假如一棵完全二叉树有n个结点,请问最后一个非叶子结点的编号是多少?
答案:n/2
思考:假如一棵完全二叉树的最后一个非叶子结点的编号为n,那么请问这棵二叉树有多少个结点?
答案:2n 或者 2n+1
思考:假如一棵完全二叉树有n个结点,请问叶子结点的个数是多少?
答案:n为偶数:n/2, n为奇数:n/2+1
4.3.4 二叉树的遍历
在对二叉树元素进行访问、插入、删除等操作时,我们需要对二叉树进行遍历,所谓的遍历指按某条搜索路线遍访每个结点且不重复(又称周游)。
对二叉树的遍历可以按照如下两种思路进行:
-
广度遍历:按层次遍历
-
深度遍历:
-
前序(先序)遍历:根结点 ---> 左子树 ---> 右子树
-
中序遍历:左子树---> 根结点 ---> 右子树
-
后序遍历:左子树 ---> 右子树 ---> 根结点
-
4.3.5 二分查找树(二叉排序树)
1、二分查找树的概念
二分查找树BST(也叫二分查找树、二叉排序树)的提出是为了提供查找效率,之所以称为二分查找树,因为该二叉树对应着二分查找算法,查找平均的时间复杂度为O(logn),所以该数据结构的提出是为了提高查找效率。
2、二分查找树的性质
二分查找树具有下列性质:
-
若它的左子树不为空,则左子树上所有结点的值均小于根结点的值
-
若它的右子树不为空,则右子树上所有结点的值均大于根结点的值
-
它的左右子树均为二分查找树
3、二分查找树的中序遍历
4、二分查找树的插入
-
遍历规则:
-
从根节点开始遍历
-
如果比遍历到的结点大,遍历该结点的右子树,如果右子树为空则将新的结点作为该结点的右子树
-
如果比遍历到的结点小,遍历该结点的左子树,如果左子树为空则将新的结点作为该结点的左子树
-
-
将4插入到二分查找树中
5、二分查找树结点的删除
A:删除的结点是叶子结点,直接删除
B:有一个分支的,删除节点,子节点上提。
先找到需要删除的结点,将该结点删除,上提子结点:
注意:不论需要删除的结点有左孩子结点还是右孩子结点,该结点上提后都是删除结点父结点的左孩子结点
C:两个分支,节点删除,找到右子树的最小子树替换删除的结点
4.3.6 二分查找树
1、结点描述
-
二叉树的结点可以有至少有三个域:一个数据域,两个指针域分别指向该结点的左孩子结点和右孩子结点
//二叉树的一个结点
typedef struct Node{
int data;//数据域
struct Node *lChild;//指向左孩子的指针
struct Node *rChild; //指向右孩子的指针
}Node;
- 二叉树的结点也可以有四个域:一个数据域,三个指针域分别指向该结点的左孩子结点、右孩子结点和父亲结点
//二叉树的一个结点
typedef struct Node{
ElemType data;//数据域
struct Node *lChild;//指向左孩子的指针
struct Node *rChild; //指向右孩子的指针
struct Node *parent; //指向父结点的指针
}Node;
我们可以定义一个结构体描述一棵二叉树:
//描述一棵树
typedef struct Tree{
Node *root;//指针指向树的根结点
//树的结点数
u32t node_num;
//树的层次
u32t level;
//树的深度
u32t deep;
}Tree;
2、二分查找树的初始化
/*
* @brief 创建一个结点
* @param data 结点上数据域的值
* @return 返回指向创建好的结点的指针
* */
Node *create_node(ElemType data)
{
Node *node;
node = (Node *)malloc(sizeof(Node));
node->data = data;
node->lChild = NULL;
node->rChild = NULL;
return node;
}
/*
* @brief 初始化一棵二叉树
* @return 返回描述一棵二叉树的结构体指针
* */
Tree *tree_init()
{
Tree *t;
t = (Tree *)malloc(sizeof(Tree));
t->root = NULL;
t->deep = 0;
t->level = 0;
t->node_num = 0;
return t;
}
3、二分查找树的创建
/*
* @brief 向树中插入一个结点,使这棵树成为一棵二分查找树
* @param root 二叉树的根节点指针
* @param data 插入的结点上的数据域
* @return 成功返回TRUE,失败返回FALSE
*/
int bst_tree(Node **root, int data)
{
if (NULL == root || NULL == *root)
return FALSE;
//思路:将data 使用bst_tree方法插入到以某一个结点为根结点的树中
//如果这棵树为空 则创建的结点就是这棵树的根结点
if (NULL == *root)
{
Node *node;
node = create_node(data);
*root = node;
return TRUE;
}
//如果data比根小则插入*root的左子树
if (data < (*root)->data)
{
bst_tree(&((*root)->lChild), data);
}
//如果data比根大则插入*root的右子树
else if (data > (*root)->data)
{
bst_tree(&((*root)->rChild), data);
}
else //如果相等则结束插入
return TRUE;
}
4、二分查找树的遍历
//以先序的方式遍历一棵树
void pre_traversal(Node *root)
{
if (NULL == root)
return ;
printf("%d\n", root->data); //打印根结点上的数据
pre_traversal(root->lChild);
pre_traversal(root->rChild);
return ;
}
//中序的方式遍历一棵树 左根右
void infix_traversal(Node *root)
{
if (NULL == root)
return ;
infix_traversal(root->lChild);
printf("%d\n", root->data);
infix_traversal(root->rChild);
return ;
}
//后序的方式遍历一棵树
void epilogue_traversal(Node *root)
{
if (NULL == root)
return ;
epilogue_traversal(root->lChild);
epilogue_traversal(root->rChild);
printf("%d\n", root->data);
return ;
}
5、查找二分查找树中的最小值
/*
* @brief 查找二分查找树中的最小值
* @param root 二叉树的根节点指针
* @return 成功返回最小值,失败返回FALSE
* */
ElemType find_min_node(Node *root)
{
//因为是二分查找树,所以最小值是在最左边
//如何找到最左边的那棵子树?从根结点开始遍历遍历所有的左子树的左子树
//直到某棵树的左子树为空
//非递归方法
// Node *t = root;
// while (t->lChild != NULL)
// t = t->lChild;
// return t->data;
if (NULL == root->lChild)
return root->data;
find_min_node(root->lChild);
}
6、查找二分查找树中的最大值
/*
* @brief 查找二分查找树中的最小值
* @param root 二叉树的根节点指针
* @return 成功返回最小值,失败返回FALSE
* */
ElemType find_max_node(Node *root)
{
//因为是二分查找树,所以最大值是在最右边
//如何找到最右边的那棵子树?从根结点开始遍历遍历所有的右子树的右子树
//直到某棵树的右子树为空
if (NULL == root->rChild)
return root->data;
find_max_node(root->rChild);
}
7、查找指定的结点
/*
* @brief 返回二分查找树上指定的值所在的结点
* @param root 二叉树的根节点指针
* @param data 需要查找的结点的值
* return 如果找到了则返回结点 如果没有找到则返回NULL
* */
Node *find_element(Node *root, int data)
{
//如果找不到
if (NULL == root)
return NULL;
if (root->data == data)
return root;
if (data < root->data)
find_element(root->lChild, data);
else if (data > root->data)
find_element(root->rChild, data);
}
8、不使用递归实现:按照深度优先的中序遍历二叉树上的所有结点
思路:使用栈进行存储
#include <stdio.h>
#include <stdlib.h>
#define TRUE 0
#define FALSE -1
typedef unsigned int uint;
typedef int ElemType ;
int sum = 0;
ElemType datas[1024];
//描述二分查找树的一个节点
typedef struct Node
{
ElemType data; //数据域
struct Node *lChild; //指向左子树的指针
struct Node *rChild; //指向右子树的指针
//记录结点被入栈的次数
int cntFlag;
}Node;
#define STACK_INIT_LEN 10
#define STACKINCREMENT 10
typedef Node * ElemType1;
typedef struct SqStack{
ElemType1 *top; //栈顶指针
ElemType1 *base; //栈底指针,指向存储栈的空间的首地址
uint stackLen; //栈能够存储的最大的元素的个数
uint len; //保存实际存储的元素的个数
}SqStack;
/*
* @brief 初始化一个顺序栈
* @param 初始顺序栈的长度
* @return 返回初始化后的栈的指针
* */
SqStack *stack_init(uint size)
{
//创建一个栈
SqStack *s = (SqStack *)malloc(sizeof(SqStack));
//栈分配存储空间
s->base = (ElemType1 *)malloc(size * sizeof(ElemType1));
s->top = s->base;
s->stackLen = size;
s->len = 0;
return s;
}
/*
* @brief 对栈进行扩容
* @param s 需要扩容的栈指针
* @return 成功返回TRUE, 失败返回FALSE
*/
int expand(SqStack *s)
{
printf("[%s %d] SqStack expand ...\n", __FUNCTION__ , __LINE__);
if (NULL == s)
{
printf("[%s %d] stack pointer is NULL ...\n", __FUNCTION__ , __LINE__);
return FALSE;
}
//为栈的存储空间重新分配空间
s->base = (ElemType1 *)realloc(s->base, (s->stackLen + STACKINCREMENT)*sizeof(ElemType1));
s->top = s->base + s->stackLen;
s->stackLen += STACKINCREMENT;
return TRUE;
}
/*
* @brief 入栈
* @param s 栈指针
* @param data 需要入栈的元素
* @return 成功返回TRUE, 失败返回FALSE
* */
int push(SqStack *s, ElemType1 data)
{
if (NULL == s)
{
printf("[%s %d] stack pointer is NULL ...\n", __FUNCTION__ , __LINE__);
return FALSE;
}
//先判断栈是否为满
if (s->top-s->base==s->stackLen)
{
printf("[%s %d] SqStack is FULL ..\n", __FUNCTION__ , __LINE__);
//对顺序栈进行扩容
expand(s);
}
//入栈
*(s->top) = data;
s->top++;
//s->len++;
return TRUE;
}
/*
* @brief 出栈
* @param s 栈指针
* @param data 存放栈顶元素的指针
* @return 成功返回TRUE, 失败返回FALSE
* */
int pop(SqStack *s, ElemType1 *data)
{
if (NULL == s || NULL == data)
{
printf("[%s %d] stack pointer is NULL ...\n", __FUNCTION__ , __LINE__);
return FALSE;
}
//先让栈顶指针--,指向栈顶的元素
s->top--;
*data = *(s->top);
return TRUE;
}
//遍历整个栈
int print_stack(SqStack *s)
{
ElemType1 *t = s->top;
while (t != s->base)
{
t--;
printf("%d ", *t);
}
printf("\n");
}
//判断栈是否为空,如果为空返回TRUE,如果不为空返回FALSE
int is_empty(SqStack *s)
{
if (s->top == s->base)
return TRUE;
return FALSE;
}
/*
* @brief 清除一个栈
* @param s 栈指针
* @return 成功返回TRUE, 失败返回FALSE
* */
int stack_clear(SqStack *s)
{
if (NULL == s)
{
printf("[%s %d] stack pointer is NULL ...\n");
return FALSE;
}
s->top = s->base;
return TRUE;
}
/*
* @brief 获得栈顶元素
* @param s 栈指针
* @param data 存放栈顶元素的指针
* @return 成功返回TRUE, 失败返回FALSE
* */
int get_top_elem(SqStack *s, ElemType1 *data) {
if (NULL == s || NULL == data)
return FALSE;
//获取栈顶元素
*data = *(s->top-1);
return TRUE;
}
//描述一棵二分查找树
typedef struct Tree
{
Node *root; //指向二分查找树的根节点的指针
}Tree;
/*
* @brief 初始化一棵二分查找树
* @return 返回二分查找树的指针
* */
Tree* tree_init()
{
Tree *t;
t = (Tree *)malloc(sizeof(Tree));
t->root = NULL;
return t;
}
/*
* @brief 创建一个新的结点
* @param data 结点的数据域的值
* @return 返回新创建的结点的地址
* */
Node *create_node(ElemType data)
{
Node *t = (Node *)malloc(sizeof(Node));
t->data = data;
t->lChild = NULL;
t->rChild = NULL;
t->cntFlag = 0;
return t;
}
/*
* @brief 插入一个结点,保持插入后的二分查找树依然是一棵二分查找树
* @param root 二分查找树的根节点指针的地址(二级指针)
* @param data 插入的结点上数据域的值
* @return 成功返回TRUE, 失败返回FALSE
* */
int bst_tree(Node **root, ElemType data)
{
if (NULL == root)
{
printf("[%s %d] root is NULL\n", __FUNCTION__ , __LINE__);
return FALSE;
}
//创建一个新的结点
Node *t = create_node(data);
//判断树是否为一棵空树,如果是一颗空树,新插入的结点就是树的根节点
if (NULL == *root)
{
*root = t; //根节点指针指向新的结点
return TRUE;
}
#if 0
//和根节点比较大小,如果比根节点小,插入到根节点的左子树中
if (data < (*root)->data)
{
bst_tree(&((*root)->lChild), data);
}
else if (data > (*root)->data) //和根节点比较大小,如果比根节点大,插入到根节点的右子树中
{
bst_tree(&((*root)->rChild), data);
} else
return FALSE;
#endif
//定义一个临时变量用来遍历二分查找树
Node *tmp = *root;
while (1)
{
//如果比遍历到的结点要小
if (data < tmp->data)
{
//如果该结点恰好没有左子树,将新的结点当作该节点的左子树
if (NULL == tmp->lChild)
{
tmp->lChild = t;
break;
}
else//如果该结点有左子树,继续遍历该结点的左子树
{
tmp = tmp->lChild;
continue;
}
}
else if (data > tmp->data)//如果比遍历到的结点要大
{
//如果该结点恰好没有右子树,将新的结点当作该节点的右子树
if (NULL == tmp->rChild)
{
tmp->rChild = t;
break;
}
else//如果该结点有右子树,继续遍历该结点的右子树
{
tmp = tmp->rChild;
continue;
}
} //如果与遍历到的结点相等
else
{
return FALSE; //退出遍历
}
}
return TRUE;
}
//以先序的方式遍历一棵树
void pre_traversal(Node *root)
{
if (NULL == root)
return ;
printf("%d\n", root->data); //打印根结点上的数据
datas[sum] = root->data;
sum++;
pre_traversal(root->lChild);
pre_traversal(root->rChild);
return ;
}
//以非递归方式,先序的方式遍历一棵树
void pre_traversal1(Node *root)
{
if (NULL == root)
return ;
//初始化一个栈
SqStack *s = stack_init(STACK_INIT_LEN);
//定义一个临时指针 遍历
Node *tmp = root;
while (1)
{
//如果栈为空并且出栈的结点的左子树和右子树全部为空 结束遍历
if (is_empty(s)==TRUE && tmp->lChild==NULL && tmp->rChild==NULL)
{
printf("%d ", tmp->data);
break;
}
//先将根节点的数据域打印出来
printf("%d ", tmp->data);
//将结点的右子树入栈
if (tmp->rChild != NULL)
push(s, tmp->rChild);
//将结点的左子树入栈
if (tmp->lChild != NULL)
push(s, tmp->lChild);
//出栈一个结点
pop(s, &tmp);
}
printf("\n");
}
//中序的方式遍历一棵树 左根右
void infix_traversal(Node *root)
{
if (NULL == root)
return ;
infix_traversal(root->lChild);
printf("%d\n", root->data);
datas[sum] = root->data;
sum++;
infix_traversal(root->rChild);
return ;
}
//非递归方式 中序的方式遍历一棵树 左根右
void infix_traversal1(Node *root)
{
if (NULL == root)
return ;
//初始化一个栈
SqStack *s = stack_init(STACK_INIT_LEN);
//定义一个临时指针 遍历
Node *tmp = root;
while (1)
{
//一直往左边遍历,只要遍历到的结点的左子树不为空,入栈
while (tmp)
{
push(s, tmp);
tmp = tmp->lChild;
}
if (is_empty(s) != TRUE)
{
//出栈
pop(s, &tmp);
printf("%d ", tmp->data);
tmp = tmp->rChild;
} else
{
break;
}
}
printf("\n");
}
//后序的方式遍历一棵树
void epilogue_traversal(Node *root)
{
if (NULL == root)
return ;
epilogue_traversal(root->lChild);
epilogue_traversal(root->rChild);
printf("%d\n", root->data);
datas[sum] = root->data;
sum++;
return ;
}
//后序的方式遍历一棵树
void epilogue_traversal1(Node *root)
{
if (NULL == root)
return ;
//初始化一个栈
SqStack *s = stack_init(STACK_INIT_LEN);
//定义一个临时指针 遍历
Node *tmp = root;
while (1)
{
//一直往左边遍历,只要遍历到的结点的左子树不为空,入栈
while (tmp)
{
tmp->cntFlag++;
push(s, tmp);
tmp = tmp->lChild;
}
if (is_empty(s) != TRUE)
{
//出栈
pop(s, &tmp);
//判断该结点是否被入栈了两次
if (tmp->cntFlag < 2)
{
tmp->cntFlag++;
push(s, tmp); //重新入栈
//遍历这个结点的右子树
tmp = tmp->rChild;
} else
{
tmp->cntFlag = 0;
printf("%d ", tmp->data);
tmp = NULL;
}
} else
break;
}
printf("\n");
}
/*
* @brief 查找二分查找树的最小值
* @param root 树的根节点指针
* @return 最小值
* */
ElemType find_min(Node *root)
{
if (NULL == root)
return -1;
//从根节点开始遍历,一直遍历左子树,直到某个结点的左子树为空
#if 0
//非递归方式
Node *tmp = root;
while (tmp->lChild != NULL)
{
tmp = tmp->lChild;
}
return tmp->data;
#endif
if (NULL == root->lChild)
return root->data;
find_min(root->lChild);
}
/*
* @brief 查找二分查找树的最大值
* @param root 树的根节点指针
* @return 最大值
* */
ElemType find_max(Node *root)
{
if (NULL == root)
return -1;
//从根节点开始遍历,一直遍历右子树,直到某个结点的右子树为空
#if 1
//非递归方式
Node *tmp = root;
while (tmp->rChild != NULL)
{
tmp = tmp->rChild;
}
return tmp->data;
#endif
#if 0
if (NULL == root->rChild)
return root->data;
find_max(root->rChild);
#endif
}
/*
* @brief 查找二分查找树中指定的结点
* @param root 根节点
* @param data 需要查找的结点的值
* @return 成功返回查找到的结点的地址,失败返回NULL
* */
Node *find_elem(Node *root, ElemType data)
{
if (NULL == root)
{
printf("[%s %d] can not find element: %d\n", __FUNCTION__ , __LINE__, data);
return NULL;
}
#if 0
Node *tmp = root;
while (1)
{
//从根节点开始遍历,如果比遍历到的节点小
if (data < tmp->data)
{
if (tmp->lChild == NULL) //并且该该节点没有左子树,认为结点不在树上
{
printf("[%s %d] can not find element: %d\n", __FUNCTION__ , __LINE__, data);
return NULL;
}
else //如果比遍历到的节点小并且该该节点有左子树,继续遍历左子树
{
tmp = tmp->lChild;
continue;
}
}
else if (data > tmp->data)//如果比遍历到的节点大
{
if (NULL == tmp->rChild) //并且该节点没有右子树,认为结点不在树上
{
printf("[%s %d] can not find element: %d\n", __FUNCTION__ , __LINE__, data);
return NULL;
}
else //如果比遍历到的节点大并且该节点有右子树,继续遍历右子树
{
tmp = tmp->rChild;
continue;
}
} else //如果跟遍历到的结点相等,找到
{
return tmp;
}
}
#endif
if (data == root->data)
return root;
if (data < root->data)
find_elem(root->lChild, data);
else if (data > root->data)
find_elem(root->rChild, data);
}
int main() {
printf("%f\n", 3/2);
//初始化一颗二分查找树
Tree *t;
t = tree_init();
bst_tree(&(t->root), 9);
bst_tree(&(t->root), 5);
bst_tree(&(t->root), 13);
bst_tree(&(t->root), 2);
bst_tree(&(t->root), 7);
bst_tree(&(t->root), 12);
bst_tree(&(t->root), 15);
bst_tree(&(t->root), 14);
bst_tree(&(t->root), 4);
#if 0
infix_traversal(t->root);
int i;
for (i = 0; i < sum; i++)
printf("%d ", datas[i]);
printf("\n");
sum = 0;
printf("min: %d\n", find_min(t->root));
printf("max: %d\n", find_max(t->root));
Node *tmp;
tmp = find_elem(t->root, 13);
if (tmp != NULL)
printf("yes\n");
else
printf("no\n");
#endif
//使用非递归的方式先序遍历
pre_traversal1(t->root);
infix_traversal1(t->root);
epilogue_traversal1(t->root);
return 0;
}
9、不使用递归实现:按照层次遍历二叉树上的所有结点
思路:使用队列进行存储
void travel(Tree *tree, List *queue)
{
if (NULL == tree || NULL == queue)
return;
//用一个临时指针指向树的根结点
TreeNode *root = tree->root;
while (1)
{
printf("%d ", root->data);
if (root->lChild==NULL && root->rChild==NULL && is_empty(queue))
break;
//判断是否有左子树
if (root->lChild != NULL)
enqueue(queue, root->lChild);
//判断是否有右子树
if (root->rChild != NULL)
enqueue(queue, root->rChild);
//从队列上出队
root = dequeue(queue);
}
}
4.4 平衡二叉树
4.4.1 平衡二叉树概述
1、引入
之前我们学习过二叉查找树,发现它的查询效率比单纯的链表和数组的查询效率要高很多,最理想的情况下时间复杂度可以达到O(logn),大部分情况下,确实是这样的,但不幸的是,在最坏情况下,二叉查找树的性能还是很糟糕。
例如我们依次往二叉查找树中插入9,8,7,6,5,4,3,2,1这9个数据,那么最终构造出来的树是长得下面这个样子:
在极端的情况下,二分查找树可能会退化成为链表,我们会发现,如果我们要查找1这个元素,查找的效率依旧会很低。效率低的原因在于这个树并不平衡,全部是向左边分支,如果我们能够把这棵二分查找数进行调整让左右子树的高度相等,并且左右子树的结点数也趋近于相等,那么查找效率就会大大提高,我们可以将这棵树调整成为一棵平衡二叉树。
2、平衡二叉树的概念
平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1,平衡二叉树又称为AVL树。平衡二叉树是一种高度平衡的二叉排序树,意思是说,要么它是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。
3、平衡因子
平衡因子(BF,Balance Factor 而不是 Boy Friend)指的是:左子树和右子树高度差。一般来说 BF 的绝对值大于 1,,平衡树二叉树就失衡,需要「旋转」纠正。
4、最小失衡子树/最小不平衡子树
距离插结点点最近的,并且 BF 的绝对值大于 1 的结点为根节点的子树叫做最小失衡子树。
4.4.2 平衡二叉树的旋转
1、2 种「旋转」方式:
-
左旋
-
旧根节点为新根节点的左子树
-
新根节点的左子树(如果存在)为旧根节点的右子树
-
-
右旋:
-
旧根节点为新根节点的右子树
-
新根节点的右子树(如果存在)为旧根节点的左子树
-
2、4 种「旋转」纠正类型:
-
LL 型:插入左孩子的左子树,右旋
-
RR 型:插入右孩子的右子树,左旋
-
LR 型:插入左孩子的右子树,先左旋,再右旋
-
RL 型:插入右孩子的左子树,先右旋,再左旋
3、LL 型失衡「右旋」
第三个节点「1」插入的 时候,BF(3) = 2,BF(2) = 1 LL 型失衡,右旋,根节点顺时针旋转。
-
旧根节点(节点 3)为新根节点(节点 2)的右子树
-
新根节点的 右子树 (如果存在)为旧根节点的左子树
4、RR 型失衡「左旋」
第三个节点「3」插入的 时候,BF(1)=-2 BF(2)=-1,RR 型失衡,左旋,根节点逆时针旋转。
-
旧根节点(节点 1)为新根节点(节点 2)的左子树
-
新根节点的左子树(如果存在)为旧根节点的右子树
5、LR型
第三个节点「3」插入的 时候,BF(3)=2 BF(1)=-1 LR 型失衡,先「左旋」再「右旋」。
左旋:
-
旧根节点(节点 1)为新根节点(节点 2)的左子树
-
新根节点的左子树(如果存在)为旧根节点的右子树
右旋:
-
旧根节点(节点 3)为新根节点(节点 2)的右子树
-
新根节点的 右子树(如果存在)为旧根节点的左子树
6、RL型
第三个节点「1」插入的 时候,BF(1)=-2 BF(3)=1 RL 型失衡,先「右旋」再「左旋」
右旋:
-
旧根节点(节点 3)为新根节点(节点 2)的右子树
-
新根节点的 右子树(如果存在)为旧根节点的左子树
左旋:
-
旧根节点(节点 1)为新根节点(节点 2)的左子树
-
新根节点的左子树(如果存在)为旧根节点的右子树
7、实例
接下来我们以 {3,2,1,4,5,6,7,10,9,8} 为实例练习刚刚的 4 种插入方式。
(1)依次插入 3、2、1 插入第三个点 1 的时候 BF(3)=2 BF(2)=1,LL 型失衡,右旋:
(2)依次插入 4 ,5 插入 5 点的时候 BF(3) = -2 BF(4)=-1,RR 型失衡,左旋:
(3)插入 4 ,5 插入 5 点的时候 BF(2)=-2 BF(4)=-1 ,RR 型失衡 对最小不平衡树进行「左旋」
新根节点(节点 4)的左子树(节点 3)为旧根节点的右子树
(4)插入 7 节点的时候 BF(5)=-2, BF(6)=-1 ,RR 型失衡,对最小不平衡树 进行「左旋」:
(5)依次插入 10 ,9 。插入 9 点的时候 BF(10) = 1,BF(7) = -2 ,RL 型失衡,对先「右旋」再「左旋」:
-
右子树先「右旋」
-
最小不平衡子树再左旋:
(6)最后插入节点 8 ,BF(6)=-2 BF(9)=1,RL 型失衡,先「右旋」再「左旋」:
-
最小不平衡子树的右子树 {9,7,10,8} 先「右旋」
-
最小不平衡子树 {6,5,7,9,8,10} 再「左旋」
-
左旋结束
4.5 红黑树
1、红黑树的概念
红黑树也叫 R-B Tree,全称是Red-Black Tree,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
2、红黑树的应用
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。 例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
3、红黑树代码
#include <stdio.h>
#include <stdlib.h>
const int RED = 0;
const int BLACK = 1;
struct rb_node{
rb_node* lchild, *rchild, *parent;
int key, colour;
};
rb_node* root;
rb_node* get_node(rb_node* parent, int key);
void rb_insert(int key);
rb_node* rb_search(int key);
void rb_delete(int key);
rb_node* clock_wise(rb_node* node);
rb_node* counter_clock_wise(rb_node* node);
void show_rb_tree(rb_node* node);
rb_node* get_node(rb_node* parent, int key){
rb_node *tmp = (rb_node*)malloc(sizeof(rb_node));
tmp->key = key;
tmp->colour = RED;
tmp->parent = parent;
tmp->lchild = tmp->rchild = NULL;
return tmp;
}
rb_node* clock_wise(rb_node* node){
if(node == NULL || node->lchild == NULL)
return NULL;
rb_node *rb_1=node, *rb_2=node->lchild, *rb_3=node->lchild->rchild;
if(rb_1->parent != NULL){
if(rb_1->parent->lchild == rb_1)
rb_1->parent->lchild = rb_2;
else
rb_1->parent->rchild = rb_2;
}else if(rb_1 == root){
root = rb_2;
}
rb_2->parent = rb_1->parent;
rb_1->parent = rb_2;
rb_2->rchild = rb_1;
rb_1->lchild = rb_3;
if(rb_3 != NULL)rb_3->parent = rb_1;
return rb_2;
}
rb_node* counter_clock_wise(rb_node* node){
if(node == NULL || node->rchild == NULL)
return NULL;
rb_node *rb_1=node, *rb_2=node->rchild, *rb_3=node->rchild->lchild;
if(rb_1->parent != NULL){
if(rb_1->parent->lchild == rb_1)
rb_1->parent->lchild = rb_2;
else
rb_1->parent->rchild = rb_2;
}
else if(rb_1 == root){
root = rb_2;
}
rb_2->parent = rb_1->parent;
rb_1->parent = rb_2;
rb_2->lchild = rb_1;
rb_1->rchild = rb_3;
if(rb_3 != NULL)rb_3->parent = rb_1;
return rb_2;
}
rb_node* rb_search(int key){
rb_node *p = root;
while(p != NULL){
if(key < p->key)
p = p->lchild;
else if(key > p->key)
p = p->rchild;
else
break;
}
return p;
}
void rb_insert(int key){
rb_node *p=root, *q=NULL;
if(root == NULL){
root = get_node(NULL, key);
root->colour = BLACK;
return;
}
while(p != NULL){
q = p;
if(key < p->key)
p = p->lchild;
else if(key > p->key)
p = p->rchild;
else return;
}
if(key < q->key)
q->lchild = get_node(q, key);
else
q->rchild = get_node(q, key);
while(q != NULL && q->colour == RED){
p = q->parent;//p won't null, or BUG.
if(p->lchild == q){
if(q->rchild != NULL && q->rchild->colour == RED)
counter_clock_wise(q);
q = clock_wise(p);
q->lchild->colour = BLACK;
}
else{
if(q->lchild != NULL && q->lchild->colour == RED)
clock_wise(q);
q = counter_clock_wise(p);
q->rchild->colour = BLACK;
}
q = q->parent;
}
root->colour = BLACK;
}
void show_rb_tree(rb_node* node){
if(node == NULL)
return;
printf("(%d,%d)\n", node->key, node->colour);
if(node->lchild != NULL){
printf("[-1]\n");
show_rb_tree(node->lchild);
}
if(node->rchild != NULL){
printf("[1]\n");
show_rb_tree(node->rchild);
}
printf("[0]\n");
}
void rb_delete(int key){
rb_node *v = rb_search(key), *u, *p, *c, *b;
int tmp;
if(v == NULL) return;
u = v;
if(v->lchild != NULL && v->rchild != NULL){
u = v->rchild;
while(u->lchild != NULL){
u = u->lchild;
}
tmp = u->key;
u->key = v->key;
v->key = tmp;
}
//u is the node to free.
if(u->lchild != NULL)
c = u->lchild;
else
c = u->rchild;
p = u->parent;
if(p != NULL){
//remove u from rb_tree.
if(p->lchild == u)
p->lchild = c;
else
p->rchild = c;
}
else{
//u is root.
root = c;
free((void*)u);
return;
}
//u is not root and u is RED, this will not unbalance.
if(u->colour == RED){
free((void*)u);
return;
}
free((void*)u);
u = c;
//u is the first node to balance.
while(u != root){
if(u != NULL && u->colour == RED){
//if u is RED, change it to BLACK can finsh.
u->colour = BLACK;
return;
}
if(u == p->lchild)
b = p->rchild;
else
b = p->lchild;
printf("%d\n", b->key);
//b is borther of u. b can't be null, or the rb_tree is must not balance.
if(b->colour == BLACK){
//If b's son is RED, rotate the node.
if(b->lchild != NULL && b->lchild->colour == RED){
if(u == p->lchild){
b = clock_wise(b);
b->colour = BLACK;
b->rchild->colour = RED;
p = counter_clock_wise(p);
p->colour = p->lchild->colour;
p->lchild->colour = BLACK;
p->rchild->colour = BLACK;
}
else{
p = clock_wise(p);
p->colour = p->rchild->colour;
p->rchild->colour = BLACK;
p->lchild->colour = BLACK;
}
return;
}
else if(b->rchild != NULL && b->rchild->colour == RED){
if(u == p->rchild){
b = counter_clock_wise(b);
b->colour = BLACK;
b->lchild->colour = RED;
p = clock_wise(p);
p->colour = p->rchild->colour;
p->rchild->colour = BLACK;
p->lchild->colour = BLACK;
}
else{
p = counter_clock_wise(p);
p->colour = p->lchild->colour;
p->lchild->colour = BLACK;
p->rchild->colour = BLACK;
}
return;
}
else{//if b's sons are BLACK, make b RED and move up u.
b->colour = RED;
u = p;
p = u->parent;
continue;
}
}
else{
if(u == p->lchild){
p = counter_clock_wise(p);
p->colour = BLACK;
p->lchild->colour = RED;
p = p->lchild;
}
else{
p = clock_wise(p);
p->colour = BLACK;
p->rchild->colour = RED;
p = p->rchild;
}
}
}
root->colour = BLACK;
}
int main(){
int i;
root = NULL;
for(i = 1; i <= 10; i++){
rb_insert(i);
}
rb_delete(9);
rb_delete(3);
rb_delete(7);
show_rb_tree(root);
printf("\n");
return 0;
}
4.6 哈夫曼树
思考:加入在网络通信中,我们需要将一篇英文文章发送给通信的另一方,在带宽一定的前提下,如何做到尽快传输呢?
4.6.1 哈夫曼树概述
1、哈夫曼树的概念
当用 n 个结点(都做叶子结点且都有各自的权值)试图构建一棵树时,如果构建的这棵树的带权路径长度最小,称这棵树为“最优二叉树”。
2、哈夫曼树相关术语
-
路径和路径长度
-
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径(在上图种,从根结点到结点 a 之间的通路就是一条路径。)。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1(上图中从根结点到结点 c 的路径长度为 3。);
-
-
结点的权
-
给每一个结点赋予一个新的数值,被称为这个结点的权。“权”一般代表“重要度”、概率等,我们形象的用一个具体的数字来表示,然后通过数字的大小来决定谁重要,谁不重要,谁的概率大,谁的概率低。
-
-
结点的带权路径长度
-
结点到根结点的路径长度乘以该节点的权(上图中结点 b 的带权路径长度为 2 * 5 = 10 )。
-
-
树的带权路径长度
-
树中各个叶结点的路径长度该叶节点的权的和(各叶子结点的带权路径长度之和),常用WPL(Weight Path Length)表示。
-
上图所示的这颗树的带权路径长度为:WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3
-
4.6.2 哈夫曼树的构建方法
第一步: 我们将所有的节点都作为独根结点。
第二步: 我们将最小的两个结点C和A组建为一个新的二叉树,权值为左右结点之和。
第三步: 将上一步组建的新节点加入到剩下的节点中,排除上一步组建过的左右子树,我们选中B组建新的二叉树,然后取权值。
第四步: 同上。
4.6.3 哈夫曼树的访问
-
当哈夫曼树构造好以后,我们该如何访问到哈夫曼树上的叶子结点呢?
-
当我们在构造哈夫曼树时可以通过一定的的方法获取到每个叶子结点从根节点开始的访问路径,如果向左记为0,向右记为1,最终每个结点都可以用若干个0 1组成一个编码,该编码我们称之为“哈夫曼编码”。
-
-
假如有如下哈夫曼树
4.6.4 哈夫曼树的应用
哈夫曼编码是一种编码方式,可以用于无损数据压缩。编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
4.7 B树
4.7.1 B树的基本概念
B树是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logn)的时间复杂度进行查找、顺序读取、插入和删除等操作。
假如当前有一颗m阶的B树(注意阶的意思是指每个节点的孩子节点的个数),那么其符合:
(1)每个节点最多有m个子节点
(2)除了根节点和叶子节点之外,其他的每个节点最少有m/2(向上取整)个孩子节点
(3)根节点至少有两个孩子节点,(除了第一次插入的时候,此时只有一个节点,根节点同时是叶子节点)
(4)所有的叶子节点都在同一层
(5)有k个子节点的父节点包含k-1个关键码
除了上面B树的性质外,B树还有几个特点:
1,树高平衡,所有的叶节点都在同一层
2,关键码没有重复,父节点中的关键码是其子节点的分解
3,B树保证树种至少有一部分比例的节点是满的。为什么这样说,在上面的性质2中,我们知道每个节点最少可以有 m/2个节点,注意这刚好是一半,没有太满,是因为可以给后续的添加,删除留有余地,这样以来节点不会频繁的触发不平衡,没有太空则意味着B树能够 保证降低树的高度。
4.7.2 B树的插入
插入规则:在叶子结点上插入结点
假设现在构建一棵四阶B树,开始插入“30”,直接作为根节点
插入“60”,大于“30”,放右边
插入“90”,大于“60”,放右边
继续插入“120”,直接添加的结果如下图,此时超过了节点可以存放容量,对于四阶B树每个节点最多存放3个值,此时需要执行分裂操作
分裂操作为,先选取待分裂节点的中值,这里为“60”,然后将中值“60”放到父节点中,因为这里还没有父节点,那么直接创建一个新的父节点存放“60”,而原来小于“60”的那些值作为左子树,原来大于“60”的那些值作为右子树
4.7.3 B树的查找
对B树进行查找就比较简单,查找过程有点类似二叉搜索树,从根节点开始查找,根据比较数值找到对应的分支,继续往子树上查找。
比如查找“250”,"250"大于“95”,往右子树,“250”介于“120 300”之间,往第二个分支,在第二个分支中可以找到250。
4.7.4 B树的应用
文件系统中对磁盘数据的存储:
我们知道,数据是存储在磁盘中的,计算机操作磁盘上的文件是通过文件系统进行操作的,在文件系统中就使用到了B树这种数据结构。
磁盘由盘片构成,每个盘片有两面,又称为盘面 。盘片中央有一个可以旋转的主轴,他使得盘片以固定的旋转速率旋转,通常是5400rpm或者是7200rpm,一个磁盘中包含了多个这样的盘片并封装在一个密封的容器内 。盘片的每个表面是由一组称为磁道同心圆组成的 ,每个磁道被划分为了一组扇区 ,每个扇区包含相等数量的数据位,通常是512个子节,扇区之间由一些间隙隔开,这些间隙中不存储数据 。
磁盘用磁头来读写存储在盘片表面的数据,由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,因此为了提高效率,要尽量减少磁盘 I/O,减少读写操作。 为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此预读可以提高I/O效率。
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(一般为4096字节),预读的长度一般为页的整倍数。主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
文件系统的设计者利用了磁盘预读原理,将一个结点的大小设为等于一个页,这样每个结点只需要一次I/O就可以完全载入。
这样设计有什么好处呢?
在实际设计中,我们把 一个结点设为一个页 ,
4.8 B+树
4.8.1 B+树的基本概念
B+树是B树的升级。
-
不同于B树,B+树的非叶子节点不再保存关键字记录的指针,只进行数据索引
-
B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取,所以每次查询效率一样
-
所有叶子结点都有一个指向右边叶子节点的指针
-
所有数据都保存在叶子结点
4.8.2 B+树的插入
B+树的插入与B树类似。
下面是一颗5阶B树的插入过程,5阶B数的结点最少2个key,最多4个key。
1)空树中插入5
2)依次插入8,10,15
3)插入16
插入16后超过了关键字的个数限制,所以要进行分裂。在叶子结点分裂时,分裂出来的左结点2个记录,右边3个记录。
4.8.3 B+树的应用
Mysql数据库中使用B+树做索引:
在对数据库进行查询时,我们可能经常会使用类似如下查询语句:select name from table where ID >=80 and ID <=90,去查找某个区间的内容。
因为B+树的所有叶子节点是一种链式结构,因此在查找80到90之间的所有数据时,我们只需要找到80,然后沿着链表往后遍历即可找到80到90之间的所有数据。
思考:如果使用B树做索引效率比B+树高还是低呢?
4.9 哈希表
4.9.1 哈希表的引入
-
思考1:假如使用一种数据结构存储全校学生的相关信息,请问如何迅速查找到该学生?
-
思考2:我们在应用程序开发过程中会定义很多的变量和函数,当编译器编译完后,会给某些变量和函数在内存中分配地址,程序在运行的时候是如何快速访问到这些变量和函数的呢?
4.9.2 哈希表的概念
-
我们必须先了解一种新的存储方式—散列技术: 散列技术是指在记录的存储位置和它的关键字(key)之间建立一个确定的对应关系f,使每一个关键字都对应一个存储位置。即:存储位置=f(关键字)。这样,在查找的过程中,只需要通过这个对应关系f 找到给定值key的映射f(key),得到对应的存储位置,再将对应存储位置中存储的关键字和需要查找的关键字进行比较即可。
-
我们把这种对应关系f 称为散列函数或哈希函数。
-
按照这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表。所得的存储地址称为哈希地址或散列地址。
-
数据的哈希地址=f(关键字的值)
-
-
通过关键字求哈希地址的过程称之为:哈希
4.9.3 哈希表的本质
-
哈希表的本质:数组 。
-
通过哈希函数得到的哈希地址(哈希值)其实就是数组的下标或者索引。
4.9.4 哈希函数的构造方法
4.9.4.1 哈希函数的构造的原则
-
计算简单:哈希函数不应该有很大的计算量,否则会降低查找效率。
-
分布均匀:哈希函数值即散列地址,要尽量均匀分布在地址空间,这样才能保证存储空间的有效利用并减少冲突。
4.9.4.2 直接定址法
哈希函数是关键字值的线性函数,即:h(key) = a * key + b (a,b为常数)
-
假设需要统计中国人口的年龄分布,以10为最小单元。今年是2018年,那么10岁以内的分布在2008-2018,20岁以内的分布在1998-2008……假设2018代表2018-2008直接的数据,那么关键字应该是2018,2008,1998……
-
那么可以构造哈希函数H(key)=(2018-key)/10
-
那么hash表建立如下:
-
4.9.4.3 除留余数法
设有一组关键字,试用除留余数法求设计哈希函数。关键字组:(19,14,23,01,68,20,84,27,55,11,10,79,12,39,21)。 可以设置为H(key)=key%13;为什么取13?我们可以假设它取9,得出的余数分别为1 5 5 1 5 2 3 0 1 2 1 7 3 3 3。可以看出1 3 5的余数非常多,当它映射到存储地址时,会更容易的造成冲突。所以我们在选择取余数时,一般取质数,让它的冲突尽可能的少。
4.9.4.4 数字分析法
如果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。
例如下表中列举的是一部分关键字,每个关键字都是有 8 位十进制数组成:
通过分析关键字的构成,很明显可以看到关键字的第 1 位和第 2 位都是固定不变的,而第 3 位不是数字 3 就是 4,最后一位只可能取 2、7 和 5,只有中间的 4 位其取值近似随机,所以为了避免冲突,可以从 4 位中任意选取 2 位作为其哈希地址。
4.9.4.5 折叠法
是将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。
例如,在图书馆中图书都是以一个 10 位的十进制数字为关键字进行编号的,若对其查找表建立哈希表时,就可以使用折叠法。
若某书的编号为:0-442-20586-4,分割方式如图 1 中所示,在对其进行折叠时有两种方式:一种是移位折叠,另一种是间界折叠:
-
移位折叠是将分割后的每一小部分,按照其最低位进行对齐,然后相加,如图 1(a);
-
间界折叠是从一端向另一端沿分割线来回折叠,如图 1(b)。
4.9.5 哈希冲突
在哈希函数的设计过程中,不管哈希函数设计的如何巧妙,总会有特殊的key导致哈希冲突。
解决哈希冲突主要有如下方法:
-
开放寻址法/开放定址法
-
再哈希法
-
拉链法/链地址法
-
建立公共溢出区
4.9.5.1 开放寻址法
1、开放寻址法的概念
开放寻址法指的是:当哈希值p = H(key)出现冲突时,以p为基础,产生另一个哈希值,如果p1仍然冲突,再以p为基础,产生另一个哈希值p2,…,直到找到一个不冲突的还行值