定义

一个有穷的结点集合,可以为空。若不为空,则它是由根结点和称为其左子树和右子树的两个互不相交的二叉树组成。

  1. 二叉树的五种基本形态:
tree_state
  1. 二叉树的子树是有顺序之分的,称为左子树和右子树
left_right_tree
  1. 几种特殊的二叉树
    • 斜二叉树
skew_tree
  • 完美二叉树(满二叉树)
full_tree
  • 完全二叉树
    有n个结点的二叉树,对树中结点按从上之下,从左至右的顺序进行编号,编号为i(1<=1<=n)结点与满二叉树中编号为i结点在二叉树中位置相同

二叉树的几个重要性质:

  1. 在二叉树的第i层上最多有2i-1个节点 。(i>=1)
  2. 二叉树中如果深度为k,那么最多有2k-1个节点。(k>=1)
  3. 对任何非空二叉树T,若n0表示度数为0的节点 n2表示度数为2的节点,那么n0=n2+1;
  4. 具有n个结点的完全二叉树的深度为 log2n + 1

这里在补充一下树的其他一些性质和概念:

  1. 结点的度:结点所拥有的子树的个数称为结点的度;
  2. 树的度:树中各节点的度的最大值;因此,二叉树的度最大为2;
  3. 结点的层数:规定根节点的层数为1,其余结点的层数为他的双亲结点层数加1
  4. 输的深度:树中所有结点的最大层数。

二叉树的抽象数据类型(ADT)

对于二叉树的元素,主要的操作包括:

  1. 判别二叉树是否为空
  2. 遍历二叉树,按特定的顺序访问每个结点
    • 前序遍历:根节点-->左子树-->右子树
    • 中序遍历:左子树-->根节点-->右子树
    • 后序遍历:左子树-->右子树-->根节点
    • 层序遍历:从上至下,从左至右。
  3. 创建一个二叉树

二叉树的存储结构

顺序存储结构

linear_tree

使用顺序存储结构,对完全二叉树这种结构是非常合适的。可以按照从上之下,从左至右顺序存储n个结点的完全二叉树的结点父子关系。

linear_tree_array

完全二叉树的这种存储结构,有以下特点

  • 非根节点(序号i>1)的父节点序号(数组下标)是 i/2 (取整)。
  • 结点(序号为i)的左孩子结点的序号是2i,如果2i>n,则没有左孩子;
  • 结点(序号为i)的右孩子结点的序号是2i+1,如果2i+1>n,则没有右孩子。

一般普通的二叉树,在其空余位置补充控制,当做是完全二叉树,采用数组结构存储,将导致存储空间的浪费。

链式存储结构

二叉树的链式存储结构中,每一个结点包含三个关键属性:指向左子树的指针,数据域,指向右子树的指针;根据这个叙述,我们可以按如下结构定义结点。

link_tree
结点定义
  1. /**
  2. * Created by engineer on 2017/10/23.
  3. * <p>
  4. * 二叉树结点定义
  5. */
  6.  
  7. public class TreeNode < T > {
  8.  
  9. // 数据域
  10. private T data;
  11. // 左子树
  12. private TreeNode < T > leftChild;
  13. // 右子树
  14. private TreeNode < T > rightChild;
  15.  
  16. public TreeNode(T data) {
  17. this(null, data, null);
  18. }
  19.  
  20. public TreeNode(TreeNode < T > leftChild, T data, TreeNode < T > rightChild) {
  21. this.leftChild = leftChild;
  22. this.data = data;
  23. this.rightChild = rightChild;
  24. }
  25.  
  26. public T getData() {
  27. return data;
  28. }
  29.  
  30. public TreeNode < T > getLeftChild() {
  31. return leftChild;
  32. }
  33.  
  34. public TreeNode < T > getRightChild() {
  35. return rightChild;
  36. }
  37. }
二叉树初始化

我们就以下图为例,构造一颗二叉树。

  1. /**
  2. * 构建二叉树
  3. *
  4. * @return 树根
  5. */
  6. TreeNode CreateTree() {
  7. TreeNode < String > nodeH = new TreeNode < >("H");
  8. TreeNode < String > nodeG = new TreeNode < >("G");
  9.  
  10. TreeNode < String > nodeF = new TreeNode < >(nodeH, "F", null);
  11. TreeNode < String > nodeE = new TreeNode < >(nodeG, "E", null);
  12. TreeNode < String > nodeD = new TreeNode < >("D");
  13.  
  14. TreeNode < String > nodeC = new TreeNode < >(null, "C", nodeF);
  15. TreeNode < String > nodeB = new TreeNode < >(nodeD, "B", nodeE);
  16. TreeNode < String > nodeA = new TreeNode < >(nodeB, "A", nodeC);
  17. return nodeA;
  18. }

这样,我们就按上图所示构建了一颗二叉树,返回二叉树的根结点。

二叉树的遍历

二叉树的遍历是二叉树最要的操作,也是二叉树的核心。从二叉树的定义我们可以得知,二叉树是一种递归形式的数据结构,根结点下的左右子树又分别是二叉树;因此这使得二叉树的遍历离不开递归这种思想。

很显然,对于二叉树的三种遍历,我们就可以借助其自身的特性,通过递归实现。

  • 二叉树的递归遍历实现
  1. /**
  2. * 访问每个结点
  3. *
  4. * @param node
  5. */
  6.  
  7. private void visitNode(TreeNode node) {
  8. System.out.print(node.getData().toString());
  9. System.out.print(" ");
  10. }
  11.  
  12. /**
  13. * 前序遍历-递归实现
  14. *
  15. * @param node
  16. */
  17. void preTraversal(TreeNode node) {
  18. if (node != null) {
  19. visitNode(node);
  20. preTraversal(node.getLeftChild());
  21. preTraversal(node.getRightChild());
  22. }
  23. }
  24.  
  25. /**
  26. * 中序遍历-递归实现
  27. *
  28. * @param node
  29. */
  30. void traversal(TreeNode node) {
  31. if (node != null) {
  32. traversal(node.getLeftChild());
  33. visitNode(node);
  34. traversal(node.getRightChild());
  35. }
  36. }
  37.  
  38. /**
  39. * 后序遍历-递归实现
  40. * @param node
  41. */
  42. void postTraversal(TreeNode node) {
  43. if (node != null) {
  44. postTraversal(node.getLeftChild());
  45. postTraversal(node.getRightChild());
  46. visitNode(node);
  47. }
  48. }

可以看到,使用递归实现二叉树的遍历十分简单,但我们也可以考虑使用非递归的形式,使用栈。

严格来说,使用栈实现二叉树的遍历,其实还是递归思想,只不过是我们自己用栈完成了递归实现中系统帮我们完成的工作。

本质上来说,二叉树这种递归的数据结构,他的遍历是离不开递归思想的,只不过看我们怎么去理解递归的实现了。

  • 二叉树的非递归实现
  1. /**
  2. * 前序遍历-迭代实现
  3. * @param node
  4. */
  5. void preTraversalIteration(TreeNode node) {
  6. // 创建一个栈
  7. Stack < TreeNode > mStack = new Stack < >();
  8. while (true) {
  9. while (node != null) { // 非叶子结点的子树
  10. // 前序遍历,先访问根结点
  11. visitNode(node);
  12. // 将当前结点压入栈
  13. mStack.push(node);
  14. // 对左子树继续进行前序遍历
  15. node = node.getLeftChild();
  16. }
  17.  
  18. if (mStack.isEmpty()) {
  19. //所有元素已遍历完成
  20. break;
  21. }
  22. // 弹出栈顶结点
  23. node = mStack.pop();
  24. // 右子树前序遍历
  25. node = node.getRightChild();
  26. }
  27. }
  28.  
  29. /**
  30. * 中序遍历-迭代实现
  31. * @param node
  32. */
  33. void TraversalIteration(TreeNode node) {
  34. // 创建一个栈
  35. Stack < TreeNode > mStack = new Stack < >();
  36. while (true) {
  37. while (node != null) { // 非叶子结点的子树
  38. // 将当前结点压入栈
  39. mStack.push(node);
  40. // 对左子树继续进行中序遍历
  41. node = node.getLeftChild();
  42. }
  43.  
  44. if (mStack.isEmpty()) {
  45. //所有元素已遍历完成
  46. break;
  47. }
  48. // 弹出栈顶结点
  49. node = mStack.pop();
  50. // 中序遍历,访问根结点
  51. visitNode(node);
  52. // 右子树中序遍历
  53. node = node.getRightChild();
  54. }
  55. }
  56.  
  57. /**
  58. * 后序遍历-迭代实现
  59. * @param node
  60. */
  61. void postTraversalIteration(TreeNode node) {
  62. // 创建一个栈
  63. Stack < TreeNode > mStack = new Stack < >();
  64. while (true) {
  65. if (node != null) {
  66. //当前结点非空,压入栈
  67. mStack.push(node);
  68. // 左子树继续遍历
  69. node = node.getLeftChild();
  70. } else {
  71. // 左子树为空
  72. if (mStack.isEmpty()) {
  73. return;
  74. }
  75.  
  76. if (mStack.lastElement().getRightChild() == null) {
  77. // 栈顶元素右子树为空,则当前结点为叶子结点,输出
  78. node = mStack.pop();
  79. visitNode(node);
  80. while (node == mStack.lastElement().getRightChild()) {
  81. visitNode(mStack.lastElement());
  82. node = mStack.pop();
  83. if (mStack.isEmpty()) {
  84. break;
  85. }
  86. }
  87. }
  88.  
  89. if (!mStack.isEmpty()) {
  90. node = mStack.lastElement().getRightChild();
  91. } else {
  92. node = null;
  93. }
  94. }
  95. }
  96. }

可以看到,虽说是非递归实现,但本质上还是依靠栈先进后出的特性,实现了递归访问每个结点的操作,无非就是在前、中、后三种顺序下,访问结点的时机不同而已。这里,前序和中序遍历的实现其实很容易理解,后续遍历的实现很考究对栈的使用理解。

  • 层序遍历

最后,再来说一说层序遍历。顾名思义,层序遍历就是从上到下按层,从左至右依次访问每个结点。这种遍历非常用规律,就是从根节点下一层开始,优先访问每一层所有的双亲结点,然后依次访问每个结点的左右儿子。也就是说,从上到下,先遇见到结点先访问,后遇到的结点后访问,这典型的就是队列的思想,因此我们可以使用队列实现二叉树的层序遍历。

  1. /**
  2. * 层序遍历
  3. * @param node
  4. */
  5. void levelTraversal(TreeNode node) {
  6. //创建队列
  7. Queue < TreeNode > mNodeQueue = new LinkedList < >();
  8. // 根结点加入队列
  9. mNodeQueue.add(node);
  10.  
  11. TreeNode temp;
  12.  
  13. while (!mNodeQueue.isEmpty()) {
  14. //元素出队列
  15. temp = mNodeQueue.poll();
  16. //输出
  17. visitNode(temp);
  18. if (temp.getLeftChild() != null) {
  19. // 左子树入队列
  20. mNodeQueue.add(temp.getLeftChild());
  21. }
  22.  
  23. if (temp.getRightChild() != null) {
  24. //右子树入队列
  25. mNodeQueue.add(temp.getRightChild());
  26. }
  27. }
  28. }

测试二叉树的实现

最后,用一个测试类测试一下我们对二叉树的实现。

  1. /**
  2. * Created by engineer on 2017/10/24.
  3. */
  4.  
  5. public class BinaryTreeTest {
  6. public
  7. static
  8. void
  9. main
  10. (String[] args)
  11. {
  12. BinaryTree mBinaryTree = new BinaryTree();
  13.  
  14. TreeNode root = mBinaryTree.CreateTree();
  15.  
  16. System.out.print("前序遍历-递归实现:");
  17. mBinaryTree.preTraversal(root);
  18. System.out.print("\n中序遍历-递归实现:");
  19. mBinaryTree.traversal(root);
  20. System.out.print("\n后序遍历-递归实现:");
  21. mBinaryTree.postTraversal(root);
  22. System.out.println();
  23. System.out.print("\n前序遍历-迭代实现:");
  24. mBinaryTree.preTraversalIteration(root);
  25. System.out.print("\n中序遍历-迭代实现:");
  26. mBinaryTree.TraversalIteration(root);
  27. System.out.print("\n后序遍历-迭代实现:");
  28. mBinaryTree.postTraversalIteration(root);
  29. System.out.println();
  30. System.out.print("\n层序遍历:");
  31. mBinaryTree.levelTraversal(root);
  32.  
  33. }
  34. }

得到输出:

  1. 前序遍历-递归实现:A B D E G C F H
  2. 中序遍历-递归实现:D B G E A C H F
  3. 后序遍历-递归实现:D G E B H F C A
  4.  
  5. 前序遍历-迭代实现:A B D E G C F H
  6. 中序遍历-迭代实现:D B G E A C H F
  7. 后序遍历-迭代实现:D G E B H F C A
  8.  
  9. 层序遍历:A B C D E F G H

嗯,和预期想象的一致。


好了,二叉树的存储结构和遍历就到这里了。