数据结构(五):树
一、 树的概述
树是计算机中应用广泛的一种数据结构,日常生活中常见的图谱,公司组织结构等,都是树结构的数据。
树结构在计算机中是根朝上,叶子结点向下的。如图,它是由N个有限结点组成的具有层次关系的集合。
树有如下特点:
- 没有父结点的称为根结点
- 每个结点有0或多个子结点
- 每一个非根结点只有一个父结点
- 每个结点及其后代结点可以看成一颗子树,称为当前结点父结点的一颗子树
二、 树的术语
- 结点的度:结点所含子树的个数称为结点的度,如下图:2,3,4为1的子树,1的度为3。5,8和6为2的子树,2的度为2。
- 叶结点:度为0的结点称为叶结点,如4即为叶结点
- 分支结点:度不为0的结点称为分支结点。
- 结点的层次:根结点层次为1,往下的后继结点层次为2,依次类推
- 结点的层序编号:将结点的元素,从上到下,从左到右依次排序,如下图为12345678
- 树的度:树中所有结点的度的最大值,不一定是根结点,如下截图的6若衍生很多的子结点,则度的最大值计算应该算6的子结点个数
- 树的深度:层次最多的结点
- 森林:去掉根结点后的N个不相交的集合,称为森林
- 孩子结点:一个结点的后继结点称为孩子结点
- 父结点:一个结点的前驱结点称为父结点
- 兄弟结点:同一父结点的孩子结点互称为兄弟结点
三、 二叉树的基本定义
二叉树:度不超过2的数称为二叉树
满二叉树:每一个结点度都达到最大值的树称为满二叉树
完全二叉树:往二叉树放元素时,按从上到下,从左到右的次序去放,即度为0的叶子结点,只能出现在最下或次下层的称为完全二叉树
四、 二叉查找树的构建
构建如上图的二叉树,需要怎样声明对象和组成二叉树呢,见如下实现:
public class Node<Key,Value> {
//结点的键 public Key key;
//结点的值 public Value value;
//结点的左子结点 public Node left;
//结点的右子结点 public Node right;
public Node(Key key, Value value, Node left, Node right) { super(); this.key = key; this.value = value; this.left = left; this.right = right; }
public static void main(String[] args) { Node node1 = new Node(1, "A", null, null); Node node2 = new Node(2, "B", null, null); Node node3 = new Node(3, "C", null, null); Node node4 = new Node(4, "D", null, null); Node node5 = new Node(5, "E", null, null); Node node6 = new Node(6, "F", null, null);
node1.left = node2; node1.right = node3;
node2.left = node4; node2.right = node5;
node3.left = node6; } } |
五、 二叉查找树的实现
二叉树插入键值对:
(1) 当前二叉树的结点为空时,把新结点作为根结点
(2) 当前二叉树的结点不为空时,若新结点的key小于当前结点,则继续查找当前结点的左子结点
(3) 若新结点的key大于当前结点,则继续查找当前结点的右子结点
(4) 若新结点的key等于当前结点,则将当前结点的value替换
二叉树获取指定结点:
(1) 从根结点开始,若查询的key小于当前结点,则继续查找当前结点的左子结点
(2) 若查询的key大于当前结点,则继续查找当前结点的右子结点
(3) 若查询的key等于当前结点,则返回当前结点
二叉树删除指定结点:
(1) 首先使用获取结点的方法,找到要删除的结点
(2) 找到被删除结点右子树中最小的元素min,并删除它
(3) 让被删除结点的左子树成为min的左子树,让被删除结点的右子树成为min的右子树
(4) 让被删除结点的父节点指向min
public class BinaryTree<Key extends Comparable<Key>,Value> {
// 根结点声明 public Node root;
//树的元素个数 public int N;
public BinaryTree() { root = null; N=0; }
/** * 二叉树元素个数 * @return */ public int size() { return N; }
/** * 二叉树是否为空 * @return */ public boolean isEmpty() { return N==0; }
/** * 往二叉树中添加元素 * @param key * @param value */ public void put(Key key,Value value) { root = put(root, key, value); }
/** * 往二叉树指定结点添加元素 * @param node * @param key * @param value */ public Node put(Node node,Key key,Value value) { if(node==null) { //总数+1 N++; return new Node(key, value, null, null); }
int compare = key.compareTo((Key) node.key);
if(compare>0) { //存放的key大于当前结点,则继续遍历当前结点的右子树 node.right = put(node.right,key,value); }else if(compare<0) { //存放的key小于当前结点,则继续遍历当前结点的左子树 node.left = put(node.left,key,value); }else { //存放的key等于当前结点,则替换 node.value = value; }
return node; }
/** * 从二叉树中获取key对应的value * @param key * @return */ public Value get(Key key) { return get(root,key); }
/** * 从二叉树指定结点中获取key对应的value * @param node * @param key * @return */ public Value get(Node node,Key key) { if(node==null) { return null; }
int compare = key.compareTo((Key) node.key);
if(compare>0) { //查找的key大于当前结点,则继续遍历当前结点的右子树 return get(node.right,key); }else if(compare<0) { //查找的key小于当前结点,则继续遍历当前结点的左子树 return get(node.left,key); }else{ return (Value) node.value; } }
/** * 删除二叉树中指定结点 * @param key */ public Node delete(Key key) { return delete(root,key); }
/** * 删除二叉树中指定结点 * @param node * @param key */ public Node delete(Node node,Key key) { if(node==null) { return null; }
int compare = key.compareTo((Key) node.key);
if(compare>0) { //删除的key大于当前结点,则继续遍历当前结点的右子树, node.right = delete(node.right,key); }else if(compare<0) { //删除的key大于当前结点,则继续遍历当前结点的右子树, node.left = delete(node.left,key); }else { //被删除结点只有左结点,没有右结点的情况 if(node.left!=null && node.right == null) { //总数减1 N--; return node.left; }
//被删除结点只有右结点,没有左节点的情况 if(node.right!=null && node.left == null) { //总数减1 N--; return node.right; }
//找到被删除结点的右子树中的最小结点,用来取代被删除的结点 Node min = node.right; while(min.left!=null) { min = min.left; }
//删除被删除结点的右子树中的最小结点 Node delNode = node.right; while(delNode.left !=null) { if(delNode.left.left==null) { delNode.left = null; }else { delNode = delNode.left; } }
//让被删除结点右子树中的最小结点,取代被删除的结点 min.right = node.right; min.left = node.left; node = min;
//总数减1 N--; } return node; } } |
六、 二叉查找树最小键
查找二叉树中最小键的思路:从根结点找起,一直找当前结点的左子树,直到当前结点的左子树为空时,则当前结点为二叉查找树的的最小键
/** * 查找二叉查找树的最小键 * @return */ public Key min() { return (Key) min(root).key; }
public Node min(Node node) { if(node.left!=null) { return min(node.left); }else { return node; } } |
七、 二叉查找树最大键
查找二叉树中最大键的思路:从根结点找起,一直找当前结点的右子树,直到当前结点的右子树为空时,则当前结点为二叉查找树的的最大键
/** * 查找二叉查找树的最大键 * @return */ public Key max() { return (Key) max(root).key; }
public Node max(Node node) { if(node.right!=null) { return max(node.right); }else { return node; } } |
八、 二叉树的最大深度
计算二叉树最大深度的思路
- 当只有一个根结点时为1,当根结点为null时是0
- 计算左子树的最大深度
- 计算右子树的最大深度
- 比较左右子树最大深度,取较大值+1(根结点)
/** * 查找二叉树的最大深度 * @return */ public int deepLength() { return deepLength(root); }
public int deepLength(Node node) { //根结点为空 if(node==null) { return 0; }
//根结点不为空,定义左结点深度maxLeft,右结点深度maxRight,最大深度等于maxLeft和maxRight的较大值+1 int maxLeft = 0; int maxRight = 0; int max = 0; System.out.println(node.key);
//右子树不为空,则持续遍历当前结点的右子树 if(node.right!=null) { maxRight = deepLength(node.right); }
//左子树不为空,则持续遍历当前结点的左子树 if(node.left!=null) { maxLeft = deepLength(node.left); }
//最大深度计算 max = maxLeft > maxRight ? maxLeft+1 : maxRight+1; return max; } |
九、 二叉树的遍历
如线性表中的链表,数组,队列,栈一样,二叉树也有遍历查找的需求,由于树的结构和线性表有很大的差异,没有办法从前往后去查询,此时我们可以根据搜索路径来查找,即通过控制根结点
左子树或右子树的访问顺序,来实现二叉树的遍历查询。
- 前序遍历:先访问根结点,再访问左子树,后访问右子树
- 中序遍历:先访问左子树,再访问根结点,后访问右子树
- 后序遍历:先遍历左子树,再访问右子树,后访问根结点
前序遍历结果:EBADCGFH
中序遍历结果:ABCDEFGH
后序遍历结果:ACDBFHGE
十、 二叉树的遍历-前序遍历
前序遍历的实现思路
- 将当前结点的key放入队列中
- 遍历当前结点的左子树,左子树不为空时,递归遍历左子树
- 遍历当前结点的右子树,右子树不为空时,递归遍历右子树
/** * 前序遍历实现 */ public Queue<Key> preSearch() { Queue<Key> keys = new Queue<Key>(); preSearch(root, keys); return keys; }
public void preSearch(Node node, Queue<Key> keys) { if (node == null) { return; }
// 将当前结点放入队列中 keys.enqueue((Key) node.key);
// 遍历左子树,不为空时递归遍历 if (node.left != null) { preSearch(node.left, keys); }
// 遍历右子树,不为空时递归遍历 if (node.right != null) { preSearch(node.right, keys); } } |
十一、 二叉树的遍历-后序遍历
后序遍历的实现思路
- 遍历当前结点的左子树,左子树不为空时,递归遍历左子树
- 遍历当前结点的右子树,右子树不为空时,递归遍历右子树
- 将当前结点的key放入队列中
/** * 后序遍历实现 */ public Queue<Key> afterSearch() { Queue<Key> keys = new Queue<Key>(); afterSearch(root, keys); return keys; }
public void afterSearch(Node node, Queue<Key> keys) { if (node == null) { return; }
// 遍历左子树,不为空时递归遍历 if (node.left != null) { afterSearch(node.left, keys); }
// 遍历右子树,不为空时递归遍历 if (node.right != null) { afterSearch(node.right, keys); }
// 将当前结点放入队列中 keys.enqueue((Key) node.key); } |
十二、 二叉树的遍历-中序遍历
中序遍历的实现思路
- 遍历当前结点的左子树,左子树不为空时,递归遍历左子树
- 将当前结点的key放入队列中
- 遍历当前结点的右子树,右子树不为空时,递归遍历右子树
/** * 中序遍历实现 */ public Queue<Key> midSearch() { Queue<Key> keys = new Queue<Key>(); midSearch(root, keys); return keys; }
public void midSearch(Node node, Queue<Key> keys) { if (node == null) { return; }
// 遍历左子树,不为空时递归遍历 if (node.left != null) { midSearch(node.left, keys); }
// 将当前结点放入队列中 keys.enqueue((Key) node.key);
// 遍历右子树,不为空时递归遍历 if (node.right != null) { midSearch(node.right, keys); } } |
十三、 二叉树的遍历-层序遍历
层序遍历的实现思路
- 创建队列A,存储当前结点
- 循环队列A,首先弹出队列中的结点key,并存储到队列B中,如果当前结点的左子树不为空,则将左节点存储到队列A中,如果当前结点的右子树不为空,则将右结点存储到队列A中
/** * 层序遍历实现 */ public Queue<Key> layerSearch() { Queue<Node> A = new Queue<Node>(); Queue<Key> B = new Queue<Key>(); A.enqueue(root);
while (!A.isEmpty()) { Node n = A.dequeue(); B.enqueue((Key) n.key);
if (n.left != null) { A.enqueue(n.left); }
if (n.right != null) { A.enqueue(n.right); } }
return B; } |