Collections 클래스

 

- Collections 클래스는 여러 유용한 알고리즘을 구현한 메소드들을 제공한다.

 

이 메소드들은 제네릭 기술을 사용하여 작성되었으며 정적 메소드의 형태로 되어있다.

 

자주 사용되는 알고리즘으로는 정렬(Sorting), 섞기(Shuffling), 탐색(Searching) 등이 있다

  • 언급한 세 메소드의 첫 번째 매개변수는 알고리즘이 적용되는 컬렉션이다.


 

 

 

정렬(Sorting)

 

데이터를 어떤 기준에 의하여 순서대로 나열하는 알고리즘이다.


퀵 정렬, 합병 정렬, 히프 정렬 등의 다양한 방법이 존재한다.


- Collections 클래스의 정렬은 속도가 비교적 빠르고 안전성이 보장되는 합병 정렬을 이용한다.


- 안전성이란 동일한 값을 가지는 원소를 다시 정렬하지 않는 것을 의미한다.


안전성은 같은 리스트를 반복하여 다른 기준에 따라 정렬할 때 중요하다.

 

 예를 들어 상품 주문 리스트를 날짜를 기준으로 먼저 정렬하고 이후 주문처를 기준으로 정렬한다면 사용자는 같은 주문처가 보낸 주문은 날짜별로 정렬될 것이라고 가정한다. 이처럼 한번 정렬된 것이 유지되는 경우를 안정성 있는 정렬이라고 말한다.

 

- Collection 클래스의 sort() 메소드는 정적메소드로, List 인터페이스를 구현하는 컬렉션에 대하여 정렬을 수행한다.

 

List<String> list = new LinkedList<String>();

list.add(“김철수”);

list.add(“김영희”);

Collections.sort(list);

 

 위의 경우는 원소가 String 타입이므로 알파벳 순서대로 정렬될 것이다. 원소가 Date 타입이라면 시간적인 순서로 정렬될 것이다. 그 이유는 String 클래스와 Date 클래스 모두 Comparable 인터페이스를 구현하기 때문이다.

 

- 정렬은 Comparable 인터페이스를 이용하여 이루어진다. List 인터페이스는 Comparable 인터페이스를 상속하므로

 

public interface Comparable<T> {

public int comparaTo(T o);

}

  • comparaTo() 메소드는 매개변수 객체를 현재의 객체와 비교하여 작으면 음수, 같으면 0, 크면 양수를 반환한다.


- 예제 1

import java.util.*;

public class SortTest1 {
    public static void main(String[] args) {
        String[] example = { "Nice", "to", "meet", "you" };
        List<String> list = Arrays.asList(example); //배열을 리스트로 무조건 변환!
        Collections.sort(list); //리스트 정렬

        System.out.println(list);
    }
}

[예제 1 출력 결과]

meet과 to의 위치가 바뀐 것을 볼 수 있다. 알파벳순이기 때문에 이러한 결과가 나온 것이다.



- 예제 2

import java.util.*;

class Student implements Comparable<Student> {
    int number;
    String name;

    Student(int number, String name) {
        this.number = number;
        this.name = name;
    }

    public String toString() {
        return name;
    }

    @Override
    public int compareTo(Student s) {
        return number - s.number; //학번순서로 정렬한다.
    }
}

public class SortTest2 {
    public static void main(String[] args) {
        Student[] students = {
            new Student(02, "선옥자"),
            new Student(03, "한영희"),
            new Student(01, "김철수")
        };

        List<Student> list = Arrays.asList(students);
        Collections.sort(list);
        System.out.println(list);
    }
}

[예제 2 출력 결과]



- 예제 3 : 역순으로 정렬하기 - Collections.sort(list, Collections.reverseOrder());

import java.util.*;

public class SortTest {
    public static void main(String[] args) {
        String[] example = { "Nice", "to", "meet", "you" };
        List<String> list = Arrays.asList(example); //배열을 리스트로 무조건 변환!
        Collections.sort(list, Collections.reverseOrder()); //리스트 역순정렬

        System.out.println(list);
    }
}

 [예제 3 출력 결과]

 역순정렬로 인해 예제 1의 출력 결과와 반대되는 결과를 볼 수 있다.





섞기(Shuffling)

 

리스트에 존재하는 정렬을 파괴하여서 원소들의 순서를 랜덤하게 만든다.

 

정렬과는 반대 동작을 한다.

 

주로 게임을 구현할 때 유용하게 쓰인다. 예를 들어 카드 게임에서 카드를 섞을 때 사용할 수 있다.


- 예제

import java.util.*;

public class Shuffle {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<Integer>();

        for(int i = 0; i <= 10; i++) {
            list.add(i);
        }

        Collections.shuffle(list);
        System.out.println(list);
    }
}

 [출력 결과 1]


[출력 결과 2]

메소드를 실행할 때마다 서로 다른 출력 결과를 보인다.

 

 


탐색(Searching)

 

리스트 안에서 원하는 원소를 찾는 알고리즘이다.

 

선형 탐색 : 처음부터 모든 원소를 방문하는 탐색 방법으로, 리스트가 정렬되어 있지 않은 경우 사용한다.

 

이진 탐색 : 리스트가 정렬되어 있는 경우 중간에 있는 원소(m)와 먼저 비교하여, 크면 그다음부(m+1)터 끝까지 비교하고, 작으면 처음부터 그 전(m-1)까지의 원소들과 비교하는 방식을 반복하여 하나의 리스트를 계속해서 두 개의 리스트로 분할한다. 이 방법은 리스트에 하나의 원소가 남을 때까지 반복된다. 이진 탐색은 문제의 크기를 반으로 줄일 수 있기 때문에 선형 탐색보다 효율적이다.

 

- Collections 클래스는 정렬된 리스트에서 지정된 원소를 이진 탐색한다. 


- 이진 탐색 메소드인 binarySearch()는 만약 반환값이 양수이면 찾고자 하는 원소의 인덱스값이고, 음수이면 탐색이 실패한 것이다. , 원소를 찾지 못했음을 의미한다.

 

int index = Collections.binarySearch(list, element);

// list는 리스트, element는 탐색할 원소이다.

 

 단, binarySearch() 메소드는 실패하더라도 도움이 되는 정보를 반환한다. , 반환값에는 현재의 데이터가 삽입될 수 있는 위치 정보가 있다. 반환값이 pos라고 한다면, (-pos-1)이 해당 데이터를 삽입할 수 있는 위치이다.

 

int pos = Collections.binarySearch(list, key);

if(pos < 0) list.add(-pos-1);

 

- binarySearch() 메소드는 위에도 언급했듯이 정렬된 리스트에 한해서 탐색을 진행한다는 점에 주의하여야 한다.

 

 



 Map 인터페이스

 

많은 데이터 중에서 원하는 데이터를 빠르게 찾을 수 있는 자료 구조이다.

 

- 일반적으로 사전에 비유를 하는데, 사전은 단어가 있으면 단어에 대한 설명이 있다. Map에서는 그 단어(데이터) (Key)“라고 부르고 단어(데이터)에 대한 설명을 (Value)“이라고 부른다.

 

중복된 키를 가질 수 없다각 키는 오직 하나의 값에만 매핑될 수 있다.

 

키가 제시되면 Map은 값을 반환한다.


 예를 들어 학생에 대한 정보를 Map에 저장할 때 키를 학번으로 두고, 값을 학생의 이름으로 할 때, 학번이 제시되면 Map은 학생의 이름을 반환한다.


소드

 void clear()

 Map의 모든 항목들을 삭제한다.

 boolean containsKey(Object key)

 Map이 해당 키에 매핑되는 값을 가지고 있다면 true, 없다면 false를 반환한다.

 boolean containsValue(Object value)

 Map이 해당 값에 대한 적어도 하나의 키를 가지고 있다면 true, 없다면 false를 반환한다.

 Set<Map.Entry<K,V>> entrySet()

 Map에 대응되는 집합(Set)을 반환한다.

 boolean equals(Object o)

 Map에 있는 객체들과 매개변수로 넘겨받은 객체와 동등한지 비교한다.

 V get(Object key)

 해당 Key에 맞는 값을 반환한다없으면 null을 반환한다.

 int hashCode()

 해쉬코드를 반환한다.

 boolean isEmpty()

 Map이 어떠한 키와 값도 가지지 않으면 true, 아니면 false를 반환한다.

 Set<K> keySet()

 Map의 Key에만 대응되는 집합(Set)을 반환한다.

 V put(K key, V value) Map에 키, 값을 추가한다.
 void putAll(Map<? extends K,? extends V> m) 매개변수로 넘겨받은 Map을 전체 복사하여 추가한다.
 V remove(Object key) 매개변수로 넘겨받은 Key Map에 존재한다면 삭제한 후 반환한다.
 int size() Map의 키-값 쌍의 개수를 반환한다.
 Collection<V> values() Map의 값에만 대응되는 Collection을 반환한다.



- Map 인터페이스를 구현한 클래스로는 HashMap, TreeMap, LinkedHashMap 등이 있다.

  • HashMap : 해싱 테이블에 데이터를 저장한다. 단, 저장한 순서와는 다르게 Key 값의 순서는 임의적이다.

  • TreeMap : 탐색 트리에 데이터를 저장한다.

  • LinkedHashMap : HashMap의 단점인 임의적인 순서를 보완하여 저장한 순서에 맞게 Key 값을 저장한다.


키들을 정렬된 순서로 방문할 필요가 없다면 HashMap TreeMap보다 빠르다.


- 예제

import java.util.*;

class Student {
    int number;
    String name;

    public Student (int number, String name) {
        this.number = number;
        this.name = name;
    }

    public String toString() {
        return name;
    }
}

public class MapTest {
    public static void main(String[] args) {
        Map<String, Student> stMap = new HashMap<String, Student>();
        stMap.put("01", new Student(01, "김철수"));
        stMap.put("02", new Student(02, "김영희"));
        stMap.put("03", new Student(03, "김자몽"));

        //모든 항목을 출력
        System.out.println("모든 항목을 출력 : " + stMap);

        //하나의 항목을 삭제
        stMap.remove("01"); //김철수 학생의 정보가 삭제된다.

        //하나의 항목을 대치
        stMap.put("03", new Student(03, "김옥자"));

        //값을 참조
        System.out.println("03번 학생의 이름 : " + stMap.get("03"));

        //모든 항목 방문
        for(Map.Entry<String, Student> s : stMap.entrySet()) {
            String key = s.getKey();
            Student value = s.getValue();
            System.out.println("key : " + key + ", value : " + value);
        }
    }
}

[출력 결과]



- Map에 저장된 데이터를 방문할 때는 Map.Entry 라는 인터페이스를 사용한다.

import java.util.*;

public class MapTest {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();

        String[] example = { "Hi", "yes", "or", "not", "Hi", "yes", "no", "a", "and" };

        //문자열에 포함된 빈도 계산하기
        for(String str : example) {
            Integer freq = map.get(str);
            map.put(str, (freq == null) ? 1 : freq+1);
        }

        System.out.println(map.size() + "개의 단어가 있음"); //결과 : 7개의 단어가 있음
        System.out.println("containKey(\"to\") = " + map.containsKey("to"));
        System.out.println("isEmplty() = " + map.isEmpty());
        System.out.println("Map : " + map);
    }
}

[출력 결과]





 

 Queue 인터페이스

 

(Queue) : 데이터를 처리하기 전에 잠시 저장하고 있는 자료 구조이다.

 

- 전형적인 큐는 원소들을 FIFO(First-In-Fist-Out) 형식으로 저장하는 FIFO 큐인데, 후단(tail)에서 원소를 추가하고, 전단(head)에서 원소를 삭제한다새로운 원소들은 큐의 끝에 추가된다.


큐에서는 중간에 원소를 추가하는 것은 허용되지 않는다.

 

디큐는 자바 버전 1.6부터 Deque 인터페이스로 추가되었는데, 전단과 후단에서 모두 원소를 추가하거나 삭제할 수 있다.

 

- Deque 인터페이스는 ArrayDeque LinkedList 클래스들로 구현된다. 

 

- Queue 인터페이스는 기본적인 Collection의 연산 외에 다음과 같은 삽입삭제검색연산을 추가로 제공한다.

 

public interface Queue<E> extends Collection<E> {

E element(); //큐의 처음에 있는 원소를 삭제하지 않고 가져온다큐가 비어있으면 예외발생

E peak(); //큐의 처음에 있는 원소를 삭제하지 않고 가져온다큐가 비어있으면 null을 반환


boolean offer(E e); //원소를 추가할 때 큐의 용량을 넘어서면 false를 반환한다.


E poll(); //큐의 처음에 있는 원소를 가져온다큐에 원소가 없으면 null을 반환한다.

E remove(); //큐의 처음에 있는 원소를 제거한다큐에 원소가 없으면 예외가 발생한다.

}



아래의 예제는 큐가 카운트 다운 타이머를 구현하는 예제이다.

import java.util.*;

public class QueueTest {
    public static void main(String[] args) throws InterruptedException {
        int time = 10;

        // LinkedList 안에 Queue 인터페이스가 구현되어 있다.
        Queue<Integer> queue = new LinkedList<Integer>();

        for(int i = time; i > 0; i--) {
            queue.add(i);
        }

        System.out.println("start time : " + System.currentTimeMillis() / 1000);
        while(!queue.isEmpty()) {
            System.out.println(queue.remove()+ " ");
            Thread.sleep(1000); //현재의 스레드를 1초간 재운다.
        }
        System.out.println("end time : " + System.currentTimeMillis() / 1000);
    }
}

[출력 결과]

start time - end time = 10



- FIFO 큐가 전형적이지만 예외적인 큐도 존재한다. 원소들을 우선순위에 따라 저장하고 기본적인 우선순위는 원소들의 값으로 결정되는 우선순위 큐(Priority Queues)가 있다.




▶ 우선순위 큐(Priority Queue)


원소들이 무작위로 삽입되었더라도 정렬된 상태로 원소들을 추출한다.


- remove()를 호출할 때마다 가장 작은 원소가 추출된다.


- 우선순위 큐는 히프(heap)라고 하는 자료 구조를 내부적으로 사용하여 큐를 정렬한다.

  • 히프는 이진 트리의 일종으로서 add() remove()를 호출하면 가장 작은 원소가 효율적으로 트리의 루트로 이동하게 된다.

  • 오름차순으로 트리가 설정된다.


- 우선순위 큐의 가장 대표적인 예는 작업 스케쥴링(job scheduling)이다각 작업은 우선순위를 가지고 있고 가장 높은 우선순위의 작업이 큐에서 먼저 추출되어 시작된다.


- 아래는 우선순위 큐에 대한 예제이다.

import java.util.*;
 
public class PriorityQueueTest {
	public static void main(String[] args) {
		PriorityQueue<Integer> pq = new PriorityQueue<Integer>();
		 
		pq.add(30);
		pq.add(80);
		pq.add(20);
		 
		for(Integer o : pq) {
			System.out.println(o); // 20, 80, 30 순으로 출력됨
		}

		System.out.println("원소를 삭제합니다.");
		
		while(!pq.isEmpty()) {
			System.out.println(pq.remove()); //20, 30 80 순으로 정렬된 상태로 출력됨.
		}
	}
}

[출력 결과]








 Set 인터페이스

 

- 순서에 상관없이 원소만 저장하고 싶은 경우 집합(Set)을 사용한다.

 

동일한 원소를 가질 수 없다.

 

해쉬 테이블(Hash Table)

  • 집합을 구현하는 가장 잘 알려진 방법이다.

  • 각각의 원소에 대해 해쉬 코드란 정수를 계산하며해쉬 코드는 대개 객체의 인스턴스 필드로부터 계산된다.

  • 각 클래스마다 해쉬 코드를 계산하는 hashCode()를 가지고 있다.

  • 해쉬 테이블은 연결 리스트의 배열로 구현되며각 리스트는 버킷(bucket)이라고 불린다.

  • 순서는 임의적이지만 검색 속도가 빠르다.

  • 테이블에서 원하는 객체를 찾기 위해 먼저 객체의 해쉬 코드를 계산하고 테이블의 크기에 맞추어 나머지 연산을 수행한 후에 결과로 나오는 숫자를 테이블의 인덱스로 사용하면 된다.

  • 쉽게 말해원하는 객체의 해쉬 코드를 테이블의 크기로 나누어서 나온 나머지가 버킷을 대표하는 번호가 된다이 때 각 리스트들은 순서가 임의적이다.


- Set 인터페이스를 구현하는 클래스는 HashSet, TreeSet, LinkedHashSet 등이 있다. 주로 사용되는 3가지에 대해 알아보자.





 HashSet


- HashSet 해쉬 테이블에 원소를 저장하기 때문에 성능면에서 가장 우수하다원소들의 순서가 일정하지 않다.

 

HashSet<String> hashSet = new HashSet<String>();

 

hashSet.add(“Milk”);

hashSet.add(“Bread”);

hashSet.add(“Butter”);

hashSet.add(“Cheese”);

hashSet.add(“Ham”);

hashSet.add(“Ham”);

 

System.out.println(hashSet);

 

출력 결과 : [Bread, Milk, Butter, Ham, Cheese] -> 순서가 임의적이다.





 TreeSet


- TreeSet 레드-블랙 트리(red-black tree)에 원소를 저장하므로 값에 따라서 순서가 결정되며 속도가 HashSet보다 느리다.

 

TreeSet<String> treeSet = new TreeSet<String>();

 

treeSet.add(“Milk”);

treeSet.add(“Bread”);

treeSet.add(“Butter”);

treeSet.add(“Cheese”);

treeSet.add(“Ham”);

treeSet.add(“Ham”);

 

System.out.println(treeSet);

 

출력 결과 : [Bread, Butter, Cheese, Ham, Milk] -> 알파벳 순으로 정렬됨

 




 LinkedHashSet


- LinkedHashSet 해쉬 테이블과 연결 리스트의 결합된 형태로 원소들의 순서는 삽입되었던 순서와 같다야간의 비용을 들여서 HashSet의 문제점인 순서의 불명확성을 제거한 방법이다.

 

LinkedHashSet<String> linked = new LinkedHashSet<String>();


linked.add(“Milk”);

linked.add(“Bread”);

linked.add(“Butter”);

linked.add(“Cheese”);

linked.add(“Ham”);

linked.add(“Ham”);

 

System.out.println(linked);

 

출력 결과 : [Milk, Bread, Butter, Cheese, Ham] -> 입력된 순서대로 출력됨.



대량 연산 메소드

 set1.containsAll(set2)

 만약 set2 set1의 부분집합이면 true를 반환한다.

 set1.addAll(set2)

 set1 set1 set2의 합집합으로 만든다.

 set1.retainAll(set2)

 set1 set1 set2의 교집합으로 만든다.

 set1.removeAll(set2) 

 set1 set1 set2의 차집합으로 만든다. (set1  set2)



집합 연산 시 주의할 점은 원집합이 파괴되면 안되므로 항상 연산 수행 전에 복사본을 만들어야 한다.

 

Set<String> union = new HashSet<String>(set1); //합집합을 위해 set1의 복사본을 union에 저장한다.

union.addAll(set2);





+ Recent posts