叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树。
叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树。
存储一颗二叉树,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
链式存储法,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。大部分二叉树都是通过这种结构。
基于数组的顺序存储法,如果节点存储在数组中下标为i的位置,小标为2i的位置存储的就是左子节点,小标为2i+1的位置存储的就是右子节点。同理,也可反推出父节点。如果不是完全二叉树就会浪费空间。
遍历
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
//前序遍历
void preOrder(Node* root) {
if (root == null) return;
print root // 此处为伪代码,表示打印 root 节点
preOrder(root->left);
preOrder(root->right);
}
//中序遍历
void inOrder(Node* root) {
if (root == null) return;
inOrder(root->left);
print root // 此处为伪代码,表示打印 root 节点
inOrder(root->right);
}
//后序遍历
void postOrder(Node* root) {
if (root == null) return;
postOrder(root->left);
postOrder(root->right);
print root // 此处为伪代码,表示打印 root 节点
}
按层遍历
package LeetCode;
import java.util.LinkedList;
public class LevelIterator {
public static class BiTree{
int val;
BiTree left=null;
BiTree right=null;
public BiTree(int val) {
this.val = val;
}
}
public void LevelIterator(BiTree root){
if(root ==null){
return ;
}
LinkedList<BiTree> queue = new LinkedList<BiTree>();
BiTree current = null;
queue.offer(root);//将根节点入队
while (!queue.isEmpty()){
current = queue.poll(); //出队对头元素并访问
System.out.println(current.val + "-->");
if(current.left != null){ //如果当前节点的左节点不为空入队
queue.offer(current.left);
}
if(current.right != null){ //如果当前节点的右节点不为空,把右节点入队
queue.offer(current.right);
}
}
}
public static void main(String[] args) {
}
}
二叉查找树最大的特点就是,支持动态数据集合的快速插入,删除,查找操作。
二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树,二叉查找树是为了实现快速查找而生的,不仅支持快速查找一个数据,还支持快速插入,删除一个数据。
二叉查找树要求,在书中任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。
二叉查找树的查找
我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。BinarySearchTree
二叉查找树的插入操作
如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。
二叉查找树的删除操作
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。
package LeetCode;
public class BinarySearchTree {
public static class Node{
private int data;
private Node left;
private Node right;
public Node(int data){
this.data = data;
}
}
private Node tree;
public Node find(int data){
Node p = tree;
while(p != null){
if(data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;
} else { // data < p.data
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
public void delete(int data) {
Node p = tree; // p 指向要删除的节点,初始化指向根节点
Node pp = null; // pp 记录的是 p 的父节点
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return; // 没有找到
// 要删除的节点有两个子节点
if (p.left != null && p.right != null) { // 查找右子树中最小节点
Node minP = p.right;
Node minPP = p; // minPP 表示 minP 的父节点
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data; // 将 minP 的数据替换到 p 中
p = minP; // 下面就变成了删除 minP 了
pp = minPP;
}
// 删除节点是叶子节点或者仅有一个子节点
Node child; // p 的子节点
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child; // 删除的是根节点
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
}
中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度为O(n),非常高效。
有了高效的散列表还是用二叉树的原因
第一,散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序,而对于二叉查找树来说,我们只需要中序遍历,就可以在O(n)的时间复杂度,输出有序的数据序列
第二,散列表扩容耗时多,当遇到散列冲突时,性能不稳定,但是平衡二叉树的性能非常稳定
第三,尽管散列表的查找等操作的时间复杂度是常量级别的,但因为哈希冲突的存在,这个常量不一定比logn小,所以实际的查找速度不一定比O(logn)快,加上哈希函数耗时,不一定就比平衡二叉查找树的效率高。
第四,散列表的构造比二叉查找树复杂,考虑的东西多。平衡二叉查找树只需要考虑平衡性这一个问题。
第五,为了避免过多的散列冲突,散列表装载因子不能过大,特别是基于开放寻址法解决冲突的散列表,不然会浪费空间。
平衡二叉树的严格定义,二叉树中任意一个节点的左右子树的高度相差不能高于1,所以完全二叉树,满二叉树都是平衡二叉树。
平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。
散列表:插入删除查找都是O(1), 是最常用的,但其缺点是不能顺序遍历以及扩容缩容的性能损耗。适用于那些不需要顺序遍历,数据更新不那么频繁的。
跳表:插入删除查找都是O(logn), 并且能顺序遍历。缺点是空间复杂度O(n)。适用于不那么在意内存空间的,其顺序遍历和区间查找非常方便。
红黑树:插入删除查找都是O(logn), 中序遍历即是顺序遍历,稳定。缺点是难以实现,去查找不方便。其实跳表更佳,但红黑树已经用于很多地方了。