数组、向量、链表都是一种顺序容器,它们提供了按位置访问数据的手段。而很多情况下,我们需要按数据的值来访问元素,而不是它们的位置来访问元素。比如有这样一个数组int num[3]={1,2,3},我们可以非常快速的访问数组中下标为2的数据,也就是说我们知道这个数据的位置,就可以快速访问。有时候我们是不知道元素的位置,但是却知道它的值是多少。假设我们有一个变量,存放在num这个数组中,我们知道它的值为2,却不知道它下标是多少,也就是说不知道它的位置。这个时候再去数组中访问这个元素就比较费劲,就得遍历数组,而且还要保证数组中没有重复的元素。
二叉树在很大程度上解决了这个缺点,二叉树是按值来保存元素,也按值来访问元素。怎么做到呢,和链表一样,二叉树也是由一个个节点组成,不同的是链表用指针将一个个节点串接起来,形成一个链,如果将这个链“拉直”,就像平面中的一条线,是一维的。而二叉树由根节点开始发散,指针分别指向左右两个子节点,像树一样在平面上扩散,是二维的。示意图如下:
和链表一样,二叉树也是由一个个节点构成,显然这一个个节点才是二叉树的基础。在链表中,如果这个链表是单向链表,那么每个节点中就需要包含一个指向后继节点的指针,如果是双向链表,还需要一个指向前驱节点的指针。那么在二叉树的节点中,就需要包含一个指向左子节点和一个指向右子节点的指针,为了方便的遍历二叉树,还需要一个指向父节点的指针,最后,还需要包含当前节点的值。那么一棵最简单的二叉树,示意图是这样的
那么用一个类来实现这个节点,假设这个二叉树中保存的都是int型的数据,为了方便起见,在构造函数中将所有的变量全部初始化为默认值,那么代码如下:
class treeNode { public: int value; treeNode *left; treeNode *right; treeNode *parent; treeNode() { value = 0; left = NULL; right = NULL; parent = NULL; } };
然后生成这个类的三个实例,分别为node0、node1、node2,作为父节点、左子节点、右子节点。节点值初始化为10、8、14。代码如下
#include <iostream> using namespace std; class treeNode { public: int value; treeNode *left; treeNode *right; treeNode *parent; treeNode() { value = 0; left = NULL; right = NULL; parent = NULL; } }; int main() { treeNode node0, node1, node2; node0.value = 10; node1.value = 8; node2.value = 14; node0.left = &node1; //node0左子节点指针指向node1 node0.right = &node2; //node0右子节点指针指向node2 node1.parent = &node0; //node1和node2的父节点指针均指向node0 node2.parent = &node0; cout << sizeof(treeNode) << endl; cout <<"node0 addr: "<< &node0 << endl; cout <<"node1 addr: "<< &node1 << endl; cout <<"node2 addr: "<< &node2 << endl; cout <<"node0 left node addr: "<< node0.left << endl; cout <<"node0 right node addr: "<< node0.right << endl; cout <<"node1 parent node addr: "<< node1.parent << endl; cout <<"node2 parent node addr: "<< node2.parent << endl; }
输出结果是这样的
这样我们就可以根据输出画出这三个节点在内存中的结构图
这样就构成了一个最最简单的二叉树,这样如果拿到父节点,就可以任意的访问它的左子节点或者右子节点,比如:
cout << (node0.left)->value << endl;
先拿到父节点,找到指向左子节点的指针,因为拿到的是一个指针,所以就可以用“->”访问左子节点。上图中有一个比较奇怪的地方就是,node0的左子节点指针指向了node1,却感觉指向了node1的value。因为value是类中的第一个成员,地址会跟类的首地址重合,而处理器知道这个left指针的类型是treeNode* ,所以 会从这个首地址向下读取16字节的内容(sizeof(treeNode)=16),这样就会读取这个类中的所有成员。假设我们将left指针强制转换为int* ,那么就会向下读取4个字节的内容,也就是value的值。比如:
cout << *(int *)(node0.left) << endl;
输出是8,原理是我们先拿到node的left指针,强制转换为int* ,再用*运算符读取其中的内容,因为类型转换为int* ,所以处理器向下读取4个字节的内容,也就是8。
到现在为止,虽然构成了一棵最简单的二叉树,但是这种树的作用不是很大。如果想要在二叉树中存储大量的数据,那么就需要定义一种结构特性。那就是左子节点的值总是小于父节点的值,右子节点的值总是大于父节点的值。
这种二叉树叫做二叉搜索树,前面说到的这种结构特性,也是二叉搜索树的精髓所在,最开始的时候说,二叉树是根据值来存储元素的。那么拿到一个新的节点,就先比较这个节点和跟节点的大小关系,如果小于跟节点,就沿左子节点下降,否则反之。按照这样的规则,直到下降到一个空节点,也就是NULL,将这个节点插入到这个位置。查找一个元素的过程也是类似的,假设拿到一个节点,就先跟根节点比较,如果小于跟节点,就沿左子节点查找,否则反之。按照这样的规则,直到查找到这个节点,或者查找到NULL,代表这个节点不在这棵树中。示意图如下:
假设现在新插入一个值为15的节点,那么比较它和12的大小关系,比12大,沿右子节点下降。再比较和16的大小关系,比16小,沿左子节点下降。再比较和14的大小关系,比14大,沿右子节点下降。这时候到达了空节点,也就是NULL。将15作为14的右子节点插入到这棵树。查找过程类似。
显然这种结构特性不能人为的去保持,而是由代码自动保持。与此同时,我们希望有一个完整的二叉搜索树类。对外提供insert和erase接口,用户只管往里面插入数据,不管内部的指针结构。而不是像上面那样,手动生成一个个节点,再调整指针。那样会累死的。那样旨在说明如何由一个个节点构成二叉树以及它们在内存中的结构。以后再学习二叉搜索树的实现。