1,树的基本概念
-
一棵树可以有节点,根节点,父节点,子节点,兄弟节点
-
空树:就是一棵树没有节点
-
根节点:一棵树只有一个节点,也就是根节点
-
树可以有,左子树,右子树
-
节点的度:就是子树的个数
-
树的度:所有节点中度的最大值
-
叶子节点:度为0的节点
-
非叶子节点:度不为0的节点
- 层树:根节点在第一层,根节点的子节点在第2层,以此类推
- 节点的深度:从根节点到当前节点的唯一路径上的节点总数
- 节点的高度:从当前节点到最远叶子节点的路径上的节点总数
- 树的深度:所有节点深度中的最大值
- 树的高度:所有节点中高度的最大值
- 树的深度等于树的高度
- 有序树:树中任意节点的子节点之间有顺序关系
- 无序树:树中任意节点的子节点之间没有顺序关系
2,二叉树
2.1,二叉树的特点
- 每个节点的度最大为2(最大拥有2棵子树)
- 左子树和右子树是有顺序的
- 即使某节点只有一棵子树,也要区分左右子树
- 二叉树是有序树
2.2,二叉树的性质
- 非空二叉树的第i层,最多有2^ (i - 1)个节点(i >= 1)
- 在高度为h的二叉树上最多有2^h - 1个节点(h >= 1)
- 对于任何一棵非空二叉树,如果叶子节点个数为n0,度为2的节点个数n2,则有:n0 = n2 + 1
- 假设度为1的节点个数为n1,那么二叉树的节点总数n = n0 + n1 + n2
- 二叉树的边数T = n1 + 2*n2 = n - 1 = n0 + n1 + n2 - 1
- 因此n0 = n2 + 1
2.3,真二叉树
- 所有节点的度都要么为0,要么为2
2.4,满二叉树
- 满二叉树:最后一层节点的度都为0,其它节点的度都为2
- 在同样高度的二叉树中,满二叉树的叶子节点数量最多,总节点数量最多
- 满二叉树一定是真二叉树,真二叉树不一定是满二叉树
假设满二叉树的高度为h( h >= 1)
第i层的节点数量为:2 ^ (i - 1)
叶子节点数量: 2 ^ (h - 1)
总节点数量 n
n = 2^h - 1 = 2^0 + 2^1 + 2^2 + …… + 2^(h - 1)
h = log2(n + 1)
2.5, 完全二叉树
- 完全二叉树:对节点从上至下,左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应
- 叶子节点只会出现在最后2层,最后1层的叶子节点都靠左对齐
- 完全二叉树从根节点到倒数第二层的一棵满二叉树
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
2.5.1,完全二叉树的性质
- 度为1的节点只有左子树
- 度为1的节点要么是1个,要么是0个
- 同样节点数量的二叉树,完全二叉树的高度最小
2.6,二叉树的遍历
2.6.1,前序遍历
力扣地址:leetcode-cn.com/problems/bi…
-
访问顺序:先访问根节点,然后前序遍历左子树,在前序遍历右子树
//前序遍历
public void preorderTraversal() {
preorderTraversal(root);
}
private void preorderTraversal(Node node) {
if (node == null) return;
System.out.println(node.element);
preorderTraversal(node.left);
preorderTraversal(node.right);
}//非递归,使用栈实现
class Solution { public List preorderTraversal(TreeNode root) { List res = new ArrayList(); preorder(root, res); return res; }
public void preorder(TreeNode root, List<Integer> res) { if (root == null) { return; } res.add(root.val); preorder(root.left, res); preorder(root.right, res); }}
2.6.2,中序遍历
力扣地址:leetcode-cn.com/problems/bi…
-
访问顺序:先遍历左子树,在遍历根节点,最后中序遍历右子树
//递归 public void inorderTraversal() {
inorderTraversal(root);
}
private void inorderTraversal(Node node) {
if (node == null) return;
inorderTraversal(node.left);//
System.out.println(node.element);
inorderTraversal(node.right);
} //栈实现 class Solution { public List inorderTraversal(TreeNode root) { List res = new ArrayList(); Deque stk = new LinkedList(); while (root != null || !stk.isEmpty()) { while (root != null) { stk.push(root); root = root.left; } root = stk.pop(); res.add(root.val); root = root.right; } return res; } }
2.6.3,后序遍历
力扣地址:leetcode-cn.com/problems/bi…
-
访问顺序:先遍历左子树,在后序遍历右子树,最后访问根节点
//递归 public void postorderTraversal() {
postorderTraversal(root);
}
private void postorderTraversal(Node node) {
if (node == null) return; postorderTraversal(node.left);
postorderTraversal(node.right);
System.out.println(node.element);
} //迭代 class Solution { public List postorderTraversal(TreeNode root) { List res = new ArrayList(); if (root == null) { return res; }Deque<TreeNode> stack = new LinkedList<TreeNode>(); TreeNode prev = null; while (root != null || !stack.isEmpty()) { while (root != null) { stack.push(root); root = root.left; } root = stack.pop(); if (root.right == null || root.right == prev) { res.add(root.val); prev = root; root = null; } else { stack.push(root); root = root.right; } } return res; }}
2.6.4,层序遍历
力扣地址:leetcode-cn.com/problems/bi…
-
从上到下,从左到右依次访问每一个节点
public void levelOrderTraversal() { if (root == null) return; //创建队列,先进先出 Queue<Node<E>> queue = new LinkedList<>(); //加入队列 queue.offer(root); //队列不为空,就循环 while (!queue.isEmpty()) { //弹出对头 Node<E> node = queue.poll(); System.out.println(node.element); //node左子节点不为null,左子节点入队 if (node.left != null) { queue.offer(node.left); } //node右子节点不为null,右子节点入队 if (node.right != null) { queue.offer(node.right); } } }
3,二叉搜索树
3.1,思考:在n个动态数组的整数中搜索某个整数,查看其是否存在?
- 假设使用动态数组存放元素,从第0个位置开始遍历搜索,平均时间复杂度为:O(n)
- 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度为:O(logn)
- 但是添加,删除的平均时间复杂是O(n)
- 针对这个需求,我们可以使用二叉搜索树进行优化
3.2,什么是二叉搜索树
- 二叉搜索树是二叉树的一种,应用非常广泛的一种二叉树,又称为:二叉查找树,二叉排序树
- 任意一个节点的值都大于其左子树所有节点的值
- 任意一个节点的值都小于其右子树所有节点的值
- 它的左右子树也是一棵二叉树
- 二叉搜索树可以大大提高搜索数据的效率
3.3,二叉搜索树的接口设计和实现
3.3.1,添加节点
找到父节点
创建新的节点
parent.left = node 或者 parent.right = node
//添加节点
public void add(E element) {
elementNotNullCheck(element);
// 添加第一个节点
if (root == null) {
root = new Node<>(element, null);
size++;
return;
}
// 添加的不是第一个节点
// 找到父节点
Node<E> parent = root;
Node<E> node = root;
int cmp = 0;
do {
//添加元素和节点元素比较
cmp = compare(element, node.element);
parent = node;
//大于0,放入右节点
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {//小于0,放入左节点
node = node.left;
} else { // 相等
node.element = element;
return;
}
} while (node != null);
// 看看插入到父节点的哪个位置
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {//大于0,放入父节点的右节点
parent.right = newNode;
} else {//放入父节点的左节点
parent.left = newNode;
}
size++;
}
3.3.2,删除节点
删除叶子节点:
node == node.parent.left , node.parent.left = null
node == node.parent.right , node.parent.right = null
node.parent == null , root = null
删除度为1的节点
用子节点代替原节点的位置
child是node.left 或者 child 是 node.right
用child代替node的位置
如果node为左子节点
child.parent = node.parent
node.parent.left = child
- 如果node为右子节点
child.parent = node.parent
node.parent.right = child
- 如果是根节点
root = child
child.parent = null
3. 删除度为2的节点
先用前驱或者后继节点的值覆盖原节点的值
然后,删除相应的前驱或者后继节点
//删除节点
private void remove(Node<E> node) {
if (node == null) return;
size--;
if (node.hasTwoChildren()) { // 度为2的节点
// 找到后继节点
Node<E> s = successor(node);
// 用后继节点的值覆盖度为2的节点的值
node.element = s.element;
// 删除后继节点
node = s;
}
// 删除node节点(node的度必然是1或者0)
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) { // node是度为1的节点
// 更改parent
replacement.parent = node.parent;
// 更改parent的left、right的指向
if (node.parent == null) { // node是度为1的节点并且是根节点
root = replacement;
} else if (node == node.parent.left) {
node.parent.left = replacement;
} else { // node == node.parent.right
node.parent.right = replacement;
}
} else if (node.parent == null) { // node是叶子节点并且是根节点
root = null;
} else { // node是叶子节点,但不是根节点
if (node == node.parent.left) {
node.parent.left = null;
} else { // node == node.parent.right
node.parent.right = null;
}
}
}
4,平衡二叉搜索树
- 平衡二叉树其实就是对,一棵普通的二叉树的左右子树的高度进行平衡
- 当节点的数量固定时,左右子树的高度越接近,这棵二叉树就越平衡
- 最理想的平衡:像完全二叉树,满二叉树那样,高度是最小的
4.1,如何改进二叉搜索树?
- 首先,节点的添加、删除顺序是无法限制的,可以认为是随机的
- 所以在节点的添加,删除操作之后,想办法让二叉搜索树恢复平衡
- 如果接着继续调整节点的位置,完全可以达到理想平衡,但是付出的代价可能会比较大
- 比较合理的改进方案是:用尽量少的调整次数达到适度平衡即可
- 一棵达到适度平衡的二叉树,可以称之为:平衡二叉搜索树
- 经典常见的平衡二叉搜索树有:AVL树,红黑树