前言
本文含操作二叉搜索树的常用方法,重点是二叉搜索树的删除,删除远比插入复杂,网上的案例多有情况考虑不周的情况,一怒之下本人手撕了一个带详细注释的Demo。
二叉搜索树的定义和性质
二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)
定义(摘自百度百科):
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
性质
- 对二叉查找树进行中序遍历可以让结点有序!
- 如果插入和删除结点,必须保证插入和删除后全树仍然是一颗二叉搜索树。
- 如何寻找关键字最小的结点:就是递归遍历左子树所能达到的最后一个结点
- 如何寻找关键字最大的结点:就是递归遍历右子树所能达到的最后一个结点
使用Java代码实现二叉搜索树的定义:
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
//Constructor
TreeNode() {
}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
有序数组转化成二叉搜索树
进去是正序排列的数组,吐出来是一个二叉搜索树:
比如入参是: [1,2,3,4,5,6,7]
那么返回值就是如下图的二叉树结构:
4
/ \
2 6
/ \ / \
1 3 5 7
思想是找中点,递归构建。
//找 start和 end 要么都找下标(比如0和arr.length-1),要么就都找实际位置(比如1和arr.length)
public static TreeNode initBinarySearchTree(Integer[] arr,int start,int end) {
if (start > end){
return null;
}
int mid = (start+end)/2;
TreeNode treeNode = new TreeNode(arr[mid], null, null); //在中点创建一个结点作为根节点
treeNode.left = initBinarySearchTree(arr,start,mid-1);
treeNode.right = initBinarySearchTree(arr,mid+1,end);
return treeNode;
}
二叉搜索树的插入操作:
相对于删除,二叉搜索树的插入非常简单。
虽说插入节点需要保持二叉搜索树的性质,使其仍然是一颗二叉搜索树。但是通过观察可得知,可以不需要调整二叉搜索树的结构,只需要插入到节点的最底部,仍旧是一个二叉搜索树
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
TreeNode pos = root;
while (pos != null) {
if (val < pos.val) {
if (pos.left == null) {
pos.left = new TreeNode(val);
break;
} else {
pos = pos.left;
}
} else {
if (pos.right == null) {
pos.right = new TreeNode(val);
break;
} else {
pos = pos.right;
}
}
}
return root;
}
二叉搜索树的删除操作:
二叉搜索树的删除相当复杂,必须动态调整二叉搜索树的结构,使其删除节点之后仍然是一颗二叉搜索树!
首先需要一些前置知识和概念:
前驱结点和后继结点
在二叉搜索树的删除中,有后继结点(记为successor)和前驱结点(记为predecessor)这么一说。其定义为:
该结点的successor: 二叉搜索树根据中序遍历(其实就是从小到大排列),该结点右边的那个数。(其实就是从小到大排列后比该结点大的第一个数字)
该结点的predecessor: 二叉搜索树根据中序遍历(其实就是从小到大排列),该结点左边的那个数。(其实就是从小到大排列后比该结点小的第一个数字)
分三种情况删除结点
- A: 如果该结点是叶子结点,直接删除即可
- B:如果该节点有右结点,将该节点的后继结点(记为successor)提到该节点的位置进行覆盖。
进行覆盖之后, successor重复了。于是想办法将successor删除(递归调用2或3来删除)
套娃这一过程,即可完成删除操作。 - C:如果该节点只有左结点而没有右结点,那它的后继结点一定在他的上面(由二叉搜索树性质决定)所以我们可以使用该节点的前驱结点(记为predecessor)提到该节点的位置进行覆盖。
进行覆盖之后,predecessor重复了。于是想办法将predecessor删除(递归调用2或3来删除)
套娃这一过程,即可完成删除操作。
LeetCode第450题的官方题解也体现了上述思想:
知道了以上的概念,下面就可以动手编写代码了。代码的实现也是比较复杂的,我会拆分成几个子函数分别实现。
如何不用中序遍历二叉树的情况下,更方便地找到该节点的后继结点(successor),和前驱结点(predecessor)?
其实非常之简单,找successor,就是找该结点的右结点(如果有的话),并将该节点的右结点看作是一颗新的二叉搜索树,然后找到该二叉搜索树最小的结点,该结点就是successor。
体现了找“比我大的最小结点”的思想
找predecessor正好相反,就是找该结点的左结点(如果有的话),并将该节点的左结点看作是一颗新的二叉搜索树,然后找到该二叉搜索树最大的结点,该结点就是predecessor
体现了找“比我小的最大结点”的思想
注意:该方法找successor和predecessor,只适用于左/右结点存在的情况。如果不存在,请老实地使用中序遍历!
注意:该方法找successor和predecessor,只适用于左/右结点存在的情况。如果不存在,请老实地使用中序遍历!
注意:该方法找successor和predecessor,只适用于左/右结点存在的情况。如果不存在,请老实地使用中序遍历!
下面直接给出Java代码:
private TreeNode findSuccessor(TreeNode node) {
if (node.right == null) { //如果没有右结点,该节点的successor肯定在它上面,所以用该方法是找不到的。
return null;
}
node = node.right;
//下面的代码其实就是调用findMin方法找最小结点
while (true){
if (node.left != null) {
node = node.left;
}else {
break;
}
}
return node;
}
private TreeNode findPredecessor(TreeNode node) {
if (node.left == null) {
return null;
}
node = node.left;
//下面的代码其实就是调用findMax方法找最大结点
while (true){
if (node.right != null) {
node = node.right;
}else {
break;
}
}
return node;
}
最终实现删除结点代码:
public class DeleteBST {
/*public TreeNode deleteNode(TreeNode root, int key) {
}*/
public TreeNode deleteNode(TreeNode root, int key) {
//先找到这个结点再说。如果找不到,直接返回原来的树
TreeNode workNode = root; //工具结点
TreeNode targetNode = null; //找到的该结点
while (workNode != null) {
if (key < workNode.val) {
workNode = workNode.left;
} else if (key > workNode.val) {
workNode = workNode.right;
} else { //root.val == key
targetNode = workNode; //已经找到该节点
break;
}
}
if (targetNode == null) { //二叉树根本没有该结点,直接返回原本的树即可
return root;
}
//情况一:该节点是叶结点,直接删除
if (targetNode.left==null && targetNode.right==null){
if (targetNode.val == root.val){ //边界检查:根节点就是叶子结点,直接return null
return null;
}
deleteLeaf(root,targetNode);
return root;
}
//情况二:如果该节点有右结点,将该节点的后继结点(记为successor)提到该节点的位置进行覆盖,并递归删除
if (targetNode.right!=null){
TreeNode successor = findSuccessor(targetNode);
coverNode(targetNode,successor);
targetNode.right = deleteNode(targetNode.right, successor.val);
}else if (targetNode.right==null && targetNode.left!=null){ //情况三:如果该节点只有左结点而没有右结点,使用该节点的前驱结点(记为predecessor)提到该节点的位置进行覆盖。
TreeNode predecessor = findPredecessor(targetNode);
coverNode(targetNode,predecessor);
targetNode.left = deleteNode(targetNode.left, predecessor.val);
}
return root;
}
//覆盖结点. oriNode指要被覆盖的结点,newNode指代替它的结点
private void coverNode(TreeNode oriNode, TreeNode newNode) {
oriNode.val = newNode.val;
}
//直接删除叶结点
private void deleteLeaf(TreeNode root,TreeNode targetNode) {
TreeNode workNode = root; //工具结点
while (true){
if (targetNode.val < workNode.val){
if (workNode.left.val!= targetNode.val){
workNode = workNode.left;
}else {
workNode.left = null; //如果相等则删除该结点
return;
}
}else {
if (workNode.right.val!= targetNode.val){
workNode = workNode.right;
}else {
workNode.right = null; //如果相等则删除该结点
return;
}
}
}
}
private TreeNode findSuccessor(TreeNode node) {
if (node.right == null) { //如果没有右结点,该节点的successor肯定在它上面,所以用该方法是找不到的。
return null;
}
node = node.right;
//下面的代码其实就是调用findMin方法找最小结点
while (true){
if (node.left != null) {
node = node.left;
}else {
break;
}
}
return node;
}
private TreeNode findPredecessor(TreeNode node) {
if (node.left == null) {
return null;
}
node = node.left;
//下面的代码其实就是调用findMax方法找最大结点
while (true){
if (node.right != null) {
node = node.right;
}else {
break;
}
}
return node;
}
}