二叉树理论基础
二叉树的种类
- 满二叉树
- 完全二叉树
- 平衡二叉树
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
如图所示:
这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
二叉搜索树
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
下面这两棵树都是搜索树
平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
如图:
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
二叉树存储方式
可以采用链式存储,也可以顺序存储。那么链式存储方式就用指针, 顺序存储的方式就是用数组。
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
二叉树遍历方式
深度优先遍历:
- 中序遍历
- 先序遍历
- 后序遍历
广度优先遍历:层次遍历
栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
栈和队列的又一应用场景
二叉树定义
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
class TreeNode:
def __init__(self, val, left = None, right = None):
self.val = val
self.left = left
self.right = right
递归遍历
先序遍历:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> 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);
preorder(root.right);
}
}
中序遍历:
public void inorder(TreeNode root, List<Integer> res) {
if (root == null) return;
preorder(root.left);
res.add(root.val);
preorder(root.right);
}
后续遍历:
public void postorder(TreeNode root, List<Integer> res) {
if (root == null) return;
preorder(root.left);
preorder(root.right);
res.add(root.val);
}
迭代遍历
先序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> st = new LinkedList<>();
if (root == null) return res;
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.add(node.val);
if (node.right != null) st.push(node.right);
if (node.left != null) st.push(node.left);
}
return res;
}
}
中序遍历
中序遍历相对先序有所不同,主要是中序遍历中先访问中间节点,并且需要一层一层的访问到最左节点在作处理,(左中右)。
这样就造成访问顺序和处理顺序不一致。
所以,可以利用指针来帮助处理节点,而栈只负责存入节点元素。
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> st = new LinkedList<>();
TreeNode cur = root;
while (cur != null || !st.isEmpty()) {
if (cur != null) {
st.push(cur);
cur = cur.left;
} else {
TreeNode node = st.pop();
res.add(node.val);
cur = node.right;
}
}
return res;
}
后续遍历
改一下先序遍历的入栈顺序,最后反转输出即可:中右左 → 左右中
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> st = new LinkedList<>();
if (root == null) return res;
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.pop();
res.add(node.val);
if (node.left != null) st.push(node.left);
if (node.right != null) st.push(node.right); // 中右左反转
}
Collections.reverse(res);
return res;
}
统一迭代
思路,把元素依次入栈,但是处理的中节点的时候加入一个null标记,用于处理节点。
中序写法
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> st = new LinkedList<>();
if (root == null) return res;
st.push(root);
while (!st.isEmpty()) {
TreeNode node = st.peek();
if (node != null) {
node = st.pop();
if (node.right != null) st.push(node.right);
st.push(node);
st.push(null);
if (node.left != null) st.push(node.left);
} else {
st.pop();
node = st.pop();
res.add(node.val);
}
}
return res;
}
}