知识提要:
1.随机访问:对数组而言,可以使用数组下标直接访问该数组中的任意元素,这叫做随机访问.
2.顺序访问:对链表而言,必须从链表首节点开始,逐个节点移动到要访问的节点,这叫做顺序访问.
3.顺序查找:从列表的开头按顺序查找.
4.二分查找:(1)规定待查找的项为目标项;(2)假设列表中的各项按字母排序,比较列表中的中间项和目标项;(3)如果相等,则查找结束;如果目标项在列表中,中间项在目标项前面,则目标项一定在列表的后半部分…(4)再取后半部分的中间项,一直按这种思路取下去…
5.数组很好完成这种二分查找,但是链表不容易完成-------所以诞生了一种既支持频繁插入和删除项(链表轻松胜任)又支持频繁查找的(数组轻松胜任)数据形式------二叉查找树.
以C Prime Plus 第17章的代码------宠物店分析举例:
下面进行代码分析:(只分析二叉查找树的定义代码(不分析声明代码和具体实现代码))
一.数据结构的定义:
1.项的建立
/*---项的建立*/
#define SLEN 20//成员中的数组的长度
typedef struct item
{
char petname[SLEN];
char petkind[SLEN];
}Item;
2.节点的建立
/*---节点的建立*/
typedef struct trnode
{
Item item;//项
struct trnode* left;//左节点指针
struct trnode* right;//右节点指针
}Trnode;
3.树的建立
/*---树的建立*/
typedef struct tree
{
Trnode * root;//树指针
int size;//树中节点数
}Tree;
二.二叉树函数的一些定义
1.几个简单的"属性类(反映一些树的信息)"函数
void InitializeTree(Tree* ptree)
{
ptree->root = NULL;
ptree->size = 0;
}
bool TreeIsEmpty(const Tree* ptree)
{
if (ptree->size == 0)
return true;
else
return false;
}
bool TreeIsFull(const Tree* ptree)
{
if (ptree->size == MAXITEMS)
return true;
else
return false;
}
int TreeItemCount(const Tree* ptree)
{
return ptree->size;
}
(1)初始化:让指向树的指针(ptree->root)指向null,树中的项(ptree->size)为0;
(2)判断------树是否是空,满和树中的节点数:通过判断树中的项数(ptree->size)的情况判断.
2.复杂一点的"属性类"操作------判断具体的项是否在数中
bool InTree(const Item* pitem, const Tree* ptree)
{
/*需要进行寻找--1.需要定义函数实现---以上*/
return (SeekItem(pitem, ptree)).chlid == NULL ? false : true;//.chlid就是表示没找到 找到的话这个值不会为null(两种情况 1.还没开始找(就不存在树) 2.找了没找到(树里面没有))
}
这里用到了一个辅助函数SeekItem(pitem, ptree)
static Pair SeekItem(const Item* pitem, const Tree* ptree)
{
//声明变量---pair结构
Pair look;
//初始化变量
look.chlid = ptree->root;//从顶点开始寻找
look.parent = NULL;
//如果没有对应的树---开始让chlid节点指向树的根节点---没有顶点
if (look.chlid == NULL)
return look;//终止函数 提前返回
//如果有对应的树---直到指针为空
while (look.chlid != NULL)
{
/*向左边寻找*/
if (ToLeft(pitem, &look.chlid->item))//带寻找项与树中的项相比 如果在树中项顺序的后边
{
//向左蔓延
look.parent = look.chlid;
look.chlid = look.chlid->left;
}
else if (ToRight(pitem, &look.chlid->item))
{
//向右蔓延
look.parent = look.chlid;
look.chlid = look.chlid->right;
}
else
break;// 找到了 不用向下探寻了(没有重复项) 当前子节点就是找到的节点
}
return look;
}
这个辅助函数中定义了一种辅助工具(Pair look)来帮忙查找
typedef struct pair
{
Trnode* parent;//记录作用
Trnode* chlid;//子节点为探照节点 与树中的节点进行信息比对
}Pair;
这个数据中由两个指向节点的指针构成.用来查找和记录位置使用
这个辅助函数中用到了其他的辅助函数ToLeft,ToRight
static bool ToLeft(const Item*item1, const Item*item2)
{
//定义strcmp接受变量
int cmp1, cmp2;
//比较两个指针指向的字符串 ---如果第一个字符串在前面 就返回true (strcmp函数返回<0)
//---宠物名的比较(先比宠物名)
if ((cmp1 = strcmp(item1->petname, item2->petname)) < 0)
return true;
//---宠物种类的比较(宠物名相同就比宠物种类)
else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) < 0))
return true;
else
return false;
}
static bool ToRight(const Item*item1, const Item*item2)
{
//定义strcmp接受变量
int cmp1, cmp2;
//比较两个指针指向的字符串 ---如果第一个字符串在前面 就返回true (strcmp函数返回<0)
//---宠物名的比较(先比宠物名)
if ((cmp1 = strcmp(item1->petname, item2->petname)) > 0)
return true;
//---宠物种类的比较(宠物名相同就比宠物种类)
else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) > 0))
return true;
else
return false;
}
这里只分析ToLeft函数,(ToRight函数同理)
这个函数比较比较项1和项2中的内容字母大小的排序(名字:strcmp(item1->petname, item2->petname))(名字相同就比种类(cmp1 = strcmp(item1->petkind, item2->petkind)) < 0)):结果是如果第一项小就返回true…
回到寻找节点函数SeekItem:现在分析这个函数的流程:
1.声明查找结构并进行初始化
2.判断是否有查找的主体(也就是确定树存不存在)
3.如果树存在就按照规定的方向查找(ToLeft,ToRight),这个就是二分查找的精髓
(1)将要查找的项先从根节点开始查找不停地用二分法查找
(2)每进行一个比对就进行查找数据的更新(父指针指向子指针,子指针指向子指针的查找方向)
(3)直到找到目标项或者找遍了所有节点.------找到了会返回查找数据的(.chlid就是指向目标节点)
在回到InTree函数:如果找到了(查找数据的.chlid成员变量不为空)就会返回true.
3.添加项函数
bool AddItem(const Item* pitem, Tree* ptree)
{
//创建一个新的节点(添加项是通过添加节点实现)---这里并不是创建 而是提出一个概念(因为不一定能创建 看有没有违反要求) 真正的创建要进行分配空间和初始化 3.创建节点函数实现
Trnode * new_node;
//判断树的情况---满了是不能添加节点的
if (TreeIsFull(ptree))
{
fprintf(stderr, "树是满的 不能添加项了 你的明白???\n");
return false;
}
//如果要添加的项已经存在 就不能进行添加----不能有重复项
if (SeekItem(pitem, ptree).chlid != NULL)
{
fprintf(stderr, "你添加的项在树中已经存在 莫添加了 ok?\n");
return false;
}
//节点的正式创建---分配内存 初始化
new_node = MakeNode(pitem);
//如果创建失败---分配空间失败
if (new_node == NULL)
{
fprintf(stderr, "哦豁,创建节点失败~\n");
return false;
}
//成功创建节点
//---树中的节点树增加
ptree->size++;
//---如果树是空的---------------------不能用TreeIsEmpty判断 因为这个函数是通过判断size来判断的 而现在的情况是 先初始化了size
if (ptree->root==NULL)
//------树的根节点就是这个新节点
ptree->root = new_node;
else//---如果树不是空的
{
//------执行添加节点函数-----创建4.添加节点函数
AddNode(new_node, ptree->root);
}
}
添加项目:
(1) 创建指向节点的指针
(2)如果创建成功(树中的节点没满),判断这个项是否存在(SeekItem)
(3)这个项如果不存在就进行初始化节点(MakeNode(pitem)😉----用到了辅助函数
static Trnode* MakeNode(const Item* pitem)
{
//创建一个节点
Trnode* new_node;
//分配空间
new_node = (Trnode*)malloc(sizeof(Trnode));
//成员变量的初始化
if (new_node != NULL)
{
new_node->item = *pitem;
new_node->left = NULL;
new_node->right = NULL;
}
return new_node;
}
(4)成功创建节点后,判断节点的位置(作为根节点还是子节点),如果是根节点(就让树的根节点指向这个新节点),如果是子节点则执行子节点操作函数-------辅助函数添加子节点
static void AddNode(Trnode* new_node, Trnode* root)
{
/*要把新节点中的内容与树中节点进行比较后才能插入*/
//到达一个节点时 如果新节点的内容比树节点的内容小
if (ToLeft(&new_node->item, &root->item))
{
//--如果这个树节点的左侧是空的
if (root->left == NULL)
{
//---就把新节点添加到树节点的左侧
root->left = new_node;
}
else
{
//--左侧不是空的
//---继续调用这个函数寻找---这次跟树节点的左边指向的节点比
AddNode(new_node, root->left);
}
}
else if(ToRight(&new_node->item,&root->item))//如果发现这个新节点的内容比这个节点下面所有的节点内容都大
{
//--如果右侧是空的
if (root->right == NULL)
{
//就把这个新节点放在此位置
root->right = new_node;
}
else//--如果不是空的
{
//就调用这个函数继续寻找
AddNode(new_node, root->right);
}
}
else//如果发现新节点的内容不大不小 ---就是内容重复--输出错误
{
fprintf(stderr, "你添加的东西重复了 懂?\n");
exit(EXIT_FAILURE);
}
}
这个辅助函数使用了递归来添加,具体流程是:
(1)将要添加的节点的项的内容与树中节点的项的内容做对比(插入也要根据二分法插入)
(2)如果比树中的当前节点小
1.如果当前节点左边是空的(root->left == NULL)那么把要添加的项放入左边
2.如果不是空的,那么将对比的内容换成--------需要添加的项与此节点左指针指向的节点进行比较
(3)如果找不到就进行右边的寻找.
4.删除项函数
bool DeleteItem(const Item* pitem, Tree* ptree)
{
/*在树中寻找到了目标项才能进行删除*/
Pair look;
//创建寻找体
//执行寻找函数 将结果反馈到寻找体中
look = SeekItem(pitem, ptree);
//如果没有树(寻找体子成员为null)
if (look.chlid == NULL)
return false;
//如果有树 找到了对应项---如果是根这个点
if (look.parent == NULL)
{
//---删除根节点-----5.创建删除节点函数
DeletNode(&ptree->root);
}
//如果是子节点
else if (look.parent->left == look.chlid)//--左节点
DeletNode(&look.parent->left);
else
DeletNode(&look.parent->right);//---右节点
ptree->size--;
return true;
}
流程是:先查找你要删除的项所在的节点,确认后执行删除节点函数.-------删除节点函数是个辅助函数
static void DeletNode(Trnode** ptree)
{
//创建节点指针----要删除就是释放内存 释放的就是内容所对应的地址
Trnode * ptemp;
//如果要删除的节点左指针是空的 右指针不是空的---让它的右指针来接上
if ((*ptree)->left == NULL)
{
//先把这个指针保存下来
ptemp = *ptree;
//让这个指针的右指针替换这个指针
*ptree = (*ptree)->right;
//释放替死鬼
free(ptemp);
}
else if((*ptree)->right == NULL)//如果要删除的节点右指针是空的 左指针不是空的---让它的左指针来接上
{
ptemp = *ptree;
*ptree = (*ptree)->left;
free(ptemp);
}
else
{
/*如果要删除的节点 左右指针都不是空的 ---左指针接上 右指针去找寻离右指针最近的左指针指向节点的子节点的null 接上*/
//寻找右节点能接上的位置----这里替死鬼先当了一回查找员 查找到符合条件的节点 自己复制它 方便右节点接上
for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)
{
continue;
}
//找到这个节点后---删除节点的右节点接到这个节点上(ptemp就是找到的节点 它的右节点是null)
ptemp->right = (*ptree)->right;
//让替死鬼指向要删除的节点
ptemp = *ptree;
//---本来就接上自己的左节点----要记住要先让替死鬼记录要删除节点的信息 在去让要删除节点的左指针上位
*ptree = (*ptree)->left;
//---释放替死鬼
free(ptemp);
}
}
删除节点函数的流程是:-----------------这里操作的是节点的地址(Trnode ptree)而不是直接操作节点----因为删除节点就是通过释放节点所在的地址来进行删除的**
(1)创建一个指向节点的指针(通过释放这个指针来释放内容)
(2)对要删除的节点进行判断:
1.如果这个节点的左边是空的((*ptree)->left == NULL),右边不是空的---------那么
- 先将这个要删除的节点保存(ptemp = *ptree;)
- 再将此节点的右指针指向的节点接到这个指针上(*ptree = (*ptree)->right;)新,这一步是保存信息
- 现在可以安全的释放这个临时工指针来达到删除的目的(free(ptemp)😉
2.如果这个节点是右边空的,左边不是空的-----原理同上
3.如果这个节点左边,右边都不是空的--------这种情况,节点左边的节点树直接接上,右边的节点要接到左边节点树中(依次向下寻找过程中)那种节点的右指针为空的节点上,所以第一步要找出这种节点
for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)-------这里这个临时节点充当符合条件的节点;然后让要删除节点的右边((*ptree)->right;)指向这个临时节点的右边(ptemp->right),从而完成对接成功.
4.最后让临时工重操旧业,让他去指向要删除的节点,通过释放临时工来进行删除操作.然后项数减1
5.用函数作用节点
void Traverse(const Tree* ptree, const void(*pfun)(Item item))
{
//如果树存在
if(ptree->root!=NULL)
{
//---按顺序作用每一个节点----创建按顺序作用的函数
InOrder(ptree->root, pfun);
}
}
函数作用也会按照顺序作用的,所以用到了一个辅助函数:InOrder
static void InOrder(const Trnode* ptree, const void(*pfun)(Item item))
{
//如果树节点不为空
if (ptree != NULL)
{
InOrder(ptree->left, pfun);//1.左边被作用2.改变节点---原节点变为原节点的左边
(*pfun)(ptree->item);//这个节点的项被作用
InOrder(ptree->right, pfun);//1.右边被作用 2.改变节点---原节点变为原节点的右边
}
/*相当于一只寻找这个节点的左边部分的节点 知道找到了那个节点左边指向为空的地方----------------注意递归的边界
然后用函数作用这个节点的项
然后去找这个节点(不是原节点)它右边的部分
同样把这个节点看做新的节点 然后还是从左边找*/
}
这个函数也是通过递归来实现--------先作用它的左边,然后作用它的项,最后作用它的右边-------这个顺序是从最下面的节点开始作用(递归的特点).
6.最后是删除函数------删除所有节点
void DeleteAll(Tree* ptree)
{
//如果树不是空的 就删除所有节点
if (ptree != NULL)
DeletAllNote(ptree->root);//------------------6.创建删除所有节点的函数
//---树指针指向空
ptree->root = NULL;
//---项数为0
ptree->size = 0;
}
这里使用了辅助函数DeletAllNote-----删除所有节点
void DeletAllNote(Trnode* pnode)
{
//创建节点---替死鬼
Trnode * pchange;
//如果要删除的节点不是空的---删除分两步 一步保存 二步删保存的
if (pnode != NULL)
{
//---根节点的右指针 指向替死鬼---先保存了右边---并没有删除
pchange = pnode->right;
//删除这个节点的左边---递归自己----这样就删了一半----实质上是删了所有节点的左节点(当然是从最底层开始) 通过释放内存 删除了右节点
DeletAllNote(pnode->left);
//--释放这个节点的内存
free(pnode);
//最后调用函数清除替死鬼
DeletAllNote(pchange);//这里就是清除了另一半
}
}
这里也是利用递归来实现--------递归这东西在二叉树中真的很常用(但也可以不用递归来实现)
具体操作:
1.设置"临时工"-------Trnode * pchange;通过释放它指向的节点来删除节点里的信息
2.进行递归:
(1)确定递归停止条件:树中节点为空的时候停止(pnode != NULL)
(2)让临时工保存信息(这里保存了右边的信息pchange = pnode->right;)(删除也是通过二分法来进行的,但是删除节点会删除节点的全部信息,这样会使得这个节点与其他节点的关系断裂,从而其他节点得不到操作),删除是一半一半的删.
(3)确定操作方向(pnode->left)-----删除节点的左边的东西(即使本来就为空,递归从最后面开始操作)
(4)释放这个节点的内容(free(pnode)😉
这里有些复杂(刚接触递归其实到现在都没整明白):我尽量表述的清楚一些:其实这个函数做了两件事:保存信息和释放内存
它先把原始点的右边部分保存起来,然后向这个点的左边开始寻找
而且边寻找边保存当前节点的右边部分的信息,直到找到一个节点(这个节点的左指针是空)
然后释放这个节点的地址(就是删除了这个节点)
然后又从当前(删掉的)节点的右指针指向的节点开始经过循环…
这样就相当于从第一项删除到了最后一项
可以发现一个规律:每次对树进行操作,在项的处理封装一层(在最外面),在节点的处理封装一层(在里面),统筹性的工作比如删除树,会影响树的信息(树包括节点和节点数-------节点包括项和指针-------项就只包括基本数据,但是最直观的确实操作项)