面试官让我手写一个平衡二叉树,我当时就笑了

简介: 平衡二叉树对于初学者一直是一个比较复杂的知识点,因为其里面涉及到了大量的旋转操作。把大量的同学都给转晕了。这篇文章最主要的特点就是通过动画的形式演示。确保大家都能看懂。最后是手写一个平衡二叉树。

一、概念


平衡二叉树是外国的两个大爷发明的。一开始发明的是二叉查找树。后来觉得不给力演化成了平衡二叉树。那什么是二叉查找树呢?我们给出一张图来看看:

v2-34a9651d11c81dceae8cccdd2a8fe602_1440w.jpg

看到这张图我们就会发现如下的特征。从每个节点出发,左边的节点一定小于右边的。但是你会发现这可以高低不平,看起来很不美观。于是慢慢的演化成了平衡二叉树。(当然不是因为美观演化的)。也就是说平衡二叉树的前提就是一颗二叉查找树


平衡二叉树定义(AVL): (1)它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1, (2)它的左子树和右子树都是一颗平衡二叉树。


也就是说以上两条规则,只要破坏了一个就不是平衡二叉树了。比如说下面这张图。

v2-3a492a27cf07b244ab5bf9176b4ed974_1440w.jpg

上面这张图就是破坏了二叉查找树这一条规则。当然了还有一条规则。也就是他的高度只差不能超过1

v2-636cb909d072ddde1ed46b7d1345d110_1440w.jpg

现在相信我们已经明白了什么是平衡二叉树。下面我们就来看看平衡二叉树的增删改查操作是怎么样的。


二、平衡二叉树的插入操作


我们先从最简单的入手,一步一步来。


1、右旋


首先我们插入几个数字,50,45,44。通过动画我们来演示一遍

链接

(1)插入50根节点不会出现任何操作

(2)插入45,往左边插入即可

(3)插入44,破坏了平衡,于是右旋。


2、左旋


我们插入几个数字,50,60,70。通过动画我们来演示一遍

链接

(1)插入50根节点不会出现旋转

(2)插入60,往右边插入即可

(3)插入70,破坏了平衡,于是左旋。


3、先右旋再左旋


我们依次插入50,60,55.通过动画我们演示一遍

链接

(1)插入55,根节点,不会出现旋转

(2)插入60,往右边插入

(3)插入55,破坏了平衡,于是先把55和60右旋,然后整体左旋。


4、先左旋后右旋


我们依次插入50,40,45.通过动画我们演示一遍。

链接

1)插入55,根节点,不会出现旋转

(2)插入40,往左边插入

(3)插入45,破坏了平衡,于是先把45和40左旋,然后整体右旋。

现在我们基本上已经把插入的几种情况罗列出来了。现在我们画一张图,来一个总结。

v2-a422c4d4d8fd29cccfcc8065f8bba2ae_1440w.jpg

上图对于每一种情况,从上往下看就好了。对于平衡二叉树的删除操作,其实也是同样的道理,找到相应的元素之后,对其进行删除,删除之后如果破坏了平衡,只需要按照上面的这几种情况进行调整即可。下面我们来分析一下平衡二叉树的查找操作。


三、平衡二叉树的查找


平衡二叉树的查找很简单,只需要按照二叉查找树的顺序执行就好。我们使用一张动画演示一下:链接

现在平衡二叉树的操作相信你已经能够理解。下面我们就来关注最后一个问题,那就是如何手写一颗平衡二叉树呢?


四、手写一颗平衡二叉树


平衡二叉树的代码操作,难点在于旋转。只要把旋转弄清楚基本上整个树就能完成了,根据上面旋转的特点我们从零开始定义一颗。


第一步:定义节点


public class AVLNode {
    public int data;//保存节点数据
    public int depth;//保存节点深度
    public int balance;//是否平衡
    public AVLNode parent;//指向父节点
    public AVLNode left;//指向左子树
    public AVLNode right;//指向右子树
    public AVLNode(int data){
        this.data = data;
        depth = 1;
        balance = 0;
        left = null;
        right = null;
    }
 }

第二步:插入数据

public void insert(AVLNode root, int data){
   //如果说插入的数据小于根节点,往左边递归插入
   if (data < root.data){
       if (root.left != null){
            insert(root.left, data);
       }else {
            root.left = new AVLNode(data);
            root.left.parent = root;
       }
   }
   //如果说插入的数据小于根节点,往左边递归插入
   else {
        if (root.right != null){
            insert(root.right, data);
        }else {
            root.right = new AVLNode(data);
            root.right.parent = root;
       }
  }
  //插入之后,计算平衡银子
   root.balance = calcBalance(root);
  // 左子树高,应该右旋
  if (root.balance >= 2){
      // 右孙高,先左旋
      if (root.left.balance == -1){
          left_rotate(root.left);
      }
      right_rotate(root);
  }
  // 右子树高,左旋
  if (root.balance <= -2){
      // 左孙高,先右旋
      if (root.right.balance == 1){
          right_rotate(root.right);
      }
      left_rotate(root);
  }
  //调整之后,重新计算平衡因子和树的深度
  root.balance = calcBalance(root);
  root.depth = calcDepth(root);
}

第三步:左旋和右旋的调整


1、右旋


// 右旋
    private void right_rotate(AVLNode p){
        // 一次旋转涉及到的结点包括祖父,父亲,右儿子
        AVLNode pParent = p.parent;
        AVLNode pLeftSon = p.left;
        AVLNode pRightGrandSon = pLeftSon.right;
        // 左子变父
        pLeftSon.parent = pParent;
        if (pParent != null){
            if (p == pParent.left){
                pParent.left = pLeftSon;
            }else if (p == pParent.right){
                pParent.right = pLeftSon;
            }
        }
        pLeftSon.right = p;
        p.parent = pLeftSon;
        // 右孙变左孙
        p.left = pRightGrandSon;
        if (pRightGrandSon != null){
            pRightGrandSon.parent = p;
        }
        p.depth = calcDepth(p);
        p.balance = calcBalance(p);
        pLeftSon.depth = calcDepth(pLeftSon);
        pLeftSon.balance = calcBalance(pLeftSon);
    }

2、左旋

private void left_rotate(AVLNode p){
        // 一次选择涉及到的结点包括祖父,父亲,左儿子
        AVLNode pParent = p.parent;
        AVLNode pRightSon = p.right;
        AVLNode pLeftGrandSon = pRightSon.left;
        // 右子变父
        pRightSon.parent = pParent;
        if (pParent != null){
            if (p == pParent.right){
                pParent.right = pRightSon;
            }else if (p == pParent.left){
                pParent.left = pRightSon;
            }
        }
        pRightSon.left = p;
        p.parent = pRightSon;
        // 左孙变右孙
        p.right = pLeftGrandSon;
        if (pLeftGrandSon != null){
            pLeftGrandSon.parent = p;
        }
        p.depth = calcDepth(p);
        p.balance = calcBalance(p);
        pRightSon.depth = calcDepth(pRightSon);
        pRightSon.balance = calcBalance(pRightSon);
    }

第四步:计算平衡和深度


1、计算平衡

public int calcBalance(AVLNode p){
        int left_depth;
        int right_depth;
        //左子树深度
        if (p.left != null){
            left_depth = p.left.depth;
        }else {
            left_depth = 0;
        }
        //右子树深度
        if (p.right != null){
            right_depth = p.right.depth;
        }else {
            right_depth = 0;
        }
        return left_depth - right_depth;
    }

2、计算深度

public int calcDepth(AVLNode p){
        int depth = 0;
        if (p.left != null){
            depth = p.left.depth;
        }
        if (p.right != null && depth < p.right.depth){
            depth = p.right.depth;
        }
        depth++;
        return depth;
    }

看起来代码有些多,其实梳理一下就不多了。


(1)首先定义一个节点,里面有get和set方法,构造函数等等做准备工作

(2)直接写业务流程,比如说这里的insert操作,里面涉及到的旋转操作先用方法代替

(3)对主业务流程的操作,缺哪一个方法,写哪一个方法即可


相关文章
经典面试题:将有序数组、有序链表转换成平衡二叉树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
224 0
经典面试题:将有序数组、有序链表转换成平衡二叉树
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
10天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
12天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
37 4
|
1月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
67 2
|
1月前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
28 0
|
3月前
|
Java C++
【Java基础面试十七】、Java为什么是单继承,为什么不能多继承?
这篇文章讨论了Java单继承的设计原因,指出Java不支持多继承主要是为了避免方法名冲突等混淆问题,尽管Java类不能直接继承多个父类,但可以通过接口和继承链实现类似多继承的效果。
【Java基础面试十七】、Java为什么是单继承,为什么不能多继承?
|
3月前
|
存储 安全 Java
这些年背过的面试题——Java基础及面试题篇
本文是技术人面试系列Java基础及面试题篇,面试中关于Java基础及面试题都需要了解哪些内容?一文带你详细了解,欢迎收藏!
|
3月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
3月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。