0.前言
我们知道对于有序数组,查找很快,并介绍可以通过二分法查找,但是想要在有序数组中插入一个数据项,就必须先找到插入数据项的位置,然后将所有插入位置后面的数据项全部向后移动一位,来给新数据腾出空间,平均来讲要移动N/2次,这是很费时的。同理,删除数据也是。
然后我们介绍了另外一种数据结构——链表,链表的插入和删除很快,我们只需要改变一些引用值就行了,但是查找数据却很慢了,因为不管我们查找什么数据,都需要从链表的第一个数据项开始,遍历到找到所需数据项为止,这个查找也是平均需要比较N/2次。
那么我们就希望一种数据结构能同时具备数组查找快的优点以及链表插入和删除快的优点,于是 树 诞生了。
树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
①、节点:上图的圆圈,比如A,B,C等都是表示节点。节点一般代表一些实体,在java面向对象编程中,节点一般代表对象。
②、边:连接节点的线称为边,边表示节点的关联关系。一般从一个节点到另一个节点的唯一方法就是沿着一条顺着有边的道路前进。在Java当中通常表示引用。
树有很多种,向上面的一个节点有多余两个的子节点的树,称为多路树,后面会讲解2-3-4树和外部存储都是多路树的例子。而每个节点最多只能有两个子节点的一种形式称为二叉树,这也是本篇博客讲解的重点。
树的常用术语
①、路径:顺着节点的边从一个节点走到另一个节点,所经过的节点的顺序排列就称为“路径”。
②、根:树顶端的节点称为根。一棵树只有一个根,如果要把一个节点和边的集合称为树,那么从根到其他任何一个节点都必须有且只有一条路径。A是根节点。
③、父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;B是D的父节点。
④、子节点:一个节点含有的子树的根节点称为该节点的子节点;D是B的子节点。
⑤、兄弟节点:具有相同父节点的节点互称为兄弟节点;比如上图的D和E就互称为兄弟节点。
⑥、叶节点:没有子节点的节点称为叶节点,也叫叶子节点,比如上图的H、E、F、G都是叶子节点。
⑦、子树:每个节点都可以作为子树的根,它和它所有的子节点、子节点的子节点等都包含在子树中。
⑧、节点的层次:从根开始定义,根为第一层,根的子节点为第二层,以此类推。
⑨、深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
⑩、高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
1.实现
定义
二叉树(binary tree)是结点的有限集合,这个集合或者空,或者由一个根及两个互不相交的称为这个根的左子树或右子树构成. 从定义可以看出,二叉树包括:
1.空树
2.只有一个根节点
3.只有左子树
4.只有右子树
5.左右子树都存在
有且仅有这5中表现形式
性质
性质1:在二叉树的第i层上至多有2^(i-1)个节点(i >= 1)
性质2:深度为k的二叉树至多有2^k-1个节点(k >=1)
性质3:对于任意一棵二叉树T而言,其叶子节点数目为N0,度为2的节点数目为N2,则有N0 = N2 + 1。
性质4:具有n个节点的完全二叉树的深度 。
性质5:完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:
若I为结点编号则 如果I>1,则其父结点的编号为I/2;
如果2*I<=N,则其左孩子(即左子树的根结点)的编号为2*I;若2*I>N,则无左孩子;
如果2*I+1<=N,则其右孩子的结点编号为2*I+1;若2*I+1>N,则无右孩子。
性质6:给定N个节点,能构成h(N)种不同的二叉树。
h(N)为卡特兰数的第N项。h(n)=C(2*n,n)/(n+1)。
性质7:设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i
代码
class Node {
int value;
Node leftChild;
Node rightChild;
Node(int value) {
this.value = value;
}
public void display() {
System.out.print(this.value + "\t");
}
@Override
public String toString() {
// TODO Auto-generated method stub
return String.valueOf(value);
}
}
二叉树:树的每个节点最多只能有两个子节点
上图的第一幅图B节点有DEF三个子节点,就不是二叉树,称为多路树;而第二幅图每个节点最多只有两个节点,是二叉树,并且二叉树的子节点称为“左子节点”和“右子节点”。上图的D,E分别是B的左子节点和右子节点。
如果我们给二叉树加一个额外的条件,就可以得到一种被称作二叉搜索树(binary search tree)的特殊二叉树。
二叉搜索树要求:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
2.操作
2.1 遍历
2.1.1 前序遍历
递归
/**
* //前序遍历(递归):
* 1、访问这个节点
* 2、调用自身来遍历节点的左子树
* 3、调用自身来遍历节点的右子树
*/
public void preOrderTraverse() {
System.out.print("前序遍历:");
preOrderTraverse(root);
System.out.println();
}
private void preOrderTraverse(Node node) {
if (node == null)
return ;
node.display();
preOrderTraverse(node.leftChild);
preOrderTraverse(node.rightChild);
}
非递归
/**
* 前序非递归遍历:
* 1)对于任意节点current,若该节点不为空则访问该节点后再将节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,栈顶节点出栈,将该节点的右子树置为current
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void preOrderByStack() {
System.out.print("前序非递归遍历:");
Stack<Node> stack = new Stack<Node>();
Node current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current.display();
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.pop();
current = current.rightChild;
}
}
System.out.println();
}
2.1.2 中序遍历
递归
/**
* //中序遍历(递归):
* 1、调用自身来遍历节点的左子树
* 2、访问这个节点
* 3、调用自身来遍历节点的右子树
*/
public void inOrderTraverse() {
System.out.print("中序遍历:");
inOrderTraverse(root);
System.out.println();
}
private void inOrderTraverse(Node node) {
if (node == null)
return ;
inOrderTraverse(node.leftChild);
node.display();
inOrderTraverse(node.rightChild);
}
非递归
/**
* 中序非递归遍历:
* 1)对于任意节点current,若该节点不为空则将该节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,栈顶节点出栈,访问节点后将该节点的右子树置为current
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void inOrderByStack() {
System.out.print("中序非递归遍历:");
Stack<Node> stack = new Stack<Node>();
Node current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.pop();
current.display();
current = current.rightChild;
}
}
System.out.println();
}
2.1.3 后序遍历
递归
/**
* //后序遍历(递归):
* 1、调用自身来遍历节点的左子树
* 2、调用自身来遍历节点的右子树
* 3、访问这个节点
*/
public void postOrderTraverse() {
System.out.print("后序遍历:");
postOrderTraverse(root);
System.out.println();
}
private void postOrderTraverse(Node node) {
if (node == null)
return ;
postOrderTraverse(node.leftChild);
postOrderTraverse(node.rightChild);
node.display();
}
非递归
/**
* 后序非递归遍历:
* 1)对于任意节点current,若该节点不为空则访问该节点后再将节点压栈,并将左子树节点置为current,重复此操作,直到current为空。
* 2)若左子树为空,取栈顶节点的右子树,如果右子树为空或右子树刚访问过,则访问该节点,并将preNode置为该节点
* 3) 重复1、2步操作,直到current为空且栈内节点为空。
*/
public void postOrderByStack() {
System.out.print("后序非递归遍历:");
Stack<Node> stack = new Stack<Node>();
Node current = root;
Node preNode = null;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.leftChild;
}
if (!stack.isEmpty()) {
current = stack.peek().rightChild;
if (current == null || current == preNode) {
current = stack.pop();
current.display();
preNode = current;
current = null;
}
}
}
System.out.println();
}
2.1.4 层次遍历
ArrayList<ArrayList<Integer>> levelOrder(TreeNode root){
ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
ArrayList<<Integer> level = new ArrayList<Integer>():
for(int i = 0;i < size ;i++){
TreeNode node = queue.poll();
level.add(node.val);
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
}
result.add(Level);
}
return result;
}
2.2 增加
public String insert(int value) {
String error = null;
Node node = new Node(value);
if (root == null) {
root = node;
root.leftChild = null;
root.rightChild = null;
} else {
Node current = root;
Node parent = null;
while (true) {
if (value < current.value) {
parent = current;
current = current.leftChild;
if (current == null) {
parent.leftChild = node;
break;
}
} else if (value > current.value) {
parent = current;
current = current.rightChild;
if (current == null) {
parent.rightChild = node;
break;
}
} else {
error = "having same value in binary tree";
}
} // end of while
}
return error;
}
2.3 查找
2.3.1 查找某结点
(这里的二叉树是二叉搜索树,左子树比根节点小,右子树比根节点大,不然就要遍历整个树)
public Node findKey(int value) {
Node current = root;
while (true) {
if (value == current.value) {
return current;
} else if (value < current.value) {
current = current.leftChild;
} else if (value > current.value) {
current = current.rightChild;
}
if (current == null) {
return null;
}
}
}
2.3.2 查找最大值和最小值
要找最小值,先找根的左节点,然后一直找这个左节点的左节点,直到找到没有左节点的节点,那么这个节点就是最小值。同理要找最大值,一直找根节点的右节点,直到没有右节点,则就是最大值。
//找到最大值
public Node findMax(){
Node current = root;
Node maxNode = current;
while(current != null){
maxNode = current;
current = current.rightChild;
}
return maxNode;
}
//找到最小值
public Node findMin(){
Node current = root;
Node minNode = current;
while(current != null){
minNode = current;
current = current.leftChild;
}
return minNode;
}
2.3.3 查找最大深度
int maxDeath(TreeNode node){
if(node==null){
return 0;
}
int left = maxDeath(node.left);
int right = maxDeath(node.right);
return Math.max(left,right) + 1;
}
2.3.4 求最小深度
int getMinDepth(TreeNode root){
if(root == null){
return 0;
}
return getMin(root);
}
int getMin(TreeNode root){
if(root == null){
return Integer.MAX_VALUE;
}
if(root.left == null&&root.right == null){
return 1;
}
return Math.min(getMin(root.left),getMin(root.right)) + 1;
}
2.3.5 求节点个数
int numOfTreeNode(TreeNode root){
if(root == null){
return 0;
}
int left = numOfTreeNode(root.left);
int right = numOfTreeNode(root.right);
return left + right + 1;
}
2.3.6 叶节点个数
int numsOfNoChildNode(TreeNode root){
if(root == null){
return 0;
}
if(root.left==null&&root.right==null){
return 1;
}
return numsOfNodeTreeNode(root.left)+numsOfNodeTreeNode(root.right);
}
2.3.7 第k层节点个数
int numsOfkLevelTreeNode(TreeNode root,int k){
if(root == null||k<1){
return 0;
}
if(k==1){
return 1;
}
int numsLeft = numsOfkLevelTreeNode(root.left,k-1);
int numsRight = numsOfkLevelTreeNode(root.right,k-1);
return numsLeft + numsRight;
}
2.4 删除
(事实上可以标记一个标记值表示是否删除,可以不用改变结构)
2.4.1 叶子节点(删除没有子节点的节点)
删除节点,我们要先找到该节点,并记录该节点的父节点。在检查该节点是否有子节点。如果没有子节点,接着检查其是否是根节点,如果是根节点,只需要将其设置为null即可。如果不是根节点,是叶节点,那么断开父节点和其的关系即可。
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查找删除值,找不到直接返回false
while(current.data != key){
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
if(current == null){
return false;
}
}
//如果当前节点没有子节点
if(current.leftChild == null && current.rightChild == null){
if(current == root){
root = null;
}else if(isLeftChild){
parent.leftChild = null;
}else{
parent.rightChild = null;
}
return true;
}
return false;
}
2.4.2 删除有一个子节点的节点
删除有一个子节点的节点,我们只需要将其父节点原本指向该节点的引用,改为指向该节点的子节点即可。
//当前节点有一个子节点
if(current.leftChild == null && current.rightChild != null){
if(current == root){
root = current.rightChild;
}else if(isLeftChild){
parent.leftChild = current.rightChild;
}else{
parent.rightChild = current.rightChild;
}
return true;
}else{
//current.leftChild != null && current.rightChild == null
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
}
2.4.3 删除有两个子节点的节点
这种情况比较复杂,需要寻找后继节点,即比要删除的节点的关键值次高的节点是它的后继节点。
后继节点就是比要删除的节点的关键值要大的节点集合中的最小值。
/**
*
* 得到后继节点,即删除节点的左后代
*/
private Node getSuccessor(Node delNode) {
Node successor = delNode;
Node successorParent = null;
Node current = delNode.rightChild;
while (current != null) {
successorParent = successor;
successor = current;
current = current.leftChild;
}
//如果后继节点不是删除节点的右子节点时,
if (successor != delNode.rightChild) {
//要将后继节点的右子节点指向后继结点父节点的左子节点,
successorParent.leftChild = successor.rightChild;
//并将删除节点的右子节点指向后继结点的右子节点
successor.rightChild = delNode.rightChild;
}
//任何情况下,都需要将删除节点的左子节点指向后继节点的左子节点
successor.leftChild = delNode.leftChild;
return successor;
}
a)如果后继节点是刚好是要删除节点的右子节点(此时可以知道,这个右子节点没有左子点,如果有,就不该这个右子节点为后继节点)
//删除的节点为父节点的左子节点时:
parent.leftChild = successor;
successor.leftChild = delNode.leftChild;
//删除的节点为父节点的右子节点时:
parent.rightChild = successor;
successor.leftChild = delNode.leftChild
b)如果后继节点为要删除节点的右子节点的左后代:
//删除的节点为父节点的左子节点时:
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
parent.leftChild = successor;
successor.leftChild = delNode.leftChild;
//删除的节点为父节点的右子节点时:
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
parent.rightChild = successor;
successor.leftChild = delNode.leftChild;
public boolean delete(int value) {
Node current = root; //需要删除的节点
Node parent = null; //需要删除的节点的父节点
boolean isLeftChild = true; //需要删除的节点是否父节点的左子树
while (true) {
if (value == current.value) {
break;
} else if (value < current.value) {
isLeftChild = true;
parent = current;
current = current.leftChild;
} else {
isLeftChild = false;
parent = current;
current = current.rightChild;
}
//找不到需要删除的节点,直接返回
if (current == null)
return false;
}
//分情况考虑
//1、需要删除的节点为叶子节点
if (current.leftChild == null && current.rightChild == null) {
//如果该叶节点为根节点,将根节点置为null
if (current == root) {
root = null;
} else {
//如果该叶节点是父节点的左子节点,将父节点的左子节点置为null
if (isLeftChild) {
parent.leftChild = null;
} else { //如果该叶节点是父节点的右子节点,将父节点的右子节点置为null
parent.rightChild = null;
}
}
}
//2、需要删除的节点有一个子节点,且该子节点为左子节点
else if (current.rightChild == null) {
//如果该节点为根节点,将根节点的左子节点变为根节点
if (current == root) {
root = current.leftChild;
} else {
//如果该节点是父节点的左子节点,将该节点的左子节点变为父节点的左子节点
if (isLeftChild) {
parent.leftChild = current.leftChild;
} else { //如果该节点是父节点的右子节点,将该节点的左子节点变为父节点的右子节点
parent.rightChild = current.leftChild;
}
}
}
//2、需要删除的节点有一个子节点,且该子节点为右子节点
else if (current.leftChild == null) {
//如果该节点为根节点,将根节点的右子节点变为根节点
if (current == root) {
root = current.rightChild;
} else {
//如果该节点是父节点的左子节点,将该节点的右子节点变为父节点的左子节点
if (isLeftChild) {
parent.leftChild = current.rightChild;
} else { //如果该节点是父节点的右子节点,将该节点的右子节点变为父节点的右子节点
parent.rightChild = current.rightChild;
}
}
}
//3、需要删除的节点有两个子节点,需要寻找该节点的后续节点替代删除节点
else {
Node successor = getSuccessor(current);
//如果该节点为根节点,将后继节点变为根节点,并将根节点的左子节点变为后继节点的左子节点
if (current == root) {
root = successor;
} else {
//如果该节点是父节点的左子节点,将该节点的后继节点变为父节点的左子节点
if (isLeftChild) {
parent.leftChild = successor;
} else { //如果该节点是父节点的右子节点,将该节点的后继节点变为父节点的右子节点
parent.rightChild = successor;
}
}
}
current = null;
return true;
}
2.3.4 不改变树结构的删除方式
通过上面的删除分类讨论,我们发现删除其实是挺复杂的,那么其实我们可以不用真正的删除该节点,只需要在Node类中增加一个标识字段isDelete,当该字段为true时,表示该节点已经删除,反正没有删除。那么我们在做比如find()等操作的时候,要先判断isDelete字段是否为true。这样删除的节点并不会改变树的结构。
public class Node {
int data; //节点数据
Node leftChild; //左子节点的引用
Node rightChild; //右子节点的引用
boolean isDelete;//表示节点是否被删除
}
2.4 其他
2.4.1 判断二叉树是否为平衡二叉树
boolean isBalanced(TreeNode node){
return maxDeath2(node)!=-1;
}
int maxDeath2(TreeNode node){
if(node == null){
return 0;
}
int left = maxDeath2(node.left);
int right = maxDeath2(node.right);
if(left==-1||right==-1||Math.abs(left-right)>1){
return -1;
}
return Math.max(left, right) + 1;
}
2.4.2 判断二叉树是否为完全二叉树
boolean isCompleteTreeNode(TreeNode root){
if(root == null){
return false;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
boolean result = true;
boolean hasNoChild = false;
while(!queue.isEmpty()){
TreeNode current = queue.remove();
if(hasNoChild){
if(current.left!=null||current.right!=null){
result = false;
break;
}
}else{
if(current.left!=null&¤t.right!=null){
queue.add(current.left);
queue.add(current.right);
}else if(current.left!=null&¤t.right==null){
queue.add(current.left);
hasNoChild = true;
}else if(current.left==null&¤t.right!=null){
result = false;
break;
}else{
hasNoChild = true;
}
}
}
return result;
}
2.4.3 二个二叉树是否完全相同
boolean isSameTreeNode(TreeNode t1,TreeNode t2){
if(t1==null&&t2==null){
return true;
}
else if(t1==null||t2==null){
return false;
}
if(t1.val != t2.val){
return false;
}
boolean left = isSameTreeNode(t1.left,t2.left);
boolean right = isSameTreeNode(t1.right,t2.right);
return left&&right;
}
2.4.4 两个二叉树是否互为镜像
boolean isMirror(TreeNode t1,TreeNode t2){
if(t1==null&&t2==null){
return true;
}
if(t1==null||t2==null){
return false;
}
if(t1.val != t2.val){
return false;
}
return isMirror(t1.left,t2.right)&&isMirror(t1.right,t2.left);
}
2.4.5 翻转二叉树
TreeNode mirrorTreeNode(TreeNode root){
if(root == null){
return null;
}
TreeNode left = mirrorTreeNode(root.left);
TreeNode right = mirrorTreeNode(root.right);
root.left = right;
root.right = left;
return root;
}
2.4.6 两个二叉树的最低公共祖父节点
TreeNode getLastCommonParent(TreeNode root,TreeNode t1,TreeNode t2){
if(findNode(root.left,t1)){
if(findNode(root.right,t2)){
return root;
}else{
return getLastCommonParent(root.left,t1,t2);
}
}else{
if(findNode(root.left,t2)){
return root;
}else{
return getLastCommonParent(root.right,t1,t2)
}
}
}
// 查找节点node是否在当前 二叉树中
boolean findNode(TreeNode root,TreeNode node){
if(root == null || node == null){
return false;
}
if(root == node){
return true;
}
boolean found = findNode(root.left,node);
if(!found){
found = findNode(root.right,node);
}
return found;
}
2.4.7 输入一个二叉树和一个整数,打印出二叉树中节点值的和等于输入整数所有的路径
void findPath(TreeNode r,int i){
if(root == null){
return;
}
Stack<Integer> stack = new Stack<Integer>();
int currentSum = 0;
findPath(r, i, stack, currentSum);
}
void findPath(TreeNode r,int i,Stack<Integer> stack,int currentSum){
currentSum+=r.val;
stack.push(r.val);
if(r.left==null&&r.right==null){
if(currentSum==i){
for(int path:stack){
System.out.println(path);
}
}
}
if(r.left!=null){
findPath(r.left, i, stack, currentSum);
}
if(r.right!=null){
findPath(r.right, i, stack, currentSum);
}
stack.pop();
}
2.4.8 二叉树的搜索区间
给定两个值 k1 和 k2(k1 < k2)和一个二叉查找树的根节点。找到树中所有值在 k1 到 k2 范围内的节点。即打印所有x (k1 <= x <= k2) 其中 x 是二叉查找树的中的节点值。返回所有升序的节点值。
ArrayList<Integer> result;
ArrayList<Integer> searchRange(TreeNode root,int k1,int k2){
result = new ArrayList<Integer>();
searchHelper(root,k1,k2);
return result;
}
void searchHelper(TreeNode root,int k1,int k2){
if(root == null){
return;
}
if(root.val>k1){
searchHelper(root.left,k1,k2);
}
if(root.val>=k1&&root.val<=k2){
result.add(root.val);
}
if(root.val<k2){
searchHelper(root.right,k1,k2);
}
}
2.4.9 二叉树内两个节点的最长距离
二叉树中两个节点的最长距离可能有三种情况:
1.左子树的最大深度+右子树的最大深度为二叉树的最长距离
2.左子树中的最长距离即为二叉树的最长距离
3.右子树种的最长距离即为二叉树的最长距离
因此,递归求解即可
private static class Result{
int maxDistance;
int maxDepth;
public Result() {
}
public Result(int maxDistance, int maxDepth) {
this.maxDistance = maxDistance;
this.maxDepth = maxDepth;
}
}
int getMaxDistance(TreeNode root){
return getMaxDistanceResult(root).maxDistance;
}
Result getMaxDistanceResult(TreeNode root){
if(root == null){
Result empty = new Result(0,-1);
return empty;
}
Result lmd = getMaxDistanceResult(root.left);
Result rmd = getMaxDistanceResult(root.right);
Result result = new Result();
result.maxDepth = Math.max(lmd.maxDepth,rmd.maxDepth) + 1;
result.maxDistance = Math.max(lmd.maxDepth + rmd.maxDepth,Math.max(lmd.maxDistance,rmd.maxDistance));
return result;
}
2.4.10 不同的二叉树
int numTrees(int n ){
int[] counts = new int[n+2];
counts[0] = 1;
counts[1] = 1;
for(int i = 2;i<=n;i++){
for(int j = 0;j<i;j++){
counts[i] += counts[j] * counts[i-j-1];
}
}
return counts[n];
}
2.4.11 判断二叉树是否是合法的二叉查找树(BST)
一棵BST定义为:
节点的左子树中的值要严格小于该节点的值。
节点的右子树中的值要严格大于该节点的值。
左右子树也必须是二叉查找树。
一个节点的树也是二叉查找树。
public int lastVal = Integer.MAX_VALUE;
public boolean firstNode = true;
public boolean isValidBST(TreeNode root) {
// write your code here
if(root==null){
return true;
}
if(!isValidBST(root.left)){
return false;
}
if(!firstNode&&lastVal >= root.val){
return false;
}
firstNode = false;
lastVal = root.val;
if (!isValidBST(root.right)) {
return false;
}
return true;
}
2.5 二叉树的效率
从前面的大部分对树的操作来看,都需要从根节点到下一层一层的查找。
一颗满树,每层节点数大概为2n-1,那么最底层的节点个数比树的其它节点数多1,因此,查找、插入或删除节点的操作大约有一半都需要找到底层的节点,另外四分之一的节点在倒数第二层,依次类推。
总共N层共有2n-1个节点,那么时间复杂度为O(logn),底数为2。
在有1000000 个数据项的无序数组和链表中,查找数据项平均会比较500000 次,但是在有1000000个节点的二叉树中,只需要20次或更少的比较即可。
有序数组可以很快的找到数据项,但是插入数据项的平均需要移动 500000 次数据项,在 1000000 个节点的二叉树中插入数据项需要20次或更少比较,在加上很短的时间来连接数据项。
同样,从 1000000 个数据项的数组中删除一个数据项平均需要移动 500000 个数据项,而在 1000000 个节点的二叉树中删除节点只需要20次或更少的次数来找到他,然后在花一点时间来找到它的后继节点,一点时间来断开节点以及连接后继节点。
所以,树对所有常用数据结构的操作都有很高的效率。
遍历可能不如其他操作快,但是在大型数据库中,遍历是很少使用的操作,它更常用于程序中的辅助算法来解析算术或其它表达式。
2.6 用数组表示树
用数组表示树,那么节点是存在数组中的,节点在数组中的位置对应于它在树中的位置。下标为 0 的节点是根,下标为 1 的节点是根的左子节点,以此类推,按照从左到右的顺序存储树的每一层。
树中的每个位置,无论是否存在节点,都对应于数组中的一个位置,树中没有节点的在数组中用0或者null表示。
假设节点的索引值为index,那么节点的左子节点是 2*index+1,节点的右子节点是 2*index+2,它的父节点是 (index-1)/2。
在大多数情况下,使用数组表示树效率是很低的,不满的节点和删除掉的节点都会在数组中留下洞,浪费存储空间。更坏的是,删除节点如果要移动子树的话,子树中的每个节点都要移到数组中新的位置,这是很费时的。
不过如果不允许删除操作,数组表示可能会很有用,尤其是因为某种原因要动态的为每个字节分配空间非常耗时。
2.7 完整的BinaryTree代码
Node.java
package com.ys.tree;
public class Node {
int data; //节点数据
Node leftChild; //左子节点的引用
Node rightChild; //右子节点的引用
boolean isDelete;//表示节点是否被删除
public Node(int data){
this.data = data;
}
//打印节点内容
public void display(){
System.out.println(data);
}
}
Tree.java
package com.ys.tree;
public interface Tree {
//查找节点
public Node find(int key);
//插入新节点
public boolean insert(int data);
//中序遍历
public void infixOrder(Node current);
//前序遍历
public void preOrder(Node current);
//后序遍历
public void postOrder(Node current);
//查找最大值
public Node findMax();
//查找最小值
public Node findMin();
//删除节点
public boolean delete(int key);
//Other Method......
}
BinaryTree.java
package com.ys.tree;
public class BinaryTree implements Tree {
//表示根节点
private Node root;
//查找节点
public Node find(int key) {
Node current = root;
while(current != null){
if(current.data > key){//当前值比查找值大,搜索左子树
current = current.leftChild;
}else if(current.data < key){//当前值比查找值小,搜索右子树
current = current.rightChild;
}else{
return current;
}
}
return null;//遍历完整个树没找到,返回null
}
//插入节点
public boolean insert(int data) {
Node newNode = new Node(data);
if(root == null){//当前树为空树,没有任何节点
root = newNode;
return true;
}else{
Node current = root;
Node parentNode = null;
while(current != null){
parentNode = current;
if(current.data > data){//当前值比插入值大,搜索左子节点
current = current.leftChild;
if(current == null){//左子节点为空,直接将新值插入到该节点
parentNode.leftChild = newNode;
return true;
}
}else{
current = current.rightChild;
if(current == null){//右子节点为空,直接将新值插入到该节点
parentNode.rightChild = newNode;
return true;
}
}
}
}
return false;
}
//中序遍历
public void infixOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.data+" ");
infixOrder(current.rightChild);
}
}
//前序遍历
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
infixOrder(current.leftChild);
infixOrder(current.rightChild);
}
}
//后序遍历
public void postOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
infixOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
//找到最大值
public Node findMax(){
Node current = root;
Node maxNode = current;
while(current != null){
maxNode = current;
current = current.rightChild;
}
return maxNode;
}
//找到最小值
public Node findMin(){
Node current = root;
Node minNode = current;
while(current != null){
minNode = current;
current = current.leftChild;
}
return minNode;
}
@Override
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查找删除值,找不到直接返回false
while(current.data != key){
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
if(current == null){
return false;
}
}
//如果当前节点没有子节点
if(current.leftChild == null && current.rightChild == null){
if(current == root){
root = null;
}else if(isLeftChild){
parent.leftChild = null;
}else{
parent.rightChild = null;
}
return true;
//当前节点有一个子节点,右子节点
}else if(current.leftChild == null && current.rightChild != null){
if(current == root){
root = current.rightChild;
}else if(isLeftChild){
parent.leftChild = current.rightChild;
}else{
parent.rightChild = current.rightChild;
}
return true;
//当前节点有一个子节点,左子节点
}else if(current.leftChild != null && current.rightChild == null){
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
}else{
//当前节点存在两个子节点
Node successor = getSuccessor(current);
if(current == root){
root= successor;
}else if(isLeftChild){
parent.leftChild = successor;
}else{
parent.rightChild = successor;
}
successor.leftChild = current.leftChild;
}
return false;
}
public Node getSuccessor(Node delNode){
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightChild;
while(current != null){
successorParent = successor;
successor = current;
current = current.leftChild;
}
//后继节点不是删除节点的右子节点,将后继节点替换删除节点
if(successor != delNode.rightChild){
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return successor;
}
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
bt.insert(50);
bt.insert(20);
bt.insert(80);
bt.insert(10);
bt.insert(30);
bt.insert(60);
bt.insert(90);
bt.insert(25);
bt.insert(85);
bt.insert(100);
bt.delete(10);//删除没有子节点的节点
bt.delete(30);//删除有一个子节点的节点
bt.delete(80);//删除有两个子节点的节点
System.out.println(bt.findMax().data);
System.out.println(bt.findMin().data);
System.out.println(bt.find(100));
System.out.println(bt.find(200));
}
}
2.8 哈夫曼(Huffman)编码
我们知道计算机里每个字符在没有压缩的文本文件中由一个字节(比如ASCII码)或两个字节(比如Unicode,这个编码在各种语言中通用)表示,在这些方案中,每个字符需要相同的位数。
有很多压缩数据的方法,就是减少表示最常用字符的位数量,比如英语中,E是最常用的字母,我们可以只用两位01来表示,2位有四种组合:00、01、10、11,那么我们可以用这四种组合表示四种常用的字符吗?
答案是不可以的,因为在编码序列中是没有空格或其他特殊字符存在的,全都是有0和1构成的序列,比如E用01来表示,X用01011000表示,那么在解码的时候就弄不清楚01是表示E还是表示X的起始部分,所以在编码的时候就定下了一个规则:每个代码都不能是其它代码的前缀。
2.8.1 哈夫曼编码
二叉树中有一种特别的树——哈夫曼树(最优二叉树),其通过某种规则(权值)来构造出一哈夫曼二叉树,在这个二叉树中,只有叶子节点才是有效的数据节点(很重要),其他的非叶子节点是为了构造出哈夫曼而引入的!
哈夫曼编码是一个通过哈夫曼树进行的一种编码,一般情况下,以字符:‘0’与‘1’表示。编码的实现过程很简单,只要实现哈夫曼树,通过遍历哈夫曼树,规定向左子树遍历一个节点编码为“0”,向右遍历一个节点编码为“1”,结束条件就是遍历到叶子节点!因为上面说过:哈夫曼树叶子节点才是有效数据节点!
我们用01表示S,用00表示空格后,就不能用01和11表示某个字符了,因为它们是其它字符的前缀。在看三位的组合,分别有000,001,010,100,101,110和111,A是010,I是110,为什么没有其它三位的组合了呢?因为已知是不能用01和11开始的组合了,那么就减少了四种选择,同时011用于U和换行符的开始,111用于E和Y的开始,这样就只剩下2个三位的组合了,同理可以理解为什么只有三个四位的代码可用。
所以对于消息:SUSIE SAYS IT IS EASY
哈夫曼编码为:100111110110111100100101110100011001100011010001111010101110
2.8.2 哈夫曼解码
如果收到上面的一串哈夫曼编码,怎么解码呢?消息中出现的字符在哈夫曼树中是叶节点,也就是没有子节点,如下图:它们在消息中出现的频率越高,在树中的位置就越高,每个圆圈外面的数字就是频率,非叶节点外面的数字是它子节点数字的和。
每个字符都从根开始,如果遇到0,就向左走到下一个节点,如果遇到1,就向右。比如字符A是010,那么先向左,再向右,再向左,就找到了A,其它的依次类推。
3 总结
树是由边和节点构成,根节点是树最顶端的节点,它没有父节点;二叉树中,最多有两个子节点;某个节点的左子树每个节点都比该节点的关键字值小,右子树的每个节点都比该节点的关键字值大,那么这种树称为二叉搜索树,其查找、插入、删除的时间复杂度都为logN;可以通过前序遍历、中序遍历、后序遍历来遍历树,前序是根节点-左子树-右子树,中序是左子树-根节点-右子树,后序是左子树-右子树-根节点;删除一个节点只需要断开指向它的引用即可;哈夫曼树是二叉树,用于数据压缩算法,最经常出现的字符编码位数最少,很少出现的字符编码位数多一些。
(部分引用自https://www.jianshu.com/p/0190985635eb)
(部分引用自“https://blog.csdn.net/fengrunche/article/details/52305748”)
(部分引用自“https://blog.csdn.net/fengrunche/article/details/52305748”)
(部分引用自“https://www.cnblogs.com/ysocean/p/8032642.html#_label5”)
(部分引用自“https://www.cnblogs.com/ysocean/p/8032642.html”)