【数据结构】二叉树
前言
数据结构还是大二的时候学过的,当然由于是非计算机专业的学生,所以学的也不怎么样,去年用c++实现了最基本的数据结构,链表/栈/队列/二叉树,三月份看的时候还贴到了博客上。然而当时由于代码量不够,其实写的并不是很好,理解也太不到位。
最近在看算法导论,当然最基本的就是数据结构,于是打算将基本的知识在回顾一下。
我是一个疯狂的人,一旦决定做一件事,就会全天埋头去干,因为总有一种恨不得赶快学完的感觉。前几天我连续写了好几篇博客,讲解一些排序算法,其实在这个过程中,我深深的发现:
书上的伪代码/算法思路能看懂不一定能用代码轻松地实现,因为有很多小细节需要注意。能把代码写出来不一定能给别人讲清楚,就如这里的树的后序非递归遍历算法,我大致感觉和先序非递归有很大相似,但想讲清楚还是想了好久的。
写博客记录自己的学习笔记其实就是一个强迫自己把学到的知识不仅仅停留在会的层次上而是上升到理解的高度的过程。
二叉树
每个节点之多有两棵子树,并且两棵子树有顺序之分,不能颠倒。二叉树的性质:
1.第i层至多有2^(i-1)个结点
2.深度为K的二叉树,至多有2^k-1个结点
3.任何一棵二叉树,叶子结点数目n0=度为2的结点数目n2 + 1
n=n0+n1+n2
n=B+1=n1+2*n2+1
于是n0=n2+1
4.n个结点的完全二叉树深度为(lgn下取整)+1
二叉树的实现
下面主要讲二叉树的链式存储结点数据结构,BinaryTreeNode类描述每个结点
public class BinaryTreeNode { public int data; public BinaryTreeNode lchild; public BinaryTreeNode rchild; public BinaryTreeNode parent; public BinaryTreeNode(int data) { this.data = data; this.lchild = null; this.rchild = null; this.parent = null; } }BinaryTree类
public class BinaryTree { public BinaryTreeNode root; // 从数组递归创建二叉树时用来作为数组下标 private int index = 0; /** * 通过数组作为输入的一棵先序遍历的树节来创建二叉树 * @param array */ public void createTree(int[] array) { index = 0; root = createPreOrder(array, root); } private BinaryTreeNode createPreOrder(int[] a, BinaryTreeNode root) { if (a[index] == 0 || index > a.length - 1) { index++; root = null; } else { root = new BinaryTreeNode(a[index]); index++; root.lchild = createPreOrder(a, root.lchild); if (root.lchild != null) root.lchild.parent = root; // 双亲节点主要用在寻找前驱和后继 root.rchild = createPreOrder(a, root.rchild); if (root.rchild != null) root.rchild.parent = root; // 双亲节点主要用在寻找前驱和后继 } return root; } /** * 通过键盘输入一棵先序遍历的树节点创建二叉树 */ public void createTree() { Scanner in = new Scanner(System.in); root = createPreOrder(in, root); } private BinaryTreeNode createPreOrder(Scanner in, BinaryTreeNode root) { int a = in.nextInt(); if (a == 0) { return null; } else { root = new BinaryTreeNode(a); root.lchild = createPreOrder(in, root.lchild); if (root.lchild != null) root.lchild.parent = root; // 双亲节点主要用在寻找前驱和后继 root.rchild = createPreOrder(in, root.rchild); if (root.rchild != null) root.rchild.parent = root; // 双亲节点主要用在寻找前驱和后继 return root; } } /** * 先序递归遍历 */ public void preOrder(BinaryTreeNode root) { if (root != null) { System.out.print(root.data + " "); preOrder(root.lchild); preOrder(root.rchild); } } /** * 先序非递归遍历 */ public void preOrder() { Stack<BinaryTreeNode> stack = new Stack<BinaryTreeNode>(); visitAlongLeft(root, stack); while (stack.size() > 0) { BinaryTreeNode node = stack.poll(); visitAlongLeft(node, stack); } } /** * 沿着某个给定结点root的左分支访问,同时将有孩子入栈 * @param root * @param stack */ private void visitAlongLeft(BinaryTreeNode root, Stack<BinaryTreeNode> stack) { while (root != null) { System.out.print(root.data + " "); if (root.rchild != null) stack.push(root.rchild); root = root.lchild; } } /** * 中序递归遍历 * @param root */ public void inOrder(BinaryTreeNode root) { if (root != null) { inOrder(root.lchild); System.out.print(root.data + " "); inOrder(root.rchild); } } /** * 中序非递归遍历 */ public void inOrder() { Stack<BinaryTreeNode> stack = new Stack<BinaryTreeNode>(); goAlongLeft(root, stack); while (stack.size() > 0) { BinaryTreeNode node = stack.poll(); System.out.print(node.data + " "); goAlongLeft(node.rchild, stack); } } /** * 沿着某个给定结点root的左分支入栈 * @param root * @param stack */ private void goAlongLeft(BinaryTreeNode root, Stack<BinaryTreeNode> stack) { while (root != null) { stack.push(root); root = root.lchild; } } /** * 后序递归遍历 * @param root */ public void postOrder(BinaryTreeNode root) { if (root != null) { postOrder(root.lchild); postOrder(root.rchild); System.out.print(root.data + " "); } } // stack2 用来访问 private void goAlongRight(BinaryTreeNode root, Stack<BinaryTreeNode> stack1,Stack<BinaryTreeNode> stack2) { while (root != null) { stack2.push(root); // 访问 if (root.lchild != null) stack1.push(root.lchild); root = root.rchild; } } /** * 后序非递归遍历 */ public void postOrder() { Stack<BinaryTreeNode> myStack1 = new Stack<BinaryTreeNode>(); Stack<BinaryTreeNode> myStack2 = new Stack<BinaryTreeNode>(); goAlongRight(root, myStack1, myStack2); while (myStack1.size() > 0) { BinaryTreeNode node = myStack1.poll(); goAlongRight(node, myStack1, myStack2); } while (myStack2.size() > 0) { BinaryTreeNode node = myStack2.poll(); System.out.print(node.data + " "); } } /** * 层序遍历 */ public void levelOrder() { Queue<BinaryTreeNode> queue = new Queue<BinaryTreeNode>(); if (root != null) { queue.offer(root); } while (!queue.isEmpty()) { BinaryTreeNode node = queue.poll(); System.out.print(node.data + " "); if (node.lchild != null) queue.offer(node.lchild); if (node.rchild != null) queue.offer(node.rchild); } } /** * 层序打印,每层用还行区分 */ public void levelOrderH() { if (root == null) return; Queue<BinaryTreeNode> current = new Queue<BinaryTreeNode>(); Queue<BinaryTreeNode> next = new Queue<BinaryTreeNode>(); current.offer(root); while (current.size() > 0) { BinaryTreeNode node = current.poll(); if (node!=null) { System.out.print(node.data + " "); next.offer(node.lchild); next.offer(node.rchild); } if (current.size() == 0) { System.out.println(""); asign(current, next); } } } // 将next行的元素给current行 private void asign(Queue<BinaryTreeNode> current, Queue<BinaryTreeNode> next) { while (next.size() > 0) { current.offer(next.poll()); } } public static void main(String[] args) { BinaryTree tree = new BinaryTree(); int[] array = {1,2,4,0,7,0,0,5,0,0,3,6,8,0,0,0,0}; tree.createTree(array); System.out.println("先序递归遍历"); tree.preOrder(tree.root); System.out.println("\n中序递归遍历"); tree.inOrder(tree.root); System.out.println("\n后序递归遍历"); tree.postOrder(tree.root); System.out.println("\n先序非递归遍历"); tree.preOrder(); System.out.println("\n中序非递归遍历"); tree.inOrder(); System.out.println("\n后序非递归遍历"); tree.postOrder(); System.out.println("\n层序遍历"); tree.levelOrder(); System.out.println("\n层序遍历"); tree.levelOrderH(); } }
相关算法
1.先序/中序/后序递归遍历,比较简单,不用解释。
2.先序非递归遍历
a.先沿着结点的左分支依次访问,访问时,顺便将其右孩子入栈b.当栈不空时,每弹出一个,重复1
代码如下
public void preOrder() { Stack<BinaryTreeNode> stack = new Stack<BinaryTreeNode>(); visitAlongLeft(root, stack); while (stack.size() > 0) { BinaryTreeNode node = stack.poll(); visitAlongLeft(node, stack); } } private void visitAlongLeft(BinaryTreeNode root, Stack<BinaryTreeNode> stack) { while (root != null) { System.out.print(root.data + " "); if (root.rchild != null) stack.push(root.rchild); root = root.lchild; } }
3.中序非递归遍历
a.沿着节点的左分支走,并将其入栈,直到最左下b.当栈不空时,每弹出一个,访问之,并对其右孩子执行a的操作
public void inOrder() { Stack<BinaryTreeNode> stack = new Stack<BinaryTreeNode>(); goAlongLeft(root, stack); while (stack.size() > 0) { BinaryTreeNode node = stack.poll(); System.out.print(node.data + " "); goAlongLeft(node.rchild, stack); } } private void goAlongLeft(BinaryTreeNode root, Stack<BinaryTreeNode> stack) { while (root != null) { stack.push(root); root = root.lchild; } }
注:两个辅助函数的命名一个是visitAlongLeft一个是goAlongLeft也能看出来他们的区别。
这里巧妙地使用了两个栈,采用和先序遍历一样的思路
代码如下
private void goAlongRight(BinaryTreeNode root, Stack<BinaryTreeNode> stack1,Stack<BinaryTreeNode> stack2) { while (root != null) { stack2.push(root); // 访问 if (root.lchild != null) stack1.push(root.lchild); root = root.rchild; } } public void postOrder() { Stack<BinaryTreeNode> myStack1 = new Stack<BinaryTreeNode>(); Stack<BinaryTreeNode> myStack2 = new Stack<BinaryTreeNode>(); goAlongRight(root, myStack1, myStack2); while (myStack1.size() > 0) { BinaryTreeNode node = myStack1.poll(); goAlongRight(node, myStack1, myStack2); } while (myStack2.size() > 0) { BinaryTreeNode node = myStack2.poll(); System.out.print(node.data + " "); } }
5.中序非递归遍历和先序思路类似,层序非递归遍历比较简单在此不再解释
6.创建二叉树
先序递归遍历同时也提供了一种创建二叉树的方法,大致思路是首先创建树根,然后递归创建左子树再递归创建右子树。
需要注意的是我在每个结点还保存了父节点,因此在创建的时候要给每个结点的父亲指针赋值。以从键盘输入一棵先序遍历树来创建二叉树为例,代码如下
public void createTree() { Scanner in = new Scanner(System.in); root = createPreOrder(in, root); } private BinaryTreeNode createPreOrder(Scanner in, BinaryTreeNode root) { int a = in.nextInt(); if (a == 0) { return null; } else { root = new BinaryTreeNode(a); root.lchild = createPreOrder(in, root.lchild); if (root.lchild != null) root.lchild.parent = root; // 双亲节点主要用在寻找前驱和后继 root.rchild = createPreOrder(in, root.rchild); if (root.rchild != null) root.rchild.parent = root; // 双亲节点主要用在寻找前驱和后继 return root; } }这里我再一次深深的厌恶java不能像C++里面那样有&引用传递或者传递指针(类比这里应该就是传递二级指针),导致无法在函数里面改变"引用"的值,而必须通过函数的返回值。所以看起来没有C++写的紧凑。
注:代码里面使用到的Stack是自己封装的,所以可能会与jdk中的有所差别。
测试
使用封装的BinaryTree类进行测试
public static void main(String[] args) { BinaryTree tree = new BinaryTree(); int[] array = {1,2,4,0,7,0,0,5,0,0,3,6,8,0,0,0,0}; tree.createTree(array); System.out.println("先序递归遍历"); tree.preOrder(tree.root); System.out.println("\n中序递归遍历"); tree.inOrder(tree.root); System.out.println("\n后序递归遍历"); tree.postOrder(tree.root); System.out.println("\n先序非递归遍历"); tree.preOrder(); System.out.println("\n中序非递归遍历"); tree.inOrder(); System.out.println("\n后序非递归遍历"); tree.postOrder(); System.out.println("\n层序遍历"); tree.levelOrder(); System.out.println("\n层序遍历,每层换行"); tree.levelOrderH(); }打印结果:
先序递归遍历 1 2 4 7 5 3 6 8 中序递归遍历 4 7 2 5 1 8 6 3 后序递归遍历 7 4 5 2 8 6 3 1 先序非递归遍历 1 2 4 7 5 3 6 8 中序非递归遍历 4 7 2 5 1 8 6 3 后序非递归遍历 7 4 5 2 8 6 3 1 层序遍历 1 2 3 4 5 6 7 8 层序遍历 1 2 3 4 5 6 7 8
add at 2015年11月13日22:25:42
经过网友Gun计划的提醒我添加了一个层序遍历的方法命名为levelOrderH,H意为Human,更人性化一点。每层打印完后换行,采用两个队列,很巧妙地实现,以前做过,所以很快能做出来哈哈。还可以加一个标记行结束也可以实现。