目录
二叉树定义
private static class Node<E> {
E element;
Node<E> left;
Node<E> right;
Node<E> parent;
public Node(E element, Node<E> parent) {
this.element = element;
this.parent = parent;
}
public boolean isLeaf() {
return left == null && right == null;
}
public boolean hasTwoChildren() {
return left != null && right != null;
}
}
1. 遍历:
1.1 先序遍历
递归
/**
* 前序遍历(递归)
*/
public void preorderTraversalRec() {
preorderTraversalRec(root);
}
private void preorderTraversalRec(Node<E> node) {
if (node == null) return;
System.out.println(node.element);
preorderTraversalRec(node.left);
preorderTraversalRec(node.right);
}
非递归
/**
* 前序遍历(非递归)
*/
public void preorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node<E> node = stack.pop();
// 访问node节点
if (visitor.visit(node.element)) return;
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
public void preorder2(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>();
while (true) {
if (node != null) {
// 访问node节点
if (visitor.visit(node.element)) return;
// 将右子节点入栈
if (node.right != null) {
stack.push(node.right);
}
// 向左走
node = node.left;
} else if (stack.isEmpty()) {
return;
} else {
// 处理右边
node = stack.pop();
}
}
}
1.2 中序遍历
递归
/**
* 中序遍历(递归)
*/
public void inorderTraversalRec() {
inorderTraversalRec(root);
}
private void inorderTraversalRec(Node<E> node) {
if (node == null) return;
inorderTraversalRec(node.left);
System.out.println(node.element);
inorderTraversalRec(node.right);
}
非递归
/**
* 中序遍历(非递归)
*/
public void inorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>();
while (true) {
if (node != null) {
stack.push(node);
// 向左走
node = node.left;
} else if (stack.isEmpty()) {
return;
} else {
node = stack.pop();
// 访问node节点
if (visitor.visit(node.element)) return;
// 让右节点进行中序遍历
node = node.right;
}
}
}
public void inorderTraversal() {
Stack<Node> stack=new Stack<Node>();
Node node=root;
//刚开始先把根节点压入栈,往后每次判断节点不为空则压栈
do {
while(node!=null){ //每一个节点都可能存在一个左子树, 左子树需要在根节点之前遍历, 所以如果存在左子树, 那么先将左子树压入栈, 后面先输出左子节点内容
stack.push(node);
node=node.left;
}
node=stack.pop();
System.out.println(node.element); //左子节点输出 ==> 左节点输出 ==> 右子节点输出
//如果出栈的结点存在右节点,则指向该节点
if(node.right!=null){
node=node.right;
}else{//否则设置为空,下次循环继续出栈
node=null;
}//当栈不为空,或者当前节点引用不为空时循环
} while (!stack.isEmpty()||node!=null);
}
1.3 后序遍历
递归
/**
* 后序遍历(递归)
*/
public void postorderTraversalRec() {
postorderTraversalRec(root);
}
private void postorderTraversalRec(Node<E> node) {
if(node == null) return;
postorderTraversalRec(node.left);
postorderTraversalRec(node.right);
System.out.println(node.element);
}
非递归
/**
* 后序遍历(非递归)
*/
public void postorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
// 记录上一次弹出访问的节点
Node<E> prev = null;
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node<E> top = stack.peek();
if (top.isLeaf() || (prev != null && prev.parent == top)) {
prev = stack.pop();
// 访问节点
if (visitor.visit(prev.element)) return;
} else {
if (top.right != null) {
stack.push(top.right);
}
if (top.left != null) {
stack.push(top.left);
}
}
}
}
public void postorderTraversal() {
Stack<Node> stack=new Stack<Node>();
//定义两个节点变量node和pre(这里需要注意pre节点的作用,下方的注释有详细地介绍)
Node node=root;
Node pre=null;
do {
//将左结点一次压入栈
while(node!=null){
stack.push(node);
node=node.left;
}
//获取栈顶节点
node = stack.peek();
//如果栈顶节点的右孩子不为空,说明还得把右子树压入栈中,这里需要注意
//node.right!=pre这个条件,因为我们在遍历的过程中,对于(子树)根节点的判断会存在两次
//第一次是弹出左孩子节点后,对根节点进行是否有右孩子的判断,如果有,则将右孩子压栈
//第二次是弹出右孩子节点后,这时候因为循环的原因(代码的原因),我们再次对根节点进行了右孩子判断,
//所以这里就必须得判断该右孩子节点是否在之前的循环中已经判断过了,如果判断过了,则弹出根节点,否则压入右孩子节点。
//总的来说,pre节点的作用是用来防止重复遍历右孩子节点的。
if(node.right!=null&&node.right!=pre){
//node指向右孩子节点
node=node.right;
}else{//只要有一个条件不满足则执行
//弹出栈顶元素
node=stack.pop();
//遍历栈顶元素
System.out.println(node.element);
//将pre指向当前弹出的元素,用来做下次对根节点的再次判断时,右孩子不重复遍历的一个条件
pre=node;
//将node设置为null,防止根节点重复压入左孩子节点。
node=null;
}
} while (node!=null||!stack.isEmpty());
}
1.4 层序遍历
非递归
/**
* 层序遍历(递归)
*/
public void levelOrder(Visitor<E> visitor) {
if (root == null || visitor == null) return;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
if (visitor.visit(node.element)) return;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
1.5 Morris遍历
中序遍历
Morris遍历二叉树, 时间复杂度O(n), 空间复杂度O(1)
执行步骤: (假设遍历到当前节点是N)
① 如果N.left != null, 找到N的前驱节点 P
如果P.right == null
p.right = N, N = N.left
回到①如果P.right == N
P.right = null, 打印N, N = N.right
回到①② 如果N.left == null
打印 N
N = N.right
回到①③重复①, ②直到 N == null
/**
* Morris中序遍历
* 思路: 在二叉树左右子节点与父节点的引用关系中, 只能通过父节点获取子节点;
* 因此在中序遍历中, 当左节点,根节点,右节点遍历后无法再获取到下一个遍历的节点,
* 由于当前遍历的最后一个节点(右节点)为下一个节点的先驱节点, 所以可以维护一个每个节点与期先驱节点的引用关系
*/
public void morrisInOrder(){
Node<Integer> node = root;
while(node != null){
// 如果当前节点的左子节点不为空,则找到node的前驱节点
if(node.left != null){
Node pred = node.left;
while(pred.right != null && pred.right != node){
pred = pred.right;
}
//正常情况下前驱节点的右子树是为null的, 但是如果该节点的右子节点已经引用了父节点node, 那么次数就不为null了
if(pred.right == null){
pred.right = node; //将前驱节点的右子节点引用为node节点
node = node.left;
}else{
System.out.print(node.element+" ");
pred.right = null; //恢复先驱节点右子节点的引用
node = node.right; //遍历右子节点
}
}else{
System.out.print(node.element+" ");
//node的right节点可能是其后继节点
node = node.right;
}
}
}
先序遍历
/**
* Morris先序遍历
*/
public void morrisPreOrder(){
Node<Integer> node = root;
while(node != null){
// 如果当前节点的左子节点不为空,则找到node的前驱节点
if(node.left != null){
// 找到前驱节点
Node pred = node.left;
while(pred.right != null && pred.right != node){
pred = pred.right;
}
//正常情况下前驱节点的右子树是为null的, 但是如果该节点的右子节点已经引用了父节点node, 那么次数就不为null了
if(pred.right == null){
/** 输出节点内容 */
System.out.print(node.element + " ");
pred.right = node; //将前驱节点的右子节点引用为node节点
node = node.left;
}else{
/** node节点的前驱节点指向node节点自身, 说明该节点已经遍历过 */
pred.right = null; //恢复前驱节点右子节点的引用
node = node.right; //遍历右子节点
}
}else{
/** 输出节点内容 */
System.out.print(node.element + " ");
//node的right节点可能是其后继节点
node = node.right;
}
}
}
2. 删除节点
2.1 前驱节点
/**
* 前驱节点
* @param node
* @return
*/
@SuppressWarnings("unused")
private Node<E> predecessor(Node<E> node) {
if (node == null) return null;
// 前驱节点在左子树当中(left.right.right.right....)
Node<E> p = node.left;
if (p != null) {
while (p.right != null) {
p = p.right;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.left) {
node = node.parent;
}
// node.parent == null
// node == node.parent.right
return node.parent;
}
2.2 后继节点
/**
* 后继节点
* @param node
* @return
*/
private Node<E> successor(Node<E> node) {
if (node == null) return null;
// 前驱节点在左子树当中(right.left.left.left....)
Node<E> p = node.right;
if (p != null) {
while (p.left != null) {
p = p.left;
}
return p;
}
// 从父节点、祖父节点中寻找前驱节点
while (node.parent != null && node == node.parent.right) {
node = node.parent;
}
return node.parent;
}
2.3 删除节点
思路:
叶子节点:
思路:直接删除
(1) 如果 node 是左子节点
node.parent.left = null
(2) 如果 node 是右子节点
node.parent.right = null
(3) 如果 node 是根节点
root = null
度为1的节点:
思路: 用子节点替代原节点的位置
child 是 node.left 或者 child 是 node.right, 用 child 替代 node 的位置
(1) 如果 node 是左子节点
child.parent = node.parent node.parent.left = child
(2) 如果 node 是右子节点
child.parent = node.parent ➢node.parent.right = child
(3) 如果 node 是根节点
root = child ➢child.parent = null
度为2的节点:
思路: 先用前驱或者后继节点的值覆盖原节点的值, 然后删除相应的前驱或者后继节点
如果一个节点的度为 2,那么它的前驱、后继节点的度只可能是 1 和 0
/**
* 移除指定节点
* @param node
*/
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;
}
}
}
3. 高度
/**
* 计算树的高度
* 思路: 层序遍历
*
* @return
*/
public int height() {
if (root == null) return 0;
// 树的高度
int height = 0;
// 存储着每一层的元素数量
int levelSize = 1;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
levelSize--;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (levelSize == 0) { // 意味着即将要访问下一层
levelSize = queue.size();
height++;
}
}
return height;
}
4. 添加
/**
* 添加元素
* @param element
*/
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;
if (cmp > 0) {
node = node.right;
} else if (cmp < 0) {
node = node.left;
} else { // 相等
node.element = element;
return;
}
} while (node != null);
// 看看插入到父节点的哪个位置
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {
parent.right = newNode;
} else {
parent.left = newNode;
}
size++;
}
5. 是否为完全二叉树
/**
* 是否为完全二叉树
* 思路:
* 如果树为空,返回 false
如果树不为空,开始层序遍历二叉树(用队列)
如果 node.left!=null,将 node.left 入队
如果 node.left==null && node.right!=null,返回 false
如果 node.right!=null,将 node.right 入队
如果 node.right==null
✓那么后面遍历的节点应该都为叶子节点,才是完全二叉树
✓否则返回 false
遍历结束,返回 true
*/
public boolean isComplete() {
if (root == null) return false;
Queue<Node<E>> queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node<E> node = queue.poll();
if (leaf && !node.isLeaf()) return false;
if (node.left != null) {
queue.offer(node.left);
} else if (node.right != null) { // node.left == null && node.right != null
return false;
}
if (node.right != null) {
queue.offer(node.right);
} else { // node.right == null && node.left == null
leaf = true;
}
}
return true;
}