이 글은 앞 글과 이어지는 글입니다. 삽입과 조회 및 성능 비교에 관한 내용은 앞 글에 있습니다.
전체 코드는 Github 에서 확인 수 있습니다.
이전 글에서 삽입, 검색을 구현하였다. 삽입, 검색의 구현도 복잡했지만 삭제는 이들 보다 훨씬 복잡하다. 경우의 수가 많아 복잡하기 때문이다. 때문에 삭제 과정의 경우의 수를 잘 이해하고 코드를 확인하기 바란다.
삽입은 노드에 key를 추가하기에 key 의 개수가 최대 개수를 넘어가면 분리하였다면, 삭제는 노드에 key를 감소하기 때문에 부족한 키의 개수를 채우는 과정이 필요하다. 여러가지 경우에 따라 작업이 달라진다.
> 이렇게 한후 부모 노드가 최소 key 의 개수를 만족하지 않는 경우 2-1, 2-2 과정을 반복한다.
>
선임자 : 나보다 작은 key 값 중 가장 큰 key 값
후임자 : 나보다 큰 key 값중 가장 작은 key 값
이제 모든 경우의 수를 알아보았으니 코드를 짜보자!
public void delete(int key) {
delete(root, key);
// 루트 노드가 키를 잃었을 때, 자식이 있으면 그 자식으로 루트 갱신
if (root.keys.isEmpty()) {
if (!root.isLeaf()) {
root = root.children.get(0);
}
}
}
private void delete(BTreeNode node, int key) {
int idx = node.findKey(key);
// 1. 키가 현재 노드에 있는 경우
if (idx != -1) {
if (node.isLeaf()) {
// 리프 노드에서 직접 삭제
node.keys.remove(idx);
} else {
// 내부 노드에서 삭제 처리 3.
deleteInternalNode(node, idx);
}
} else {
// 키가 리프 노드에 없는 경우
if (node.isLeaf()) {
return; // 찾으려는 key 가 없음
}
// 키가 없으면 적절한 자식 노드로 이동
idx = node.getNextNodeIndex(key);
BTreeNode nextChild = node.children.get(idx);
//자식 노드에서 삭제
delete(nextChild, key);
// 삭제 후 자식 노드의 최소 키 값이 어긋나면
if (nextChild.keys.size() < minimumKeyCount) {
// 채워주기
fill(node, idx);
}
}
}
deleteInternalNode()
를 호출한다.fill()
을 호출해 채워준다.fill
private void fill(BTreeNode node, int deletedNodeIndex) {
// 왼쪽 형제에게 빌리는 경우 2-1
if (deletedNodeIndex != 0 && node.children.get(deletedNodeIndex - 1).keys.size() > minimumKeyCount) {
borrowFromPrev(node, deletedNodeIndex);
}
// 오른쪽 형제에게 빌리는 경우 2-1
else if (deletedNodeIndex != node.children.size() - 1 && node.children.get(deletedNodeIndex + 1).keys.size() > minimumKeyCount) {
borrowFromNext(node, deletedNodeIndex);
}
else {
// 왼쪽 형제와 부모노드를 합치는 경우 2-2
if (deletedNodeIndex != 0) {
leftMerge(node, deletedNodeIndex);
}
// 오른쪽 형제와 부모노드를 합치는 경우 2-2
else {
rightMerge(node, deletedNodeIndex);
}
}
}
부족한 key 의 개수를 채우는 메서드이다. deletedNodeIndex
는 node
의 자식 중 삭제된 노드의 인덱스를 나타낸다.
borrowFromPrev
, borrowFromNext
leftMerge
, rightMerge
borrowFrom
, merge
private void borrowFromPrev(BTreeNode node, int deletedNodeIndex) {
BTreeNode deletedChild = node.children.get(deletedNodeIndex);
BTreeNode sibling = node.children.get(deletedNodeIndex - 1);
// 부모의 키를 삭제된 자식에 옮김
deletedChild.addKey(node.keys.get(deletedNodeIndex - 1));
if (!deletedChild.isLeaf()) {
deletedChild.children.add(0, sibling.children.remove(sibling.children.size() - 1));
}
// 형제 노드의 키를 부모로 옮김
node.keys.set(deletedNodeIndex - 1, sibling.keys.remove(sibling.keys.size() - 1));
}
private void leftMerge(BTreeNode node, int deletedNodeIndex) {
BTreeNode deletedChild = node.children.get(deletedNodeIndex);
BTreeNode sibling = node.children.get(deletedNodeIndex - 1);
// 형제 노드의 키를 삭제된 자식에 옮김
for (int i = 0; i < sibling.keys.size(); i++) {
deletedChild.keys.add(i, sibling.keys.get(i));
}
if (!deletedChild.isLeaf()) {
for (int i = 0; i < sibling.children.size(); i++) {
deletedChild.children.add(i, sibling.children.get(i));
}
}
// 부모 노드의 키를 삭제된 자식에 옮김
deletedChild.addKey(node.keys.remove(deletedNodeIndex - 1));
// 합쳐진 형제 노드 삭제
node.children.remove(sibling);
}
borrowFromPrev
, borrowFromNext
→ 2-1leftMerge
, rightMerge
→ 2-2deleteInternalNode
private void deleteInternalNode(BTreeNode node, int deletedKeyIndex) {
BTreeNode nextChild;
// 선임자와 바꾸는 경우
if (node.children.get(deletedKeyIndex).keys.size() >= minimumKeyCount) {
int pred = getPredecessor(node, deletedKeyIndex);
node.keys.set(deletedKeyIndex, pred);
nextChild = node.children.get(deletedKeyIndex);
delete(nextChild, pred);
}
// 후임자와 바꾸는 경우
else {
int succ = getSuccessor(node, deletedKeyIndex);
node.keys.set(deletedKeyIndex, succ);
nextChild = node.children.get(deletedKeyIndex + 1);
delete(nextChild, succ);
}
// 선임자 혹은 후임자와 바꾸고 삭제 한 후 최소 key 개수를 만족하지 않는다면
if (nextChild.keys.size() < minimumKeyCount) {
// 부족한 값을 채운다.
fill(node, deletedKeyIndex);
}
}
// 선임자 혹은 후임자의 key 값을 반환
private int getPredecessor(BTreeNode node, int idx) {
BTreeNode current = node.children.get(idx);
while (!current.isLeaf()) {
current = current.children.get(current.keys.size());
}
return current.keys.get(current.keys.size() - 1);
}
삭제 조건 3. 에 해당하는 경우를 처리하는 메서드이다.
borrowFromPrev
,borrowFromNext
,leftMerge
,rightMerge
,getPredecessor
,getSuccessor
들은 두가지 경우 모두 비슷한 코드라서 하나의 코드만 설명했다.
이렇게 삭제 코드 까지 알아보았다.
Bài viết thật hữu ích