4 분 소요

노드 삭제와 ‘합치기’ 연산

기존 이진 검색 트리에서의 노드 삭제와 별반 다를 게 없다.

두 서브트리를 합칠 때 어느 쪽이 루트가 되어야 하는지를 우선순위를 통해 판단하는 점만 제외하면 거의 똑같다.

기존 이진 검색 트리에서의 삭제를 구현하는 방법은 여러가지 있지만,

그중 합치기 연산으로 구현하는 것이 간편하다.

노드 t를 지울 거라면, t의 두 서브트리를 합친 새로운 트리를 만든 뒤

이 트리를 t를 루트로 하는 서브트리와 바꿔치기한다.

트립의 루트는 우선순위로 결정되기 때문에, A의 최대 우선순위와 B의 최대 우선순위를 비교해줘야 한다.

  • a(1)의 우선순위가 b(4)의 우선순위보다 더 크다면 a(1)의 오른쪽 자식으로 a(1)의 오른쪽 서브트리와 b(4)를 합쳐서 붙인다. a.setRight(merge(a.right, b));
  • 반대로 b의 우선순위가 더 크다면 b의 왼쪽 자식으로 a와 b의 왼쪽 자식을 합쳐서 붙인다. b.setLeft(merge(a,
  • b.left)); Untitled

구현

// root를 루트로 하는 트립에서 key를 지우고 결과 트립의 루트를 반환한다.
public Node erase(Node root, int key){
	if(root == null) return root;
	// root를 지우고 양 서브트리를 합친 뒤 반환한다.
	if(root.key == key){
		Node ret = merge(root.left, root.right);
		root = null;
		return ret;
	}

	if(root.key > key){
		root.setLeft(erase(root.left, key));
	} else{
		root.setRight(erase(root.right, key));
	}
	return root;
}

// a와 b가 두 개의 트립이고, max(a) < min(b)일 때 이 둘을 합친다.
private Node merge(Node a, Node b){
	if(a == null) return b;
	if(b == null) return a;

	if(a.priority < b.priority){
		b.setLeft(merge(a,  b.left));
		return b;
	}
	a.setRight(merge(a.right, b));
	return a;
}

K번째 원소 찾기

그와 같은 기능 중 하나가 주어진 서브트리의 노드들을 포함한 원소의 크기 순으로 나열했을 때

k번째로 오는 노드를 찾는 연산이다.

Node 클래스에서 서브트리의 크기 size를 계산해 저장해 두기 때문에 이것을 구현하기란 어렵지 않다.

root를 루트로 하는 서브트리는 왼쪽 자식 root → left를 루트로 하는 서브트리와

오른쪽 자식 root → right를 루트로 하는 서브트리 그리고 root 자신으로 구성되어 있다.

각 서브트리의 크기를 알고 있으면 k번째 노드가 어디에 속해 있을지 쉽게 알 수 있다.

왼쪽 서브트리의 크기를 l이라 할 때, 다음과 같이 세 가지의 경우로 나눌 수 있다.

  • k ≤ l : k번째 노드는 왼쪽 서브트리에 속해 있다.
  • k = l+1 : 루트가 k번째 노드이다.
  • k > l+1 : k번째 노드는 오른쪽 서브트리에서 k-l-1번째 노드가 된다.
// root를 루트로 하는 트리 중에서 k번째 원소를 반환한다.
public Node kth(Node root, int k) {
	// 왼쪽 서브트리의 크기를 우선 계산한다.
	int leftSize = 0;
	if (root.left != null) {
		leftSize = root.left.size;
	}
	if (k <= leftSize){
		return kth(root.left, k);
	}
	if (k == leftSize + 1)
		return root;
	}
	return kth(root.right, k - leftSize - 1);
}

Untitled (1) | root | k | kth(root, k) | k ≤ l | k == leftSize + 1 | kth(root→right, k - leftSize -1) | | — | — | — | — | — | — | | 5 | 4 | kth(5, 4) | 4 ≤ 4 True | | | | | | kth(2, 4) | 4 ≤ 1 False | | kth(4, 4-1-1) kth(4, 2) | | | | kth(4, 2) | 2 ≤ 1 False | 2 == 1 + 1 return 4 | |

**X보다 작은 원소 세기**

특정 범위 [a, b]가 주어질 때 이 범위 안에 들어가는 원소들의 숫자를 계산하는 연산이 있다.

이 연산은 주어진 원소 X보다 작은 원소의 수를 반환하는 countLessThan(x)로 가능하다.

  • [a, b] 범위 안에 들어가는 원소들의 수는 countLessThan(b) - countLessThan(a)로 표현

countLessThan(x)는 탐색 함수를 변형해 간단하게 만들 수 있다.

  1. 먼저 root의 원소가 x보다 큰지 비교한다.
  2. 만약 root의 원소가 x보다 같거나 크면 root와 오른쪽 서브트리에 있는 원소들은 모두 x이상이므로, 왼쪽 서브트리에 있는 원소들만 재귀적으로 세서 반환하면 된다.
  3. 만약 root의 원소가 x보다 작다면,
    1. 그 왼쪽 서브트리에 있는 원소들을 모두 x보다 작으므로 재귀적으로 세지 않아도 전부 답에 들어간다는 것을 알 수 있다.
    2. 오른쪽 서브트리에서 x보다 작은 원소를 재귀적으로 찾고 이것에 왼쪽 서브트리에 포함돈 노드 및 루트의 개수를 더해주면 된다.
// X보다 작은 키를 갖는 노드의 수를 반환
public int countLessThan(Node root, int x) {
	if (root == null) {
		return 0;
	}

	if (root.key >= x) {
		return countLessThan(root.left, x);
	}

	// x가 현재 노드보다 클 경우
	// 왼쪽 서브트리 크기 + 현재 노드 + 오른쪽에서 x보다 작은 노드 수
	int ls = root.left == null ? 0 : root.left.size;
	return ls + 1 + countLessThan(root.right, x);
}

우선순위 큐와 힙

  • 우선순위 큐는 일반 큐와는 달리 우선순위가 가장 높은 자료가 가장 먼저 꺼내지는 자료구조
  • 수행할 작업이 여러 개 있고 시간이 제한되어 있을 때 우선순위가 높은 것부터 수행하는 경우 사용

우선순위 큐를 구현하는 방법

  1. 연결리스트, 배열
    1. 료구조에 원소를 모두 집어넣고, 원소를 꺼낼 때 마다 모든 원소를 순회하며 우선순위가 높은 원소를 찾는 방식을 사용해야 하는데, 이러면 원소 추가 O(1), 검색 O(n)이 걸린다.
  2. 균형잡힌 이진 검색 트리
    1. 구현이 어렵다.
    2. 원소들을 우선순위로 정렬해 두면 최대 원소 삭제와 원소 삽입 모두 O(logN) 시간에 할 수 있다.
    1. 호율적인 방법은 바로 힙(Heap)으로 구현하는 것
    2. 이진 검색 트리보다 훨씬 간단한 구조로 구현할 수 있다
    3. 최적화된 형태의 이진 트리로, 힙을 사용하면 원소를 추가하는 연산과 가장 큰 원소를 꺼내는 연산 모두 O(logN) 시간에 할 수 있다.

Untitled (2)

힙 구현

힙의 모양 규칙에 의해, n개의 노드가 있을 때

이 노드들은 arr[0]에서 arr[n-1]까지 순차적으로 대응된다.

  • arr[i]의 왼쪽 자식은 arr[2*i + 1]에 대응된다.
  • arr[i]의 오른쪽 자식은 arr[2*i]에 대응된다.
  • arr[i]의 부모는 arr[(i-1)/2]에 대응된다. (나눗셈 결과는 내림한다.)

Untitled (3)