二叉树
前言
通过阅读本篇博文,我们可以大致掌握哪些知识?
- 二叉树的概念
- 二叉树的种类
- 二叉树的性质
- 二叉树的三种遍历方式(递归、非递归)
- 先序遍历
- 中序遍历
- 后序遍历
- 二叉搜索树
- 二叉搜索树的API实现(递归与非递归)
- 增
- 删
- 查
- 遍历
- 获取集合属性
- 获取节点的元素个数
- 获取树的深度
- 二叉树的两种构建方式
- 已知先序和中序
- 已知中序和后序
引入
树是一种非线性的数据结构,相对于线性的数据结构(链表、数组)⽽⾔,树的平均运⾏时间更短(往往与树相关的排序时间复杂度都不会⾼)
⼀般的树是有很多很多个分⽀的,分⽀下⼜有很多很多个分⽀,如果在程序中研究这个会⾮常麻烦。因为本来树就是⾮线性的,⽽我们计算机的内存是线性存储的,太过复杂的话我们⽆法设计出来的。
因此,我们先来研究简单⼜经常⽤的---> ⼆叉树
一、概念:
二叉树就是指每一个节点的子节点个数都不超过2个的树
一颗树至少会有一个节点(root)
树由节点组成,每个节点都包括该节点的key和指向两个子节点的节点引用(若子节点为空,则节点引用指向null)
class TreeNode<E> { TreeNode LChild; E key; TreeNode RChild; }
我们定义树的时候往往是定义节点,节点连接起来就成了树,而节点的定义就是:一个数据、两个指针。
二、二叉树的种类
满二叉树:最后一层的叶子结点是满的
完全二叉树:叶子节点只会出现在最后两层,且最后一层的叶子节点都是靠左对齐
注:完全二叉树从根节点到倒数第二层,组成了一棵满二叉树。(满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树)
若完全二叉树的总节点树为n,则叶子节点的个数为 (n+1)>>1;
- 国外说的完满二叉树(Full Binary Tree)就是国内的真二叉树。
- 国外说的完美二叉树(Perfect Binary Tree)就是国内说的满二叉树。
- 完全二叉树的定义,国内外一样。
三、二叉树的性质
在二叉树的第 i 层至多有 2^(i -1)个结点。(i>=1)
深度为 k 的二叉树至多有 2^(k-1)个结点(k >=1)
对任何一棵二叉树T, 如果其叶结点数为n0, 度为2的结点数为 n2,则n0=n2+1
具有 n (n>=0) 个结点的完全二叉树的深度为+1
如将一棵有n个结点的完全二叉树自顶向下,同层自左向右连续为结点编号0,1, …, n-1,则有:
1)若i = 0, 则 i 无双亲, 若i > 0, 则 i 的双亲为」(i -1)/2」 2)若2*i+1 < n, 则i 的左子女为 2*i+1,若2*i+2 < n, 则 i 的右子女为2*i+2 3)若结点编号i为偶数,且i != 0,则左兄弟结点i-1. 4)若结点编号i为奇数,且i != n-1,则右兄弟结点为i+1. 5)结点i 所在层次为」log2(i+1) 」
四、二叉树遍历有三种方式
先序遍历:
先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
中序遍历:
先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
后序遍历:
先访问左节点,然后访问右节点,最后访问根节点(左->右->根)
注:
- 先、中、后代表的是根节点的位置
如上二叉树的三种遍历方式分别是:
先序(根->左->右):1 2 4 5 7 3 6
中序(左->根->右):4 2 7 5 1 6 3
后序(左->右->根):4 7 5 2 6 3 1
一句话总结:先序(根->左->右),中序(左->根->右),后序(左->右->根)。如果访问有孩⼦的节点,先处理孩⼦的,随后返回。
无论先中后遍历,每个节点的遍历如果访问有孩⼦的节点,先处理孩⼦的(逻辑是⼀样的)
因此我们很容易想到递归
递归出口就是:没有子节点了,就返回
1、递归实现
// 利用递归,先序遍历 // 创建一个列表,将遍历过的节点保存到列表中 public List<> preOrder(TreeNode root) { List<> list = new ArrayList<>(); preOrder(root, list); return list; } public void preOrder(TreeNode root, List<> list) { // 递归出口 if (root == null) return ; // 先序:根->左->右 list.add(root.key); preOrder(root.left, list); preOrder(root.right, list); }
// 利用递归,中序遍历 // 创建一个列表,将遍历过的节点保存到列表中 public List<> inOrder(TreeNode root) { List<> list = new ArrayList<>(); inOrder(root, list); return list; } public void inOrder(TreeNode root, List<> list) { // 递归出口 if (root == null) return ; // 中序:左->根->右 inOrder(root.left, list); list.add(root.key); inOrder(root.right, list); }
// 利用递归,后序遍历 // 创建一个列表,将遍历过的节点保存到列表中 public List<> postOrder(TreeNode root) { List<> list = new ArrayList<>(); inOrder(root, list); return list; } public void postOrder(TreeNode root, List<> list) { // 递归出口 if (root == null) return ; // 序:左->右->根 postOrder(root.left, list); inOrder(root.right, list); list.add(root.key); }
2、非递归实现
本质上还是模仿递归实现的过程
先序遍历
// 非递归实现先序遍历,借助栈来实现(后进先出) public List<E> preOrder(TreeNode root) { List<E> list = new ArrayList<>(); if (root == null) return list; // 用栈实现 Deque<TreeNode> stack = new LinkedList<>(); stack.push(root); // 将根节点入栈 // 判断栈是否为空 while (!stack.isEmpty()) { TreeNode x = stack.pop(); list.add(x.value); // 出栈并将根节点的值保存到list中 // 判断是否有右孩子,有则入栈 if (x.right != null) stack.push(x.right); if (x.left != null) stack.push(x.left); } return list; }
中序遍历
// 非递归实现中序遍历,借助栈来实现(后进先出) public List<E> inOrder() { List<E> list = new ArrayList<>(); if (root == null) return list; Deque<TreeNode> stack = new LinkedList<>(); TreeNode x = root; while(!stack.isEmpty() || x != null) { while(x != null) { stack.push(x); x = x.left; // 一直往左走,直到左子树的最左节点才退出循环 } // 此时x指向了左子树的最左节点 x = stack.pop(); list.add(x.value); x = x.right; // 往右走 } return list; }
后序遍历
// 非递归实现后序遍历,借助栈来实现(后进先出) // 通过LinkedList的addFirst()方法实现头插 public List<E> postOrder() { LinkedList<E> list = new LinkedList<>(); if (root == null) return list; Deque<TreeNode> stack = new LinkedList<>(); // 将根节点入栈 stack.push(root); while (!stack.isEmpty()) { TreeNode x = stack.pop(); list.addFirst(x.value); // LinkedList的特有方法 // 判断是否有左孩子 if (x.left != null) stack.push(x.left); // 判断是否有右孩子 if (x.right != null) stack.push(x.right); } return list; } // 注:此处思考,我们用来保存节点的集合list,为什么选择LinkedList而不选择ArrayList来实现?
五、二叉搜索树
1、满足条件:
- 左子树上的所有节点值均小于根节点值
- 右子树上的所有节点值均不小于根节点值
- 左右子树也满足上述两个条件
public class BinarySearchTree<E extends Comparable<? super E>> { // 属性 private TreeNode root; private int size; private class TreeNode { TreeNode left; E value; TreeNode right; public TreeNode(E value) { this.value = value; } } // 默认构造方法 // API的实现: / * 增: boolean add(E e) 删: void clear() boolean remove(Object obj) 查: boolean contains(Object obj) E min() E max() 遍历: List<E> preOrder() List<E> inOrder() List<E> postOrder() List<E> levelOrder() 获取集合属性: int size() boolean isEmpty() 建树: */ }
2、二叉搜索树的API实现
(1)增加节点:
思路:
由于二叉搜索树的特殊性质确定了二叉搜索树中每个元素只可能出现一次,所以在插入的过程中如果发现这个元素已经存在于二叉搜索树中,就不进行插入,否则就查找合适的位置进行插入。因此,在进行插入之前我们需要先找到插入的位置。
- 第一种情况:根节点为空 —— 直接插入,return true;
- 第二种情况:要插入的元素已经存在,如上面所说,如果在二叉搜索树中已经存在该元素,则不再进行插入,直接return false;
- 第三种情况:能够找到合适位置进行插入
// 非递归方式实现 public boolean add(E e) { if (e == null) throw new IllegalArgumentException("Key cannot be null"); // 如果根节点为null if (root == null) { root = new TreeNode(e); size++; return true; } // 如果根节点不为null int cmp = e.compareTo(root.value); // 此处先行判断一下,e 是否 等于root.value,排除cmp==0的情况 // 因为最后找到要添加到上面的节点时,遍历的那个变量是指向null的,所以要用p保留它的父节点引用 TreeNode p = root; TreeNode x = cmp > 0 ?p.right:p.left; while(x != null) { p = x; cmp = e.compareTo(x.value); if (cmp > 0) x = x.right; else if (cmp < 0) x = x.left; else return false; } TreeNode node = new TreeNode(e); // 退出循环后,p指向了e要添加到的那个节点上 if(e.compareTo(p.value) > 0) p.right = node; else p.left = node; size++; return true; }
// 递归实现 public boolean add(E e) { if (e == null) throw new IllegalArgumentException("Key cannot be null"); int oldSize = size; root = add(root, e); return size > oldSize; } private TreeNode add(TreeNode node, E e) { // 递归出口 if (node == null) { // 找到了添加结点的位置 size++; return new TreeNode(e); } int cmp = e.compareTo(node.value); if (cmp < 0) node.left = add(node.left, e); else if (cmp > 0) node.right = add(node.right, e); return node; }
(2)删除节点:
思路:
- 先找到待删除的节点
- 在右子树中找到最左节点(或在左子树中找到最右节点)
- 先将将度为2的情况退化为度为1或度为0
- 删除节点
// 递归实现 public boolean remove(E e) { if(e == null) throw new IllegalArgumentException("Key cannot be null"); // 1、查找到与e相等的节点 TreeNode p = null; TreeNode x = root; while(x != null) { int cmp = e.compareTo(x.value); if(cmp < 0) { // 在左子树中寻找 p = x; x = x.lieft; } else if(x > 0) { // 在右子树中寻找 x = x.right; x = x.right; } else break; // 已经找到,或已经遍历完却没找到 } if (x == null) return false; // 此时x 指向了目标删除节点 // 2、先处理度为2的情况 if (x.left != null && x.right != null) { // 寻找右子树中的最左节点(或寻找左子树中的最右节点) TreeNode node = findSuccessor(x); // 交换值 x.value = node.left.value; p = node; x = node.left; } // 3、处理度为0或1的情况(删除节点) TreeNode child = x.left != null ? x.left:x.right; if(p == null) root = child; else if(p.left == x) p.left = child; else p.right = child; size--; return node; } // 寻找后继节点 private TreeNode findSuccessor(TreeNode node) { if (node == null) return null; TreeNode x = node; x = x.right; while (x.left != null) { node = x; x = x.left; } return node; }
// 递归实现
Task:
-
二叉树添加节点
- 递归
- 非递归
-
二叉树删除节点
- 递归
- 非递归
-
层次遍历
- BFS
-
先序、中序、后序遍历
- 递归
- 非递归
-
构建二叉搜索树
- 先序 + 中序
- 中序 + 后序