二叉算法通常指的是应用于二叉树这一数据结构的各种操作与算法。二叉树是一种特殊的树形结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树在计算机科学中有广泛应用,如二叉查找树(Binary Search Tree, BST)、平衡二叉搜索树(如AVL树)、二叉堆(包括最大堆和最小堆)、二叉表达式树等。
节点树
先定义一个类用于放置节点信息树,该类就只包含三个元素val:根节点;left:左节点;rigth:右节点;其中左右节点下方又是相同的。
/**
* 二叉树节点
*/
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;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
'}';
}
}
一、二叉树遍历
二叉树遍历:
前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),中序遍历结果为有序序列。
后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
给一个数据模型,如下:
TreeNode treeNode = new TreeNode(38,
new TreeNode(13,
new TreeNode(11),
new TreeNode(35,
new TreeNode(32),
new TreeNode(37)
)
),
new TreeNode(45,
new TreeNode(43),
new TreeNode(53)
)
);
1、前序遍历(根-左-右)
前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
/**
* 前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
*
* @param root 数据
*/
private void preOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
list.add(root.val);
preOrderHelper(root.left, list);
preOrderHelper(root.right, list);
}
}
2、中序遍历(左-根-右)
中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),中序遍历结果为有序序列。
/**
* 中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),
* 中序遍历结果为有序序列。
*
* @param root 数据
*/
private void inOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
inOrderHelper(root.left, list);
list.add(root.val);
inOrderHelper(root.right, list);
}
}
3、后序遍历(左-右-根)
后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
/**
* 后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
*
* @param root 数据
*/
private void postOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
postOrderHelper(root.left, list);
postOrderHelper(root.right, list);
list.add(root.val);
}
}
总结:二叉树遍历的顺序:前序遍历、中序遍历、后序遍历。总体思路是一致的,只是优先查找的顺序不同,前序就先从根节点,中序就先从左节点,根次之,后序就先从叶节点左右再到根节点。
二、二叉查找树(BST)操作
二叉查找树(BST)操作:
二叉查找树(BST,Binary Search Tree)是一种特殊的二叉树数据结构,其每个节点包含一个值以及指向左子节点和右子节点的引用。BST具有以下性质:
节点值特性:对于任意节点,其左子树中所有节点的值均小于该节点的值,而右子树中所有节点的值均大于该节点的值。
基于这些特性,二叉查找树提供了高效的查找、插入和删除操作。
插入:将新节点按其值与现有节点的大小关系插入到适当位置,保持左子节点小于根节点,右子节点大于根节点的特性。
删除:删除一个节点可能涉及替换其值、将其子节点提升为父节点,或者合并两个子树。需要处理三种情况:删除无子节点的节点、删除有一个子节点的节点,以及删除有两个子节点的节点。
查找:从根节点开始,按照值的大小关系逐层向下比较,直到找到目标节点或遇到空节点。
1、插入节点
将一整个节点拆分成多个小节点,如新插入的数值小于根节点,则插入左节点,又再次执行当前操作,直到节点为空直接赋值,大于根节点类似。
/**
* 插入节点
*
* @param root 二叉树
* @param val 插入数值
* @return
*/
private TreeNode insertRecursive(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
} else if (val < root.val) {
root.left = insertRecursive(root.left, val);
} else if (val > root.val) {
root.right = insertRecursive(root.right, val);
}
return root;
}
2、查找节点
查找节点
和插入操作类似:查找该二叉树是否为空或者是否找到目标值,如果找到就返回该节点。
没有找到就接着叶节点,大于往右边找,小于往左边找。
private TreeNode searchRecursive(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
} else if (val < root.val) {
return searchRecursive(root.left, val);
} else {
return searchRecursive(root.right, val);
}
}
// 数据源
TreeNode treeNode = new TreeNode(38,
new TreeNode(13, new TreeNode(11),
new TreeNode(35, new TreeNode(32), new TreeNode(37))),
new TreeNode(45, new TreeNode(43), new TreeNode(53)));
// 结果
TreeNode resultNode = binarySearchTree.searchTree(treeNode, 38);
if (resultNode == null) {
System.out.println("没找到");
} else {
System.out.println("找到了");
}
3、删除节点
删除一个节点可能涉及替换其值、将其子节点提升为父节点,或者合并两个子树。
需要处理三种情况:
- 删除无子节点的节点、删除有一个子节点的节点,以及删除有两个子节点的节点。
- 删除操作相对插入和查找来说更为复杂,因为它涉及到节点的移除以及可能的结构调整以保持BST的性质。
删除操作步骤:
1 查找待删除节点:首先使用查找算法找到要删除的节点(目标节点)。若未找到,则无需执行删除操作。
2 处理三种不同情况:
- 目标节点没有子节点(叶节点):直接删除目标节点。
- 目标节点只有一个子节点:将目标节点替换为其唯一子节点。
- 目标节点有两个子节点:找到目标节点的后继节点(右子树中最小节点或左子树中最大节点),将其值复制到目标节点,然后删除后继节点。这样做的目的是确保删除操作仅涉及至多一个子节点的情况。
3 调整结构:删除目标节点(或其后继节点)后,如果其有子节点,需要将其子节点连接到父节点或更新根节点以保持BST性质。
private TreeNode delete(TreeNode root, int val) {
// 查找,找到才删除,没找到就不动
TreeNode treeNode = this.searchRecursive(root, val);
if (treeNode == null) {
return null;
}
return deleteRecursive(root, val);
}
private TreeNode deleteRecursive(TreeNode root, int val) {
if (root == null) {
return null;
}
if (val < root.val) {
root.left = deleteRecursive(root.left, val);
} else if (val > root.val) {
root.right = deleteRecursive(root.right, val);
} else {
// 找到目标节点
// 1、目标节点没有子
if (root.left == null && root.right == null) {
return null;
} else if (root.left != null && root.right == null) {
// 2、目标节点只有一个子
return root.left;
} else if (root.left == null) {
return root.right;
}
TreeNode minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteRecursive(root.right, minNode.val);
}
return root;
}
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
图解删除的两种情况:(归结起来就是两种情况)
1、目标节点只有一个子节点或者没有节点:将子节点替换到当前节点或者直接删除;
2、目标节点又左右节点,找到右侧最小节点替换到当前节点,删除右侧最小节点;
源文件:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 二叉算法通常指的是应用于二叉树这一数据结构的各种操作与算法。二
* 叉树是一种特殊的树形结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。
* 二叉树在计算机科学中有广泛应用,如二叉查找树(Binary Search Tree, BST)、
* 平衡二叉搜索树(如AVL树)、二叉堆(包括最大堆和最小堆)、二叉表达式树等。
* <p>
* 1、二叉树遍历:
* <p>
* 前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
* 中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),中序遍历结果为有序序列。
* 后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
* <p>
* 2、二叉查找树(BST)操作:
* <p>
* 插入:将新节点按其值与现有节点的大小关系插入到适当位置,保持左子节点小于根节点,右子节点大于根节点的特性。
* 删除:删除一个节点可能涉及替换其值、将其子节点提升为父节点,或者合并两个子树。需要处理三种情况:删除无子节点的节点、删除有一个子节点的节点,以及删除有两个子节点的节点。
* 查找:从根节点开始,按照值的大小关系逐层向下比较,直到找到目标节点或遇到空节点。
* <p>
* 3、平衡二叉搜索树(如AVL树)的调整:
* <p>
* 旋转操作:当插入或删除导致树失去平衡时,通过旋转操作(单旋、双旋)重新调整树的结构以恢复平衡。单旋包括左旋和右旋,用于处理局部失衡;双旋通常由两次单旋组合而成,用于处理更复杂的失衡情况。
* <p>
* 4、二叉堆操作:
* <p>
* 构建堆:给定一个无序数组,通过从底部向上调整节点(下沉操作)构建一个满足堆性质的完全二叉树。
* 堆排序:基于堆的插入和删除操作实现的一种排序算法,首先构建堆,然后反复删除堆顶元素(最大堆则得到降序序列,最小堆则得到升序序列)并重新调整堆。
* 插入:在堆末尾添加新元素后,可能破坏堆性质,需要自下而上进行调整(上升操作)。
* 删除(删除堆顶元素):移除堆顶元素后,将堆尾元素移动到堆顶,再自顶向下调整堆(下沉操作)。
* <p>
* 5、其他二叉树算法:
* <p>
* 层次遍历:按照树的层级顺序访问节点,通常使用队列辅助实现。
* 深度优先搜索(DFS)与广度优先搜索(BFS):虽然不是针对二叉树特有的,但它们常用于解决二叉树问题,如节点计数、路径查找、节点间最短距离等。
* 二叉树剪枝:根据特定条件去除不满足要求的子树,如题目中提到的剪除所有节点值全为0的子树。
*
* @author hexionly
* @datetime 2024/04/17 11:13:35
*/
public class BinaryTree {
public static void main(String[] args) {
// 二叉遍历
TreeNode treeNode = new TreeNode(38,
new TreeNode(13, new TreeNode(11),
new TreeNode(35, new TreeNode(32), new TreeNode(37))),
new TreeNode(45, new TreeNode(43), new TreeNode(53)));
// BinaryTreeTraversal binaryTreeTraversal = new BinaryTreeTraversal();
// binaryTreeTraversal.preOrder(treeNode);
// 二叉查找树操作
BinarySearchTree binarySearchTree = new BinarySearchTree();
// Random rand = new Random();
// TreeNode resultNode = null;
// for (int i = 0; i < 10; i++) {
// int nextInt = rand.nextInt(100);
// resultNode = binarySearchTree.searchTree(resultNode, nextInt);
// }
// System.out.println(resultNode);
// TreeNode resultNode = binarySearchTree.searchTree(treeNode, 38);
// if (resultNode == null) {
// System.out.println("没找到");
// } else {
// System.out.println("找到了");
// }
binarySearchTree.deleteRecursive(treeNode, 13);
System.out.println(treeNode);
}
/**
* 二叉树遍历:
* <p>
* 前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
* 中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),中序遍历结果为有序序列。
* 后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
* </p>
* <p>
* 总结:二叉树遍历的顺序:前序遍历、中序遍历、后序遍历。总体思路是一致的,只是优先查找的顺序不同。
* </p>
*/
public static class BinaryTreeTraversal {
private void preOrder(TreeNode root) {
List<Integer> list = new ArrayList<>();
// 前序遍历
preOrderHelper(root, list);
// 中序遍历
// inOrderHelper(root, list);
// 后序遍历
// postOrderHelper(root, list);
list.forEach(System.out::println);
}
/**
* 前序遍历(根-左-右):首先访问根节点,然后递归地访问左子树,最后访问右子树。
*
* @param root 数据
*/
private void preOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
list.add(root.val);
preOrderHelper(root.left, list);
preOrderHelper(root.right, list);
}
}
/**
* 中序遍历(左-根-右):先递归地访问左子树,接着访问根节点,最后访问右子树。对于二叉搜索树(BST),
* 中序遍历结果为有序序列。
*
* @param root 数据
*/
private void inOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
inOrderHelper(root.left, list);
list.add(root.val);
inOrderHelper(root.right, list);
}
}
/**
* 后序遍历(左-右-根):先递归地访问左子树和右子树,最后访问根节点。这种遍历方式常用于计算一个表达式树的值或释放树中的资源。
*
* @param root 数据
*/
private void postOrderHelper(TreeNode root, List<Integer> list) {
if (root != null) {
postOrderHelper(root.left, list);
postOrderHelper(root.right, list);
list.add(root.val);
}
}
}
/**
* 二叉查找树(BST)操作:
* <p>
* 插入:将新节点按其值与现有节点的大小关系插入到适当位置,保持左子节点小于根节点,右子节点大于根节点的特性。
* 删除:删除一个节点可能涉及替换其值、将其子节点提升为父节点,或者合并两个子树。需要处理三种情况:删除无子节点的节点、删除有一个子节点的节点,以及删除有两个子节点的节点。
* 查找:从根节点开始,按照值的大小关系逐层向下比较,直到找到目标节点或遇到空节点。
*
* <p>
* 二叉查找树(BST,Binary Search Tree)是一种特殊的二叉树数据结构,其每个节点包含一个值以及指向左子节点和右子节点的引用。BST具有以下性质:
* 节点值特性:对于任意节点,其左子树中所有节点的值均小于该节点的值,而右子树中所有节点的值均大于该节点的值。
* 基于这些特性,二叉查找树提供了高效的查找、插入和删除操作。
*/
public static class BinarySearchTree {
private TreeNode searchTree(TreeNode root, int val) {
// return insertRecursive(root, val);
// return searchRecursive(root, val);
return delete(root, val);
}
/**
* 插入节点
* 每次就顺着根节点往下找,如果比根节点小,就往左找,如果比根节点大,就往右找,如果不存在为空了就插入该位置。
*
* @param root 二叉树
* @param val 插入数值
* @return TreeNode
*/
private TreeNode insertRecursive(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
} else if (val < root.val) {
root.left = insertRecursive(root.left, val);
} else if (val > root.val) {
root.right = insertRecursive(root.right, val);
}
return root;
}
/**
* 查找节点
* 和插入操作类似:查找该二叉树是否为空或者是否找到目标值,如果找到就返回该节点。
* 没有找到就接着叶节点,大于往右边找,小于往左边找
*
* @param root 二叉树
* @param val 查找值
* @return TreeNode
*/
private TreeNode searchRecursive(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
} else if (val < root.val) {
return searchRecursive(root.left, val);
} else {
return searchRecursive(root.right, val);
}
}
/**
* 删除一个节点可能涉及替换其值、将其子节点提升为父节点,或者合并两个子树。需要处理三种情况:
* 删除无子节点的节点、删除有一个子节点的节点,以及删除有两个子节点的节点。
* 删除操作相对插入和查找来说更为复杂,因为它涉及到节点的移除以及可能的结构调整以保持BST的性质。
* 删除操作步骤:
* 1、查找待删除节点:首先使用查找算法找到要删除的节点(目标节点)。若未找到,则无需执行删除操作。
* 2、处理三种不同情况:
* 目标节点没有子节点(叶节点):直接删除目标节点。
* 目标节点只有一个子节点:将目标节点替换为其唯一子节点。
* 目标节点有两个子节点:找到目标节点的后继节点(右子树中最小节点或左子树中最大节点),将其值复制到目标节点,然后删除后继节点。这样做的目的是确保删除操作仅涉及至多一个子节点的情况。
* 3、调整结构:删除目标节点(或其后继节点)后,如果其有子节点,需要将其子节点连接到父节点或更新根节点以保持BST性质。
*
* @param root 二叉树
* @param val 删除值
* @return boolean
*/
private TreeNode delete(TreeNode root, int val) {
// 查找,找到才删除,没找到就不动
TreeNode treeNode = this.searchRecursive(root, val);
if (treeNode == null) {
return null;
}
return deleteRecursive(root, val);
}
private TreeNode deleteRecursive(TreeNode root, int val) {
if (root == null) {
return null;
}
if (val < root.val) {
root.left = deleteRecursive(root.left, val);
} else if (val > root.val) {
root.right = deleteRecursive(root.right, val);
} else {
// 找到目标节点
// 1、目标节点没有子
if (root.left == null && root.right == null) {
return null;
} else if (root.left != null && root.right == null) {
// 2、目标节点只有一个子
return root.left;
} else if (root.left == null) {
return root.right;
}
TreeNode minNode = findMin(root.right);
root.val = minNode.val;
root.right = deleteRecursive(root.right, minNode.val);
}
return root;
}
private TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}
}
/**
* 二叉树节点
*/
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;
}
@Override
public String toString() {
return "TreeNode{" +
"val=" + val +
", left=" + left +
", right=" + right +
'}';
}
}