故事是这样开始的,项目经理有一天终于还是拍拍我肩膀说:
无论你的链表写得多么的好,无论是多么的灵活,我也得费老半天才查找到想要的数据;
这让我的工作非常苦恼,听说有一种叫做二叉树的数据结构,你看能不能给我弄来;
Probelm:
看看如下的数据:
我们每次都要从头到尾的查看我们的数据链表里面是否存在着目标数据;
每次都小心翼翼的害怕漏掉哪个,这样的工作的确让人烦恼;
Solution
再看看如下的解决解决方案;
显然,我们很清楚自己要查找的目标大致会在那里出现;
例如查找的目标是6,那么我知道6小于9所以根本不会去看右边的数据;
我们继续看6大于5所以找到啦目标;
换句话说我们只对比了两次找到啦目标;
而对于链表,我们发现6排在了链表的尾部;
我们花啦大量的时间。。
好啦!到此为止我们知道这样的二叉树的确是高效的;
说干就干,我们来实现它;
typedef struct _node { int data; struct _node *left; struct _node *right; }Node;
节点定义可以如上面的方式,这也是大多数二叉树定义的方式;
但是试想想,左右子树的差别是什么呢?
我们都知道要是存在x节点,那么x.left.data < x.data < x.right.data这个规律;
我们为什么不充分的考虑考虑有没有其他可以更好的定义我们的左右节点呢?
让它自动的区决定什么时候是左节点存数据,什么时候是右节点存数据呢;
我们都知道,假设a=9;BOOL B = a < 10;那么布尔值b是什么呢?
我们可以写下面这段代码分析分析,注意我们任然是在学习二叉树,你没有走神:)
#include <stdio.h> int main(void) { int a = 9; printf("a < 10 return :%d/n",a < 10); printf("a < 8 return :%d/n",a < 8); return 0; }
结果是:
显然,我们希望10存放在右子树,8存放在左子树;
所以你会想是这样写;
a = 8; new = 10; if (x->a < new){ x->right = new; }else{ x->left = new; }
当然,上面的代码是接近伪代码的表述方式啦!相信你不会去运行它的;
然而我想说上面的代码是非常的繁琐的,虽然感觉很清晰;
试试下面的写法吧:)
typedef struct _node { int data; struct _node *link[2]; }Node;
我们重新定义啦节点;
注意我们把左右节点改成啦一个指针数组的形式;
我们这样写的好处待会你就能体验到;
现在我们继续定义一个树;
typedef struct _tree{ struct _node *root; }Tree;
我们的树定义得更加简单,注意我们是先定义节点,再定义树;
因为树的定义需要用到节点结构体;
接下来我们需要初始化我们的树;
Tree * init_tree() { Tree *temp = (Tree*)malloc(sizeof(Tree)); temp->root = NULL; return temp; }
这种写法是非常简洁的,但是它也有弊端;
它没有错误处理,例如:万一malloc分配内存错误;
通常我们可能会这样写:
Tree * init_tree() { Tree *temp = (Tree*)malloc(sizeof(Tree)); if(!temp) return NULL; temp->root = NULL; return temp; }
或者这样写:
Tree * init_tree() { Tree *temp = (Tree*)malloc(sizeof(Tree)); if(temp){ temp->root = NULL; } return temp; }
都是为啦!让程序运行的时候万一出问题,我们知道错误在哪?
你甚至可以写上提示信息,但是咱们的核心问题不是这些,所以我们都假设程序不会出现这些问题;
我们现在需要做的是创建节点;
Node * make_node(int data) { Node *temp = (Node*)malloc(sizeof(Node)); temp->link[0] = temp->link[1] = NULL; temp->data = data; return temp; }
创建节点也比较容易理解,首先申请内存,然后初始化数据成员;
最后返回该节点;
下面是如何插入这个创建好的节点呢?
我们需要一个函数大概会叫做insert,希望你能够通过名字大概能找到感觉;
Node * insert_recursive(Node *root,int data) { if(root == NULL){ root = make_node(data); }else if(root->data == data){ return root; }else{ int dir = root->data < data; root->link[dir] = insert_recursive(root->link[dir],data); } return root; }
注意,希望你能够一眼看出recursive的意思是什么。我不会告诉你的,我们希望你自己查查哈;
我们分析分析代码吧;
第一个if语句,告诉我们如果这个节点为空,那么就创建它;
else if语句,告诉我们如果这个数据与树上找的一致,那么就返回这个节点给调用者;
else语句,告诉我们的就稍微复杂一点啦;
它首先声明一个dir的整形变量,我们可以偷偷告诉你它其实是一个只有两种可能值得整形;
我们通常都可以用BOOL类型替换它;你可以发现它马上就露馅啦;
后面马上赋值,root->data < data;是一个表达式,这个表达式只会返回1,0;
希望你还记得什么时候返回1,什么时候返回0;(我们前面写过测试的哦)
最后,这个节点已经存放到该存放的位置啦;
我们不应该直接去调用点,我们需要的是去操作一颗树;
因此我们需要包装一下我们的insert函数;
int insert(Tree * tree, int data) { tree->root = insert_recursive(tree->root,data); return 1; }
很简单就ok啦;
为啦看到我们的成果,你一定希望它能够打印些什么,而且证明它的确是一颗树;
我们需要一种叫做中序遍历的方式打印它,这种方式的好处是你可以看到它会按照从小到大得方式输出;
void print_inorder_recursive(Node *root) { if(root){ print_inorder_recursive(root->link[0]); printf("data:%d/n",root->data); print_inorder_recursive(root->link[1]); } return ; }
同理,我们任然写一个包装函数;
void print_inorder(Tree *tree) { print_inorder_recursive(tree->root); return ; }
好啦,最后测试一下我们的二叉树;
int main(void) { Tree * tree = init_tree(); insert(tree,9); insert(tree,7); insert(tree,3); insert(tree,19); insert(tree,29); insert(tree,82); insert(tree,1); print_inorder(tree); return 0; }
编译运行,看看我们的结果如下;
Discussion
我们的功能是实现啦!但是这远远不够的;
例如,
1,我们该证怎么查找一个目标节点呢?
2,我们该如何删除一个节点呢?
3,插入的方式只能用递归吗?
4,递归有什么缺点吗?
我们下次再聊!