Java로 구현하는 자료구조
2025. 3. 6. 11:22ㆍBackend/자료구조&알고리즘
0. 자료구조란?
0-1. 자료구조의 정의
- 자료구조란 데이터를 효율적으로 저장하고 관리하기 위한 방법과 규칙을 의미한다.
- 일련의 자료들을 조직하고 구조화하는 것
- 프로그램에서 사용하기 위한 자료를 기억장치의 공간 내에 저장하는 방법
- 저장된 그룹 내에 존재하는 자료 간의 관계, 처리 방법등을 연구 분석하는 것
- 효율적인 프로그램을 작성할 때 가장 우선적인 고려사항은 저장 공간의 효율성과 실행시간의 신속성이다.
0-2. 자료구조의 분류

- 원시형/비원시형
- 원시형: 기본형, 내장형 자료구조
- 비원시형: 원시형을 묶어서 만든 자료구조
- 선형/비선형 구조
선형구조 | 비선형구조 | |
데이터 저장 구조 | 순차적 | 계층적, 비순차적 |
데이터간 관계 | 명확, 데이터가 메모리 상에 연속적으로 저장되거나 포인터로 연결 | 복잡, 특정키를 통해 데이터를 검색하거나 계층 구조를 표현할 때 사용 |
예시 | 배열, 리스트, 스택, 큐, 데크 | 트리, 그래프 |
1. 배열 (Array)
1-1. 개념

- 같은 타입을 여러 변수를 하나의 묶음으로 다루는 것
- 동일한 자료형의 데이터들이 같은 크기로 나열되어 순서를 갖고 있는 집합
- 메모리에서 물리적으로 연달아서 저장된다
- 사용한 첨자의 개수에 따라 n차원 배열이라 부른다.
- 장점
- 접근 속도가 빠름. 반복적인 처리 작업에 적합.
- 첨자(index)를 이용하여 데이터에 바로 접근하므로 요소마다 동일한 이름의 변수를 사용하여 처리가 간편하다.
- 단점
- 정적인 자료구조로 기억장소를 고정하므로 생성 이후 추가가 어렵다 (가변성이 없음)
- 데이터 삭제 시 데이터가 저장되어 있던 기억장소는 빈 공간으로 남아있어 메모리의 낭비가 발생한다.
1-2. 자바에서 사용
int[] arr; //선언 (배열을 다루기 위한 참조변수 선언)
int[] arr = new int[5]; //선언+생성 (실제 저장공간을 생성)
int[] arr2 = new int[5] {1,2,3,4,5}; //선언+생성+초기화 (배열 생성할 때만 가능)
int[] arr3 = {1,2,3,4,5}; //생략가능
int[0] = 10; //값 넣기
int a = int[0]; //값 가져오기
//향상된 for문
for(int i : arr) {..}
2. 리스트 (List)
2-1. 개념

- 순서가 있는 데이터의 집합으로, 인덱스나 포인터로 접근할 수 있는 자료구조
- 자료들을 반드시 연속적으로 배열시키지는 않고 임의의 기억공간에 기억시키되 자료 항목의 순서에 따라 노드의 포인터 부분을 이용하여 서로 연결 시킴
- 장점
- 연결리스트는 노드의 삽입과 삭제 작업이 용이하다
- 메모리를 효율적으로 사용 가능
- 단점
- 연결을 위한 포인터를 찾는 시간이 필요하기 때문에 접근 속도가 느리다.
- 연결을 위한 링크(포인터)부분이 필요하기 때문에 순차리스트에 비해 기억공간을 많이 차지한다.
2-2. 자바에서 사용
1) List 인터페이스
- List 컬렉션은 객체를 일렬로 늘어놓은 구조로 이루어져 있다.
- 순서가 있는 데이터의 집합으로 같은 데이터의 중복 저장을 허용한다.
- List 컬렉션 인터페이스에는 객체를 추가(add), 검색(contain, get, size), 삭제(remove, clear) 기능의 메서드가 선언되어 있다. 그러므로 List 컬렉션 인터페이스를 상속해서 구현하는 클래스는 해당 기능을 오버라이드 해야한다.
- 구현한 클래스: ArrayList, LinkedList, Vector, Stack, Queue..
2) ArrayList

- ArrayList는 인스턴스를 생성하게 되면 내부적으로 10칸짜리 배열을 생성해서 관리한다.
- 일반 배열과 ArrayList는 인덱스로 객체를 관리하는 공통점이 있지만, 크기를 동적으로 늘릴 수 있는 차이가 있다.
- 배열의 단점을 보완하기 위해 만들어졌기 때문에 크기 변경, 요소 추가/삭제/정렬 기능들을 메소드로 제공하고 있다. 단, 기능적으로 수행되는 것이지 속도가 빨라지는 것은 아니다.
- ArrayList 장점: 인덱스가 있으므로 객체 검색, 맨 마지막에 객체 추가 등에서 좋은 성능을 발휘한다.
- ArrayList 단점: 객체 삭제와 삽입이 빈번하게 일어나는 곳에서 ArrayList보다 LinkedList를 사용하는 것이 좋다.
(배열처럼 ArrayList에서 특정 인덱스의 객체를 제거하게 되면, 제거한 객체의 인덱스부터 마지막 인덱스까지 모두 앞으로 1칸씩 앞으로 이동함
3) LinkedList

- ArrayList는 내부 배열에 객체를 저장해서 인덱스로 관리하지만, LinkedList는 저장되는 데이터들이 연속된 공간에 저장되는 것이 아니라 각 데이터를 링크(Link)를 연결하여 구성한다.
- 스택, 큐, 양방향 큐 등을 구성하기 용이하다.
- 장점: 데이터의 삽입, 삭제가 빈번할 경우 연결되는 링크 정보만 수정하면 되기 때문에 ArrayList보다 더 적합하다.
(특정 인덱스의 객체를 제거하게 되면, 제거되는 인덱스의 앞 뒤 링크만 변경되고 나머지 링크는 변경되지 않는다. ArrayList는 제거되는 인덱스를 기준으로 뒤에 있는 객체가 한칸씩 이동 했었던 점과 차이가 있다.) - LinkecdList의 종류
- 단일 연결 리스트: 저장한 요소가 순서를 유지하지 않고 저장되지만 이러한 요소들 사이를 링크로 연결하여 구성하며 마치 연결된 리스트 형태인 것 처럼 만든 자료구조이다. 요소의 저장과 삭제 시 다음 요소를 가리키는 참조 링크만 변경하면 되기 때문에 요소의 저장과 삭제가 빈번히 일어나는 경우 ArrayList보다 성능면에서 우수하다.
- 이중 연결 리스트: 단일 연결 리스트는 다음 요소만 링크하는 반면 이중 연결 리스트는 이전 요소도 링크하여 이전 요소로 접근하기 쉽게 고안된 자료구조 이다.
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
List<String> linkedList = new LinkedList<>();
//다형성 적용하여 선언 가능
List list = new ArrayList();
Collection clist = new ArrayList();
//1. add(): 요소 추가
arrayList.add("apple");
//arrayList.add(123); //제네릭 에러
linkedList.add("apple");
//원하는 인덱스에 값 추가도 가능
arrayList.add(1, "banana");
linkedList.add(1, "banana");
//2. toString() 메소드가 오버라이딩 되어 있음
System.out.println("arrayList : " + arrayList);
System.out.println("linkedList : " + arrayList);
//3. get(): 인덱스에 해당하는 값을 가져옴
System.out.println(arrayList.get(0));
System.out.println(linkedList.get(1));
//4. size(): 저장된 요소의 개수 확인
System.out.println(arrayList.size());
System.out.println(linkedList.size());
//5. remove(): 요소를 제거, 제거된 요소 인덱스 리턴
linkedList.remove(0); //인덱스로 가능
arrayList.remove("apple"); //값으로 가능
System.out.println("arrayList : " + arrayList);
System.out.println("linkedList : " + linkedList);
//6. set(): 요소 수정
arrayList.set(0, "fineapple");
linkedList.set(0, "fineapple");
System.out.println("arrayList : " + arrayList);
System.out.println("linkedList : " + linkedList);
//7. clear(): 리스트 내 요소를 모두 제거
arrayList.clear();
//linkedList.clear();
//8. isEmpty(): list가 비어있는지를 확인
System.out.println(arrayList.isEmpty()); //true
System.out.println(linkedList.isEmpty()); //false
//9. 향상된 for문 사용도 가능하다
for(String a : arrayList) System.out.println(a);
for(String s : linkedList) System.out.println(s);
}
리스트 생성 시 변수 타입 선택에 따른 차이점 (다형성)
LinkedList<String> linkedList = new LinkedList<>(); | List<String> linkedList = new LinkedList<>(); |
LinkedList 고유 메서드 사용 가능 addFirst(), removeLast() .. |
List 인터페이스의 메서드만 사용 가능 add(), remove() .. |
단, LinkedList 전용이므로 다른 구현체로 바꾸기 어려움 | ArrayList 등 다른 List 구현체로 쉽게 변경 가능한 유연한 코드 |
3. 스택
3-1. 개념

- 한쪽 끝으로만 자료의 삽입, 삭제 작업이 이루어지는 자료 구조
- LIFO(Last In First Out) 방식으로 자료를 처리한다.
- 메소드가 호출될 때 해당 구조로 메모리에 올라가게 된다.
- Top/Bottom
- Top: 스택의 할당된 기억공간에서 가장 마지막으로 삽입된 자료가 기억된 위치를 가리키는 요소
- Bottom: 스택의 가장 밑바닥
- 오버플로/언더플로
- 오버플로(Overflow): 스택의 모든 기억공간이 꽉 채워져 있는 상태에서 데이터가 삽입되면 발생
- 언더플로(Underflow): 더이상 삭제할 데이터가 없는 상태에서 데이터를 삭제하면 발생
3-2. 자바에서 사용
- 리스트 계열 클래스인 Vector 클래스를 상속받아 구현됨
- 하지만 Java에서 Stack이 Vector를 상속한 것은 설계적으로 최선은 아니라고 평가받는다.
- Vector는 동기화를 지원하는 옛날 List 구현체라서, 성능상 불필요한 오버헤드가 발생할 수 있기 때문에 요즘은 Deque(특히 ArrayDeque)을 사용해서 Stack을 구현하는 게 더 추천된다.
import java.util.Stack; //import
Stack<Element> stack = new Stack<>(); //int형 스택 선언
stack.push(값); // 값 추가
stack.pop(); // stack의 첫번째 값을 반환하고 제거, 비어있다면 null
stack.peek(); // stack의 가장 상단의 값 반환만하고 제거X
stack.clear(); // stack의 전체 값 제거 (초기화)
stack.size(); // stack의 크기 출력
stack.empty(); // stack이 비어있는지 check (비어있다면 true)
stack.contains(1) // stack에 1이 있는지 check (있다면 true)
4. 큐 (Queue)
4-1. 개념

- 큐는 Front 포인터와 Rear 포인터를 정하고 한 곳에는 삭제만, 다른 한 곳에서는 삽입 연산만 처리한다.
- FIFO(First In First Out) 방식으로 자료를 처리한다 .
4-2. 자바에서 사용
- Queue 인터페이스를 상속받는 하위 인터페이스 들은 Deque, BlockingQueue, BlockingDeque, TreansferQueue 등 다양하지만 대부분의 큐는 LinkedList를 이용한다.
- LinkedList는 List와 Queue 둘 다 지원하는 클래스.
- LinkedList는 내부적으로 연결 리스트(Linked List) 구조를 사용해서 요소를 저장
- List 인터페이스를 구현해서 get(index), add(index, element) 같은 인덱스 기반 접근이 가능
- Queue 인터페이스도 구현해서 offer(), poll(), peek() 같은 FIFO 방식 접근이 가능하다.
- Priority Queue
- 높은 우선순위의 요소를 먼저 꺼내서 처리하는 구조 (큐에 들어가는 원소는 비교가 가능한 기준이 있어야함
- 내부 요소는 힙으로 구성되어 이진트리 구조로 이루어져 있음
import java.util.LinkedList; //import
import java.util.Queue; //import
Queue<Element> queue = new LinkedList<>() //큐 선언
queue.offer(값); // 값 추가
queue.poll(); // queue에 첫번째 값을 반환하고 제거, 비어있다면 null
queue.peek(); // queue의 첫번째 값 반환만하고 제거X
queue.clear(); // queue 초기화
queue.size(); // queue의 크기 출력
queue.isEmpty(); // queue가 비어있는지 check (비어있다면 true)
//우선순위큐
PriorityQueue<Integer> queSmall = new PriorityQueue<>(); //작은 수가 우선
PriorityQueue<Integer> queBig = new PriorityQueue<>(Collections.reverseOrder()); //큰 수가 우선
queBig.offer(1);
queBig.offer(9);
queBig.offer(7);
System.out.println(queBig.poll()); //9
System.out.println(queBig.poll()); //7
System.out.println(queBig.poll()); //3
add()와 offer()의 차이 ▼
|
Throws exception
|
Returns special value
|
Insert
|
add(e)
|
offer(e)
|
Remove
|
remove()
|
poll()
|
Examine
|
element()
|
peek()
|
5. 데크 (Deque)

- 양쪽 끝에서 삽입과 삭제 작업이 가능하도록 구성한 자료구조
- 스택과 큐를 혼합시킨 형태
- 두 개의 bottom포인터가 존재한다. (L-bottom, R-bottom)
- 종류: 입력 제한 데크(스크롤), 출력 제한 데크 (쉘프)
<비선형구조>
그래프 : 엉망진창 리스트
트리: 그래프 + 형태 상의 규칙
힙: 그래프 + 내용 상의 규칙
6. 그래프 (Graph)

- 정점(Vertex)와 간선(Edge)들로 이루어진 자료구조
- 트리와 달리 루트 노드 개념이 없으며 정점 간의 부모 자식관계가 없다.
- 그래프의 간선은 방향성을 가지거나, 가지지 않을 수 있다. (방향/무방향 그래프)
- 그래프는 자기 자신을 잇는 간선을 가질 수 있다.
- 구현: 인접행렬, 인접리스트
- 종류: 방향/무방향 그래프, 다중/단순 그래프, 가중치 그래프, 이분 그래프, 희소/밀집 그래프, 완전/부분 그래프
7. 트리 (Tree)
7-1. 개념

- 정점(Node, 노드)과 선분(Branch, 가지)을 이용하여 "사이클을 이루지 않도록 구성한" 그래프의 특수한 형태
- 트리는 나무를 뒤집은 모습으로 계층 구조를 표현하기에 적합
- 종류: 이진트리, 이진탐색트리
7-2. 용어
- 노드: 하나의 기억공간, 링크: 노드와 노드를 연결하는 선
- 루트노드: 트리 맨 위에 있는 노드, 단말노드: 자식이 하나도 없는 노드
- 부모, 자식, 형제 노드
- 차수: 각 노드에서 뻗어나온 가지의 수 (트리의 차수는 노드들의 차수 중 가장 많은 수)
- 깊이: 트리의 최대 레벨
7-3. 종류
- 이진 트리: 모든 노드들의 자식 노드가 두 개 이하인 트리, 배열이나 연결리스트로 구현 가능
- 이진 탐색 트리
- 이진 트리이면서, 중복 값을 갖는 노드가 없는 트리.
- 데이터의 삽입, 삭제, 탑색들리 자주 발생하는 경우에 효율적인 구조이다.
8. 힙 (Heap)
- 완전 이진 트리 형태로 구성된 비선형 자료구조
- 주로 우선순위 큐(priority queue)를 구현하는 데 사용된다.
- 특성: 부모 노드의 키 값이 자식 노드의 키 값보다 항상 크거나 작다.
- 최대 힙(Max Heap): 부모 노드가 자식 노드보다 값이 크거나 같은 구조 (최대값이 루트에 위치).
- 최소 힙(Min Heap): 부모 노드가 자식 노드보다 값이 작거나 같은 구조 (최소값이 루트에 위치).
- 힙 트리에서는 중복된 값을 허용한다. (이진 탐색 트리에서는 중복된 값을 허용하지 않는다.)
- 힙의 주요 연산은 삽입(Insert)과 삭제(Delete)이며, 둘 다 O(log n)의 시간 복잡도를 가진다.
9. 셋 (Set)
9-1. 개념
- Set이란 집합처럼 중복되지 않는 데이터를 저장하는 자료구조
- 데이터 저장 순서가 보장되지 않는다.
- Set 사용 사례
- 유일한 값을 저장하고 싶을 때
- 중복 데이터 제거가 필요할 때
9-2. 자바에서 사용
- HashSet: 해시 테이블을 이용하여 데이터를 저장하는 구조, 중복X 순서X
- LinkedHashSet: HashSet과 동일한 구조를 가지지만 데이터 삽입 순서대로 출력된다.
- TreeSet: 데이터가 정렬된 상태로 저장되는 이진 검색 트리의 형태로 요소를 저장한다.
//HashSet (순서 보장X)
HashSet<Integer> set = new HashSet<Integer>();
set.add(1); //값 추가
set.remove(1); //값 1 제거
set.clear(); //모든 값 제거
set.contains(1); //boolean 리턴
//출력
Iterator iter = set.iterator(); // Iterator 사용
while(iter.hasNext()) {//값이 있으면 true 없으면 false
System.out.println(iter.next()); //순서 보장되지 않음
}
//LinkedHashSet: 등록 순서대로 저장
LinkedHashSet<String> lhset = new LinkedHashSet<>();
lhset.add("banana");
lhset.add("orange");
lhset.add("apple");
System.out.println("lhset : " + lhset); //banana, orange, apple
//TreeSet : 자동 오름차순 정렬
TreeSet<String> tset = new TreeSet<>();
tset.add("banana");
tset.add("orange");
tset.add("apple");
System.out.println("tset : " + tset); //apple, banana, orange
10. 맵 (Map)
10-1. 개념
- Map은 키와 값을 하나의 쌍으로 저장하는 자료구조
- 키(Key) : 키를 통해 값에 접근할 수 있다, 키는 고유한 값으로 중복 불가
- 값(Value) : 값에 매핑된 데이터, 값은 중복될 수 있다.
- 데이터 저장 순서가 보장되지 않는다.
- 장점: 엑세스가 빠르면서(배열), 추가 삭제가 편함(리스트)
- 단점: 메모리를 많이쓴다 (배보타 배꼽이 커질수있다)
- 사용사례
- 데이터의 매핑 관계를 표현할 때 (예: 학생 ID와 이름).
- 빠른 검색, 삽입, 삭제가 필요한 경우.
- Map과 Set의 차이점 요약
Map | Set | |
데이터 구조 | 키-값(Key-Value) 쌍 | 고유한 요소의 집합 |
중복 허용 여부 | 키는 중복 불가, 값은 중복 가능 | 중복 요소 불가 |
주요 사용 목적 | 키로 값에 빠르게 접근 | 중복 제거 및 고유 값 관리 |
주요 구현체 | HashMap, TreeMap, LinkedHashMap | HashSet, TreeSet, LinkedHashSet |
10-2. 자바에서 사용
- Map은 Collection 인터페이스와는 다른 저장 방식을 가지고 있기 때문에 하위 인터페이스에 포함되지 않는다.
- Collection은 단일 값을 저장하는 자료 구조이고, Map은 키-값의 쌍을 저장하는 자료구조 이다.
- 따라서 Map은 Collection의 하위 클래스가 아니고 독립적인 구조로 존재
- 종류: HashMap, HashTable(-Properties), TreeMap 등
- HashMap
- Map 인터페이스를 구현하고 있는 클래스 중에 가장 자주 쓰이는 클래스
- 저장은 느리지만 Hashing 이라는 해시 함수를 이용해서 데이터를 해시 테이블에 저장하기 때문에 검색 측면에서 뛰어나다.
- 중복된 키 값에 값을 저장하면 기존의 값에 새로운 값이 덮어 씌어진다.
HashMap<Integer,String> map = new HashMap<>(); //HashMap 선언 및 생성
map.put(1,"사과"); //값 추가
map.put(1,"바나나"); //키 중복 저장 안됨 -> 값 갱신
map.put(2,"바나나"); //값 중복 저장 가능
map.get(1); //key값 1의 value얻기 : 바나나
map.remove(1); //key값 1 제거
map.clear(); //모든 값 제거
map.size(); //개수 리턴
map.containsKey(1) //key값 1 검색 -> true
map.containsValue("바나나"); //value값 "바나나" 검색 -> false
//getOrDefault(map에서 찾을 key값(있으면 key값의 value 리턴), map에 해당하는 key값이 없으면 리턴할 디폴트값);
map.getOrDefault(1, "없음"); // map의 key값으로 1이 있으면 "사과" 리턴, 없으면 "없음" 리턴
// map 출력
for (String key : map.keySet()) {
String value = map.get(key);
System.out.println("[key]:" + key + ", [value]:" + value);
}
//keySet()을 이용해서 키만 따로 set으로 만들고,
Iterator<String> keyIter = map.keySet().iterator();
while(keyIter.hasNext()) {
String key = (String)keyIter.next();
String value = (String) hmap2.get(key);
System.out.println(key + " = " + value);
}