一、二叉树的查找
1.二叉查找树
对于树结构的查找,因为实际运用中大多都是使用二叉树的结构,因此我们只讨论二叉树的查找。
若有一棵非空二叉树,他满足如下条件:
-
若他的左子树不为空树,则有左子树上所有结点的值小于根结点的值。
-
若他的右子树不为空树,则有右子树上所有节点的值大于根结点的值。
-
他的左右子树也满足上述两个条件。
-
不能有值相等的结点。
则称这棵树为二叉查找/排序/搜素树。对二叉查找树进行中序遍历可以获取一个有序(升序)排列的集合。
若对一棵普通的二叉查找树查找操作,则过程与二分查找类似,且查找的次数不会超过树的高度。此时查找到额时间复杂度为O(log2n);但若是出现极端情况,一棵二叉查找树完全没有左孩子或完全没有右孩子,此时变成链表结构,查找的时间复杂度也随之变成O(n)。因此,为了提升查询效率,在构建查找树时要尽可能的保证左右子树的结点数量相近,以保证查找效率。
二叉查找树的简单实现:
/**
* 二叉查找功能接口
* Created by bzhang on 2019/3/2.
*/
public interface TreeList {
//获取根结点
Node getRoot();
// 返回树中的结点数,即数据元素的个数。
int size();
//树的高度
int getHeight();
// 返回树中排在 key 的数据元素
Object get(int key);
//向树中增加结点
void put(int key,Object value);
// 如果树为空返回 true,否则返回 false。
boolean isEmpty();
// 判断树中是否包含数据元素 e
boolean contains(int key);
//删除树中排在key位置的结点
boolean remove(int key);
//中序遍历
void inOrder();
}
接口实现类:
/**
* 底层为链表实现的二叉查找树的实现。
* Created by bzhang on 2019/3/2.
*/
public class TreeListImpl implements TreeList {
private int size; //树中的结点数
private Node root; //跟结点
@Override
public Node getRoot() {
return root;
}
@Override
public int size() {
return size;
}
@Override
public int getHeight() {
return getHeight(root);
}
//实际计算二叉树高度的方法
private int getHeight(Node node) {
if (node==null){
return 0;
}else {
int r = getHeight(node.getRight());
int l = getHeight(node.getLeft());
return r>l?(r+1):(l+1);
}
}
@Override
public Object get(int key) {
Node temp = root;
while (temp!=null){ //当前结点不为空,进入循环
if (temp.getKey()==key){ //key与结点的key相等,则返回结点的value值。
return temp.getValue();
}else if(temp.getKey()>key){ //key小于当前结点的key值,去查找当前节点的左子树
temp = temp.getLeft();
}else if (temp.getKey()<key){ //key大于当前结点的key值,去查找当前节点的右子树
temp = temp.getRight();
}
}
return null; //树中不存在key,返回null
}
@Override
public void put(int key, Object value) {
if (root==null){ //第一传入的结点设为根结点
Node node = new Node(key,value,null,null,null);
root = node;
size++;
}else {
addNode(key,value); //调用向树中增加结点的方法
}
}
//实际向二叉查找树中增加结点的方法
private void addNode(int key, Object value) {
Node temp = root;
Node res = null;
while (temp!=null){
if (temp.getKey()==key){ //树中已存在key的结点,更新其value值
temp.setValue(value);
return;
}else if(temp.getKey()>key){ //key小于当前结点的key值,往其左子树中增加结点
res = temp;
temp = temp.getLeft();
}else if (temp.getKey()<key){ //key大于当前结点的key值,往其右子树中增加结点
res = temp;
temp = temp.getRight();
}
}
Node newNode = new Node(key,value,null,null,res); //新建以查找到的结点为父结点的结点
if (res.getKey()>key){ //判断新建结点是res左孩子还是右孩子
res.setLeft(newNode);
}else {
res.setRight(newNode);
}
size++;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(int key) {
Node temp = root;
while (temp!=null){
if (temp.getKey()==key){
return true;
}else if(temp.getKey()>key){
temp = temp.getLeft();
}else if (temp.getKey()<key){
temp = temp.getRight();
}
}
return false;
}
@Override
public boolean remove(int key) {
Node temp = root;
while (temp!=null){ //查询要删除掉的结点
if (temp.getKey()==key){
remove(temp); //实际进行删除操作的方法
return true;
}else if(temp.getKey()>key){
temp = temp.getLeft();
}else if (temp.getKey()<key){
temp = temp.getRight();
}
}
return false;
}
/**
* 实际删除结点方法
* @param temp
*/
private void remove(Node temp) {
if (temp==root){ //判断要删除的结点是否为根结点
if (temp.getLeft()==null&&temp.getRight()==null){ //根结点没有左右子树
root = null;
}else if(temp.getLeft()!=null&&temp.getRight()==null){ //仅有左子树
root = temp.getLeft();
root.setParent(null);
}else if (temp.getLeft()==null&&temp.getRight()!=null){ //仅有右子树
root = temp.getRight();
root.setParent(null);
}else { //左右子树都有,则将右孩子设为根结点,并向右子树中最小的结点上添加整个左子树
root = temp.getRight();
root.setParent(null);
Node minRight = getMinNode(temp.getRight());
minRight.setLeft(temp.getLeft());
temp.getLeft().setParent(minRight);
}
}else { //要删除的结点不是根结点
Node parent = temp.getParent();
if (temp.getLeft()==null&&temp.getRight()==null){ //要删除的结点没有左右子树
if (parent.getLeft()==temp){
parent.setLeft(null);
}else {
parent.setRight(null);
}
}else if(temp.getLeft()!=null&&temp.getRight()==null){ //仅有左子树
if (parent.getLeft()==temp){
parent.setLeft(temp.getLeft());
}else {
parent.setRight(temp.getLeft());
}
}else if (temp.getLeft()==null&&temp.getRight()!=null){ //仅有右子树
if (parent.getLeft()==temp){
parent.setLeft(temp.getRight());
}else {
parent.setRight(temp.getRight());
}
}else { //左右子树都有
/**
* 判断要删除的结点temp是其父结点parent的左孩子还是右孩子。
* 若是左孩子,则设置要temp的右孩子为parent结点的左孩子,
* 并将temp的整个左子树设为其右子树中最小结点的左孩子。
* 若是右孩子,则将temp的左孩子设为parent结点的右孩子,
* 并将temp的整个右子树设为temp左子树中最大结点的右孩子。
*
*/
if (parent.getLeft()==temp){
Node minRight = getMinNode(temp.getRight());
parent.setLeft(temp.getRight());
temp.getRight().setParent(parent);
minRight.setLeft(temp.getLeft());
temp.getLeft().setParent(minRight);
}else {
Node maxLeft = getMaxNode(temp.getLeft());
parent.setRight(temp.getLeft());
temp.getLeft().setParent(parent);
maxLeft.setRight(temp.getRight());
temp.getRight().setRight(maxLeft);
}
}
}
size--;
}
//获取以root为根结点的二叉查找树的最大结点
private Node getMaxNode(Node left) {
Node temp = root;
while (temp.getRight()!=null){
temp = temp.getRight();
}
return temp;
}
//获取以root为根结点的二叉查找树的最小结点
private Node getMinNode(Node root) {
Node temp = root;
while (temp.getLeft()!=null){
temp = temp.getLeft();
}
return temp;
}
@Override
public void inOrder() {
checkedNotNull(root);
System.out.print("中序遍历:");
Deque<Node> stack = new LinkedList();
Node temp = root;
while (temp!=null||!stack.isEmpty()){
while (temp!=null){
stack.push(temp);
temp = temp.getLeft();
}
if (!stack.isEmpty()){
temp = stack.pop();
System.out.print("key:"+temp.getKey()+"*value:"+temp.getValue()+"||");
temp = temp.getRight();
}
}
System.out.println("");
}
//判断是否为空树
private void checkedNotNull(Node root) {
if (root==null){
throw new RuntimeException("空树不能进行遍历");
}
}
}
2.平衡二叉树
在二叉查找树中有个很打的缺陷:可能出现二叉查找树变为链表,即时间复杂度向O(n)倾斜。为了解决该问题,我们需要在向树中添加元素时做到自动平衡功能,即要让树保持:树中任意结点的左右子树的高度差(平衡因子)不能大于1。满足这个条件的二叉查找树,称之为平衡二叉树。
-
平衡因子:又称平衡度,表示的是结点的左右子树的差值。
建立平衡二叉树的目的为了减少二叉查找树的查找次数。我们知道,在二叉查找树中查找某个元素的次数与二叉查找树的高度有着:n<=high的关系(其中,n为查找次数,high为高度)。因此,减少二叉树的高度(层次),便能有效的提高查找的速度。
常见的实现二叉查找树自平衡的算法有:AVL树,红黑树,替罪羊树等。另外还有不是二叉树的平衡树,B树,B+树等(不做过多讨论)。因为Java中HashMap、TreeSet和TreeMap底层采用红黑树,这里只讨论红黑树。
来源:oschina
链接:https://my.oschina.net/u/4085994/blog/3018222