S E P H ' S
[자료구조] 10. 힙 (Heap) 본문
Heap (Using Array)
'우선순위 큐'가 힙 자료구조를 이용하여 구현되기 때문에 힙을 다뤄보려고 한다.
힙은 '최솟값 또는 최댓값을 빠르게 찾아내기 위해 완전이진트리 형태로 만들어진 자료구조'다.
먼저 완전이진트리를 이해해야 한다.
기본적으로 트리의 구조를 먼저 이해해보자.
위의 이미지 같은 자료구조를 Tree 구조라고 한다. 트리에 대해 더 자세히 알아보자
- 부모노드(parent node) : 자기 자신(노드)과 연결된 노드 중 자신보다 높은 노드를 의미
- 자식노드(child node) : 자기 자신(노드)과 연결된 노드 중 자신보다 낮은 노드를 의미
- 루트노드(root node) : 뿌리 노드라고도 하며 루트 노드는 하나의 트리에선 하나만 존재하고 부모 노드가 없다. 최상위 노드이다.
- 단말노드(leaf node) : 리프 노드라고 불리며 자식 노드가 없는 노드를 의미한다.
- 내부노드(internal node) : 단말 노드가 아닌 노드
- 형제노드(sibling node) : 부모가 같은 노드를 말한다.
- 깊이(depth) : 특정 노드에 도달하기 위해 거쳐가야 하는 간선의 개수를 의미한다. (F의 깊이는 A - B - F로 깊이는 2이다.)
- 레벨(level) : 특정 깊이에 있는 노드들의 집합을 말하며, 구현에 따라 0 혹은 1부터 시작한다.
- 차수(degree) : 특정 노드가 하위(자식) 노드와 연결된 개수 (B의 차수는 3이다.)
이제 이 모든 노드의 최대 차수를 2로 제한한 것이 이진 트리(Binary Tree)이다.
그림 1은 B의 차수가 3이기 때문에 이진 트리가 아니다. 그림 2 처럼 모든 노드의 차수가 2를 넘지 않아야 이진 트리이다. 이제 여기서 '완전 이진 트리'란 무엇일까?
완전 이진 트리는 이진 트리에서 두 가지 조건을 더 만족해야 한다.
1. 마지막 레벨을 제외한 모든 노드가 채워져 있어야 한다.
2. 모든 노드들은 왼쪽 부터 채워져 있어야 한다.
그래서 그림 2의 트리도 완전 이진 트리는 아니다. 완전 이진 트리에서 마지막 레벨을 제외한 모든 노드는 두 개의 자식 노드를 갖는다는 조건을 추가하면 포화 이진 트리가 된다. 그림으로 살펴보자.
그렇다면 이 트리를 사용해서 어떻게 힙을 구현할까? 이걸 사용해서 최솟값, 최댓값을 어떻게 빠르게 찾아낼 수 있을까를 고민해야한다.
만약 리스트에 값을 넣었다가 빼려고 할때, 우선순위를 고려하여 빼려고 한다면 대개 정렬을 생각할 것이다. 쉽게 생각해서 숫자가 낮을 수록 우선순위가 노패다고 가정한다면 매 번 새 원소가 들어올때 마다 이미 리스트에 있던 값들과 비교를 하고 정렬을 진행해야 한다. 이렇게 하면 비효율적이기 때문에 더 효율이 좋도록 다음과 같은 조건을 붙인다.
부모 노드는 항상 자식보다 우선순위가 높다.
모든 요소들을 고려하여 우선순위를 정할 필요 없이 부모 노드는 자식노드보다 항상 우선순위가 앞선다는 조건만 만족시키면서 완전이진트리 형태로 채워나가는 것이다.
루트 노드는 항상 우선순위가 제일 높은 노드이다. 이러한 원리로 최댓값 혹은 최솟값을 빠르게 찾아낼 수 있다는 장점 (시간복잡도 : O(1))과 함께 삽입 삭제 연산시에도 부모노드가 자식노드보다 우선순위만 높으면 되므로 결국 트리의 깊이 만큼만 비교를 해가면된다. 그래서 O(logN)의 시간복잡도를 가지기 때문에 매우 빠르게 수행된다.
그리고 부모노드와 자식노드간의 관계만 신경쓰면 되므로 형제간 우선순위는 고려되지 않는다. 이러한 정렬 상태를 '반 정렬 상태', '느슨한 정렬 상태', '약한 힙' 이라고도 불린다. 우선순위가 높은 순서대로 뽑는 것이 포인트이다. 원소를 넣을 때도 우선순위가 높은 순서대로 나올 수 있도록 유지가 되어야 하고 뽑을 때도 우선순위가 높은 순서대로 나오기만 하면 된다.
Heap의 종류
힙은 우선순위가 높은 순서대로 나온다. 어떤 우선순위에 따라 다르겠지만 기본적으로 정수, 문자, 문자열 같은 경우 언어에서 지원하는 기본 정렬이 있다. 예를 들면 정수나 문자의 낮은 값이 높은 값보다 우선된다는 정렬말이다. 기본적으로 정렬되는 순서에 따라서 힙은 최소 힙과 최대 힙으로 나뉘게 된다.
최소 힙 : 부모 노드의 값(key 값) <= 자식 노드의 값(key 값)
최대 힙 : 부모 노드의 값(key 값) >= 자식 노드의 값(key 값)
기본적으로 오름차순을 생각하기 때문에 최소 힙을 구현할 것이다. 최대힙의 경우 비교연산자만 바꿔주면 된다.
트리는 표준적으로 배열로 구현된다. 연결리스트로도 구현이 가능하지만 특정 노드의 검색과 이동이 번거롭다. 배열의 경우에는 인덱스로 바로 접근할 수 있기 때문에 보다 더 효율적이다.
배열로 구현했을 때 특징을 살펴보자.
[특징]
1. 구현의 용이함을 위해 시작 인덱스(root)는 1부터 시작.
2. 각 노드와 대응되는 배열의 인덱스는 불변한다.
위 특징을 기준으로 각 인덱스별로 채우면 다음과 같은 성질이 나타난다.
1. 왼쪽 자식 노드 인덱스 = 부모 노드 인덱스 * 2
2. 오른쪽 자식 노드 인덱스 = 부모 노드 인덱스 * 2 + 1
3. 부모 노드 인덱스 = 자식 노드 인덱스 / 2
이 성질은 불변하다. index 3의 왼쪽 자식 노드를 찾고 싶다면 3 * 2, index 6이 index 3의 자식 노드이다.
index 5의 부모 노드를 찾고 싶다면 5 / 2, index 2가 index 5의 부모 노드이다.
이를 바탕으로 Heap을 구현할 것이다.
Heap 구현
최소 힙을 기준으로 구현한다. 최대 힙은 비교 연산만 반대로 하면 된다.
<필수 목록>
- 클래스 및 생성자, 필수 메소드 구성
- resize 메소드
- add 계열 메소드
- remove 메소드
- size, peek, isEmpty, toArray 메소드
Heap 클래스 및 생성자 구하기
public class Heap<E> {
private final Comparator<? super E> comparator;
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] array;
// 생성자 (초기 공간 할당 X)
public Heap() {
this(null);
}
public Heap(Comparator<? super E> comparator) {
this.array = new Object[DEFAULT_CAPACITY];
this.size = 0;
this.comparator = comparator;
}
// 생성자 (초기 공간 할당 O)
public Heap(int capacity) {
this(capacity, null);
}
public Heap(int capacity, Comparator<? super E> comparator) {
this.array = new Object[capacity];
this.size = 0;
this.comparator = comparator;
}
private int getParent(int index) {
return index / 2;
}
private int getLeftChild(int index) {
return index * 2;
}
private int getRightChild(int index) {
return index * 2 + 1;
}
}
멤버 변수 중 comparator 는 만약 객체를 정렬하거나 임의의 순서를 정렬하고 싶을 때 Comparator 를 파라미터로 받아 설정할수 있도록 해준다.
resize() 메소드
/**
* @param newCapacity 새로운 용량 크기
*/
private void resize(int newCapacity) {
Object[] newArray = new Object[newCapacity];
for (int i = 1; i <= size; i++) {
newArray[i] = array[i];
}
/**
* 현재 배열은 GC 처리를 위해 Null 로 처리한 뒤,
* 새 배열을 연결.
*/
this.array = null;
this.array = newArray;
}
add 메소드
Heap 삽입은 크게 두 가지로 나뉜다.
1. 사용자가 Comparator를 사용하여 정렬 방법을 Heap 생성단계에서 넘겨받은 경우 (comparator가 Null 이 아닌 경우)
2. 클래스 내에 정렬 방식을 Comparable로 구현했거나 기본 정렬 방식을 따르는 경우 (comparator가 Null 인 경우)
힙에서 원소가 추가되는 과정은 다음과 같다.
배열의 마지막 부분에 원소를 넣고 부모 노드를 찾아가면서 부모 노드가 삽입 노드보다 작을 때까지 요소를 교환해가면서 올라간다. 위와 같은 과정을 sift-up (상향 선별) 이라고도 한다. 즉 값을 추가할 때는 size + 1 위치에 새로운 값을 추가하고 상향 선별 과정을 거쳐 '재배치'해준다고 생각하면 된다. 이때 재배치 되는 노드를 타겟 노드라고 생각하면 된다.
public void add(E value) {
// 용량이 꽉찼으면 resize()
if (size + 1 == array.length) {
resize(array.length * 2);
}
siftUp(size + 1, value);
size++;
}
/**
*
* @param idx 추가할 노드의 인덱스
* @param target 재배치할 노드
*/
private void siftUp(int idx, E target) {
if (comparator != null) {
siftUpComparator(idx, target, comparator);
} else {
siftUpComparable(idx, target);
}
}
// Comparator 를 이용한 sift-up
@SuppressWarnings("unchecked")
private void siftUpComparator(int idx, E target, Comparator<? super E> comp) {
// root 노드 보다 클 때까지만 탐색한다.
while (idx > 1) {
int parent = getParent(idx); // 삽입 노드의 부모 노드 인덱스 구하기
Object parentVal = array[parent]; // 부모노드의 값
// 타겟 노드 값이 부모노드보다 크면 반복문 종료
if (comp.compare(target, (E) parentVal) >= 0) {
break;
}
/**
* 부모 노드가 타겟 노드보다 값이 크므로
* 현재 삽입될 위치에 부모 노드 값으로 교체해주고
* 타겟 노드의 위치를 부모 노드의 위치로 변경한다.
*/
array[idx] = parentVal;
idx = parent;
}
// 최종적으로 삽입될 위치에 타겟 노드 값을 저장한다.
array[idx] = target;
}
// 삽입할 객체의 Comparable 을 이용한 sift-up
@SuppressWarnings("unchecked")
private void siftUpComparable(int idx, E target) {
// 타겟 노드가 비교될 수 있도록 한 변수를 만든다.
Comparable<? super E> comp = (Comparable<? super E>) target;
while (idx > 1) {
int parent = getParent(idx);
Object parentVal = array[parent];
if (comp.compareTo((E) parentVal) >= 0) {
break;
}
array[idx] = parentVal;
idx = parent;
}
array[idx] = comp;
}
요소를 추가하기 전에 추가할 공간이 있는지 확인하고 꽉찼다면 resize 메소드로 용량을 확장시킨다.
그런 다음 siftUp 메소드를 호출한다.
Comparator의 유무에 따라 siftUp 메소드를 다른 것을 호출한다. Comparator가 있다면 compare 메소드를 활용하고 없다면 Comparable의 compareTo 메소드를 활용하여 비교한다.
remove 메소드
삭제는 add와 정반대로 구현하면 된다. root에 있는 노드를 삭제하고 마지막에 위치한 노드를 root 노드로 가져와서 add와는 반대로 자식 노드가 재배치하려는 노드보다 크거나 자식노드가 없을 때 까지 반복한다.
add 는 sift-up (상향 선별)이라면 remove는 아래로 내려가면서 재배치하기 때문에 sift-down (하향 선별)이라고 한다.
@SuppressWarnings("unchecked")
public E remove() {
if (array[1] == null) {
throw new NoSuchElementException();
}
E result = (E) array[1]; // 삭제된 요소를 반환하기 위한 임시 변수
E target = (E) array[size]; // 타겟이 될 요소
// 삭제할 노드의 인덱스와 이후 재배치할 타겟 노드를 넘겨준다.
siftDown(1, target); // 루트 노드가 삭제되므로 1을 넘겨준다.
return result;
}
/**
*
* @param idx 삭제할 노드의 인덱스
* @param target 재배치 할 노드
*/
private void siftDown(int idx, E target) {
if (comparator != null) {
siftDownComparator(idx, target, comparator);
} else {
siftDownComparable(idx, target);
}
}
@SuppressWarnings("unchecked")
private void siftDownComparator(int idx, E target, Comparator<? super E> comp) {
array[idx] = null;
size--;
int parent = idx;
int child;
// 왼쪽 자식 노드의 인덱스가 요소의 개수보다 작을 동안만 반복
while ((child = getLeftChild(parent)) <= size) {
int right = getRightChild(parent); // 오른쪽 자식 인덱스
Object childVal = array[child]; // 왼쪽 자식의 값 (교환될 값)
/*
* 오른쪽 자식 인덱스가 size 를 넘지 않으면서
* 왼쪽 자식이 오른쪽 자식보다 큰 경우
* 재배치할 노드는 작은 자식과 비교해야 하므로 child 와 childVal 을 오른쪽 자식으로 바꿔준다.
*/
if (right <= size && comp.compare( (E) childVal, (E) array[right]) > 0) {
child = right;
childVal = array[child];
}
// 재배치할 노드가 자식 노드보다 작을 경우 반복문 종료
if (comp.compare(target, (E) childVal) <= 0) {
break;
}
/**
* 현재 부모 인덱스에 자식 노드 값을 대체해주고
* 부모 인덱스를 자식 인덱스로 교체
*/
array[parent] = childVal;
parent = child;
}
array[parent] = target;
/**
* 용량이 최소 용량보다는 크면서 요소의 개수가 전체 용량의 1/4일 경우 resize()
*/
if (array.length > DEFAULT_CAPACITY && size < array.length / 4) {
resize(Math.max(DEFAULT_CAPACITY, array.length / 2));
}
}
@SuppressWarnings("unchecked")
private void siftDownComparable(int idx, E target) {
Comparable<? super E> comp = (Comparable<? super E>) target;
array[idx] = null;
size--;
int parent = idx;
int child;
while ((child = getLeftChild(parent)) <= size) {
int right = getRightChild(parent);
Object childVal = array[child];
if (right <= size && ((Comparable <? super E>) childVal).compareTo((E) array[right]) > 0) {
child = right;
childVal = array[child];
}
if (comp.compareTo((E) childVal) <= 0) {
break;
}
array[parent] = childVal;
parent = child;
}
array[parent] = comp;
if (array.length > DEFAULT_CAPACITY && size < array.length / 4) {
resize(Math.max(DEFAULT_CAPACITY, array.length / 2));
}
}
size, peek, isEmpty, toArray
public int size() {
return size;
}
@SuppressWarnings("unchecked")
public E peek() {
if (array[1] == null) {
throw new NoSuchElementException();
}
return (E) array[1];
}
public boolean isEmpty() {
return size == 0;
}
public Object[] toArray() {
return Arrays.copyOf(array, size + 1);
}
전체 코드
package Z_DataStructure.Heap;
import java.util.Arrays;
import java.util.Comparator;
import java.util.NoSuchElementException;
public class Heap<E> {
private final Comparator<? super E> comparator;
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] array;
// 생성자 (초기 공간 할당 X)
public Heap() {
this(null);
}
public Heap(Comparator<? super E> comparator) {
this.array = new Object[DEFAULT_CAPACITY];
this.size = 0;
this.comparator = comparator;
}
// 생성자 (초기 공간 할당 O)
public Heap(int capacity) {
this(capacity, null);
}
public Heap(int capacity, Comparator<? super E> comparator) {
this.array = new Object[capacity];
this.size = 0;
this.comparator = comparator;
}
private int getParent(int index) {
return index / 2;
}
private int getLeftChild(int index) {
return index * 2;
}
private int getRightChild(int index) {
return index * 2 + 1;
}
/**
* @param newCapacity 새로운 용량 크기
*/
private void resize(int newCapacity) {
Object[] newArray = new Object[newCapacity];
for (int i = 1; i <= size; i++) {
newArray[i] = array[i];
}
/**
* 현재 배열은 GC 처리를 위해 Null 로 처리한 뒤,
* 새 배열을 연결.
*/
this.array = null;
this.array = newArray;
}
public void add(E value) {
// 용량이 꽉찼으면 resize()
if (size + 1 == array.length) {
resize(array.length * 2);
}
siftUp(size + 1, value);
size++;
}
/**
*
* @param idx 추가할 노드의 인덱스
* @param target 재배치할 노드
*/
private void siftUp(int idx, E target) {
if (comparator != null) {
siftUpComparator(idx, target, comparator);
} else {
siftUpComparable(idx, target);
}
}
// Comparator 를 이용한 sift-up
@SuppressWarnings("unchecked")
private void siftUpComparator(int idx, E target, Comparator<? super E> comp) {
// root 노드 보다 클 때까지만 탐색한다.
while (idx > 1) {
int parent = getParent(idx); // 삽입 노드의 부모 노드 인덱스 구하기
Object parentVal = array[parent]; // 부모노드의 값
// 타겟 노드 값이 부모노드보다 크면 반복문 종료
if (comp.compare(target, (E) parentVal) >= 0) {
break;
}
/**
* 부모 노드가 타겟 노드보다 값이 크므로
* 현재 삽입될 위치에 부모 노드 값으로 교체해주고
* 타겟 노드의 위치를 부모 노드의 위치로 변경한다.
*/
array[idx] = parentVal;
idx = parent;
}
// 최종적으로 삽입될 위치에 타겟 노드 값을 저장한다.
array[idx] = target;
}
// 삽입할 객체의 Comparable 을 이용한 sift-up
@SuppressWarnings("unchecked")
private void siftUpComparable(int idx, E target) {
// 타겟 노드가 비교될 수 있도록 한 변수를 만든다.
Comparable<? super E> comp = (Comparable<? super E>) target;
while (idx > 1) {
int parent = getParent(idx);
Object parentVal = array[parent];
if (comp.compareTo((E) parentVal) >= 0) {
break;
}
array[idx] = parentVal;
idx = parent;
}
array[idx] = comp;
}
@SuppressWarnings("unchecked")
public E remove() {
if (array[1] == null) {
throw new NoSuchElementException();
}
E result = (E) array[1]; // 삭제된 요소를 반환하기 위한 임시 변수
E target = (E) array[size]; // 타겟이 될 요소
// 삭제할 노드의 인덱스와 이후 재배치할 타겟 노드를 넘겨준다.
siftDown(1, target); // 루트 노드가 삭제되므로 1을 넘겨준다.
return result;
}
/**
*
* @param idx 삭제할 노드의 인덱스
* @param target 재배치 할 노드
*/
private void siftDown(int idx, E target) {
if (comparator != null) {
siftDownComparator(idx, target, comparator);
} else {
siftDownComparable(idx, target);
}
}
@SuppressWarnings("unchecked")
private void siftDownComparator(int idx, E target, Comparator<? super E> comp) {
array[idx] = null;
size--;
int parent = idx;
int child;
// 왼쪽 자식 노드의 인덱스가 요소의 개수보다 작을 동안만 반복
while ((child = getLeftChild(parent)) <= size) {
int right = getRightChild(parent); // 오른쪽 자식 인덱스
Object childVal = array[child]; // 왼쪽 자식의 값 (교환될 값)
/*
* 오른쪽 자식 인덱스가 size 를 넘지 않으면서
* 왼쪽 자식이 오른쪽 자식보다 큰 경우
* 재배치할 노드는 작은 자식과 비교해야 하므로 child 와 childVal 을 오른쪽 자식으로 바꿔준다.
*/
if (right <= size && comp.compare( (E) childVal, (E) array[right]) > 0) {
child = right;
childVal = array[child];
}
// 재배치할 노드가 자식 노드보다 작을 경우 반복문 종료
if (comp.compare(target, (E) childVal) <= 0) {
break;
}
/**
* 현재 부모 인덱스에 자식 노드 값을 대체해주고
* 부모 인덱스를 자식 인덱스로 교체
*/
array[parent] = childVal;
parent = child;
}
array[parent] = target;
/**
* 용량이 최소 용량보다는 크면서 요소의 개수가 전체 용량의 1/4일 경우 resize()
*/
if (array.length > DEFAULT_CAPACITY && size < array.length / 4) {
resize(Math.max(DEFAULT_CAPACITY, array.length / 2));
}
}
@SuppressWarnings("unchecked")
private void siftDownComparable(int idx, E target) {
Comparable<? super E> comp = (Comparable<? super E>) target;
array[idx] = null;
size--;
int parent = idx;
int child;
while ((child = getLeftChild(parent)) <= size) {
int right = getRightChild(parent);
Object childVal = array[child];
if (right <= size && ((Comparable <? super E>) childVal).compareTo((E) array[right]) > 0) {
child = right;
childVal = array[child];
}
if (comp.compareTo((E) childVal) <= 0) {
break;
}
array[parent] = childVal;
parent = child;
}
array[parent] = comp;
if (array.length > DEFAULT_CAPACITY && size < array.length / 4) {
resize(Math.max(DEFAULT_CAPACITY, array.length / 2));
}
}
public int size() {
return size;
}
@SuppressWarnings("unchecked")
public E peek() {
if (array[1] == null) {
throw new NoSuchElementException();
}
return (E) array[1];
}
public boolean isEmpty() {
return size == 0;
}
public Object[] toArray() {
return Arrays.copyOf(array, size + 1);
}
}
정리 및 테스트
toArray, clone 등 여러 부가 메소드들은 우선순위 큐에서 다룰 것이다. 이미 위에 구현한 힙으로도 우선순위 큐로 활용이 가능하지만 트리, 힙 구조에 대해 집중적으로 먼저 살펴보았다. 다음은 테스트 코드들이다.
import java.util.Random;
public class HeapTest {
public static void main(String[] args) {
Heap<Integer> heap = new Heap<>();
Random rand = new Random();
for (int i = 0; i < 15; i++) {
heap.add(rand.nextInt(100));
}
System.out.print("내부 배열 상태 : ");
for (Object val: heap.toArray()) {
System.out.print(val + " ");
}
System.out.println();
System.out.print("힙 요소 뽑기 : \t");
while (!heap.isEmpty()) {
System.out.print(heap.remove() + " ");
}
}
}
배열을 얻을 때 index 1 부터 얻기 때문에 null이 처음에 나오는 것은 당연하다. 힙 요소 뽑에 낼때 오름차순으로 고르게 정렬되어 있는 것을 확인할 수 있다. 사용자 정의 클래스일 경우도 테스트해보자. 학생 클래스를 만들고 Comparable을 사용한것과 Comparator 둘다 사용해볼 것이다. Comparable은 이름 순으로 정렬하는데 이름이 같다면 나이 순(오름차순)으로 정렬을 해주고 Comparator는 나이 내림차순으로 정렬하는데 나이가 같다면 이름 순으로 정렬할 것이다.
package Z_DataStructure.Heap;
import java.util.Comparator;
public class HeapTest2 {
public static void main(String[] args) {
Heap<Student> heap1 = new Heap<>();
Heap<Student> heap2 = new Heap<>(comparator);
heap1.add(new Student("마이클", 40));
heap2.add(new Student("마이클", 40));
heap1.add(new Student("조던", 27));
heap2.add(new Student("조던", 27));
heap1.add(new Student("백종원", 48));
heap2.add(new Student("백종원", 48));
heap1.add(new Student("천종원", 28));
heap2.add(new Student("천종원", 28));
heap1.add(new Student("짱구", 5));
heap2.add(new Student("짱구", 5));
heap1.add(new Student("둘리", 200000000));
heap2.add(new Student("둘리", 200000000));
System.out.println("[Heap 1] : 이름순(같을 경우 나이 오름차순)");
while(!heap1.isEmpty()) {
System.out.println(heap1.remove());
}
System.out.println();
System.out.println("[Heap 2] : 나이 내림차순(같을 경우 이름순)");
while(!heap2.isEmpty()) {
System.out.println(heap2.remove());
}
System.out.println();
}
private static Comparator<Student> comparator = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 나이가 같다면 이름순
if (o1.age == o2.age) {
return o1.name.compareTo(o2.name);
}
return o2.age - o1.age;
}
};
static class Student implements Comparable<Student> {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Student o) {
if (this.name.compareTo(o.name) == 0) {
return this.age - o.age;
}
return this.name.compareTo(o.name);
}
public String toString() {
return "이름 : " + name + "\t나이 : " + age;
}
}
}
'Programing & Coding > Data Structure' 카테고리의 다른 글
[자료구조] 12. 우선순위 큐(Priority Queue) (0) | 2023.04.13 |
---|---|
[자료구조] 11. 연결리스트 덱(LinkedList Deque) (1) | 2023.04.13 |
[자료구조] 9. 배열 덱(Array Deque) (0) | 2023.04.10 |
[자료구조] 8. 연결리스트 큐(LinkedList Queue) 구현 (0) | 2023.04.01 |
[자료구조] 7. 배열 큐(Array Queue) 구현 (0) | 2023.03.29 |