树是一种非线性的数据结构。由n(n>=0)个节点组成.
- 如果n=0,是一颗空数。
- 如果n>0,树有一个特殊的节点,这个节点没有父节点,称为根节点(root)
- 除根节点之外的其余数据元素被分为m(m>=0)个互不相交的集合T1,T2…Tm-1,其中每一个集合Ti(1<=i<=m),本身也是一棵树,被称作原数的子树。
下图是一棵树
树的相关概念
1、节点
它包含数据项,和指向其它节点的指针,上图中的树有11个节点。
2、节点的度
有几个分支(箭头),度就是几。
3、叶节点
度为0的节点被称为叶节点。如上图的5 7 8 9 10 11
4、分支节点
除了叶节点之外的节点就是分支节点。1 2 3 4 6
5、子女节点
若节点m有子树,则这棵子树的根节点就是节点m的子女节点,如2,3,4都是1的子女节点。
6、父节点
若节点m有子女节点,则m为子女节点的父节点,如1是2,3,4的父节点 2是5,6的父节点。
7、兄弟节点
同一个父节点的子女节点互称为兄弟。5和6是兄弟节点
8、祖先节点
从跟节点到该节点所经过分支上的所有节点,如节点5,它的祖先节点为1 2
9、子孙节点
某一个节点的子女,以及这些子女节点的子女都是该节点的的子孙节点。如5 6 11都是2的子孙节点
10、节点所在的层次
根节点在第一层,它的子女在第二层,依次类推。
11、树的深度
树中距离根节点最远的节点所处的层次就是树的深度。上图中树的深度是4.
12、树的高度
叶节点的高度为1,非叶节点的高度都是它的子女节点高度的最大值加1,高度与深度的数值是相等的,但计算方式不一样。
13、树的度
树中节点的度的最大值,
14、有序树
树中节点的各子树T1,T2。。。是有次序的,T1是第一颗子树,T2是第2颗子树。
15、无序树
树中节点的各子树之间的次序不重要,可以相互交换位置。
16、森林
森林是m(m>=0)颗数的集合
二叉树
二叉树是一种特殊情况,每个节点最多有两个子女,分别为左子女和右子女。二叉树中不存在度大于2的节点。
二叉树的子树有左右之分,次序不能颠倒。
二叉树的性质
- 二叉树的第i(i>=1)层,最多存在2的i-1次方个节点。
- 深度为k(k>=0)的二叉树,最少有K个节点,最多有2的k-1次方个节点。
- 对于一个非空二叉树,叶节点的数量等于度为2的节点数量加1。
特殊二叉树
- 满二叉树
深度为k的满二叉树,是有2的k-1次方个节点的二叉树,每一层都达到了可以容纳的最大数量的节点。 - 完全二叉树
深度为k的完全二叉树,从第一层到第k-1层都是满的,第k层,或是满的,或是从右向左连续缺若干个节点。
下面我们来定义一颗树:
//定义二叉树
var BinTreeNode = function(data){
this.data = data;//数据
this.leftChild = null;//左节点
this.rightChild = null;
this.parentNode = null;//父节点
}
function BinTree(){
var root = null;//根节点
//初始化 采用广义表表示的建立二叉树方法
this.init_tree = function(string){
var stack = new Stack();
var k = 0;//标识识别的是左子树还是右子树
var new_node = null;
for (let i = 0; i < string.length; i++) {
var item = string[i];
// console.log(item)
if(item=="#"){
break;
}
if(item=="("){
stack.push(new_node);
k=1;
}else if(item==","){
k=2;
}else if(item==")"){
stack.pop();
}else{
new_node = new BinTreeNode(item);//创建节点
if(root == null){
root = new_node;
}else{
//k==1 说明new_node是左子节点
if(k==1){
var top_item = stack.top();
top_item.leftChild = new_node;
new_node.parentNode = top_item;
}else if(k==2){
var top_item = stack.top();
top_item.rightChild = new_node;
new_node.parentNode = top_item;
}
}
}
}
}
//返回根节点
this.get_root = function(){
return root;
}
//中序遍历(遍历顺序:当前节点的左节点->当前节点->当前节点的右节点)
this.in_order = function(node){
if(node==null){
return ;
}
this.in_order(node.leftChild);
console.log(node.data);
this.in_order(node.rightChild)
}
//前序遍历(遍历顺序:当前节点->当前节点的左节点->当前节点的右节点)
this.pre_order = function(node){
if(node==null){
return ;
}
console.log(node.data)
this.pre_order(node.leftChild);
this.pre_order(node.rightChild)
}
// 后序遍历(遍历顺序:当前节点的左节点->当前节点的右节点->当前节点)
this.post_order = function(node){
if(node==null){
return ;
}
this.post_order(node.leftChild);
this.post_order(node.rightChild);
console.log(node.data);
}
var tree_node_count = function(node){
//左子树的节点数量+右子树节点的数量+当前节点
if(node==null){
return 0;
}
var left_node_count = tree_node_count(node.leftChild);
var right_node_count = tree_node_count(node.rightChild);
return left_node_count + right_node_count + 1;
}
//树的节点个数
this.size = function(){
return tree_node_count(root)
}
var tree_height = function(node){
if(node==null){
return 0;
}
//先计算左子树的高度
var left_child_height = tree_height(node.leftChild);
// 再计算右子树的高度
var right_child_height = tree_height(node.rightChild);
//比较大小 返回大的
if(left_child_height>right_child_height){
return left_child_height + 1;
}else{
return right_child_height + 1;
}
}
// 树的高度
this.height = function(){
return tree_height(root);
}
var find_node = function(node,data){
if(node==null){
return null;
}
//当前节点的值等于data
if(node.data==data){
return node;
}
//先到左子树里去找
var left_res = find_node(node.leftChild,data);
if(left_res){//找到了
return left_res;
}
// 没找到 ,去右子树里找
return find_node(node.rightChild,data)
}
//查找节点
this.find = function(data){
return find_node(root,data);
}
}
上面代码中init_tree的实现思路:
init_tree的实现 它接收一个表示二叉树的广义表,创建一棵树。
以广义表A(B(D,E(G,)),C(,F))#为例,算法如下:
-
广义表的表名放在表前,表示树的根节点,括号中的是根的左右子树。
-
每个节点的左右子树用逗号隔开,如果仅有右子树没有左子树,逗号不省略。
-
整个广义表的最后加上特殊符号#表示输入结束。
-
遍历广义表字符串,来建立一颗二叉树。
-
遇到左括号的时候,说明前面有一个节点,这个括号里的两个节点都是它的子节点,但是子节点后面还会有子节点,因此,
-
我们需要一个先进后出的数据结构把前面的节点保存下来,这样栈顶就是当前要处理的两个节点的父节点。
-
逗号分隔了左右子树,因此需要一个变量来标识遇到的是左子树还是右子树,假设这个变量为k,遇到左括号的时候,k=1,表示开始识别左子树,
-
遇到逗号,k=2表示开始识别右子树。
-
遇到右括号,说明一棵子树结束了,那么栈顶的元素就是这颗子树的根节点,执行pop方法出栈。
这里需要用到一个栈,栈的实现:
function Stack(){
var items = [];
// 从栈顶添加元素,---压栈
this.push = function(item){
items.push(item)
}
//弹出栈顶元素
this.pop = function(){
return items.pop();
}
// 返回栈顶元素
this.top = function(){
return items[items.length-1];
}
// 判断栈是否为空
this.isEmpty = function(){
return items.length == 0;
}
// 返回栈的长度
this.size = function(){
return items.length;
}
// 清空栈
this.clear = function(){
items = [];
}
}
测试代码:
var bt = new BinTree();
bt.init_tree("A(B(D,E(G,)),C(,F))#")
var root = bt.get_root();
bt.in_order(root);// D B G E A C F
console.log(bt.size());//7
树的练习
1、求一颗树的镜像
对于一棵树,如果每个节点的左右子树互换位置,那么就变成了这颗树的镜像。
// 思路1
var mirror_1 = function(node){
if(node==null){
return ;
}
//互换位置
var temp = node.leftChild;
node.leftChild = node.rightChild;
node.rightChild = temp;
mirror_1(node.leftChild);
mirror_1(node.rightChild);
}
//思路2
var mirror_2 = function(node){
if(node==null){
return ;
}
var left = mirror_2(node.leftChild);
var right = mirror_2(node.rightChild);
node.leftChild = right;
node.rightChild = left;
return node;
}
mirror_1(root);
bt.in_order(root);// F C A E G B D
2、使用非递归的方式实现前序遍历
思路:递归转为非递归,可以用while循环。
前序遍历的顺序是当前节点->当前节点的左节点->当前节点的右节点。
在while循环里面,让curr_node = curr_node.leftChild,就可以实现对左子树的访问处理。问题在于,处理左子树之后,要找到对应的右子树,需要一种数据结构,能够在左子树访问结束后返回对应的右子树。栈可以实现。
以上图为例:
开始curr_node = 1;
1、处理curr_node(1),3入栈,curr_node = curr_node.leftChild;
2、处理curr_node(2),5入栈,curr_node = curr_node.leftChild;
3、处理curr_node(4),处理结束,不能继续访问左子树,执行pop,curr_node = 5;
4、处理curr_node(5),处理结束,不能继续访问左子树,执行pop,curr_node = 3;
5、处理curr_node(3),7入栈,curr_node = curr_node.leftChild;
6、处理curr_node(6),处理结束,不能继续访问左子树,执行pop,curr_node = 7;
7、处理curr_node(7),处理结束,栈里没有元素。结束。
// 前序遍历的非递归实现
function pre_order(node){
var stack = new Stack();
var curr_node = node;
while(curr_node){
console.log(curr_node.data)
if(curr_node.rightChild){
stack.push(curr_node.rightChild)
}
if(curr_node.leftChild){
curr_node = curr_node.leftChild;
}else{
//没有左子树
curr_node = stack.pop();
}
}
}
pre_order(root);