文章目录
1. 搜索树
1. 特点
- 搜索树不一定是二叉树
二叉搜索树:对于任意一个节点,左边的值一定比这个节点的值小,右边的值一定比这个节点的值大;
特点:中序遍历是有序的;
2. 二叉搜索树OJ题
1. 寻找指定节点(力扣700)
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
TreeNode cur = root;
if(cur != null) {
if(cur.val == val) {
return cur;
}else if(cur.val < val) {
cur = cur.right;
}else {
cur = cur.left;
}
return searchBST(cur, val);
}
return null;
}
}
注意:一定要在末尾才判断是否为空,如果你在开头就判空返回null的话就会错误;
2. 二叉搜索树的插入
在二叉搜索树中插入指定节点,注意二叉搜索树的插入规则,一定是插入在树的最下端哟;
1. 迭代版
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
TreeNode cur = root;
TreeNode parent = null;
while(cur != null) {
parent = cur;
if(cur.val == val) {
return root;
}else if(val < cur.val) {
cur = cur.left;
}else {
cur = cur.right;
}
}
TreeNode newNode = new TreeNode(val);
if(val > parent.val) {
parent.right = newNode;
}else {
parent.left = newNode;
}
return root;
}
}
2. 递归版(⭐)
public TreeNode insertIntoBST(TreeNode root, int val) {
if(root == null){
return new TreeNode(val);
}
if(root.val < val){
root.right = insertIntoBST(root.right,val);
} else {
root.left = insertIntoBST(root.left,val);
}
return root;
}
3. 二叉搜索树的删除(力扣450)(迭代版)
删除的前提是要先查找到,所以框架是在查找的基础上实现的;
定义要删除的节点是cur;(下面的parent代表cur的父节点)
- 如果cur.left == null
- 让 parent.left 或者 parent.right = cur.right;
- 如果cur.right == null
- 让 parent.left 或者 parent.right = cur.left;
- 如果cur的左右子树都不为空(cur.left!=null && cur.right !=null)
- 用替换法:
- 比cur的值大的值中最小的;
- 比cur的值小的值中最大的;
- 以上两种方法任选一种即可,下面的代码选的第一种;
- 用替换法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
TreeNode cur = root;
//创建这个节点的目的是防止要删除的第一个节点就是root,没有这个节点的话那种情况下的parent为空, //会报空指针异常
TreeNode parentRes = new TreeNode(-1);
TreeNode parent = parentRes;
parent.right = root;
while (cur != null) {
if(key == cur.val) {
//要删除的是cur,parent是cur的双亲节点;
if(cur.right == null) {
//判断cur是parent的左子树还是右子树
if(cur == parent.left) {
parent.left = cur.left;
}else {
parent.right = cur.left;
}
}else if(cur.left == null) {
if(cur == parent.left) {
parent.left = cur.right;
}else {
parent.right = cur.right;
}
}else if(cur.left!=null && cur.right!=null) {
TreeNode replacedParent = cur;
TreeNode replaced = cur.right;
while(replaced.left != null) {
replacedParent = replaced;
replaced = replaced.left;
}
cur.val = replaced.val;
//注意这里一定要分这两种情况,因为有可能没有执行上面的while循环,此时replaced是 //replacedParent的右子树,而不是左子树,其他所有情况都是左子树;
if(replaced == replacedParent.left) {
replacedParent.left = replaced.right;
}else {
replacedParent.right = replaced.right;
}
}else {
//左右子树都为空,即要删除的节点为叶子节点
if(cur == parent.left) {
parent.left = null;
}else {
parent.right = null;
}
}
return parentRes.right;
}else if(key < cur.val) {
//将parent = cur这一句放在这两个代码块中才能保证当上面的if块成立的时候parent是cur的父节点
parent = cur;
cur = cur.left;
}else {
parent = cur;
cur = cur.right;
}
}
return root;
}
}
2. 平衡树概述(也属于搜索树)
搜索树的时间复杂度和树的高度有关,但是树的高度是在变化的;
为了解决树的高度会退化成O(n)的问题,引入了平衡树:
- 二叉平衡搜索树
- AVL树(不常用)
- 红黑树 (常用)
- 多叉平衡搜索树
- B-树
- B+树
- B*树
以后提到搜索树,就代表已经是平衡的;
3. AVL树
1. 特点
- 首先它是一颗搜索树,即左孩子大于根节点,右孩子小于根节点;
- 树的每个节点的左右子树高度差绝对值不能超过一;
AVL树中保存平衡因子(balance factor)
- 平衡因子:左子树高度 - 右子树高度;
- 所以在AVL树中,每个节点的平衡因子只有三种情况:
- 0:左右子树一样高;
- -1:左子树比右子树高度小一;
- 1:左子树比右子树高度大一;
- 所以在AVL树中,每个节点的平衡因子只有三种情况:
2. AVL树的插入
这里不在叙述AVL树的查找,他和普通搜索树的查找是一样的;
但是插入和删除就跟上面的普通搜索树不一样了,因为插入和删除有可能导致树不再平衡;
下面来展开探究:
1. 右右失衡(左旋)
如下图插入元素99后导致右右失衡
-
右右失衡:以失衡点为基准到插入的那个元素(图中有标记)
右右失衡的解决方法: -
做左旋:失衡点左旋
- 具体细节:左旋后60那个节点直接就放在50的右孩子上;
上面左旋后的结果如下:
可以看到,左旋后就是平衡树了;
2. 右左失衡(右旋)
步骤:
- (1):先对70做右旋(70上面的不变)
- (2):再对50做左旋;
变换结果如下:
3. AVL树失衡情况总结
AVL树失衡情况只有如下四种:
- 左左失衡
- 对失衡节点做右旋;
- 左右失衡
- 对失衡节点左孩子做左旋,再对失衡节点做右旋;
- 右右失衡
- 对失衡节点做左旋;
- 右左失衡
- 对失衡节点右孩子做右旋,再对失衡节点做左旋;
(下面两种与上面两种对称记忆)
4. 旋转的代码实现(⭐?)
要对一个节点进行左旋,上面已经了解了它的具体过程,这里我们直接翻译成代码即可:
class Node {
int key;
int value;
Node left;
Node right;
Node parent; //做旋转的话必须需要parent,不然没法做;
}
//左旋方法
public static void rotateLeft(Node node) {
Node right = node.right; //node的右节点(不可能为空?)
Node parent = node.parent; //node的父节点(可能为空)
Node leftOfRight = right.left; //node的右节点的左节点(可能为空)
//开始旋转----调整孩子部分
right.left = node;
node.right = leftOfRight;
if(parent != null) {
if(node == parent.left) {
parent.left = right;
}else {
parent.right = right;
}
}
//调整父节点部分
right.parent = parent;
node.parent = right;
if(leftOfRight != null) { //注意这里要判空
leftOfRight.parent = node;
}
}
4. 红黑树
1. 特点:
- 节点只能是红色或黑色
- 根是黑色的
- 叶子节点(null)是黑色的
- 红色不能和红色相邻
- 从跟到任意叶子路径上,黑色的数量一样多
以上特征保证了:
最长路径(黑红黑红黑红黑…)不会超过最短路径(黑黑黑黑…)的两倍, 这保证了树的高度差;
红黑树插入规则:
红黑树插入的节点一定是红色的 ;
- 根一定是黑色 (⭐)
- 红色和红色不能相邻
2. 具体插入细则
在我举的例子中:(C:current P:parent G:grandparent U:uncle)
c代表破坏规则的红色节点,p是c的双亲,g是p的双亲,u是c的叔叔(即p的兄弟,可能不存在)
第一种: (U存在且是红色)
- 即叔叔存在且红色
- 这种好办,直接将P和U变黑,G变红就行
- 务必注意G变红了可能影响上面的,如果破坏了上面的,则以G当成C(当前插入的节点),再来做对应变换;
- 这种好办,直接将P和U变黑,G变红就行
第二种: (U不存在或者存在是黑色的)
- ① P是G的左,C是P的左
- 解决:G右旋,P变黑,G变红;
- 解决:G右旋,P变黑,G变红;
- ② P是G的右,C是P的右
- 解决:G左旋,P变黑,G变红;
第三种: (U不存在或者存在是黑色的)
- P是G的左,但C是P的右
- 解决:P左旋,把P当成C,C当成P,成了上面的第二种中①的情况;
- 解决:P左旋,把P当成C,C当成P,成了上面的第二种中①的情况;
- P是G的右,但C是P的左
- 解决:对P右旋,把P当成C,C当成P,成了上面的第二种中②的情况;
3. AVL树和红黑树的区别
- AVL树的搜索效率稍高
- 红黑树插入时调整频率更低
实际情况还得看实践,以上只是理论;
5. 二叉搜索树(AVL/红黑)的作用(⭐?)
上面讲述了二叉搜索树和红黑树的原理结构,那么在实际应用中,这些树有什么作用呢?
- 二叉搜索树只用于内存搜索
为什么只用于内存搜索呢?能不能用于磁盘(硬盘的一种,硬盘包含机械、固态等)搜索呢?
因为二叉搜索树是链式的,所以存储不用顺序,一般在存储中都是乱序的,如下图:
假如上图的蓝色是内存条,当我们要获取6时,先读取内存得到根节点8,然后再次读取内存获取到5,然后再次读取内存获取到节点6;
设想如果蓝色是机械硬盘,当我们每获取一个节点,就得发起一次读磁盘,磁盘的IO速度是非常慢的,所以二叉搜索树只用于内存搜索;
这也是数据库的引擎的底层结构不是红黑树(数据库数据都是存在硬盘上的);
(当数据稍多,试想树高得多高(二叉嘛),树一高,读取磁盘次数可能就变多,效率就下降)