二叉树学习笔记-概述
数组、向量、链表都是一种顺序容器,它们提供了按位置訪问数据的手段。而非常多情况下,我们须要按数据的值来訪问元素,而不是它们的位置来訪问元素。比方有这样一个数组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接口,用户仅仅管往里面插入数据。无论内部的指针结构。
而不是像上面那样,手动生成一个个节点,再调整指针。那样会累死的。
那样旨在说明怎样由一个个节点构成二叉树以及它们在内存中的结构。以后再学习二叉搜索树的实现。