序文
この記事では、二分探索木の削除に焦点を当てた、二分探索木の一般的な操作方法について説明します。削除は挿入よりもはるかに複雑です。インターネット上では、よく考えられていないケースが多くあります。激怒して、詳細なメモを引き裂きました。デモ。
二分探索木の定義と特性
バイナリ検索ツリー(バイナリ検索ツリー、バイナリ検索ツリー、またはバイナリソートツリーとも呼ばれます)
定義(Baidu百科事典から):
バイナリ検索ツリー(また、バイナリ検索ツリー、バイナリソートツリー)これは、空のツリー、または次のプロパティを持つバイナリツリーのいずれかです。左側のサブツリーが空でない場合は、左側のサブツリー上のすべてのノードの値はルートノードの値よりも小さいです。右側のサブツリーが空でない場合、右側のサブツリーのすべてのノードの値はルートノードの値よりも大きくなります。左側の右側のサブツリーも二分探索木です。それぞれ。古典的なデータ構造として、バイナリ検索ツリーは、リンクリストの高速挿入と削除の特性を備えているだけでなく、高速配列検索の利点も備えているため、広く使用されています。たとえば、ファイルシステムやデータベースシステムは一般的にこれを使用します。一種のツリー。効率的なソートおよび取得操作のためのデータ構造。
自然
- 二分探索木の順序のないトラバースは、ノードの順序を維持します。
- ノードを挿入および削除する場合は、挿入および削除後もツリー全体がバイナリ検索ツリーであることを確認する必要があります。
- 最小のキーワードを持つノードを見つける方法:左側のサブツリーを再帰的にトラバースすることによって到達できる最後のノード
- 最大のキーワードを持つノードを見つける方法:右のサブツリーを再帰的にトラバースすることによって到達できる最後のノード
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;
}
二分探索木の削除操作:
二分探索木の削除は非常に複雑であり、二分探索木の構造は、ノードを削除した後も二分探索木のままになるように動的に調整する必要があります。
まず、いくつかの前提知識と概念が必要です。
前任者および後継者
二分探索木の削除には、後継ノード(後継ノードと表記)と先行ノード(先行ノードと表記)があります。これは次のように定義されます。
ノードの後継:バイナリ検索ツリーは、ノードの右側の番号(実際には小さいものから大きいものへ)に従ってトラバースされます。(実際には、小さいものから大きいものへと並べた後、ノードよりも大きい最初の数です。)
ノードの前身:バイナリ検索ツリーは、順番に従って(実際には、小さいものから大きいものへと並べられます)トラバースされます。ノードの左側の番号。(実際、これは、小さいものから大きいものにソートした後のノードよりも小さい最初の数値です)
3つのケースでノードを削除する
- A:ノードがリーフノードの場合は、直接削除してください
- B:ノードに右側のノードがある場合、ノードの後続ノード(後続ノードと呼ばれる)は、カバーするノードの位置を参照します。
オーバーライドした後、後継者は繰り返します。
したがって、入れ子人形のプロセスを後継者を削除する方法を見つけて(再帰的に2または3を呼び出して削除します) 、削除操作を完了することができます。 - C:ノードに左側のノードのみがあり、右側のノードがない場合、後続のノードはその上にある必要があります(バイナリ検索ツリーの性質によって決定されます)。そのため、ノードの先行ノード(先行ノードとして示される)を使用できます。ノードの場所を指定してオーバーライドします。
オーバーライドした後、前のバージョンが繰り返されます。
したがって、入れ子人形のプロセスを削除する方法を見つけて(再帰的に2または3を呼び出して削除し) 、削除操作を完了できます。
LeetCodeの質問450に対する公式の解決策も、上記の考えを反映しています。
上記の概念を知っていると、以下のコードを書き始めることができます。コードの実装も比較的複雑なので、個別に実装するためにいくつかのサブ関数に分割します。
バイナリツリーを順番にトラバースせずに、ノードの後続と先行をより便利に見つけるにはどうすればよいですか?
実際、それは非常に簡単です。後継者を見つけるには、ノードの右側のノード(存在する場合)を見つけ、ノードの右側のノードを新しい二分探索木と見なしてから、二分探索を見つけます。ツリーの最小ノードはサクセサです。
それは「私よりも大きい最小のノード」を見つけるという考えを具体化しています
先行ノードの検索は正反対です。つまり、ノードの左側のノード(存在する場合)を検索し、ノードの左側のノードを新しい二分探索木と見なしてから、最大の二分探索木を検索します。ノード、ノードは前身であり、
「私よりも小さい最大のノード」を見つけるというアイデアを具体化しています
注:この方法は、後続および先行を検出し、左/右ノードが存在する場合にのみ適用できます。存在しない場合は、正直に順序トラバーサルを使用してください。
注:この方法は、後続および先行を検出し、左/右ノードが存在する場合にのみ適用できます。存在しない場合は、正直に順序トラバーサルを使用してください。
注:この方法は、後続および先行を検出し、左/右ノードが存在する場合にのみ適用できます。存在しない場合は、正直に順序トラバーサルを使用してください。
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;
}
}