알고리즘 문제를 푸는 중 문득 Queue는 왜 ArrayList가 아닌 LinkedList로 생성을 할까 궁금해졌다. 'Queue는 FIFO이기 때문에 ArrayList 처럼 인덱스를 통한 접근이 필요없어서가 아닐까?'라고 생각은 해보았다. 그러나 이 index 존재 여부로 ArrayList와 LinkedList가 성능 면에서 정확히 차이를 보이는지 궁금해졌다.
ArrayList
LinkedList
index가 존재하는 ArrayList는 조회 시 평균적으로 좋은 성능을 보이지만, 삽입/삭제 시 인덱스 재배치를 통해 공간을 조절하기 때문에 비효율적인 성능을 보인다.
순차적으로 삽입/삭제하거나 조회하는 경우에는 ArrayList가 빠르지만, 중간 데이터(비 순차적)를 삽입/삭제하는 경우에는 LinkedList가 더 빠르다.
즉, FIFO(First In First Out)나 FILO(First In Last Out)처럼 삽입/삭제가 일정하게 앞이나 뒤에서 일어나는 경우를 제외하고는
삽입/삭제가 잦은 경우LinkedList
를, 조회가 잦은 경우ArrayList
를 이용하면 조금 더 효율적인 퍼포먼스를 낼 수 있을 것이다.
method | ArrayList | LinkedList | |
---|---|---|---|
add at last index | add(value) (LinkedList -> add(value),addLast()) | O(1) | O(1) |
add at given index | add(index,value) | O(N) | O(1) |
remove by index | remove(index) (LinkedList->remove(index),removeFirst()) | O(N) | O(1) |
remove by value | remove(value) | O(N) | O(1) |
get by index | get(index) | O(1) | O(N) |
search by value | contain(value), indexOf(value) | O(N) | O(N) |
참고한 블로그에 삽입/삭제 성능 테스트가 있었는데 흥미로웠다. 성능 차이가 얼마나 나는지 눈으로 정확한 수치를 확인하니 그랬던 것 같다.
그래서 조회 성능도 테스트해보았다.
랜덤 단어를 생성하여 각 List(LinkedList, ArrayList)에 추가한 뒤, get()
과 indexOf()
의 실행시간을 출력하여 비교해봤다.
public class ListPerformanceTest {
public static void main(String[] args){
final int testDataSize = 5000;
final int leftLimit = 97; // letter 'a'
final int rightLimit = 122; // letter 'z'
final int targetStringLength = 5;
Random random = new Random();
ArrayList arList = new ArrayList<>();
LinkedList lkList = new LinkedList();
for(int i=0; i<testDataSize; i++){
String ran = random.ints(leftLimit,rightLimit + 1)
.filter(k -> (k <= 57 || k >= 65) && (k <= 90 || k >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
arList.add(ran);
lkList.add(ran);
}
System.out.println("=====search by value======");
System.out.println("ArrayList = " + test_search_by_value(random,testDataSize, leftLimit, rightLimit, targetStringLength, arList));
System.out.println("LinkedList = " + test_search_by_value(random,testDataSize, leftLimit, rightLimit, targetStringLength, lkList));
System.out.println("\n=====get by index======");
System.out.println("ArrayList = " + test_search_by_index(random,testDataSize, leftLimit, rightLimit, targetStringLength, arList));
System.out.println("LinkedList = " + test_search_by_index(random,testDataSize, leftLimit, rightLimit, targetStringLength, lkList));
}
private static long test_search_by_value(Random random, int testDataSize, int leftLimit, int rightLimit, int targetStringLength, List list){
long start = System.currentTimeMillis();
for(int i = 0; i < testDataSize; i++) {
String ran = random.ints(leftLimit,rightLimit + 1)
.filter(k -> (k <= 57 || k >= 65) && (k <= 90 || k >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
list.indexOf(ran);
}
long end = System.currentTimeMillis();
return end - start;
}
private static long test_search_by_index(Random random, int testDataSize, int leftLimit, int rightLimit, int targetStringLength, List list){
long start = System.currentTimeMillis();
for(int i = 0; i < testDataSize; i++) {
int ran = random.nextInt(testDataSize);
list.get(ran);
}
long end = System.currentTimeMillis();
return end - start;
}
}
Test Data Size | Try | Result |
---|---|---|
5000 | 1st | ![]() |
5000 | 2nd | ![]() |
5000 | 3rd | ![]() |
500 | 1st | ![]() |
500 | 2nd | ![]() |
500 | 3rd | ![]() |
Queue
는 Front Pointer를 통해 항상 첫 번째 데이터만 삭제하고, Rear Pointer를 통해 가장 뒤에 데이터를 저장하므로 삽입/삭제가 O(1)인LinkedList
를 사용한다.
즉 인덱스 번호로 탐색할 때 효율적인 성능을 보이는
ArrayList
기능이 딱히 필요없었던Queue
는 탐색 메서드로는 첫 번째 요소만 가져오는 peek() 메서드만 지원하고 있다.