Tree(树 数据结构 java版)
文章目录
简介
Tree(树)是数据结构中很重要的一类存储结构。
为什么需要树
我们先来回顾一下数组和链表的优缺点
数组
优点:查询快。缺点:插入或删除某个值是效率低
链表
优点:插入或删除某个值的效率高,可是查询的效率低。
我们可以发现链表和数组的优缺点是非常明显的,那么有没有一种数据结构可以将它们两个的优缺点互补呢?
这个时候**树(Tree)**就诞生了
树的定义
树是由n (n >= 0)个节点的有限集,当n=0时,这颗树又被称为空树
树的术语
节点
父节点:A为B,C,D的父级节点简称父节点
子节点:B,C,D为A的孩子节点简称子节点
根节点:特指没有父节点的节点,一颗树中只有一个根节点,图中A节点就为树根节点简称**根节点**
叶子节点:特指没有子节点的节点,一颗树可以有很多叶子节点,图中E,F,G,H,I节点都为叶子节点
子树
以上图为例,A是这颗树的根节点,单看C,F,G,H的话它们也构成了一颗树
那么用虚线框出来的这颗小树,被称为A的子树
一个节点的子节点不为叶子节点的时候,那么那个子节点和它的孩子统称为该节点的子树
度
一个节点拥有子树的个数,就称为该节点的度,那么A节点的度为3
层次
一颗树从它的根开始,根所在的位置为第一层,它的孩子为第二层,以此来递增
森林
森林是由m (m >= 0)颗不相交的树的集合。
像上图把根节点A去掉以后B、C、D为根节点的三棵子树就可以称为森林
无序树
无序树指的是子节点没有顺序关系,这种树叫做无序树
有序树
有序树指的是树中任意节点的子节点之间都有顺序关系
在有序树的衍生下又分为二叉树和非二叉树
二叉树
简介
故名思意,二叉树的特点就是每个节点顶多就只有两颗子树(子节点)。二叉树的本意上还是有序树,有左右之分,且不能随意颠倒。
特征
1.在二叉树中,第i层最多有2^(i-1)个节点
2.如果深度为i,那么二叉树最多有2^i-1个节点
3.在二叉树中,终端节点(叶子)个数 = 度为2的节点个数 + 1 ,即为 n0 = n2 + 1;
特殊的二叉树
斜树
斜树分为两种,左斜树和右斜树
**左斜树:**只有左子树的树
**右斜树:**只有右子树的树
满二叉树
满二叉树指的是,除开叶子之外的节点的度都为2
满二叉树的高度为h,节点数为
2
h
−
1
2^h-1
2h−1
个。
完全二叉树
完全二叉树是最后一层的节点依次从左到右分布的二叉树。
特征
**由n个节点组成的完全二叉树深度为 **
l
o
g
2
N
+
1
log_2N+1
log2N+1
1.满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
2.完全二叉树第i层至多有
2
(
i
−
1
)
2^{(i-1)}
2(i−1)
个节点,共i层的完全二叉树最多有
2
i
−
1
2^{i-1}
2i−1
个节点。
遍历
我们先建立一个Node类和Tree类,来表示节点和树
这里我们使用java来演示
private static class Node{
int data;//存放的节点数据
Node leftChild,rightChild;//leftChild:左节点,rightChild:右节点
public Node(int data){
this.data = data;
}
@Override
public String toString(){
return "Node[data=]"+data;
}
}
public class Tree{
private Node root;
}
先序,中序,后序的遍历 如何去区分 以根节点来为基准
先序遍历
先序遍历 :访问顺序 根节点 左子节点 右子节点
java代码
public class Tree{
private Node root;
public void preOrder(){
preOrder(root);
}
//先序遍历
private void preOrder(Node node){
if(node != null){//判断节点是否为空
System.out.println(node);//输出节点
preOrder(node.leftChild);//继续往左递归
preOrder(node.rightChild);//继续往右递归
}
}
}
中序遍历
中序遍历 :访问顺序 左子节点 根节点 右子节点
public class Tree{
private Node root;
public void inOrder(){
inOrder(root);
}
//中序遍历
private void inOrder(Node node){
if(node != null){//判断节点是否为空
preOrder(node.leftChild);//继续往左递归
System.out.println(node);//输出节点
preOrder(node.rightChild);//继续往右递归
}
}
}
后序遍历
后序遍历 :访问顺序 左子节点 右子节点 根节点
public class Tree{
private Node root;
public void postOrder(){
inOrder(root);
}
//后序遍历
private void postOrder(Node node){
if(node != null){//判断节点是否为空
preOrder(node.leftChild);//继续往左递归
preOrder(node.rightChild);//继续往右递归
System.out.println(node);//输出节点
}
}
}
二叉查找树/二叉搜索树(Binary Search Tree)
简介
二叉查找树(Binary Search Tree)简写BST,是满足某些特有条件的二叉树
特征
任何一个节点的左子树都小于当前节点,右节点则相反。
在二叉查找树当中是不存在重复的节点的
BST的优点
二叉查找树最明显也是最具有代表性的优点是**查找对应值特别快**
比如:我要查找4
从根(root)开始
根为1,4大于1,往右边查找
查找到5,5比4大,往左边查找
BST的一些特性
二叉查找树中的最小的节点一定是整颗树中最左下的叶子节点
二叉查找树中的最大的节点一定是整颗树中最右下的叶子节点
二叉查找中的查找/插入/删除 平均的时间复杂度为O(logN) 最坏的为O(N)
BST中的删除
叶子删除
叶子删除(叶子指的是没有子树的结点,一般在树的末端)
这样我们就把叶子3删除完成了,是不是很简单
节点删除
节点删除也分为两种情况:只有一个子树和有左右两颗子树的
先来看第一种情况,只有一颗子树的
这时我们只需要解绑节点6的rightNode让它指向节点5即可
第二种情况:既有左子树又有右子树
在解开第二种情况之前我们先了解一下**后继节点**
后继节点
假设我们要查找节点X的后继节点,那就要从X这个点的右子树开始往左边一直查找,查找到不能继续往左边查找了,那么这个节点或者叶子则是节点X的后继节点
java代码如下
public Node getSuccessor(Node node){
if(node.left == null){//如果左节点为空返回该节点
return node;
}
return getSuccessor(node.left);//递归向左查找
}
学习完后继节点之后,我们就可以来应对节点删除的第二种情况
我们去查找X的后继节点,发现是叶子7
先将X替换成7
然后将叶子7给删除
这样子我们就成功的删除了节点X
平衡二叉树(AVL)
简介
平衡二叉树是指树中任意一个节点的左子树和右子树的高度差不得超过1
为什么要有平衡二叉树?
看下图
当一个序列{1,2,3,4,5}构成二叉树,这时这颗树就会变成右斜树,这时来看这颗树你会发现一个问题
哎?这不是单向链表吗?
没错这时这颗树就会退化成一个单向链表,它的效率就会变成O(n);
这时平衡二叉树出现了,解决了这个问题
它保持了整颗树的左右平衡,树的查找效率提高
判断
如何判断此树是否为平衡二叉树,看下两图
通过上面两张图,相信应该知道如何去判断是否为平衡二叉树
平衡因子
平衡因子是指某个节点的左子树和右子树的高度差即为该节点的平衡因子
-1:右子树高
0:等高
1:左子树高
失衡
下面是一颗平衡二叉树
当我们插入新的节点20的时候
这时我们会发现破坏了AVL(平衡二叉树)树的特性,这时这颗树就不是一个平衡二叉树了。
最小失衡子树
在上图中,节点50的左子树高度为3,右子树的1,平衡因子为2,树失去了平衡,但是其它节点并没有破坏平衡二叉树的特性,那么节点50就称为最小失衡子树
这个时候我们就要把这颗树旋转,把它重新弄平衡,哪边树高,就把那边的树向上旋转。
旋转分为左旋和右旋,比如右子树高于左子树,平衡因子小于-1时需要使用到左旋,反之则需要右旋。
被破坏节点与麻烦节点
新插入节点被弄失衡的节点叫做被破坏节点,而那个新插入的节点叫做麻烦节点
在上图中,节点50被新插入的节点20弄失衡了。
那么节点50叫做被破坏节点
节点20叫做麻烦节点
左旋
当某节点的平衡因子小于-1(右子树的高度比左子树高)时,则需要右旋(left rotate)
第一步:先新建两个节点,让两个节点赋值,这里我们为了直观一点将节点50的子树省略了
第二步:重新指向这三者的关系
这时我们将新建节点50的子树再展示出来
将原本的节点去掉,就得到了下图
我们再重新排版一下,就得到了最终结果
下面让我们用java代码实现一下
Note类:
public static class AVLNode<T>{
T data;
AVLNode left,right;
public AVLNode(T data){
this.data = data;
}
}
AVLTree类:
public AVLNode<T> rightRotate(AVLNode<T> node){//node:被破坏节点
AVLNode<T> right = node.right;
AVLNode<T> leftOfRight = right.left;
//重新指向
right.left = node;
node.right = leftOfRight;
//返回代替了被破坏节点原本位置的节点
return right;
}
右旋
当某节点的平衡因子大于1(左子树的高度比右子树高)时,则需要右旋(right rotate)
第一步:先新建两个节点,让两个节点赋值,这里我们为了直观一点将节点40的子树省略了
第二步:重新指向这三者的关系
这时我们将新建节点40的子树再展示出来
将原本的节点去掉,就得到了下图
我们再重新排版一下,就得到了最终结果
下面我们来用java代码实现一下
Node类:
public static class AVLNode<T>{
T data;
AVLNode left,right;
public AVLNode(T data){
this.data = data;
}
}
AVLTree类:
public AVLNode<T> rightRotate(AVLNode<T> node){//node:被破坏节点
AVLNode<T> left = node.left;
AVLNode<T> rightOfLeft = left.right;
//重新指向
left.right = node;
node.left = rightOfLeft;
//返回代替了被破坏节点原本位置的节点
return left;
}
四种失衡类型
LL型
LL型(左左型),在被破坏节点的左子节点的左子节点插入新节点而导致失衡的,称为LL型
下面举例了一些LL型的案例
LL型的解决方法也很简单,在被破坏节点的基础上进行右旋即可。
RR型
RR型(右右型),在被破坏节点的右子节点的右子节点插入新节点而导致失衡的,称为RR型
下面举例了一些RR型的案例
RR型的解决方法也很简单,在被破坏节点为基础上使用左旋即可
LR型
LR型(左右型),在被破坏节点的左子节点的右子节点上插入新节点而导致失衡的,称为LR型
下面举例一些LR型的案例
LR型的解决方法,在被破坏节点的左子节点先做一次左旋,然后再在被破坏节点的基础上做一次右旋。
RL型
RL型(右左型),在被破坏节点的右子节点的左子节点上插入新节点而导致失衡的,称为RL型
下面举例一些RL型的案例
RL型的解决方法,在被破坏节点的右子节点上做一次右旋,然后再在被破坏节点的基础上做一次左旋。
Java代码实现
下面是一个完整的java代码实现AVL
package com.yohane.Tree.AVLTree;
import java.awt.*;
public class AVLTree<T extends Comparable<T>> {
/**
* 根节点
*/
private AVLNode<T> root;
/**
* Node类(节点)
* @param <T>
*/
private static class AVLNode<T>{
T data;
AVLNode<T> left,right;
int height;
public AVLNode(T data) {
this.data = data;
this.height = 1;
}
}
public void add(T data){
root = insert(root,data);
}
/**
* 插入新节点
* @param node 当前节点
* @param data 新数据
* @return 当前节点
*/
private AVLNode<T> insert(AVLNode<T> node,T data){
//如果当前节点不存在,则创建当前节点并赋值
if(node == null)
return new AVLNode<>(data);
//使用compareTo函数来判断比较两个对象的大小
//cmp小于0表示node.data小于当前节点的data,则往左走
//cmp大于0表示node.data大于当前节点的data,则往右走
int cmp = data.compareTo(node.data);
if(cmp < 0)
node.left = insert(node.left,data);
else if(cmp > 0)
node.right = insert(node.right,data);
else
return node;//如果相等则直接返回
node.height = addHeight(node);
//节点的平衡因子
int balance = getBalance(node);
return Rotate(balance,node);
}
public void delete(T data){
delete(root,data);
}
/**
* 删除对应节点
* @param node 节点
* @param data 对应数据
* @return 根节点
*/
private AVLNode<T> delete(AVLNode<T> node,T data){
if(node == null)
return null;
//使用compareTo函数来判断比较两个对象的大小
//cmp小于0表示node.data小于当前节点的data,则往左走
//cmp大于0表示node.data大于当前节点的data,则往右走
int cmp = data.compareTo(node.data);
if(cmp < 0)
node.left = delete(node.left,data);
else if (cmp > 0)
node.right = delete(node.right,data);
else {
//只有左子树或者右子树
if(node.left == null || node.right == null)
node = node.left == null ? node.right : node.left;
//既有左子树也有右子树
else{
//minNode:后继节点
AVLNode<T> minNode = findMin(node.right);
node.data = minNode.data;
node.right = delete(node.right,minNode.data);
}
}
if(node == null)
return null;
//平衡因子
int balance = getBalance(node);
return Rotate(balance,node);
}
/**
* 失衡旋转
* @param balance 平衡因子
* @param node 节点
* @return 节点
*/
private AVLNode<T> Rotate(int balance,AVLNode<T> node){
//LL型
if(balance > 1 && getBalance(node.left) >= 0)
return rightRotate(node);
//LR型 先将被破坏节点的左子节点左旋,然后在将被破坏节点右旋
if(balance > 1 && getBalance(node.left) < -1){
node.left = leftRotate(node.left);
return rightRotate(node);
}
//RR型
if(balance < -1 && getBalance(node.right) <= 0)
return leftRotate(node);
//RL型 先将被破坏节点的右子节点右旋,然后在将被破坏节点左旋
if (balance < -1 && getBalance(node.right) > 1){
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
public T getRootValue(){
return root.data;
}
public int getTreeRightHeight(){
return getRightHeight(root);
}
public int getTreeLeftHeight(){
return getLeftHeight(root);
}
private int getRightHeight(AVLNode<T> node){
int n = 0;
while (node.right != null){
n++;
node = node.right;
}
return n;
}
private int getLeftHeight(AVLNode<T> node){
int n = 0;
while (node.left != null){
n++;
node = node.left;
}
return n;
}
private int height(AVLNode<T> node){
return node == null ? 0 : node.height;
}
/**
* 获取对应节点的平衡因子
* @param node 节点
* @return 平衡因子
*/
private int getBalance(AVLNode<T> node){
return node == null ? 0 : height(node.left) - height(node.right);
}
private int addHeight(AVLNode<T> node){
return 1 + Math.max(height(node.left),height(node.right));
}
/**
* 查找后继节点
* @param node
* @return
*/
private AVLNode<T> findMin(AVLNode<T> node){
while (node.left != null){
node = node.left;
}
return node;
}
/**
* 右旋
* @param node
* @return
*/
private AVLNode<T> rightRotate(AVLNode<T> node){
AVLNode<T> left = node.left;
AVLNode<T> rightOfLeft = left.right;
left.right = node;
node.left = rightOfLeft;
node.height = addHeight(node);
left.height = addHeight(left);
return left;
}
/**
* 左旋
* @param node
* @return
*/
private AVLNode<T> leftRotate(AVLNode<T> node){
AVLNode<T> right = node.right;
AVLNode<T> leftOfRight = right.left;
right.left = node;
node.right = leftOfRight;
node.height = addHeight(node);
right.height = addHeight(right);
return right;
}
public void printOrder(){
printOrder(root);
}
/**
* 中序排序
* @param node
*/
private void printOrder(AVLNode<T> node){
if(node!=null){
printOrder(node.left);
System.out.println(node.data + "");
printOrder(node.right);
}
}
}
Treap(树堆)
简介
Treap = Tree + Heap
Treap(树堆)就是树+堆,树为二叉查找树,堆为二叉堆。
Treap保留了二叉查找树的特性,和堆的优先级,Treap是一种弱平衡树,核心是利用**优先级**操作复杂度平均为O(logN)
Treap的节点中有两个值,一个为堆的值,一个为键值
堆的特点:如果此堆为大根堆则子节点比父节点小,如果此堆为小根堆则子节点比父节点大
下图为一个Treap,这里的堆为小根堆:
堆的核心就是利用了优先级使得二叉树平衡。在给Treap插入新节点的时候,需要同时遵循树和堆的特性,这时候就出现了两种方法来维护,分别是旋转和分裂合并。
其中使用旋转方式的Treap就称为有旋Treap,使用分裂合并方式的Treap就为无旋Treap
有旋Treap(有旋树堆)
有旋Treap的失衡判断是根据优先级来判断
有旋Treap的旋转方式与平衡二叉树(AVL)的操作类似,这里我们不过多阐述。
java代码实现
package com.yohane.Treap.Treap;
import java.util.Random;
public class Treap {
/**
* 生成优先级
*/
private static final Random rand = new Random();
/**
* 根
*/
private TreapNode root;
private int size;
/**
* 节点
*/
private static class TreapNode{
TreapNode left,right;
int val,key;
int size,priority;//优先级
public TreapNode(int key,int val){
this.key = key;
this.val = val;
this.size = 1;
this.priority = rand.nextInt();
}
}
public void put(int key,int val){
root = put(root,key,val);
}
public void remove(int key){
root = remove(root,key);
}
public boolean contains(int key){
return get(root,key) != null;
}
public int size(){
return size;
}
public TreapNode get(int key){
return get(root,key);
}
/**
* 存入
* @param node 节点
* @param key 新键
* @param val 新值
* @return 节点
*/
private TreapNode put(TreapNode node, int key, int val){
if(node == null){
size++;
return new TreapNode(key, val);
}
if(key == node.key)
node.val = val;
else if(key < node.key){
node.left = put(node.left,key,val);
//若左子节点的优先级高于本节点则右旋调整
if(node.left.priority > node.priority)
node = rotateRight(node);
}else {
node.right = put(node, key, val);
//若右子节点的优先级高于本节点则左旋调整
if(node.right.priority > node.priority)
node = rotateLeft(node);
}
updateSize(node);
return node;
}
/**
* 移除对应节点
* @param node 节点
* @param key 键
* @return 节点
*/
private TreapNode remove(TreapNode node,int key){
if(node == null)
return null;
if(key == node.key){
size--;
//若left为空则返回right,反之返回left
if(node.left == null)
return node.right;
else if (node.right == null)
return node.left;
else {
if(node.left.priority > node.right.priority){
node = rotateRight(node);
node.right = remove(node.right,node.right.key);
}else {
node = rotateLeft(node);
node.left = remove(node.left,node.left.key);
}
}
}
else if (key > node.key)
node.right = remove(node.right,key);
else
node.left = remove(node.left,key);
updateSize(node);
return node;
}
private TreapNode get(TreapNode node,int key){
if(node == null)
return null;
if (node.key == key)
return node;
else if (key < node.key)
return get(node.left,node.left.key);
else
return get(node.right,node.right.key);
}
private TreapNode rotateRight(TreapNode node){
TreapNode left = node.left;
node.left = left.right;
left.right = node;
updateSize(node);
updateSize(left);
return left;
}
private TreapNode rotateLeft(TreapNode node){
TreapNode right = node.right;
node.right = right.left;
right.left = node;
updateSize(node);
updateSize(right);
return right;
}
private void updateSize(TreapNode node){
node.size = getSize(node.left) + getSize(node.right) + 1;
}
private int getSize(TreapNode node){
return node == null ? 0 : node.size;
}
}
FHQ Treap (无旋树堆)
简介
FHQ Treap,又名无旋Treap,是一种不需要旋转的平衡树,它有两种核心操作,分裂和合并,FHQ Treap通过这两种操作来保持平衡。
Split(分裂)
根据一个值去把一颗树分为两颗,比此值小的分为一棵树并重新排序,比此值大的分为另一棵树并重新排序。
FHQ Treap树会根据key值从根节点来递归查询比它小的节点,并分为一棵树
30比key值大,往左子树查找
25节点无子树,无需继续查找,分裂完成
Merge(合并)
将两个Treap合成一个,要保证前一个Treap的节点值都小于后一个Treap的节点值
在合并时候按照优先级高低,和判断堆为小根堆或者大根堆来判断节点是在上还是在下
java代码实现
Splay Tree(伸展树)
简介
伸展树的定义是建立在二叉搜索树上
伸展树通过伸展操作不断将某个节点移动到根节点,可以**均摊O(logN)时间**内完成插入、查找、删除操作。
对于查找频率高的节点,使它处于离根节点相对较近。
暂未更完,敬请期待