通过有序线性结构构造AVL树
通过有序线性结构构造AVL树
本博客旨在结局利用有序数组和有序链表构造平衡二叉树(下文使用AVL树代指)问题。
直接通过旋转来构造AVL树似乎是一个不错的选择,但是稍加分析就会发现,这样平白无故做了许多毫无意义的旋转。因为直接通过旋转调整二叉查找树(下文使用BST代指)并没有利用数组或链表本身是有序的信息,进行了大量无意义的操作。
下面通过leetcode两道例题来说明这个问题。
1. 108. 将有序数组转换为二叉搜索树
题目分析
重点的问题在于:如何利用数组的有序信息呢?
首先我们先来观察一个有序数组和一棵AVL树所呈现的关系
我们可以发现数组中间元素恰好是对应AVL树的root节点(若数组长为偶数,可取中间偏左或偏右元,此时左右子树高度差为1)
我们只需要选取中间元素,构造头节点,在递归地构造其左右子树即可。
代码实现与说明
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
private TreeNode build(int[] nums, int l, int r) {
// 为什么采用l小于r而不是等于来判断呢?
// 我们这里是采用的闭区间的写法,当然可以采用闭开区间的写法,也就是等于时返回null
// 循环和递归函数的边界条件一定要多加注意
if (l > r)
return null;
// 防止因为l和r过大造成溢出,不过这道题数组长度不会那么大,但最好还是养成这样的习惯
int mid = l + (r - l) / 2;
// 其实这是一个中序遍历,先建立root节点,在递归地建立左子树和右子树
TreeNode node = new TreeNode(nums[mid]);
node.left = build(nums, l, mid - 1);
node.right = build(nums, mid + 1, r);
// 建立好后返回root节点即可
return node;
}
}
递归函数是如何设计的呢?
- 参数:我们需要访问nums数组元素,因此需要将其传入函数,也可以在Solution类中设计一个实例变量引用nums数组。同时,为例确定中间元素,我们需要明确数组的左右边界,因此传入参数l和r,它们也是判断数组内是否还有元素用来建立AVL树节点,也就是说当l小于r时,返回null节点。
- 返回值:这个比较显而易见,我们需要递归函数返回一个TreeNode节点(或者说它是已构造好子树的头节点)。
复杂度分析
时间复杂度:这道题本质上是使用先序遍历的方式构造一棵树,每个节点都被构造一次且路过三次,所以时间复杂度显然为O(N)
空间复杂度:递归调用栈深度为树的深度,由于构造的树为AVL树,其深度不超过lg(N),递归调用栈深度也不超过O(lgN),故空间复杂度为O(lgN)
2. 109. 有序链表转换二叉搜索树
题目分析
这道题是108题的兄弟版本,数组可以随机访问元素,因此我们可以轻而易举地得到中间元素,但是链表不再具备这个特性。因此我们需要采取其他的方法来解决这个问题。
- 将链表元素依次取出,构造出一个有序数组,再利用108的方法去做。不过空间复杂度提升为O(N)
- 采用快慢指针法取出中间节点,但是每次构造子树root节点均需使用快慢指针法,导致时间复杂度会降低为O(lgN)
- 更优秀的方法:利用AVL树中序遍历生成的序列即为所给有序链表这一性质,采用中序遍历构造AVL树,兼具方法1和方法2的优点
代码实现与说明
方法1不给出代码,其实相比于108题只是多了一步构造数组罢了。
方法2代码
class Solution {
public TreeNode sortedListToBST(ListNode head) {
return build(head, null);
}
private TreeNode build(ListNode l, ListNode r) {
if (l == r)
return null;
ListNode slow = l, fast = slow;
// 快慢指针寻找中间节点,可以说是本做法的核心,注意循环的条件
while (fast != r && fast.next != r) {
slow = slow.next;
fast = fast.next.next;
}
// 其实这是一个先序遍历的过程,在过程AVL树
TreeNode node = new TreeNode(slow.val);
node.left = build(l, slow);
node.right = build(slow.next, r);
return node;
}
}
递归函数设计说明:
- 返回值:返回值显然要返回树节点,因为我们需要向上一级调用函数返回构造好子树的头节点
- 参数:本函数参数选择是重中之重,向上面代码所示选用闭开区间的写法可以避免一些麻烦,不要忘记这是个单链表,如写成闭区间的模式,那么需要额外的prev指针来指示slow指针的前一个元素(对照108的代码去看)
方法2缺陷:快慢指针寻找中间元素所需时间复杂度是o(lgN),我们每次在构造子树root节点时均需要使用它一次,这无疑造成了一些浪费。让我们回到108题所示图片中(把那个数组想象为一个链表),AVL树中序遍历产生的序列与链表是一致的。可以利用这个特点改进方法2吗?
方法3思路说明:
这里附上一份详细的参考链接
方法3代码
class Solution {
// 递归过程中各个函数均维护一个链表头,故将其设为实例变量
ListNode listHead;
public TreeNode sortedListToBST(ListNode head) {
listHead = head;
int length = getLength(head);
return build(0, length - 1);
}
// 一定要多注意该函数参数的设计,参数是整数索引!不再像方法2那样是ListNode引用
// 一定要好好理解这个函数
private TreeNode build(int left, int right) {
if (left > right)
return null;
TreeNode node = new TreeNode();
int mid = left + (right - left) / 2;
node.left = build(left, mid - 1);
node.val = listHead.val;
listHead = listHead.next;
node.right = build(mid + 1, right);
return node;
}
// 辅助函数,作用是遍历链表,统计其长度
private int getLength(ListNode head) {
int counter = 0;
while (head != null) {
counter++;
head = head.next;
}
return counter;
}
}
递归函数的说明:
- 参数设计:有些人(包括我在内)开始可能很诧异为什么不像方法2一样使ListNode作为参数呢?不要忘记,一旦参数使用ListNode那不就变成方法2了嘛,无法直接访问中间元素。要想直接访问中间元,就要使用整数索引。
- 如何理解递归函数的流程:最好的方法就是逐行分析代码,尝试模拟运行。不过这里还是给出一些说明,首先我们先建立node节点,先递归地建立其左子树,然后将链表头listHead指向下一个元素,此时listHead指向的节点恰好为中间节点,我们取出它的值,再递归地建立其右子树。
通过有序线性结构构造AVL树的更多相关文章
- 二分法构造AVL树
public class ConvertSortedArrayToBinarySearchTree { public static TreeNode sortedArrayToBST(int[] nu ...
- 红黑树与AVL树
概述:本文从排序二叉树作为引子,讲解了红黑树,最后把红黑树和AVL树做了一个比较全面的对比. 1 排序二叉树 排序二叉树是一种特殊结构的二叉树,可以非常方便地对树中所有节点进行排序和检索. 排序二叉树 ...
- 数据结构与算法——AVL树类的C++实现
关于AVL树的简单介绍能够參考:数据结构与算法--AVL树简单介绍 关于二叉搜索树(也称为二叉查找树)能够參考:数据结构与算法--二叉查找树类的C++实现 AVL-tree是一个"加上了额外 ...
- 图解AVL树
1:AVL树简介 二叉搜索树在一般情况下其搜索的时间复杂度为O(logn),但某些特殊情况下会退化为链表,导致树的高度变大且搜索的时间复杂度变为O(n),发挥不出树这种数据结构的优势,因此平衡二叉树便 ...
- AVL树的插入和删除
一.AVL 树 在计算机科学中,AVL树是最早被发明的自平衡二叉查找树.在AVL树中,任一节点对应的两棵子树的最大高度差为 1,因此它也被称为高度平衡树.查找.插入和删除在平均和最坏情况下的时间复杂度 ...
- 006-数据结构-树形结构-二叉树、二叉查找树、平衡二叉查找树-AVL树
一.概述 树其实就是不包含回路的连通无向图.树其实是范畴更广的图的特例. 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合. 1.1.树的特性: 每个结点有零个或多个子 ...
- 二叉树,AVL树和红黑树
为了接下来能更好的学习TreeMap和TreeSet,讲解一下二叉树,AVL树和红黑树. 1. 二叉查找树 2. AVL树 2.1. 树旋转 2.1.1. 左旋和右旋 2.1.2. 左左,右右,左右, ...
- AVL树、红黑树以及B树介绍
简介 首先,说一下在数据结构中为什么要引入树这种结构,在我们上篇文章中介绍的数组与链表中,可以发现,数组适合查询这种静态操作(O(1)),不合适删除与插入这种动态操作(O(n)),而链表则是适合删除与 ...
- 数据结构与算法16—平衡二叉(AVL)树
我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度O(log2n)同时也由此而决定.但是,在某些极端的情况下(如在 ...
随机推荐
- Spring与Web项目整合的原理
引言: 在刚开始我们接触IOC时,我们加载并启用SpringIOC是通过如下代码手动加载 applicationContext.xml 文件,new出context对象,完成Bean的创建和属性的注入 ...
- IOC——Spring的bean的管理(注解方式)
注解(简单解释) 1.代码里面特殊标记,使用注解可以完成一定的功能 2.注解写法 @注解名称(属性名称=属性值) 3.注解使用在类上面,方法上面和属性上面 注意:注解方式不能完全替代配置文件方式 Sp ...
- js技术之根据name获取input的值
一.前端的代码 <p>Name: <input type='text', name = 'name'/></p> <p>Age: <input t ...
- c语言代码规范
什么叫规范?在C语言中不遵守编译器的规定,编译器在编译时就会报错,这个规定叫作规则.但是有一种规定,它是一种人为的.约定成俗的,即使不按照那种规定也不会出错,这种规定就叫作规范.虽然我们不按照规范也不 ...
- C 语言中 include <> 与include "" 的区别?
#include < > 引用的是编译器的类库路径里面的头文件. #include " " 引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则 ...
- 【Matlab】简单的滑模控制程序及Simulink仿真
文章: [控制理论]滑模控制最强解析 滑模控制程序及Simulink仿真 这篇文章仿真和输出U的推到有些问题,博主根据此篇文章进行修改进行对sin(t)曲线的追踪(使用滑模控制) 使用滑模控制对sin ...
- 每天坚持一个CSS——社会人
每天一个CSS-社会人 实现效果 想法 之前看到一篇博客,使用python绘制出了小猪佩奇,所以自己想试一试,采用纯html + CSS绘制出低配版的小猪佩奇. 实现思路 使用上一篇,圆与边框实现.最 ...
- ECMAScript中有两种属性:数据属性和访问器属性。
ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们.为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如 [[Enumerable ...
- 前端基础之CSS(1)
1.css3的新特性有哪些 (1)CSS3选择器(基本.属性.伪类具体见下) (2)CSS3边框与圆角 圆角border-radius 属性:border-top-left-radius 左上角 bo ...
- potoshop cs6安装配置16错误解决办法(win10系统)
问题截图如下: 解决方法: 右击图标选择属性:选择兼容性-->兼容模式-->以管理员身份运行-->应用 然后就可以打开了!