읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

Programmers. 지형편집

input 범위가 10억이라 당연히 이분탐색일 거라 생각했으나 그냥 전체 지형을 모두 탐색하는 완전탐색 문제였다. 주어진 지형의 최솟, 최댓값을 기준으로 전부 최소, 최대 높이를 통일하는 비용이 양 극단에 해당하고 그 사이 어딘가 지점에 최적점이 존재할 것이다. 즉, 변곡점이 존재한다면 해당 지점이 최소비용 지점이 될 것이고 변곡점이 없다면 양 극단 중 하나가 최소비용 지점일 것이다.

  • 지형의 높이를 담은 배열을 생성하고 정렬
  • 초기값으로 모든 배열을 최소 높이로 통일했다 가정
  • 전체 지형 높이를 담은 배열을 순회하면서 이전과 동일한 높이면 생략하고 동일하지 않다면 이후의 값에 대해선 이전보다 모두 높음을 의미하므로 이전까지의 대해선 현재 높이만큼 쌓고 이후에 대해선 앞서 제거했던 비용만큼 복구시켜야 한다.
  • 갱신한 결과가 이전 답보다 작으면 답을 갱신하고 그렇지 않으면 변곡점을 의미하므로 그대로 반복문을 종료한다.

python 코드

def solution(land, P, Q):
    total = 0
    h = []
    R, C = len(land), len(land[0])
    for r in range(len(land)):
        for c in range(len(r)):
            h.append(land[R * r + c])
    h.sort()

    cost = sum(h) - (h[0] * len(h)) * Q
    answer = cost
    for i in range(1, len(h)):
        if h[i - 1] == h[i]:
            continue
        else:
            v = h[i] - h[i - 1]
            cost = cost + P * i * v  - v * Q * (len(h) - i)
            if answer < cost:
                break
            answer = min(answer, cost)
    return answer

'Algorithms > Programmers' 카테고리의 다른 글

Programmers. 삼각 달팽이  (0) 2021.09.09
Programmers. 쿠키구입  (0) 2021.09.06
Programmers. 사칙연산  (0) 2021.09.06
Programmers. 단어 퍼즐  (0) 2021.09.04
Programmers. 징검다리  (0) 2021.08.14

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

Programmers. 사칙연산

문제 풀이

dp 문제로 아마 코테에 출제되었다면 상당한 난이도를 자랑해 해결하지 못했을 문제라 생각한다. 처음에는 분할 정복으로 가능한 모든 경우의 수에 대해 탐색했으나 TLE를 출력하여 결국 write up을 볼 수 밖에 없었다.

피연산자에 대해 모아두고 i번째 피연산자에서 j번째 피연산자까지의 연산 결과를 저장하는 2차원 dp 배열을 생성하되 길이는 피연산자의 개수로 제한한다.

덧셈의 최댓값은 최댓값 + 최댓값이나 뺄셈의 최댓값은 최댓값 - 최솟값으로 최댓, 최솟값을 모두 관리해야 하기에 dp 배열을 2개 생성해야 한다.

  • 최댓값과 최솟값을 저장할 dp 배열을 2개 생성
  • 각 피연산자 좌표에 대해 dp[i][i]=피연산자 값저장
  • 연산 범위 저장을 위해 1부터 피연산자의 개수만큼 반복문 정의
  • 연산 시작 지점 지정을 위해 0부터 피연산자 길이 - 연산범위에 대해 반복문 정의
  • 연산 마지막 범위 좌표를 위해 시작지점 + 연산 범위 정의
  • 연산 시작지점부터 마지막 범위까지의 반복문 정의, 연산좌 좌표를 정의해야 함.
  • 연산자의 상태에 따라 최댓값, 최솟값 정의
  • 마지막까지 순회 수 0부터 마지막 피연산자까지의 결과를 의미하는 좌표값을 반환한다.

python 코드

def solution(arr):
    INF = float('inf')
    min_dp = [INF for _ in range(len(arr) // 2 + 1)]
    max_dp = [-INF for _ in range(len(arr) // 2 + 1)]
    for i in range(len(arr) // 2 + 1):
        min_dp[i][i] = arr[i * 2 + 1]
        max_dp[i][i] = arr[i * 2 + 1]
    for c in range(1, len(max_dp)):
        for i in range(len(max_dp) - c):
            j = i + c
            for k in range(i, j):
                if arr[k * 2 + 1] == "+":
                    max_dp[i][j] = max(max_dp[i][j], max_dp[i][k] + max_dp[k + 1][j])
                    min_dp[i][j] = min(min_dp[i][j], min_dp[i][k] + min_dp[k + 1][j])
                elif arr[k * 2 + 1] == "-":
                    max_dp[i][j] = max(max_dp[i][j], max_dp[i][k] - min_dp[k + 1][j])
                    min_dp[i][j] = min(min_dp[i][j], min_dp[i][k] - max_dp[k + 1][j])
    return max_dp[0][-1]    

'Algorithms > Programmers' 카테고리의 다른 글

Programmers. 쿠키구입  (0) 2021.09.06
Programmers. 지형편집  (0) 2021.09.06
Programmers. 단어 퍼즐  (0) 2021.09.04
Programmers. 징검다리  (0) 2021.08.14
Programmers. N으로 표현  (0) 2021.08.10

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

문제 링크

Programmers. 단어 퍼즐

문제 풀이

dp와 완전 탐새을 섞은 문제다. 레벨 4로 마킹되어 있지만 카카오 코테에 출제되었다면 레벨 3에 3번 정도에 위치하지 않았을까 싶다. 첫 글자부터 마지막 글자까지 순회하며 단어 조각의 길이가 1에서 5까지만 존재함을 유념해 하위 문제와 결합을 어떻게 할 지 고민해야 한다.

특히 하위 문제의 값을 기록한 좌표와 가능한 퍼즐 조각의 길이만큼의 좌표를 잘 추출해야 문제를 수월히 해결할 수 있다.

  • 1부터 t의 길이만큼의 dp 배열 생성
  • 초항(첫 번째 글자) 체크 후 dp[0]에 입력
  • 가능한 퍼즐 조각 길이 1부터 5에 대해 해당 퍼즐 조각 이전 dp 좌표가 valid한 지 체크하고 퍼즐 조각만큼 길이에 대해 배열에 존재하는 지 탐색, 존재하면 현재 dp좌표를 갱신한다.
  • 순회를 모두 마치고 마지막 자리에 대해 valid하면 값을 반환하고 그렇지 않으면 -1을 반환한다.

python 코드

def solution(strs, t):
    if t in strs:
        return 1
    INF = float('inf')
    dp = [INF] * len(t)
    if t[0] in strs:
        dp[0] = 1
    for i in range(1, len(t)):
        if t[:i + 1] in strs:
            dp[i] = 1
        for j in range(1, 6):
            if i >= j:
                if dp[i - j] != INF and t[i - j + 1:i+1] in strs:
                    dp[i] = min(dp[i], dp[i - j] + 1)
    answer = -1
    if dp[-1] != INF:
        answer = dp[-1]
    return answer

'Algorithms > Programmers' 카테고리의 다른 글

Programmers. 지형편집  (0) 2021.09.06
Programmers. 사칙연산  (0) 2021.09.06
Programmers. 징검다리  (0) 2021.08.14
Programmers. N으로 표현  (0) 2021.08.10
Programmers. 다리를 지나는 트럭  (0) 2021.08.10

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.

B+Tree란?

B-Tree의 변형된 형태로 트리 탐색하지 않고 데이터를 linear하게 탐색해야 할 경우 순차 접근을 해야 하는데 그러면 중위 탐색을 수행하게 되고 트리 전체를 탐색하게 되어 성능 저하가 발생한다. B+Tree는 B-Tree의데이터의 순차 접근 문제를 개선하기 위해 제시되었다. B-Tree에서는 중복값 없이 한 개의 노드에만 key가 유일하게 존재했으나 B+Tree는 leaf 노드와 그 외 노드에 공존할 수 있다.

B+Tree의 리프 노드는 연결리스트로 구성되어 있으며 순차 집합(Sequence Set)이라고도 한다. 여기에 데이터가 담긴다. 리프 노드가 아닌 비단말 노드는 인덱스 집합(Index Set)이라고 부르며 데이터로의 빠른 접근을 위한 인덱스 역할만 하기 때문에 키와 포인터로만 구성된다. 따라서 기존 B-Tree의 데이터를 모두 리프 노드로 내려서 리프 노드를 B-Tree의 구조 + 연결리스트로 구현된 색인 구조로 구성하여 순차적인 탐색에 유리하다.

Data Structure_B+-Tree_001.png

Data Structure_B+-Tree_002.png

B+Tree의 성질

  1. 모든 데이터는 리프 노드에만 존재한다.
  2. 모든 리프 노드는 연결리스트로 구성된다.
    • 다시 루트노드부터 내려와 데이터 순회 시 시간이 절약됨
  3. 데이터의 삽입/삭제 연산은 리프 노드에서만 발생한다.
  4. 모든 리프 노드의 첫 번째 key는 index set에 존재한다.

M차 B+Tree라고 가정하면 다음의 성질들을 만족해야 한다.

  • 노드는 최대 M개부터 최소 $\lceil \cfrac{M}{2} \rceil$개까지의 자식을 가질 수 있다.
  • 위 조건에 따라 노드는 최대 $M - 1$개부터 최소 $\lceil\cfrac{M}{2}\rceil-1$개의 키를 가질 수 있다.
    • 노드의 키가 $n$개라면 자식의 수는 $n+1$개이기 때문!
  • 최소차수는 자식 수의 하한선을 의미한다. 따라서, 최소차수가 $t$라면 $M = 2t-1$이다. 최소차수가 적용되지 않는 노드는 root 노드이다.
    • key의 하한이 1개인 경우가 가장 차수가 낮을 것이다. $t-1=1$로 $t=2$가 되며 $M = 2t-1$이므로 3차 B+트리가 된다.

B+Tree에서의 검색(Search)

B-Tree와 동일하나 B-Tree는 Best Case의 경우 루트 노드에서 끝날 수 있지만 B+Tree는 모든 경우에 대해 리프 노드로 가서 순차 탐색함으로써 데이터를 찾아야 한다.

B+Tree에서의 삽입(Insert)

통상적인 검색으로 삽입하고자 하는 노드가 어느 리프 노드에 해당하는지 체크

  1. 리프 노드가 full 상태가 아니라면 오름차순 정렬에 맞게 삽입한다.

  2. 리프 노드가 full 상태라면 노드를 분할해야 한다.

    a) 새로운 리프 노드를 구성하고 기존 리프 노드 데이터의 절반을 옮긴다.

    • 새로운 리프 노드의 최소 키(기존 리프 노드의 중간값)를 부모 노드에 삽입하여 기준 포인터로 삼는다.
    • 부모 노드 역시 리프 노드의 key를 받았으므로 full 상태인지 체크해야 한다.
      • 부모 노드가 쪼개지지 않는 시점이 올 때까지 상향식으로 반복한다.
  3. 루트가 쪼개지면 새로운 루트 노드를 생성하되 1개의 키와 2개의 양쪽 포인터로 구성되어야 하며 기존 루트 노드의 중간 key는 새로운 루트 노드에 저장한 뒤 제거한다.

Data Structure_B+-Tree_003.png

Data Structure_B+-Tree_004.png

B+Tree에서의 삭제(Delete)

루트에서 출발하여 key가 속한 leaf 노드를 찾아 리프 노드의 key를 삭제한다. 삭제한 뒤 리프 노드가 최소 key 이상인지 체크해야 한다.

  1. 삭제할 key가 index set에 존재하지 않을 경우

    a) 리프 노드의 key 개수가 $t-1$보다 클 때

    • 리프 노드의 key만 삭제하고 종료

    b) 리프 노드의 key 개수가 $t-1$일 때

    • 형제 노드 중 하나라도 key의 개수가 $t-1$보다 큰 경우

      • 왼쪽 노드가 크다면 왼쪽 노드의 가장 큰 값을 받고 현재 노드의 첫 번째 값이 바뀌므로 부모 노드의 key도 바뀐 값으로 변경
      • 오른쪽 노드가 크다면 현재 노드는 변화가 없으나 우측 노드의 최솟값이 현재 노드로 옮겨감에 따라 우측 노드의 부모 key를 다음 값으로 바꿔준다.
    • 형제 노드가 모두 $t-1$인 경우

      • 어떤 경우라도 최소 차수를 만족할 수 없으므로 형제 노드들 중 하나와 병합해야 한다. 왼쪽 혹은 오른쪽 노드와 병합하되 왼쪽은 현재 노드의 부모 key, 오른쪽에 합치면 우측 노드의 부모 key가 삭제된다.
      • 부모 노드의 key 개수가 1 감소한 경우 부모 노드의 key 개수가 $t-1$미만이면 다시 reconstruct를 해야한다.

      Data Structure_B+-Tree_005.png

  2. 삭제할 key가 index set에 존재할 경우

    a) 리프 노드의 key 개수가 $t-1$보다 클 때

    • 리프 노드를 삭제하고 기존 key가 있던 index set 자리에 inorder successor 값으로 바꾼다.

    b) 리프 노드의 key 개수가 $t-1$일 때

    • 형제 노드 중 하나라도 key의 개수가 $t-1$보다 큰 경우

      • 왼쪽에서 가져오면 현재 노드의 첫 번째 값이 바뀌므로 index set의 key 값도 가져온 값으로 교체
      • 오른쪽에서 가져오면 우측 노드의 첫 번째 값이 바뀌므로 index set의 기존 우측 노드의 첫 번째 값을 다음 값으로 교체
    • 형제 노드가 모두 $t-1$인 경우

      • 한쪽 형제와 현재 노드를 병합한다. 왼쪽과 병합한 경우 현재 노드의 부모 key를 삭제, 오른쪽과 병합한 경우 오른쪽 노드의 부모 key를 삭제한다.
      • 부모 key가 삭제되었으므로 부모 노드의 key 개수가 $t-1$미만이면 다시 reconstruct를 수행한다.

      Data Structure_B+-Tree_006.png

B*Tree란?

B-Tree의 개량된 형태이다. 순차 탐색 문제를 해결하기 위해 B+Tree가 제시된 것과는 달리 B*Tree는 B-Tree의 메모리 낭비, 보조 연산 문제를 해결하기 위해 제시되었다. 실제 대용량 시스템에서 B-Tree의 운용 시 전체 노드의 상당 부분이 비어있어 메모리 낭비 문제를 초래한다. 그리고 B-Tree의 삽입/삭제 연산 시 노드 혹은 key를 옮기는 등의 작업을 수행하는 분할, 병합 연산을 최소로 줄여 성능 개선을 도모하려는 취지에서 사용된다.

B*Tree의 성질

B*트리는 B-Tree에서 노드 최소용량의 차이의 변화가 주요하다.

  1. 루트 노드를 제외한 모든 노드는 최대 $2t-1$에 대해 $\cfrac{2}{3}$이상의 자료가 저장되어야 한다.
    • B-Tree는 $t-1$이므로 약 절반 이상의 자료를 저장해야 했다.
  2. 노드가 넘칠 경우(overflow) 일단 양쪽 형제 노드로 key들을 재분배한다. 그리고 모든 형제 노드가 전부 full 상태여야만 B-Tree의 분할 연산을 수행한다.
    • B-Tree는 현재 노드가 full이면 무조건 중간값을 부모 노드로 보내고 split했었다.

'Algorithms > Data Structure' 카테고리의 다른 글

B-트리(B-Tree)  (0) 2021.08.31
레드-블랙 트리(Red-Black Tree)  (2) 2021.08.26
세그먼트 트리 비재귀 구현  (0) 2021.08.16
세그먼트 트리(Segment Tree)  (0) 2021.08.14
최소 신장 트리(Minimum Spanning Tree)  (1) 2021.08.02

읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 배운 점을 정리한 글입니다.
  • 개념이 섞인 채 정리된 포스팅이 많아서(심지어 긱스도 가독성이 영...) 최대한 여러 군데에서 정보를 모아 정리했습니다.

B-Tree란?

자가 균형 트리로 AVL 트리, 레드-블랙 트리와 더불어 skewed tree를 해결할 수 있는 자료구조 중 하나다. 다만, AVL 트리나 레드-블랙 트리를 모든 데이터가 메모리에 적재할 수 있는 경우에 적용함과는 달리 B-Tree는 대용량 데이터를 다뤄야하는 DB나 디스크에 주로 적용된다.

대부분 트리에서의 연산이 높이에 따라 결정됨을 볼 때 B-Tree는 h를 줄이기 위해 B-Tree의 노드에 가능한 많은 값을 집어넣어 높이를 낮춤으로써 fat tree의 형태를 보인다.

일반적으로 B-Tree 노드의 크기는 디스크 블록의 크기에 따라 결정되며 높이를 낮춤으로써 디스크로의 접근지연 시간을 최소화한다.

B-Tree의 시간복잡도

Data Structure_B-Tree_001.png

B-Tree의 성질

  1. 노드 내 자료를 key, 자식은 child로 정의한다. 각 노드는 자신이 리프 노드인지 표시하기 위한 bool값을 갖는다.

  2. 모든 리프 노드는 같은 레벨에 있어야 한다.

  3. 노드 내 자료는 모두 오름차순 정렬된 상태다.

    • 노드 내 키 값 $k_1$과 $k_2$ 사이의 child의 모든 키는 $k_1$과 $k_2$ 사이에 속한다.
  4. 최소차수 $t$(Minimum Degree)에 대해 노드는 최소 $t - 1$개의 key를 가져야 하고 최대 $2t-1$개의 key를 가질 수 있다.

    • 삽입해야 하는 노드의 key 개수가 $2t-1$개라면 노드를 쪼개는 split 과정을 수행
  5. B-Tree의 계수 $M$(order)은 노드가 가질 수 있는 자식 노드의 최대 개수를 의미한다. 일반적으로 M차 B-Tree에서의 M이 계수를 지칭한다.

  6. 루트 노드가 리프 노드가 아니라면 루트는 적어도 2개의 자식 노드와 1개의 키를 갖는다.

  7. 노드가 k개의 key를 가지면 child의 수는 k + 1개이다.

여러 정리된 글에서 degree와 order의 관계에 대해 명시하지 않고 섞어서 서술하고 있다. 적어도 2 * minimum degree($t$​) - 1 = maximum degree = order - 1 정도로 인지하고 만약 order가 홀수라면 minimum degree $=t=\lfloor\cfrac{M}{2}\rfloor$이다.

B-Tree에서의 검색(Search)

BST와 비슷하나 key의 개수가 여러 개이고 그에 따라 적절히 child를 선정하는 로직을 추가해야 한다. 현재 노드에 값이 존재하는지 여부는 키들이 정렬되어 있으므로 이분탐색으로 빠르게 찾을 수 있다. 노드에 값이 없다면 찾고자 하는 값보다 큰 값 중 최솟값(lower bound)의 좌표대로 자식 노드를 순회한다. 만약 탐색 노드가 리프 노드에도 없다면 트리에 값이 존재하지 않음을 의미한다.

class BTreeNode:
    def __init__(self, keys, children, is_leaf):
        self.keys = keys
        self.children = children
        self.is_leaf = is_leaf

class BTree:
    def __init__(self, t):
        self.root = BTreeNode([], [], True)
        self.t = t

    def search(self, key):
        return self._search_key(key, self.root)

    def _search_key(self, key, node):
        # get binary search lower bound index
        # if current node has key, return
        # if key dosen't exist and cur node is leaf node, return False
        # if cur node isn't leaf node, call recursive func for child[lower_bound]
        i = bisect.bisect_left(node.keys, key)
        if i < len(node.keys) and key == node.keys[i]:
            return (node, i)
        elif node.is_leaf:
            return None
        else:
            return self._search_key(key, node.children[i])

B-Tree에서의 삽입(Insert)

  1. 루트 노드가 full상태인지 체크한다.
    • full이면 중앙값을 기준으로 새로운 루트 노드 생성한 뒤 양 옆을 새로운 루트 노드의 child로 생성(split)
  2. 루트 노드가 full이 아니라면 삽입과정을 수행한다.
    • 이진 탐색으로 lower bound 좌표 산출, linear 탐색으로도 가능
    • 만약 현재 노드가 리프 노드면 해당 좌표에 삽입
      • 리프 노드가 아니라면 현재 노드가 full 상태인지 체크하여 split 수행
    • 재귀 호출로 lower bound 좌표를 갖는 자식 노드에 대해 수행
    def insert(self, key):
        # if root node is full, make root node as child
        # split new root as child
        # new root would get value(median) from old root
        # call insert routine from root to leaf node recursively
        if len(self.root.keys) == 2 * self.t - 1:  # root is full
            self.root = BTreeNode([], [self.root], False)
            self._split_child(self.root, 0)
        self._insert_key(key, self.root)

    def _insert_key(self, key, node):
        # get lower bound idx -> keys[i] > key
        # if current node is leaf, then insert key at idx
        # else, check ith child is full. if is, do split ith child
        # after split ith child, add ith child's median value in cur node key
        # check value and change lower bound idx for key
        # call insert routine to ith child
        i = bisect.bisect_left(node.keys, key)
        if node.is_leaf:
            node.keys.insert(i, key)
        else:
            if len(node.children[i].keys) == 2 * self.t - 1:
                self._split_child(node, i)
                if key > node.keys[i]:
                    i = i + 1
            self._insert_key(key, node.children[i])

split 과정

중간값을 기준으로 주어진 좌표의 자식 노드를 양쪽으로 쪼개고 중간값을 위쪽 노드로 올리는 작업이다.

Data Structure_B-Tree_002.png

    def _split_child(self, node, i):
        # get ith child from node
        # split key and child half, except median value
        # get median value and insert in node
        # change ith child to left part(cause smaller then median)
        # insert left in i + 1 idx (case bigger than median)
        c = node.children[i]
        c_l = BTreeNode(c.keys[0:self.t - 1], c.children[0:self.t], c.is_leaf)
        c_r = BTreeNode(c.keys[self.t:2 * self.t - 1],
                        c.children[self.t:2 * self.t], c.is_leaf)
        median = c.keys[self.t - 1]
        node.keys.insert(i, median)
        node.children[i] = c_l
        node.children.insert(i + 1, c_r)

B-Tree에서의 삭제(Delete)

여느 탐색 트리와 마찬가지로 삭제 과정이 꽤 어렵다. 데이터를 삭제한 뒤 루트 노드를 제외한 모든 노드는 minimum degree만큼의 key를 가져야 하는 조건에 위배될 수 있기 때문에 reconstruct하는 과정이 필요하다. 으레 그렇듯이 이 과정이 난이도 상승을 초래한다.

주의점 : 작업 노드가 키 삭제 후에도 $\lfloor\cfrac{M}{2}\rfloor=t-1$만큼의 key를 가지게끔 보장해야 한다.

  1. 리프 노드에서 key를 삭제하는 경우

    a) 현재 key의 개수가 $t-1$보다 큰 경우 : 그대로 삭제 후 종료

    b) 현재 key의 개수가 $t-1$인 경우

    • 왼쪽이나 오른쪽 형제 중 한 쪽의 key 개수가 $t-1$보다 큰 경우

      • $t-1$보다 큰 형제에서 key를 받아야 한다. 허나 바로 받으면 부모와의 대소관계가 틀어지기 때문에 우선 형제로부터 부모로 key를 넘기고 해당 좌표의 부모 key를 받아와야 한다. 그러면 현재 노드는 $t-1$보다 커지기 때문에 key를 제거할 수 있다.

      Data Structure_B-Tree_003.png

    • 왼쪽, 오른쪽 모두 key의 개수가 $t - 1$인 경우

      • 부모 key, 형제 노드, 현재 노드를 하나의 모두로 병합한다. 현재 노드 + 현재 노드 + 부모 key = $(t - 1) \cdot 2 +1=2t-1$이 되어 full 상태인 노드가 생성된다. 이 노드에서 key를 제거하면 된다.

      Data Structure_B-Tree_004.png

  2. 내부 노드에서 key를 삭제하는 경우 - 노드나 자식 중 하나의 key 개수가 $t -1$보다 큰 경우

    데이터의 삭제는 리프 노드에서 작업하는 게 편리하므로 대체해 줄 데이터를 찾아야 한다. 일반적으로 inorder predecessor(key보다 작은 값 중 최댓값)이나 inorder successor(key보다 큰 값 중 최솟값)을 대체하게 된다. 이는 BST, 레드-블랙 트리 등에서의 삭제 key를 대체하는 방법과 유사하다. 포스팅에서는 successor를 사용한다.

    • inorder successor key를 찾아서 현재 노드의 삭제하고자 하는 key와 교체한다.
    • 이후 값을 교체한 지점(반드시 리프 노드임)으로 가서 1번 처리루틴으로 회귀한다.
  3. 내부 노드에서 key를 삭제하는 경우 - 노드나 자식 모두 key 개수가 $t -1$인경우

    a) 노드의 삭제할 key를 삭제하고 key 사이의 양쪽 노드를 하나로 병합한다.

    b) 삭제된 key의 부모 key를 형제 노드에 붙인 뒤 3-a에서 병합한 노드를 옮겨진 key의 자식 노드로 설정한다.

    • 부모 key가 더해진 인접한 형제 노드 key 개수가 $2t-1$보다 크다면 삽입 연산의 split 과정을 수행

    Data Structure_B-Tree_005.png

B-Tree에서의 삭제 구현

B-Tree 삭제 방법에 관해선 위 내용이 주를 이룬다. 다만, 구현하면서 다소 번잡한 점이 있기에 하향식 접근을 채택한 방법도 소개하고자 한다.

삭제 연산을 위해 노드에 접근하면서 다음 행선지 노드가 결정되면 해당 노드의 key 개수가 $t-1$개인지 체크 후 $t-1$​이라면 reconstruct를 수행, key 개수를 늘려준다.

삭제 연산을 위해 진행하는 모든 경로에 대해 빈곤한 상태(key의 개수가 $t-1$개)라면 재조정하면서 탐색하는 방식이다. 구현, 이해에는 굉장히 편리하나 보조연산의 횟수가 늘어나 성능에는 유리한 점이 없다. 삭제 연산 시 다음 행선지 노드의 key 개수가 $t-1$보다 크다는 점을 보장한다면 삭제연산이 보다 수월해질 것이다. 이 방법은 "Introduction To Algorithms" 책에 소개된 방법이라는 듯하다.

  1. root 노드의 key가 1개이고 양쪽 자식 노드의 key 개수가 $t - 1$개일 때

    • 높이가 줄어드는 작업이 필요하다.
    • 새로운 루트 노드를 선언, 키는 왼쪽 자식, 기존 root, 오른쪽 자식의 키를 넣고 자식 노드는 왼족, 오른쪽 자식의 자식노드를 넣는다.
    • 새로운 루트 노드의 자식 노드가 없다면 높이가 1이므로 leaf 태크를 True로 바꾼다.
  2. 1번을 통과하여 삭제 루틴 진행 시, 이진 탐색 lower bound 좌표 산출, 현재 노드에 존재하는지 체크

    a) 현재 노드에 존재하고 리프 노드인 경우

    • 앞서 $t-1$보다 크게끔 로직이 보장해주므로 그대로 pop

    b) 내부 노드일 경우 inorder successor 값을 찾아서 교체한다.

    • inorder successor 자리에 있는 값을 삭제해야 하므로 행선지는 1좌표를 더해야 한다.
    • 위치가 바뀌었으므로 다음 행선지를 i+1 좌표 자식 노드로 진행한다.
  3. 매칭되지 않은 경우 다음 행선지 진입에 앞서 다음 노드가 $t-1$개의 key를 갖는지 체크, $t-1$개라면 reconstruct 진행

    a) 왼쪽, 오른쪽 형제 노드 중 하나사 $t-1$개보다 key가 많은 경우

    • 부모의 key를 현재 노드에 삽입하고 $t-1$보다 큰 노드에서 key를 부모로 이동

    b) 양쪽 모두 $t-1$개인 경우

    • 부모 key와 형제 노드, 현재 노드를 하나로 병합
    • 부모는 $t-1$보다 크다는 것을 앞 단계에서 보장받음
    • 현재 노드 + 부모 키 + 현재 노드 = $2t-1$로 full상태가 됨
  4. 3번 과정 수행한 뒤 새롭게 lower bound 좌표 선정하여 진행

Data Structure_B-Tree_006.png

    def delete(self, key):
        # if root key cnt is 1 and all child's node key count also t - 1
        # B-Tree would shrink to h - 1
        # so, before delete operation, root needs to be preprocessed
        if len(self.root.keys) == 1:
            root = self.root
            t = self.t
            left, right = root.children[0], root.children[1]
            if len(left.keys) == t - 1 and len(right.keys) == t - 1:
                self.root = BTreeNode([], [], False)
                self.root.keys.extend(left.keys)
                self.root.keys.extend(root.keys)
                self.root.keys.extend(right.keys)
                self.root.children.extend(left.children)
                self.root.children.extend(right.children)
                if len(self.root.children) == 0:
                    self.root.is_leaf = True
        self._delete_key(key, self.root)

    def _delete_key(self, key, node):
        i = bisect.bisect_left(node.keys, key)
        # if key matched from currnet node
        if i < len(node.keys) and node.keys[i] == key:
            if node.is_leaf:
                node.keys.pop(i)
                return
            else:
                successor = self._find_successor(node.children[i + 1])
                node.keys[i], successor.keys[0] = successor.keys[0], node.keys[i]
                self._delete_key(key, node.children[i + 1])
        # if key didn't matched then check next node status
        else:
            if len(node.children[i].keys) == self.t - 1:
                self._delete_balancing(node.children[i], node, i)
            i = bisect.bisect_left(node.keys, key)
            self._delete_key(key, node.children[i])

    def _delete_balancing(self, node, parent, i):
        # if next destination index is last pos
        # next dst just has left siblings
        if i == len(parent.children) - 1:
            if len(parent.children[i - 1].keys) > self.t - 1:
                node.keys.insert(0, parent.keys.pop())
                parent.keys.append(parent.children[i - 1].keys.pop())
                if len(parent.children[i - 1].children) > 0:
                    node.children.insert(
                        0, parent.children[i - 1].children.pop())
            else:
                parent.children[i - 1].keys.append(parent.keys.pop())
                parent.children[i - 1].keys.extend(node.keys)
                parent.children.pop()
        # if next destination index is first pos
        # next dst just has right siblings
        elif i == 0:
            if len(parent.children[i + 1].keys) > self.t - 1:
                node.keys.append(parent.keys.pop(0))
                parent.keys.insert(0, parent.children[i + 1].keys.pop(0))
                if len(parent.children[i + 1].children) > 0:
                    node.children.append(
                        parent.children[i + 1].children.pop(0))
            else:
                parent.children[i + 1].keys.insert(0, parent.keys.pop(0))
                for k in node.keys:
                    parent.children[i + 1].keys.insert(0, k)
                parent.children.pop(0)
        else:
            if len(parent.children[i - 1].keys) > self.t - 1:
                node.keys.insert(0, parent.keys[i])
                parent.keys[i] = parent.children[i - 1].keys.pop()
                if len(parent.children[i - 1].children) > 0:
                    node.children.insert(
                        0, parent.children[i - 1].children.pop())
            elif len(parent.children[i + 1].keys) > self.t - 1:
                node.keys.append(parent.keys[i])
                parent.keys[i] = parent.children[i + 1].keys.pop(0)
                if len(parent.children[i + 1].children) > 0:
                    node.children.append(
                        parent.children[i + 1].children.pop(0))
            else:
                parent.children[i - 1].keys.append(parent.keys.pop(i - 1))
                parent.children[i - 1].keys.extend(node.keys)
                parent.children.pop(i)

    def _find_successor(self, node):
        if node.is_leaf:
            return node
        else:
            return self._find_successor(node.children[0])

전체 코드

import bisect


class BTreeNode:
    def __init__(self, keys, children, is_leaf):
        self.keys = keys
        self.children = children
        self.is_leaf = is_leaf


class BTree:
    def __init__(self, t):
        self.root = BTreeNode([], [], True)
        self.t = t

    def search(self, key):
        return self._search_key(key, self.root)

    def _search_key(self, key, node):
        # get binary search lower bound index
        # if current node has key, return
        # if current node doesn't have key and is leaf node, return False
        # if current node isn't leaf node, call recursive for child[lower_bound]
        i = bisect.bisect_left(node.keys, key)
        if i < len(node.keys) and key == node.keys[i]:
            return (node, i)
        elif node.is_leaf:
            return None
        else:
            return self._search_key(key, node.children[i])

    def insert(self, key):
        # if root node is full, make root node as child
        # split new root as child -> new root would get value(median) from old root
        # call insert routine from root to leaf node recursively
        if len(self.root.keys) == 2 * self.t - 1:  # root is full
            self.root = BTreeNode([], [self.root], False)
            self._split_child(self.root, 0)
        self._insert_key(key, self.root)

    def _insert_key(self, key, node):
        # get lower bound idx -> keys[i] > key
        # if current node is leaf, then insert key at idx
        # else, check ith child is full. if is, do split ith child
        # after split ith child, ith child's median value added in cur node key
        # check value and change lower bound idx for key
        # call insert routine to ith child
        i = bisect.bisect_left(node.keys, key)
        if node.is_leaf:
            node.keys.insert(i, key)
        else:
            if len(node.children[i].keys) == 2 * self.t - 1:
                self._split_child(node, i)
                if key > node.keys[i]:
                    i = i + 1
            self._insert_key(key, node.children[i])

    def _split_child(self, node, i):
        # get ith child from node
        # split key and child half, except median value
        # get median value and insert in node
        # change ith child to left part(cause smaller then median)
        # insert left in i + 1 idx (case bigger than median)
        c = node.children[i]
        c_l = BTreeNode(c.keys[0:self.t - 1], c.children[0:self.t], c.is_leaf)
        c_r = BTreeNode(c.keys[self.t:2 * self.t - 1],
                        c.children[self.t:2 * self.t], c.is_leaf)
        median = c.keys[self.t - 1]
        node.keys.insert(i, median)
        node.children[i] = c_l
        node.children.insert(i + 1, c_r)

    def delete(self, key):
        # if root key cnt is 1 and all child's node key count also t - 1
        # B-Tree would shrink to h - 1
        # so, before delete operation, root needs to be preprocessed
        if len(self.root.keys) == 1:
            root = self.root
            t = self.t
            left, right = root.children[0], root.children[1]
            if len(left.keys) == t - 1 and len(right.keys) == t - 1:
                self.root = BTreeNode([], [], False)
                self.root.keys.extend(left.keys)
                self.root.keys.extend(root.keys)
                self.root.keys.extend(right.keys)
                self.root.children.extend(left.children)
                self.root.children.extend(right.children)
                if len(self.root.children) == 0:
                    self.root.is_leaf = True
        self._delete_key(key, self.root)

    def _delete_key(self, key, node):
        i = bisect.bisect_left(node.keys, key)
        # if key matched from currnet node
        if i < len(node.keys) and node.keys[i] == key:
            if node.is_leaf:
                node.keys.pop(i)
                return
            else:
                successor = self._find_successor(node.children[i + 1])
                node.keys[i], successor.keys[0] = successor.keys[0], node.keys[i]
                self._delete_key(key, node.children[i + 1])
        # if key didn't matched then check next node status
        else:
            if len(node.children[i].keys) == self.t - 1:
                self._delete_balancing(node.children[i], node, i)
            i = bisect.bisect_left(node.keys, key)
            self._delete_key(key, node.children[i])

    def _delete_balancing(self, node, parent, i):
        # if next destination index is last pos
        # next dst just has left siblings
        if i == len(parent.children) - 1:
            if len(parent.children[i - 1].keys) > self.t - 1:
                node.keys.insert(0, parent.keys.pop())
                parent.keys.append(parent.children[i - 1].keys.pop())
                if len(parent.children[i - 1].children) > 0:
                    node.children.insert(
                        0, parent.children[i - 1].children.pop())
            else:
                parent.children[i - 1].keys.append(parent.keys.pop())
                parent.children[i - 1].keys.extend(node.keys)
                parent.children.pop()
        # if next destination index is first pos
        # next dst just has right siblings
        elif i == 0:
            if len(parent.children[i + 1].keys) > self.t - 1:
                node.keys.append(parent.keys.pop(0))
                parent.keys.insert(0, parent.children[i + 1].keys.pop(0))
                if len(parent.children[i + 1].children) > 0:
                    node.children.append(
                        parent.children[i + 1].children.pop(0))
            else:
                parent.children[i + 1].keys.insert(0, parent.keys.pop(0))
                for k in node.keys:
                    parent.children[i + 1].keys.insert(0, k)
                parent.children.pop(0)
        else:
            if len(parent.children[i - 1].keys) > self.t - 1:
                node.keys.insert(0, parent.keys[i])
                parent.keys[i] = parent.children[i - 1].keys.pop()
                if len(parent.children[i - 1].children) > 0:
                    node.children.insert(
                        0, parent.children[i - 1].children.pop())
            elif len(parent.children[i + 1].keys) > self.t - 1:
                node.keys.append(parent.keys[i])
                parent.keys[i] = parent.children[i + 1].keys.pop(0)
                if len(parent.children[i + 1].children) > 0:
                    node.children.append(
                        parent.children[i + 1].children.pop(0))
            else:
                parent.children[i - 1].keys.append(parent.keys.pop(i - 1))
                parent.children[i - 1].keys.extend(node.keys)
                parent.children.pop(i)

    def _find_successor(self, node):
        if node.is_leaf:
            return node
        else:
            return self._find_successor(node.children[0])

    # Print the tree
    def print_tree(self, node, l=0):
        print("Level ", l, " ", end=":")
        for i in node.keys:
            print(i, end=" ")
        print()
        l += 1
        if len(node.children) > 0:
            for i in node.children:
                self.print_tree(i, l)


def main():
    B = BTree(2)

    for i in [10, 20, 30, 40, 50, 60, 70, 80, 90]:
        B.insert(i)

    B.print_tree(B.root)

    if B.search(80) is not None:
        print("\n80 Found")
    else:
        print("\n80 Not found")

    for val in [50, 60]:
        print(f'delete val : {val}')
        B.delete(val)
        print(B.search(val))


if __name__ == '__main__':
    main()

Data Structure_B-Tree_007.png

+ Recent posts