一、树
比如上图中,A节点就是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫作根节点,也就是图中的节点E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中的G、H、I、J、K、L都是叶子节点
树有三个比较相似的概念:高度、深度、层
- 节点的高度=节点到叶子节点的最长路径(边数)
- 节点的深度=根节点到这个节点所经历的边的个数
- 节点的层数=节点的深度+1
- 树的高度=根节点的高度
二、二叉树
二叉树每个节点最多有两个子节点,分别是左子节点和右子节点
上图中,编号2的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树
编号3的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其它层的节点个数都要达到最大,这种二叉树叫做完全二叉树
三、二叉树的性质
1)在二叉树的第i层上,至多有 个节点( )
2)深度为k的二叉树至多有 个节点
3)对于任何一棵二叉树T,如果叶子节点数为n0,度为2的节点数为n2,则 (叶子节点数=度为2的节点数+1)
推导:
假设度为1的节点数为n1,则二叉树的节点总数为
连接数总是等于总结点数n-1,并且等于 ,即:
所以, ,最后推导出
例题:
一棵二叉树有7个度为1的结点,6个度为2的结点,则该二叉树共有个多少个结点?
题解:
4)具有n个节点的完全二叉树的深度为 (向下取整)
5)如果对一棵有n个节点的完全二叉树的节点按层序编号,对任一节点i( )有以下性质:
- 如果 ,则节点i是二叉树的根,无双亲;如果 ,则其双亲是节点 (向下取整)
- 如果 ,则节点i无左孩子;否则其左孩子是节点
- 如果 ,则节点i无右孩子;否则其右孩子是节点
四、二叉树的遍历
二叉树的遍历方法有三种:前序遍历、中序遍历和后序遍历。其中,前、中、后序表示的是节点与它的左右子树节点遍历打印的先后顺序
- 前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树
- 中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树
- 后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身
二叉树的前、中、后序遍历就是一个递归的过程
前序遍历的递推公式:
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)
中序遍历的递推公式:
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)
后序遍历的递推公式:
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r
从前面的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数n成正比,也就是说二叉树遍历的时间复杂度是
五、二叉查找树
二叉查找树是指一棵空树或者具有下列性质的二叉树:
- 左子树上所有节点的值均小于它的根节点的值
- 右子树上所有节点的值均大于它的根节点的值
- 以此类推:左、右子树也分别为二叉查找树
二叉查找树的中序遍历后元素升序排列
二叉查找树的查找、插入、删除操作的时间复杂度均为
二叉搜索树Demo:https://visualgo.net/zh/bst
1、查找操作
先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找
2、二叉查找树的插入操作
新插入的数据一般都是在叶子节点上,所以只需要从根节点开始,依次比较要插入的数据和节点的大小关系
如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树有空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置
3、二叉查找树的删除操作
针对要删除节点的子节点个数的不同,需要分三种情况来处理
第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为null。比如下图中的删除节点55
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如下图中的删除节点13
第三种情况是,如果要删除的节点有两个子节点,我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除这个最小节点,因为最小节点肯定没有左子节点(如果有左子节点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如下图中的删除节点18
4、二叉查找树的时间复杂度分析
上图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了
最理想的情况,二叉查找树是一棵完全二叉树(或满二叉树),时间复杂度其实都跟树的高度成正比,也就是 。完全二叉树的层数小于等于 ,也就是说,完全二叉树的高度小于等于 。平衡二叉查找树的高度接近于 ,所以插入、删除、查找操作的时间复杂度也比较稳定,是
5、有了如此高效的散列表,为什么还需要二叉树?
散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 ,非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找时间复杂度才是 ,相对于散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?
1)、散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,只需要中序遍历,就可以在 的时间复杂度内,输出有序的数据序列
2)、散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在
3)、笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的 ,但因为哈希冲突的存在,这个常量不一定比 小,所以实际的查找速度可能不一定比 快。加上哈希函数的耗时,也不一定就比平衡二叉树的效率高
4)、散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定
六、树相关题目
1、LeetCode94:二叉树的中序遍历
1)递归:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
helper(root, result);
return result;
}
private void helper(TreeNode root, List<Integer> result) {
if (root != null) {
if (root.left != null) {
helper(root.left, result);
}
result.add(root.val);
if (root.right != null) {
helper(root.right, result);
}
}
}
2)基于栈的中序遍历:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode current = root;
while (current != null || !stack.empty()) {
if (current != null) {
stack.push(current);
current = current.left;
} else {
current = stack.pop();
result.add(current.val);
current = current.right;
}
}
return result;
}
2、LeetCode98:验证二叉搜索树
1)递归中序遍历:
double last = -Double.MAX_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (isValidBST(root.left)) {
if (last < root.val) {
last = root.val;
return isValidBST(root.right);
}
}
return false;
}
2)基于栈的中序遍历:
Stack<TreeNode> stack = new Stack<>();
double last = -Double.MAX_VALUE;
public boolean isValidBST(TreeNode root) {
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if (root.val <= last) return false;
last = root.val;
root = root.right;
}
return true;
}
3、LeetCode226:翻转二叉树
1)递归:
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
2)迭代:
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode current = queue.poll();
TreeNode temp = current.left;
current.left = current.right;
current.right = temp;
if (current.left != null) queue.add(current.left);
if (current.right != null) queue.add(current.right);
}
return root;
}
4、LeetCode111:二叉树的最小深度
给定一个二叉树,找出其最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量
说明:叶子节点是指没有子节点的节点
示例:
给定二叉树[3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回它的最小深度2
题解:
public int minDepth(TreeNode root) {
if (root == null) return 0;
return helper(root);
}
private int helper(TreeNode root) {
//到达叶子节点就返回1
if (root.left == null && root.right == null) return 1;
//左孩子为空,只考虑右孩子的方向
if (root.left == null) return helper(root.right) + 1;
//右孩子为空,只考虑左孩子的方向
if (root.right == null) return helper(root.left) + 1;
//既有左孩子又有右孩子,那么就选一个较小的
return Math.min(helper(root.left), helper(root.right)) + 1;
}
5、LeetCode429:N叉树的层序遍历
题解:
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> levelOrder(Node root) {
helper(root, 0);
return result;
}
private void helper(Node node, int depth) {
if (node == null) return;
if (depth >= result.size()) {
result.add(new ArrayList<>());
}
result.get(depth).add(node.val);
for (int i = 0; i < node.children.size(); ++i) {
helper(node.children.get(i), depth + 1);
}
}
6、LeetCode235、236:二叉搜索树的最近公共祖先、二叉树的最近公共祖先
1)、利用二叉查找树的性质:左子树上所有节点的值均小于它的根节点的值、右子树上所有节点的值均大于它的根节点的值(235)
1)递归:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if ((root.val - p.val) * (root.val - q.val) <= 0) {
return root;
} else if (root.val - p.val > 0 && root.val - q.val > 0) {
return lowestCommonAncestor(root.left, p, q);
} else {
return lowestCommonAncestor(root.right, p, q);
}
}
2)迭代:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (root != null) {
if (p.val - root.val > 0 && q.val - root.val > 0) {
root = root.right;
} else if (p.val - root.val < 0 && q.val - root.val < 0) {
root = root.left;
} else {
return root;
}
}
return null;
}
2)、递归(235、236)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//return left == null ? right : right == null ? left : root;
if (left == null) {
return right;
} else if (right == null) {
return left;
} else {
return root;
}
}
解析:
两个节点p、q分为两种情况:
- p和q在相同子树中
- p和q在不同子树中
从根节点遍历,递归向左右子树查询节点信息
递归终止条件:如果当前节点为空或等于p或q,则返回当前节点
递归遍历左右子树,如果左右子树查到节点都不为空,则表明p和q分别在左右子树中,因此,当前节点即为最近公共祖先
如果左右子树其中一个不为空,则返回非空节点
7、LeetCode105:从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树
注意:可以假设树中没有重复的元素
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
题解:
public TreeNode buildTree(int[] preorder, int[] inorder) {
return helper(preorder, 0, inorder, 0, inorder.length);
}
private TreeNode helper(int[] preorder, int p, int[] inorder, int i, int j) {
if (i >= j) return null;
TreeNode root = new TreeNode(preorder[p]);
int k = 0;
while (inorder[k] != root.val) k++;
root.left = helper(preorder, p + 1, inorder, i, k);
root.right = helper(preorder, p + 1 + k - i, inorder, k + 1, j);
return root;
}
题解:
https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/qian-xu-zhong-xu-bian-li-gou-zao-er-cha-shu-mo-ni-/
常用数据结构的时间、空间复杂度:
https://www.bigocheatsheet.com/