前言
前段时间整理了一篇关于MYSQL索引的文章,其中谈到了B-Tree和B+Tree,加上这两天coding涉及到了一些树结构的处理。恰逢数据结构已经忘干净了,所以准备重新捡起来看一下,先从二叉搜索树开始吧,后续会陆续更新。
1、二叉树
先整理一下二叉树的基本性质:
性质1:二叉树的第i层上至多有
2
i
−
1
2^{i-1}
2i−1(i≥1)个节点:
- 当i=1时,第1层的节点个数为 2 0 2^0 20 = 1,成立
- 当i>1时,第i层的节点个数为 2 i − 1 2^{i-1} 2i−1,那么第i+1层的节点个数就是 2 i 2^i 2i
- 因为二叉树节点最大的度是2,那么第i+1层的节点节点个数最多是第i层的两倍,于是就有2 ∗ \ast ∗ 2 i − 1 2^{i-1} 2i−1= 2 i 2^i 2i
性质2:深度为h的二叉树中至多含有 2 h − 1 2^h-1 2h−1个节点
- 深度为h的二叉树节点个数最多为 2 0 2^0 20 + 2 1 2^1 21 + 2 2 2^2 22 + … + 2 h − 1 2^{h-1} 2h−1 = 2 h − 1 2^h-1 2h−1
性质3:若在任意一棵二叉树中,有
n
0
n_{0}
n0个叶子节点,有
n
2
n_{2}
n2个度为2的节点,则必有n0=n2+1
设0度节点个数为
n
0
n_0
n0,1度节点个数为
n
1
n_1
n1,2度节点个数为
n
2
n_2
n2,树节点总个数为n,那么就有n =
n
0
n_0
n0 +
n
1
n_1
n1 +
n
2
n_2
n2 ①
0度节点没有孩子,1度节点有一个孩子,2度节点有2个孩子,只有root节点不是任何节点的孩子,则n =
n
1
n_1
n1 +
n
2
n_2
n2
∗
2
\ast2
∗2+1 ②
由①②得:
n
0
n_0
n0 =
n
2
n_2
n2 + 1
性质4:具有n个节点的满二叉树深为log2n+1
由性质2得,深度为h的二叉树中至多含有
2
h
−
1
2^h-1
2h−1个节点 ,所以具有n个节点的二叉树深度至少为
log
2
(
n
+
1
)
\log_2(n+1)
log2(n+1)
二叉树:
2、二叉搜索树
二叉搜索树,见名知其意:也是一个二叉树,与一般二叉树不同的是,对于树中任意一个节点,其左子树的所有节点值都小于该节点的值,右子树的所有节点都大于该节点的值,且任意左右子树也是二叉搜索树。二叉树的中序遍历是有序的,是一个递增序列,相对来说,对查询会变得更“友好”一些。
以下是一个二叉搜索树:
通过图示可以看出,任意节点的左子树的任意节点值都小于该值,反之亦然。
树整体结构是从左到右依次递增,左永远小于右,根节点在中间。所以根据写出中序(左根右)遍历得出结果:2 3 4 5 6 9 12 14 15 16 17 。
要认真理解这个遍历顺序,能够更容易帮助理解其中的一些规律。
3、二叉搜索树实现
先定义树节点,其中包括节点值key、左子树、右子树以及父节点,如下:
public class BinarySearchNode<T extends Comparable<T>> {
T key;
BinarySearchNode<T> left;
BinarySearchNode<T> right;
BinarySearchNode<T> parent;
public BinarySearchNode(T key, BinarySearchNode<T> parent, BinarySearchNode<T> left, BinarySearchNode<T> right) {
this.key = key;
this.parent = parent;
this.left = left;
this.right = right;
}
public T getKey() {
return key;
}
public String toString() {
return "key:"+key;
}
}
二叉搜索树具体实现,包括树的前中后序遍历,节点查找,插入节点,删除节点,节点的前驱、后继等操作,
package com.binarysearch;
public class BSTree<T extends Comparable<T>> {
private BinarySearchNode<T> root; //root
public BSTree() {
root=null;
}
/**
* 前序遍历
* @param tree
*/
private void previous(BinarySearchNode<T> tree) {
if(tree != null) {
System.out.print(tree.key+" ");
previous(tree.left);
previous(tree.right);
}
}
public void previous() {
previous(root);
}
/**
* 中序遍历
* @param tree
*/
public void middle(BinarySearchNode<T> tree) {
if(tree != null) {
middle(tree.left);
System.out.print(tree.key+" ");
middle(tree.right);
}
}
public void middle() {
middle(root);
}
/**
* 后序遍历
* @param tree
*/
public void after(BinarySearchNode<T> tree) {
if(tree != null)
{
after(tree.left);
after(tree.right);
System.out.print(tree.key+" ");
}
}
public void after() {
after(root);
}
/**
* 查找target节点
* @param root
* @param target
* @return
*/
public BinarySearchNode<T> search(BinarySearchNode<T> root, T target) {
if (root == null){
return root;
}
//比较对象大小
int cmp = target.compareTo(root.key);
if (cmp < 0){
return search(root.left, target);
}
else if (cmp > 0){
return search(root.right, target);
}else {
return root;
}
}
public BinarySearchNode<T> search(T key) {
return search(root, key);
}
/*
* (非递归实现)查找"二叉树x"中键值为key的节点
*/
private BinarySearchNode<T> iterativeSearch(BinarySearchNode<T> x, T key) {
while (x!=null) {
int cmp = key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else
return x;
}
return x;
}
public BinarySearchNode<T> iterativeSearch(T key) {
return iterativeSearch(root, key);
}
/**
* 返回最小节点,最小节点一定在"最左边"
* @param tree
* @return
*/
private BinarySearchNode<T> getMin(BinarySearchNode<T> tree) {
if (tree == null)
return null;
while(tree.left != null)
tree = tree.left;
return tree;
}
public T getMin() {
BinarySearchNode<T> p = getMin(root);
if (p != null)
return p.key;
return null;
}
/**
* 查找最大节点 ,最大节点一定在"最右边"
* @param tree
* @return
*/
private BinarySearchNode<T> getMax(BinarySearchNode<T> tree) {
if (tree == null)
return null;
while(tree.right != null)
tree = tree.right;
return tree;
}
public T getMax() {
BinarySearchNode<T> p = getMax(root);
if (p != null)
return p.key;
return null;
}
/**
* 查找节点的前驱节点
* 前驱节点: 小于该节点的最大节点
* 查找前驱和后继节点和树的中序遍历,前驱其实获取的就是该节点在中序遍历序列中的前一个节点
*/
public BinarySearchNode<T> precursor(BinarySearchNode<T> node) {
// 分两种情况讨论
// 1.如果该节点有左子树,那前驱节点就是左子树的最大节点,它应该位于左子树的"最右节点"
if (node.left != null)
return getMax(node.left);
// 如果没有左子树,需要判断该节点和直接父节点p之间的关系
// 1.如果该节点是父节点的左孩子,那前驱节点就是父节点,
// 2.如果该节点是父节点的右孩子,则沿着父节点一直向上查找,直到找到一个父节点p,p需要满足(假设p的父节点为q):是其父节点q的右孩子,那么q就是要找的前驱节点
// 这不难理解,中序遍历,遍历完父节点就会遍历右子树,从右子树递归先找最左节点,那么父节点就是最左节点的前驱。
BinarySearchNode<T> y = node.parent;
while ((y != null) && (node == y.left)) {
node = y;
y = y.parent;
}
return y;
}
/**
* 查找指定节点的后继节点
* 后继节点:大于该节点的最小节点,
*
*/
public BinarySearchNode<T> successor(BinarySearchNode<T> node) {
// 与前驱原理类似
//1.如果该节点有右子树,前驱节点就是右子树上的最小节点,它应该位于右子树的"最左边"
if (node.right != null)
return getMin(node.right);
// 如果没有右子树,就判断该节点和直接父节点p之间的关系
// 1.如果该节点是左孩子,那么他的后继节点就是它的父节点
// 2.如果该节点是右孩子,则继续沿着父节点向上查找,直到找到一个父节点p,p需满足:是其父节点q的左孩子,那么q就是要找的后继节点
// 或者这么理解,因为该节点是右孩子,根据中序遍历的规则,遍历完该节点的树,就应该向上遍历父级树,如果找到一个父节点是一个左孩子,那么该父节点的父节点一定是第一个大于它的值。
BinarySearchNode<T> y = node.parent;
while ((y != null) && (node == y.right)) {
node = y;
y = y.parent;
}
return y;
}
private void insert(BSTree<T> bst, BinarySearchNode<T> z) {
int cmp;
BinarySearchNode<T> y = null;
BinarySearchNode<T> x = bst.root;
// 先找到要插入节点的位置
while (x != null) {
y = x;
cmp = z.key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else
x = x.right;
}
z.parent = y;
//没有根节点 插入节点就是根节点
if (y == null)
bst.root = z;
else {
//判断节点放在左边还是右边
cmp = z.key.compareTo(y.key);
if (cmp < 0)
y.left = z;
else
y.right = z;
}
}
/**
* 节点插入
*/
public void insert(T key) {
BinarySearchNode<T> z=new BinarySearchNode<T>(key,null,null,null);
// 如果新建结点失败,则返回。
if (z != null)
insert(this, z);
}
//获取根节点
private BinarySearchNode<T> getRootNode(BSTree<T> bst){
return bst.root;
}
/**
* 删除节点
* @param root
* @param z
* @return
*/
private BinarySearchNode<T> remove(BinarySearchNode<T> root, BinarySearchNode<T> z){
if (root == null) return root;
//递归找到要删除的节点
int cmp = z.key.compareTo(root.key);
if (cmp < 0){
root.left = remove(root.left,z);
}else if (cmp > 0){
root.right = remove(root.right,z);
}else {
//如果该节点没有右子树,就用该节点的左子树代替该节点
if (root.right == null){
return root.left;
}
//如果该节点没有左子树,就用该节点的右子树代替该节点
if (root.left == null){
return root.right;
}
//如果既有左子树,也有右子树,那就把节点的左子树作为右子树的"最左子树"的左子树
//找到右子树的最左树
BinarySearchNode<T> r = root.right;
while (r.left != null){
r = r.left;
}
r.left = root.left;
root = root.left;
}
return root;
}
public void remove(T key) {
BinarySearchNode<T> z, node;
if ((z = search(root, key)) != null)
if ( (node = remove(getRootNode(this), z)) != null)
node = null;
}
/**
* 销毁
* @param tree
*/
private void destroy(BinarySearchNode<T> tree) {
if (tree==null)
return ;
//递归删除即可
if (tree.left != null)
destroy(tree.left);
if (tree.right != null)
destroy(tree.right);
tree=null;
}
public void clear() {
destroy(root);
root = null;
}
}
总结
以上就是一个二叉搜索树的基本原理和代码实现,相对二叉树就是多了一个排序功能,总体相对简单,后续时间充裕的话陆续会更新一些其他的数据结构知识,能力一般,如果有什么问题,还请各位看官及时纠正。