
삽입 연산은 Node를 return하는 재귀적인 구조인데 마지막까지 반복한 경우 삽입할 노드를 return하고 그렇지 않은 경우 삽입할 노드와 비교한 노드를 return한다. 재귀적인 구조라 글로 쓰면 복잡해 보이지만 코드로 보면 그리 복잡하지는 않다.
재귀적인 구조의 장단점
public void add(int key) {
Node newNode = new Node();
newNode.key = key;
if (root == null) {
root = newNode;
} else {
root = addNode(root, newNode);
}
}
private Node addNode(Node node, Node newNode) {
if (node == null)
return newNode;
else if (node.key > newNode.key)
node.left = addNode(node.left, newNode);
else
node.right = addNode(node.right, newNode);
return node;
}

Leaf 노드의 부모 노드가 해당 Leaf 노드와의 연결을 끊어 주면 된다.

중간 노드를 삭제한다면 전체적인 이진 탐색 트리의 구조가 무너지기 때문에 삭제할 노드를 대체할 노드를 찾아 이진 탐색 트리의 구조에 맞게 해당 위치로 옮겨야 한다. 실제 구현시에는 노드를 정말 옮기는 것은 아니고 단지 두 노드의 key값을 바꾼다.
key 값을 대체할 노드는 삭제한 노드의 키 기준 왼쪽 서브트리의 가장 오른쪽에 있는 노드 또는 오른쪽 서브트리의 가장 왼쪽에 있는 노드이다. 서브트리가 하나만 있으면 해당 서브트리에서 대체 노드를 찾으면 되며 두 개의 서브트리가 있다면 어느 쪽의 서브트리에서 대체 노드를 찾아도 무방하다.
왼쪽 서브트리의 가장 오른쪽에 있는 노드 = 해당 서브트리에서 가장 큰 값
오른쪽 서브트리의 가장 왼쪽에 있는 노드 = 해당 서브트리에서 가장 작은 값

결국 삭제한 노드 기준 가장 가까운 값을 키로 갖는 노드를 대체함으로써 노드의 이동을 최소화하면서 전체 이진 탐색 트리의 구조를 유지하는 것이다.
key값을 바꿔 삭제할 노드가 옮겨진 위치의 서브트리에 대해 삭제할 노드가 단말 노드가 될 때까지 재귀적으로 반복한다. 어떻게 보면 분할 정복(Divide and Conquer)에 해당되는데 위에서 살펴본 경우1. leaf node 삭제의 연산은 간단했다. 따라서 복잡한 경우2. 중간 노드를 삭제를 간단한 경우1.로 만들어 해결하는 것이다.
구체적인 삭제 방법은 다음과 같다. 그림은 왼쪽 서브트리의 가장 오른쪽에 있는 노드로 삭제할 노드를 대체하고 있다.


구체적인 구현 방법은 다음과 같다
public void remove(int key) {
root = removeNode(root, key);
}
private Node removeNode(Node node, int key) {
if (node == null)
throw new RuntimeException("해당 값을 가진 노드를 찾을 수 없습니다.");
if (node.key > key)
node.left = removeNode(node.left, key);
else if (node.key < key) {
node.right = removeNode(node.right, key);
} else {
//삭제할 노드를 찾은 경우
if (node.left != null) {
//왼쪽 서브트리에서 가장 오른쪽에 있는 값 찾아 대체하기
Node child = findMaxNode(node.left);
int removedKey = node.key;
node.key = child.key;
child.key = removedKey;
//다시 옮겨진 위치에서 서브트리에 대해 재귀적으로 실행
node.left = removeNode(node.left, key);
} else if (node.right != null) {
//오른족 서브트리에서 가장 왼쪽에 있는 값 찾아 대체하기
Node child = findMinNode(node.right);
int removedKey = node.key;
node.key = child.key;
child.key = removedKey;
//다시 옮겨진 위치에서 서브트리에 대해 재귀적으로 실행
node.right = removeNode(node.right, key);
} else {
//삭제할 노드가 단말 노드인 경우 부모 노드와의 연결 종료
return null;
}
}
return node;
}
private Node findMaxNode(Node node) {
if (node.right == null)
return node;
else
return findMaxNode(node.right);
}
private Node findMinNode(Node node) {
if (node.left == null)
return node;
else
return findMinNode(node.left);
}
순회는 처음부터 끝까지 전체 노드에 접근하는 것이고 탐색은 순회를 하다 특정 값을 찾으면 순회를 멈추는 것이기에 결국 순회와 탐색은 동일한 논리 구조를 갖는다. 따라서 순회와 탐색을 별도로 구분하지 않고 같이 알아본다.
이진 트리 순회에는 크게는 3가지 방법이 있으며 3가지 방법 모두 재귀적인 방법으로 구현된다.
1. 전위 순회(preorder traversal)
2. 중위 순회(inorder traversal)
3. 후위 순회(postorder traversal)
3가지 순회 방법중 이진 탐색 트리의 순차적 탐색을 위한 순회에는 중위 순회가 가장 적절하다. 이는 이진 탐색 트리가 부모 노드 기준 왼쪽 자식 노드는 값이 작고 오른쪽 자식 노드는 값이 크다는 성질을 갖고 있기 때문이다.
중위 순회도 왼쪽 서브트리, 오른쪽 서브트리 중 어떤 트리를 먼저 순회할 것인가에 따라 좌측 중위 순회, 우측 중위 순회로 나뉜다. 좌측 중위 순회는 왼쪽 서브트리를 먼저, 우측 중위 순회는 오른쪽 서브트리를 먼저 탐색하는 중위 순회이다. 단순히 서브트리의 탐색 순서를 바꾼 것 같지만 이 탐색순서로 내림차순 탐색 혹은 오름차순 탐색이 결정된다.
좌측 중위 순회 => 오름차순 순회

우측 중위 순회 => 내림차순 순회

public void search(int key) {
searchNode(root, key);
}
private Node searchNode(Node node, int key) {
if (node == null)
throw new RuntimeException("해당 값을 가진 노드를 찾을 수 없습니다.");
if (node.key > key)
node.right = searchNode(node.right, key);
else if (node.key < key)
node.left = searchNode(node.left, key);
else
System.out.println("해당 값을 가진 노드를 찾았습니다.");
return node;
}
public void descendingTraversal() {
//내림차순 순회
System.out.print("내림차순 순회: ");
rightInorderTraversal(root);
System.out.println();
}
private void rightInorderTraversal(Node node){
//우측 중위 순회
if(node == null)
return;
rightInorderTraversal(node.right);
System.out.printf("%d ", node.key);
rightInorderTraversal(node.left);
}
public void ascendingTraversal() {
//오름차순 순회
System.out.print("오름차순 순회: ");
leftInorderTraversal(root);
System.out.println();
}
private void leftInorderTraversal(Node node) {
//좌측 중위 순회
if(node == null)
return;
leftInorderTraversal(node.left);
System.out.printf("%d ", node.key);
leftInorderTraversal(node.right);
}