在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第i层至多有2^{i-1}个结点;深度为k的二叉树至多有2^k-1个结点;对任何一棵二叉树T,如果其终端结点数为n_0,度为2的结点数为n_2,则n_0=n_2+1。
一棵深度为k,且有2^k-1个节点称之为满二叉树;深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为完全二叉树。
二、基本术语
1、结点的度和树的度
树中每个结点具有的非空子树数或者说后继结点数被定义为该 结点的度。树中所有结点的度的最大值被定义为该 树的度。
2、分支节点和叶子结点
度为0的为叶子结点或终端结点,度大于0的为分支节点。
3、孩子结点、双亲结点、兄弟结点。
在一棵树中,每个结点的子树的根或者说每个结点的后继,称为该结点的孩子,该结点称为孩子结点的双亲结点,具有同一个双亲结点的结点互称兄弟。结点的所有子树中的结点称为该结点的子孙。
4、结点的层数和深度
三、二叉树的存储结构
1、顺序存储结构 顾名思义,以各结点编号为下标,把结点值对应存储到一维数组中。
这种方式的好处,查找速度快,但是只适用于存放完全二叉树,对于存放单支结点比较多的二叉树会有很多存储空间空闲,浪费存储空间。这种结构适合存放完全二叉树或者单支结点较少的二叉树。
2、链式存储结构
链表中的每个结点设置4个域:值域,左指针域,右指针域,双亲指针域。这样的结构即便于向上查找双亲结点,也方便查找该结点的孩子结点,缺点就是存储空间要比顺序存储结构大。
结构定义代码实现:
package com.data.tree.po; /** * 二叉树结点:数组元素结点 * Created by xiaoyun on 2016/7/18. */ public class ArrayBTreeNode { private Object element; int left, right; public ArrayBTreeNode(Object obj) { this.element = obj; } public ArrayBTreeNode(Object obj, int lt, int rt) { this.element = obj; left = lt; right = rt; } }
package com.data.tree.po; /** * 二叉树节点:链式存储结构 * Created by xiaoyun on 2016/7/18. */ public class BTreeNode { public Object element; public BTreeNode left,right; public BTreeNode(Object obj) { this.element = obj; left = right = null; } public BTreeNode(Object obj, BTreeNode lt, BTreeNode rt) { this.element = obj; this.left = lt; this.right = rt; } }
四、常用算法及实现
1、根据二叉树的抽象数据类型,在Java中对应的接口定义为
package com.data.tree.po; /** * 二叉树接口(标准行为规范定义) * Created by xiaoyun on 2016/7/19. */ public interface BinaryTree { // 由mode数组提供遍历二叉树的4中不同的访问次序(前序,中序,后续,层级) final String[] mode = {"preOrder", "inOrder", "postOrder", "levelOrder"}; // 根据二叉树的广义表表示在计算机中建立对应的二叉树 boolean createBTree(String gt); // 判断二叉树是否为空 为空,返回true,不为空,返回false boolean isEmpty(); // 按照字符串s所给的次序遍历一棵二叉树,每个结点均被访问一次 void traverseBTree(String s); // 从二叉树中查找值为obj的结点,若存在则返回完整值,否则返回空 Object findBTree(Object obj); // 求出一棵二叉树的深度 int depthBTree(); // 求出一棵二叉树的结点数 int countBTree(); // 按照树的一种表示方法输出一棵二叉树 void printBTree(); // 清除二叉树的所有结点,使之变为一棵空树 void clearBTree(); }
2、链接存储的二叉树定义
package com.data.tree.po; import com.sun.org.apache.xalan.internal.xsltc.compiler.util.StringStack; import java.util.ArrayDeque; import java.util.Queue; import java.util.Stack; /** * 链接二叉树(链接存储) * Created by admin on 2016/7/19. */ public class LinkBinaryTree implements BinaryTree { // 定义根节点 protected BTreeNode root; // 无参数构造器 public LinkBinaryTree() { this.root = null; // 二叉树的结点为空,即把空值null献给root } /** * 将一个二叉树的跟的引用赋值给root * @param root */ public LinkBinaryTree(BTreeNode root) { this.root = root; } /** * 先序遍历 * @param root */ private void preOrder(BTreeNode root) { if (root != null) { System.out.print(root.element + " "); // 访问根节点 preOrder(root.left); // 先序遍历左子树 preOrder(root.right); // 先序遍历右子树 } } /** * 中序遍历 * @param root */ private void inOrder(BTreeNode root) { if(root != null) { inOrder(root.left); System.out.print(root.element + " "); inOrder(root.right); } } /** * 后序遍历 * @param root */ private void postOrder(BTreeNode root) { if(root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.element + " "); } } /** * 层次遍历 * @param root * 访问每个结点时,都需要经过入队和出队以及输出节点值的操作,这些操作的时间复杂度均为O(1),所以整个算法的时间复杂度为O(n) */ private void levelOrder(BTreeNode root) { Queue queue = new ArrayDeque(); BTreeNode p = null; queue.offer(root);// 将该树的根节点入队 while (!queue.isEmpty()) { p = (BTreeNode) queue.poll();// 获取并移除该队列的头 System.out.print(p.element + " "); if(p.left != null) { queue.offer(p.left); } if(p.right != null) { queue.offer(p.right); } } } /** * 查找的递归算法 * @param root * @param x * @return * 思路:类似先序遍历 * 1、如果树为空那么直接返回空 * 2、比较根结点,如果相等直接返回根结点 * 3、递归左子树,如果查找成功则返回结点的值 * 4、递归查找右子树,如果查找成功则返回结点的值 * 5、左右子树均为找到则返回null,查找失败 */ private Object findBTree(BTreeNode root, Object x) { if(root == null) { return null; }else { if(root.element.equals(x)) { return root.element; }else { Object target; target = findBTree(root.left, x); if(target != null) { return target; } target = findBTree(root.right, x); if(target != null) { return target; } return null; } } } /** * 求出二叉树深度的递归算法 * @param root * @return * 思路:若一一棵二叉树为空,那么它的深度为0,否则它的深度等于左子树和右子树中的最大深度+1 */ private int depthBTree(BTreeNode root) { if(root == null) { return 0; }else { int dep1 = depthBTree(root.left); int dep2 = depthBTree(root.right); if(dep1 > dep2) { return dep1 + 1; }else { return dep2 + 1; } } } /** * 计算二叉树节点总数 * @param root * @return * 思路:如果一棵二叉树为空,那么结点数为0,否则它等于左子树中的结点数与右子树中的结点数之和再加1 */ private int countBTree(BTreeNode root) { if(root == null){ return 0; }else { int countLt = countBTree(root.left); int countGt = countBTree(root.right); return countLt + countGt + 1; } } /** * 输出二叉树广义表的递归算法 * 说明:以广义表的形式输出一棵二叉树,应该首先输出根结点,然后再依次输出它的左子树和右子树, * 在输出左子树和之前答应左括号,输出右子树后打印右括号,具体广义表的输出规则,请查看数据结构之二叉树广义表的表示 * @param root */ private void printBTree(BTreeNode root) { if(root != null) { System.out.print(root.element); if (root.left != null || root.right != null) { System.out.print("("); printBTree(root.left); if(root.right != null) { System.out.print(","); } printBTree(root.right); System.out.print(")"); } } } /** * 建立二叉树的存储结构的方法 * @param str * @return * 该算法的时间复杂度为O(n),n表示二叉树广义表中字符的个数,由于平均每两至三个字符之中就要一个元素字符, * 所以n也可以看做二叉树中元素结点的个数 */ public boolean createBTree(String str) { Stack stack = new Stack(); // 定义和创建一个保存结点指针的栈 this.root = null; // 把树根指针置为空,即从空树开始 BTreeNode p = null; // 定义p为指向二叉树结点的指针 int k = 1; // 用k作为处理结点的左子树和右子树的标记 char[] a = str.toCharArray(); // 将字符串转换为字符数组 for(int i = 0; i < a.length; i++) { switch (a[i]) { case ' ': break; case '(': // 处理左括号 stack.push(p); k = 1; break; case ')': // 处理右括号 if(stack.isEmpty()){ System.out.println("二叉树广义表字符串错,返回假"); return false; } stack.pop(); break; case ',': k = 2; break; default: // 扫描到的为字母,其他非法字符 if(a[i] >= 'a' && a[i] <= 'z' || a[i] >= 'A' && a[i] <= 'Z') { p = new BTreeNode(a[i]); if(root == null) { root = p; // 将p结点定义为树根 }else { // 链接到双亲结点 if(k == 1) { ((BTreeNode)stack.peek()).left = p; }else { ((BTreeNode)stack.peek()).right = p; } } }else { System.out.println("二叉树广义表中存在非法字符,返回假!"); } } } if(stack.isEmpty()) { return true; }else { return false; } } public boolean isEmpty() { return this.root == null; } /** * 按照一定的次序访问二叉树,使得每个结点均被访问一次 * @param s */ public void traverseBTree(String s) { if(s.equals(mode[0])){ preOrder(root); }else if(s.equals(mode[1])) { inOrder(root); }else if(s.equals(mode[2])) { postOrder(root); }else if(s.equals(mode[3])) { levelOrder(root); } System.out.println(); // 输出换行 } public Object findBTree(Object obj) { return findBTree(root, obj); } public int depthBTree() { return depthBTree(root); } public int countBTree() { return countBTree(root); } public void printBTree() { printBTree(root); } public void clearBTree() { root = null; } }
3、测试类及运行结果
import com.data.tree.po.BinaryTree; import com.data.tree.po.LinkBinaryTree; /** * Created by admin on 2016/7/23. * 二叉树测试算法 */ public class Example6_1 { public static void main(String[] args) { // 1、定义并创建链接存储结构的一棵空二叉树bt BinaryTree bt = new LinkBinaryTree(); // 2、初始化二叉树广义表字符串 String s = "A(B(C,D),E(,F(G)))"; //String s = "a(b(c),d(e(f,g),h(,i)))"; // 3、根据字符串建立链式存储结构的二叉树 boolean yn = bt.createBTree(s); if (!yn) { System.out.println("表示二叉树的广义表字符串有误,请修正后在执行,错误的字符串s:" + s); System.exit(0); } // 4、广义表输出该二叉树 System.out.println("二叉树的广义表表示形式:"); bt.printBTree(); // 5、前序遍历 System.out.println("前序遍历:"); bt.traverseBTree(BinaryTree.mode[0]); // 6、中序遍历 System.out.println("中序遍历:"); bt.traverseBTree(BinaryTree.mode[1]); // 7、后序遍历 System.out.println("后序遍历:"); bt.traverseBTree(BinaryTree.mode[2]); // 8、层级遍历 System.out.println("层级遍历:"); bt.traverseBTree(BinaryTree.mode[3]); // 9、求二叉树的深度 System.out.println("计算该二叉树的深度:" + bt.depthBTree()); // 10、计算该二叉树的结点数 System.out.println("该二叉树的结点数:" + bt.countBTree()); // 11、查找并输出二叉树中值为'C'和'F'的结点 System.out.println("查找结点:" + bt.findBTree('c') + " " + bt.findBTree('F')); // 12、清空该二叉树 bt.clearBTree(); } }
运行结果为:
二叉树的广义表表示形式:
A(B(C,D),E(,F(G)))前序遍历:
A B C D E F G
中序遍历:
C B D A E G F
后序遍历:
C D B G F E A
层级遍历:
A B E C D F G
计算该二叉树的深度:4
该二叉树的结点数:7
查找结点:null F
总结:本周学习和温习了二叉树的性质,定义,属性,基本术语,存储结构,常用算法。
忙碌的一周,导致今天才把二叉树的基本算法写完和理解。
二叉树的应用场景:搜索、索引、查询、排序等。
明天将学习二叉搜索树和堆。
下周计划:哈夫曼树、平衡二叉树,B树,红黑树了解,以及二叉树在软件、互联网和数据库中的应用。