本章节的源代码位于gitee上,想要下载的请点击数据存储——二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
二叉树中主要存在两种特别的二叉树,一种叫满二叉树,一种叫完全二叉树。通俗来说就是满二叉树的所有父节点下面都是两个子节点。完全二叉树就是节点的编号和节点在二叉树的位置相同。直接上图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYRfOfKn-1602135539202)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201007222653166.png)]
由于这一章节主要将如何实现一个二叉树及如何对二叉树进行增删改查操作,所以一些二叉树的特性、术语这里就不细讲了,不然篇幅太多。如果对二叉树不是很理解的,请跳转二叉树百度百科。理解了之后在来看这篇文章会更好。
二叉树的存储和链表类似,但是它又有所不同,不同在于,二叉树的左节点和右节点存储的数据是需要进行判断的。不能说这个数据都是随机排布的。当然这个排序可以是从小到大,也可以是从大到小。我们直接看图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sERXmIv7-1602135539210)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201008091114811.png)]
在存放数据的时候需要考虑的问题是,子节点是否为空,如果为空就可以直接添加数据,如果不为空需要继续向下判断,判断要插入的数据于直接点的大小关系,总之一个原则,父结点左边的数据全都比父结点小(大),右边的节点数据都比父结点大(小)即可。
现在用代码来实现上述操作。
interface ITree<T>{ void add(T data); } class TreeImpl<T> implements ITree<T>{ private Node root ;//保存根结点 private int size ;//保存树中数据个数 @Override public void add(T data) { if(data==null) return ; Comparable result = (Comparable) data; Node newNode = new Node(result); if(root==null){ this.root = newNode; } else { this.root.addNode(this.root, newNode); } this.size ++; } class Node{ private Node left;//左节点 private Node right;//右节点 private Node parent;//父节点 private Comparable<T> data; private Node(Comparable<T> data){ this.data = data; } private void addNode(Node parentNode , Node newNode){ if(this.data.compareTo((T) newNode.data)<=0){//添加的数据大于节点数据 if(this.right==null){//右子节点是否为空 this.right = newNode;//将数据放到右子节点 newNode.parent = parentNode;//为子节点上添加父结点 } else { this.right.addNode(this.right,newNode);//否则继续向下判断 } } else {//数据小于父结点,放左边 if(this.left==null){//和上面类是的操作 this.left = newNode; newNode.parent = parentNode; } else { this.left.addNode(this.left,newNode); } } } } }在树中添加数据和链表添加数据虽然都是通过节点进行的,但是链表的添加速度更快。而且使用的是双指针的操作形式,而树在每一次添加数据的时候都需要进行判断,只有满足某个条件之后才能将数据添加到节点上。不过好处在于排序上。如果说链表也需要进行排序,那么树的排序速度是快于链表的,因为每次判断后,数据都会减半。
如果想要将二叉树中的所有数据都查询出来,有三种遍历方式:
前序遍历:根 - 左 - 右中序遍历:左 - 根 - 右后序遍历:左 - 根 - 右可以看出中序遍历实际上就是对数据进行排序后输出,所以这里我就只实现中序遍历,代码实现为:
interface ITree<T>{ int size(); T[] toArrayCentre();//中序遍历 } class TreeImpl<T> implements ITree<T>{ private Node root ;//保存根结点 private int size ;//保存树中数据个数 private T[] returnData;//保存数据 private int len;//数组下标 @Override public int size() { return this.size; } @Override public T[] toArrayCentre() { if(this.size==0) return null; this.len = 0; returnData = (T[]) new Object[this.size];//开辟数组空间 this.root.toArrayNode(); return returnData; } class Node{ private Node left;//左节点 private Node right;//右节点 private Node parent;//父节点 private Comparable<T> data; private void toArrayNode(){ if(this.left!=null){ this.left.toArrayNode();//继续向左遍历 } TreeImpl.this.returnData[TreeImpl.this.len++] = (T) this.data;//添加数据 if(this.right!=null){ this.right.toArrayNode();//继续向右 } } } }测试代码
public class TreeDemo { public static void main(String[] args) { ITree tree = new TreeImpl(); tree.add(55); tree.add(12); tree.add(34); tree.add(88); tree.add(66); tree.add(99); tree.add(8); System.out.println(tree.size()); System.out.println(Arrays.toString(tree.toArrayCentre())); } }[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-msDeN1y6-1602135539214)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201008102557681.png)]
二叉树作为链表的升级版本,最大的优势就在于数据的查询上,相较于链表来说,树状结构可以保证较高的查询速度,最优的数据查询性能为log2(n)。在代码中实现方式如下:
interface ITree<T>{ boolean query(T data);//查询数据 } class TreeImpl<T> implements ITree<T>{ private Node root ;//保存根结点 @Override public boolean query(T data) { if(this.size == 0) return false; return this.root.queryNode(data); } class Node{ private Node left;//左节点 private Node right;//右节点 private Node parent;//父节点 private Comparable<T> data; private boolean queryNode(T data){ if(this.data.equals(data)){//这里使用compareTo进行判断也是可以的,在自定义类中都需要覆写这两个方法。 return true; } else { if(this.data.compareTo(data)<0){//判断到哪边查找数据 if(this.right!=null){//右子节点还有数据,继续向下 return this.right.queryNode(data); } else { return false;//没有数据返回false } } else { if(this.left != null){//继续向左子节点查询 return this.left.queryNode(data); } else { return false; } } } } } }测试代码
public class TreeDemo { public static void main(String[] args) { ITree tree = new TreeImpl(); tree.add(55); tree.add(12); tree.add(34); tree.add(88); tree.add(66); tree.add(99); tree.add(8); System.out.println(tree.query(8)); System.out.println(tree.query(9)); } }[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aB0a8Uuw-1602135539219)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201008105201862.png)]
相比于链表来说,树中的数据删除是比较麻烦的,因为要设计到父节点的问题。如果你删除的是叶子节点,那么就可以直接删除,但是如果是删除的是父节点,你还需要将叶子节点中的一个数据提上来做父结点。
删除子节点,直接删除删除父结点,父结点只有一个子节点,子节点直接成为父结点删除父结点,父结点下有多个数据,从子节点中选出新的父结点[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6qSmvaI1-1602135539223)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201008133630299.png)]
代码实现
interface ITree<T>{ void remove(T data);//删除节点 } class TreeImpl<T> implements ITree<T>{ private Node root ;//保存根结点 private int size ;//保存树中数据个数 private T[] returnData;//保存数据 private int len;//数组下标 @Override public void remove(T data) { Node deleteNode = this.root.queryRemoveNode(data);//查询要删除的节点 if(deleteNode!=null){ if(deleteNode == root){//该节点是否为根结点 if(this.size==1){ this.root = null; } this.root = removeNode(deleteNode); } else { removeNode(deleteNode); } } this.size--; } private Node removeNode(Node deleteNode){ Node moveSubNode = null; if(deleteNode.left == null && deleteNode.right == null){//该节点是否是叶子节点 if(deleteNode.parent.left == deleteNode){//该节点位于左边 deleteNode.parent.left = null; } else { deleteNode.parent.right = null;//位于右边 } } if((deleteNode.left != null & deleteNode.right == null)|| (deleteNode.left == null & deleteNode.right != null)) { moveSubNode = null;//要移动的节点,可能是左节点,或者右节点 if(deleteNode.left != null) { moveSubNode = deleteNode.left; }else { moveSubNode = deleteNode.right; } if(deleteNode.parent != null) { if(deleteNode.parent.left == deleteNode){//该节点位于左边 deleteNode.parent.left = moveSubNode;//移动子节点 }else { deleteNode.parent.right = moveSubNode;//移动子节点 } } moveSubNode.parent = deleteNode.parent;//修改父节点 } if(deleteNode.left != null && deleteNode.right != null) { moveSubNode = deleteNode.right;//获取移动节点 while(moveSubNode.left!=null) { this.len++; moveSubNode = moveSubNode.left;//找到最左的节点 } if(moveSubNode.right != null) {//移动节点存在有右节点 moveSubNode.parent.left = moveSubNode.right; moveSubNode.right.parent = moveSubNode.parent; }else { if(deleteNode.right != moveSubNode) { moveSubNode.parent.left = null;//删除移动节点对应的左节点 } } moveSubNode.parent = deleteNode.parent;//改变移动节点的父节点 moveSubNode.left = deleteNode.left; if(deleteNode.right != moveSubNode) { moveSubNode.right = deleteNode.right; } if(deleteNode.parent != null) { if(deleteNode.parent.left == deleteNode){//该节点位于左边 deleteNode.parent.left = moveSubNode;//移动子节点 }else { deleteNode.parent.right = moveSubNode;//移动子节点 } } } return moveSubNode; } class Node{ private Node left;//左节点 private Node right;//右节点 private Node parent;//父节点 private Comparable<T> data; private Node queryRemoveNode(T data){//查询要删除的数据是否存在 if(this.data.equals(data)){ return this; } else { if(this.data.compareTo(data)<0){ if(this.right != null){ return this.right.queryRemoveNode(data); } else { return null; } } else { if(this.left != null){ return this.left.queryRemoveNode(data); } else { return null; } } } } } }代码测试
public class TreeDemo { public static void main(String[] args) { ITree tree = new TreeImpl(); tree.add(55); tree.add(12); tree.add(34); tree.add(88); tree.add(66); tree.add(99); tree.add(8); System.out.println(Arrays.toString(tree.toArrayCentre())); tree.remove(8);//叶子节点 System.out.println(Arrays.toString(tree.toArrayCentre())); tree.remove(12);//只有一个子节点 System.out.println(Arrays.toString(tree.toArrayCentre())); tree.remove(55);//删除根节点 System.out.println(Arrays.toString(tree.toArrayCentre())); } }[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rK8QArOP-1602135539226)(C:\Users\C3H2\AppData\Roaming\Typora\typora-user-images\image-20201008125354388.png)]
链表是一种最为基础的线性数据结构,在进行链表操作的时候使用若干个节点进行数据的存储,那么在进行数据存储的时候由于本身不具有排序的特点,所以在使用链表查询数据时,它的时间复杂度为“O(n)”,链表可以实现的优化解决方案使用跳表方案来完成,实际上和二分查找的意思相同,利用排序的模式,随机抽取几个中心点进行若干次比较,但是比链表存储结构更加高级一点的就是树状结构。
上述只写了一种中序遍历,在源码中还有另外两种遍历方式,如果有需求,直接到源码数据存储——二叉树中复制或下载。