|  들어가기 전에 

- 왜 우선순위 큐는 비선형 구조일까? 
: 큐는 선형구조로 1 : 1의 대응방식을 가진다. 앞에서 부터 데이터를 꺼내고, 뒤에서부터 데이터를 축척한다.
: 우선순위 큐는 이러한 큐에 우선순위 개념을 추가한 형태로, 우선순위를 따져 우선순위가 높은 데이터를 먼저 뽑는다.
: 우선순위 큐는 배열, 연결리스트, 힙으로 구현이 가능한데 이중에서 힙으로 구현을 하는 것이 가장 효율적이다.
: 따라서, 자바에서도 우선순위 큐는 힙으로 구현되었다. 힙은 완전 이진 트리 형태이며, 이는 1 : N 관계를 갖는 비선형구조이다.

자바에서는 우선순위 큐를 힙으로 구현한다. 왜?
: 위에서도 잠시 언급했던 것처럼, 힙으로 큐를 구현하는 것이 가장 효율적이기 때문에, 자바도 구현방식으로 힙을 쓴다.
  - 배열의 경우, 중간 삽입 또는 삭제 시 O(N)의 시간이 소요되고,
  - 연결리스트의 경우, 첫 위치에 삽입/삭제할 경우 O(1)이 걸리지만 그렇지 않다면 모든 구간을 돌아 O(N)이 소요된다.
  - 힙의 경우, 루트에 최소값 또는 최대값을 두며 하위 트리로 파생되기 때문에 삽입/삭제 모두 logN이 걸린다.
자료구조 삽입 삭제
순서 없는 배열 O(1) O(N)
순서 없는 연결리스트 O(1) O(N)
정렬된 배열 O(N) O(1)
정렬된 연결리스트 O(N) O(1)
O(logN) O(logN)
: 이처럼, 힙을 사용하면 삽입/삭제의 시간 복잡도를 줄일 수 있기에 자바도 우선순위 큐를 힙으로 구현한다.
: 참고로, 자바의 PriorityQueue 객체는 Queue와 달리 인터페이스가 아니라 일반 클래스이다.

 

|  힙이란?

- 사전적으로, "무언가를 차곡차곡 쌓아올린 자료구조"라는 뜻이다.

- 완전 이진 트리 형태의 자료구조

  ** 완전 이진 트리란? 마지막 레벨을 제외하고 모든 레벨의 노드가 꽉 채워진 상태인 트리

- 중복값을 허용하며, 반 정렬 상태(느슨한 정렬 상태)이다.

  ** 반 정렬 (또는 느슨한 정렬) 이란?
  : 큰 값이 상위에 있고, 작은 값이 하위에 있을 뿐, 이진 탐색 트리처럼 좌우가 정렬되어 있지 않다.
  ** 중복값을 허용한다는 것은?
  : 이진 탐색 트리와 달리, 힙은 중복값을 허용한다.

- 종류 : 최소힙, 최대힙

출처:https://gmlwjd9405.github.io/2018/05/10/data-structure-heap.html

 - 응용 분야 :

힙은 최대값과 최소값을 구해야할 때 가장 적절하다.
그렇기에 우선순위 큐를 구현하거나, 힙 정렬를 만드는 게 사용되며, 허프만 코드와 같은 알고리즘에 쓰인다.

 

|  힙의 구현

- 힙은 배열로 가장 잘 구현할 수 있다.

- 힙의 삽입은 아래와 같이 가장 끝에 데이터를 삽입하고 각 구간을 비교하여 교체하는 방식이며,

- 힙의 삭제는 최상단의 루트를 삭제하는 것으로, 가장 끝의 노드를 루트에 놓고 비교하여 교체하는 방식을 취한다.

삽입 (1) 트리의 가장 끝 위치에 데이터를 삽입
(2) 삽입한 위치의 부모노드와 비교하여 작으면 부모와 자리를 교체한다 (작지 않을 때까지 반복)
삭제 (1) 루트를 꺼내서 삭제한다.
(2) 트리의 가장 끝 위치의 노드를 루트에 삽입
(3) 루트부터 시작해서 자식과 비교하여 자식보다 크면 교체한다. (크지 않을 때까지 반복)

[ 코드 - 더보기 ]

더보기

1. 최소힙 구현하기

import java.util.ArrayList;

class MinHeap{
    ArrayList<Integer> tree;

    public MinHeap(){
        this.tree = new ArrayList<Integer>();
        this.tree.add(0); // dummy data
    }

    // 삽입 : (1) 트리의 가장 뒤에 삽입 (2) 삽입 위치에서부터 부모와 비교
    public void add(int data){
        this.tree.add(data);

        int curIdx = this.tree.size() - 1; // 현재 위치
        int parentIdx = curIdx / 2; // 부모 위치
        while (curIdx > 1 && tree.get(parentIdx) > tree.get(curIdx)) {
            int tmp = tree.get(parentIdx);
            tree.set(parentIdx, tree.get(curIdx));
            tree.set(curIdx, tmp);
            curIdx = curIdx / 2;
        }
    }

    // 삭제 : (1) 루트를 삭제 (2) 루트에 가장 끝 노드를 삽입 (3) 루트에서부터 비교
    public int remove(){

        if (this.tree.size() == 1){
            System.out.println("this heap is empty!");
            return -1;
        }

        // (1)-(2)
        int min = this.tree.get(1);
        int last = this.tree.remove(this.tree.size() - 1);
        this.tree.set(1, last);

        // (3)
        int curIdx = 1;
        while(true){
            int leftIdx = curIdx * 2;
            int rightIdx = curIdx * 2 + 1;
            int childIdx = -1;

            if (rightIdx < this.tree.size()) { // 자식노드가 둘 다 있는 경우
                childIdx = tree.get(leftIdx) < tree.get(rightIdx) ? leftIdx : rightIdx;
            } else if (leftIdx < this.tree.size()) { // 자식노드가 하나인 경우
                childIdx = leftIdx;
            } else { // 자식이 하나도 없을 때
                break;
            }
            
            if (tree.get(curIdx) < tree.get(childIdx)){
                break;
            } else {
                int tmp = tree.get(curIdx);
                tree.set(curIdx, tree.get(childIdx));
                tree.set(childIdx, tmp);
                curIdx = childIdx;
            }
        }

        return min;
    }
}

 

2. 최대힙 구현하기

import java.util.ArrayList;

class MaxHeap{
    ArrayList<Integer> tree;

    public MaxHeap(){
        this.tree = new ArrayList<Integer>();
        this.tree.add(0); // dummy data
    }

    // 삽입 : (1) 트리의 가장 뒤에 삽입 (2) 삽입 위치에서부터 부모와 비교
    public void add(int data){
        this.tree.add(data);

        int curIdx = this.tree.size() - 1; // 현재 위치
        int parentIdx = curIdx / 2; // 부모 위치
        while (curIdx > 1 && tree.get(parentIdx) < tree.get(curIdx)) {
            int tmp = tree.get(parentIdx);
            tree.set(parentIdx, tree.get(curIdx));
            tree.set(curIdx, tmp);
            curIdx = curIdx / 2;
        }
    }

    // 삭제 : (1) 루트를 삭제 (2) 루트에 가장 끝 노드를 삽입 (3) 루트에서부터 비교
    public int remove(){

        if (this.tree.size() == 1){
            System.out.println("this heap is empty!");
            return -1;
        }

        // (1)-(2)
        int max = this.tree.get(1);
        int last = this.tree.remove(this.tree.size() - 1);
        this.tree.set(1, last);

        // (3)
        int curIdx = 1;
        while(true){
            int leftIdx = curIdx * 2;
            int rightIdx = curIdx * 2 + 1;
            int childIdx = -1;

            if (rightIdx < this.tree.size()) { // 자식노드가 둘 다 있는 경우
                childIdx = tree.get(leftIdx) > tree.get(rightIdx) ? leftIdx : rightIdx;
            } else if (leftIdx < this.tree.size()) { // 자식노드가 하나인 경우
                childIdx = leftIdx;
            } else { // 자식이 하나도 없을 때
                break;
            }

            if (tree.get(curIdx) > tree.get(childIdx)){
                break;
            } else {
                int tmp = tree.get(curIdx);
                tree.set(curIdx, tree.get(childIdx));
                tree.set(childIdx, tmp);
                curIdx = childIdx;
            }
        }

        return max;
    }
}

 

우선순위 큐란?

- 우선순위가 있는 큐이다.

- 모든 데이터가 우선순위가 있으며, Dequeue를 할 때에 우선순위가 높은 순으로 출력된다.

  * 만약, 우선순위가 같은 경우에는 FIFO 순으로 출력된다.

■ 우선순위 큐를 어떻게 쓸 수 있을까?

- 우선순위 큐는 데이터를 삽입할때, 정해진 기준대로 오름차순/내림차순으로 데이터를 넣을 수 있고

- 최소값 또는 최대값부터 데이터를 뽑을 수 있다.

- 삽입/삭제 시간복잡도는 그럼 얼마나 될까? 앞에서 힙에서 언급했듯 logN이 걸린다.

- 이 점을 이용해서 K번째로 가장 큰 수를 뽑는다거나, 강도가 다른 돌을 부딪혀 남는 돌을 다시 넣거나, 

  중복된 수가 있는 수열에서 빈도수 기준으로 정렬하되 같은 빈도면 더 작은 수가 앞에 오게 정렬하는 등

  순서에 따라 넣고 빼는 일이 잦거나 재정렬이 필요한 데이터들에서 사용할 수 있다.

 

Comparable 인터페이스

- 우선순위 큐를 사용할 때, 우선순위의 기준이 되는 객체에 Comparable인터페이스를 사용하면

  오름차순 또는 내림차순으로 데이터를 enqueue/dequeue할 수 있다.
- Comparable인터페이스를 사용하는 방법은 크게 두가지이다.


[1] 객체에 Comparable를 구현하는 방법

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

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

    @Override
    public int compareTo(Student o) {
        // this.Student(new)와 o(old) Student를 비교
        // 1 : 변경하지 않음, -1 : 변경
        return this.score >= o.score ? 1 : -1;
        // return this.score - o.score; -- 같은 말이다.
    }
}

[2] 람다식을 사용하여 비교 연산 메소드(compareTo)를 바로 구현하는 방법

import java.util.PriorityQueue;

class Student{
    String name;
    int score;

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

class test{
    public static void main(String[] args) {
        PriorityQueue<Student> pq = new PriorityQueue<>(
            (Student newSt, Student oldSt) ->  newSt.score - oldSt.score
        );

        for (int i = 0; i < 10; i++) {
            pq.add(new Student("Student"+(i+1), i * 10));
        }

        while(!pq.isEmpty()) {
            Student st = pq.poll();
            System.out.println(st.name + " : " + st.score);
        }
    }
}

[+] 문자열을 Comparable를 통해 사전순으로 offer/poll하는 방법

/* 생략 (위의 Student 객체 그대로 사용) */

class test{
    public static void main(String[] args) {
        PriorityQueue<Student> pq = new PriorityQueue<>(
            (Student newSt, Student oldSt) ->  newSt.name.compareTo(oldSt.name)
        );

        for (int i = 0; i < 10; i++) {
            char c = (char)('A' + (int)(Math.random()*32));
            pq.add(new Student((char)(c+i)+"_Student", i * 10));
        }

        /* 출력 생략 */
    }
}

 

|  힙과 우선순위 큐 문제

https://www.acmicpc.net/workbook/view/1819

 

문제집: 힙 쓰는 문제 (kcaladram)

 

www.acmicpc.net

https://www.acmicpc.net/step/13

 

우선순위 큐 단계

우선순위 큐를 응용하여 중앙값을 빠르게 찾는 문제

www.acmicpc.net

 

[ 참고 및 출처 ]

- 부트 캠프의 강의를 듣고 여러 자료들을 참조하여 정리한 내용입니다.

- https://namu.wiki/w/%ED%9E%99%20%ED%8A%B8%EB%A6%AC

- https://gmlwjd9405.github.io/2018/05/10/data-structure-heap.html

 

|  요약

  핵심 단어 핵심 특성 언제 쓰면 좋을까
트리 계층, Acyclic, 4가지의 순회 포화 이진 트리의 경우, 하위로 갈수록
노드의 갯수가 2^0, 2^1, 2^2..로 증가
계층 관계의 데이터를 다룰 때
그래프 네트워크, cyclic, DFS/BFS 루트가 없고, 각 정점들간 단방향 또는
양방향의 관계를 가질 수 있음
연관 관계를 가진 데이터들에서
최적의 결과를 찾아야할 때

 

|  트리와 그래프의 차이

  트리 그래프
개요 그래프의 한 종류 정점(vertex, 노드)과 간선(edge)으로 이루어진 자료구조
그림

출처 : 위키백과

출처 : 위키백과
방향 (단)방향 그래프  무(양)방향, (단)방향 그래프
사이클 Acyclic Cyclic
모델 계층 모델 네트워크 모델
루트 최상위 노드 없음
상-하위 관계 인접한 상하위 노드 없음
간선의 갯수 N개의 노드의 간선은 N-1개 그래프에 따라 다름
순회 PreOrder, InOrder, PostOrder
LevelOrder
DFS
BFS
경로 두 노드 간 경로는 하나(단방향) 두 노드 간 경로 여러 개 가능(양방향)
종류 이진 트리, 이진 탐색 트리,
균형 트리(AVL 트리, red-black트리),
이진 힙(최대힙, 최소힙) 등
-
예시 폴더 구조(디렉토리, 서브 디렉토리),
조직도, 가계도 등
지도, 지하철 노선도의 최단 경로, 
전기 회로의 소자들,
도로 (교차점과 일방 통행길), 선수 과목

 

■ 트리 개요

- 트리의 구조와 특징
- 이진 트리
- 이진 탐색 트리
- 균형 이진 탐색 트리

|  트리의 구조와 특징

출처:https://velog.io/@alstn3265/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%8A%B8%EB%A6%AC

[특징]
1. 모든 노드는 연결되어 있다.
2. 노드 간 이동 경로는 하나이다.(단방향)
3. 하나의 간선을 끊으면 2개의 sub-Tree로 나뉜다.
4. 이진트리에서, 노드가 N개인 트리의 간선 갯수는 N-1개이다.
5. 그래프와 달리 cycle이 존재하지 않는다(acyclic)
Basic 노드(Node, 또는 정점) data를 담고 있는 트리구조의 단위
간선(Edge) 노드의 연결선 (= link, branch)
구분 루트(Root) 꼭대기 노드
내부 노드(Internal) 단말을 제외한 모든 노드
단말(Leaf) 자식이 없는 노드
관계 부모(Parent) 연결된 두 노드 중 상위 노드
자식(Child) 연결된 두 노드 중 하위 노드
형제(Sibling) 같은 부모를 공유하는 노드
깊이 깊이(Depth) 루트에서 어떤 노드까지의 간선의 수 (ex. P에서 E까지 깊이 : 4)
레벨(Level) 트리의 특정 깊이를 가지는 노드 집합 (ex. level 1의 노드는 Q와 R)
높이(Height) 트리에서 가장 큰 레벨 값 (ex. 위에 트리는 4)
노드 갯수 크기(Size) 자신을 포함한 자식 노드의 갯수
간선 갯수 차수(Degree) 각 노드가 가진 간선 갯수 (ex. L의 차수는 3)
트리의 차수 트리의 최대 차수 (ex. 위 트리의 최대 차수는 L의 차수인 3)

 

|  이진트리(BT, Binary Tree)

이진트리 : 최대 2개의 자식을 가질 수 있는 트리

1. 이진 트리의 종류와 특징

포화 이진 트리(Perfect Binary Tree) 모든 레벨에서 노드가 꽉 채워진 트리
완전 이진 트리(Complete Binary Tree) 마지막 레벨을 제외하고
모든 레벨에서 노드가 꽉 채워진 트리

정 이진 트리(Full Binary Tree) 모든 노드가 0 또는 2개의 자식을 가진 트리
(자식이 없거나 2개 모두 있거나)
편향 트리(Skewed Binary Tree, 사향 트리) 한쪽으로 기울어진 트리
균형 이진 트리(Balanced Binary Tree) 모든 노드의 좌우 서브 트리 높이가
1 이상 차이나지 않는 트리

(ex. 옆 그림에서 A의 좌측은 2개, 우측은 1개의 간선이 있어 차이가 1이 난다)

- 관련 수식

N = 2^(h+1) - 1

* N : 포화트리 노드의 갯수
* h : 높이
h = logN
* N : 노드의 갯수
* h : 포화 또는 완전 트리 높이
maxH = N
*maxH : 이진 트리의 최대 가능 높이
*N : 노드의 갯수

 

2. 이진 트리의 순회(Traversal)

전위 순회(PreOrder) 현재 -> 왼쪽 -> 오른쪽 
중위 순회(IntOrder) 왼쪽 -> 현재 -> 오른쪽 
후위 순회(PostOrder) 왼쪽 -> 오른쪽 -> 현재
레벨 순회(LevelOrder) 각 레벨에서 왼쪽 -> 오른쪽
[ 이진 트리를 구현할 때의 Tip ]
- 배열을 통해 이진 트리를 구현한다고 가정할 때 위치를 찾는 방법
(1) 좌측 노드 = 나의 노드 x 2 + 0 --> 배열idx에서는 나의 노드 x 2 + 1
(2) 우측 노드 = 나의 노드 x 2 + 1 --> 배열idx에서는 나의 노드 x 2 + 2
(3) 부모 노드 = 나의 노드 / 2
- 연결리스트를 통해 구성할 수도 있다.

- 이진 트리 순회 구현  ▼ 더보기

더보기

1. 배열을 통한 이진트리 순회

// 배열을 통한 이진트리 구현
class BinaryTree{
    char[] arr;
    
    BinaryTree(char[] data){
    	this.arr = data;
    }
    
    public void preOrer(int idx){      
        System.out.print(arr[idx] + " ");
        
        int left = 2 * idx + 1;
        int right = 2 * idx + 2;
        
        if (left < this.arr.length){
        	preOrder(left);
        }
        
        if (right < this.arr.length){
            preOrder(right);
        }
    }
    
    public void inOrder(int idx){
    	int left = 2 * idx + 1;
        int right = 2 * idx + 2;
        
        if (left < arr.length){
           inOrder(left);
        }
        
        System.out.print(arr[idx] + " ");
        
        if (right < arr.lenght){
           inOrder(right);
        }
    }
    
    public void postOrder(int idx){
    	int left = idx * 2 + 1;
        int right = idx * 2 + 2;
        
        if (left < arr.length) {
           postOrder(left);
        }
        
        if (right < arr.length) {
           postOrder(right);
        }
        
        System.out.print(arr[idx] + " ");
    }
    
    public void levelOrder(int idx){
    	for (int i = idx; i < arr.length; i++){
        	System.out.print(arr[i] + " ");
        }
        System.out.println("");
    }
}

public class Main{
   public static void main(String[] args){
      BinaryTree bt = new BinaryTree(new char[]{'A','B','C','D','E','F','G'});
      //     A
      //   B   C
      //  D E F G
   }
}

 

2. 연결리스트를 통한 이진트리 순회

// 노드의 연결(연결리스트)을 통한 이진트리 구현
class Node{
    char data;
    Node left;
    Node right;
    
    Node(char data){
    	this.data = data;
    }
}

class BinaryTree{
    Node root;
    
    BinaryTree(char[] datas){
    	Node nodes[] = new Node[datas.length];
    
        // 각각의 노드를 생성
    	for (int i = 0; i < datas.length; i++){
        	nodes[i] = new Node(datas[i]);
        }
        
        // 트리 형태로 노드를 연결
        root = nodes[0];
        for (int i = 0; i < nodes.length; i++){
        	int left = i * 2 + 1;
            int right = i * 2 + 2;
            
            nodes[i].left = nodes[left];
            nodes[i].right = nodes[right];
        }
    }
    
    public void preOrder(Node node){
    	if (node == null) {
        	return;
        }
        
        preOrder(node.left);
        System.out.print(node.data + " ");
        preOrder(node.right);
    }
    
    public void inOrder(Node node){
    	if (node == null) {
        	return;
        }
    
    	inOrder(node.left);
        System.out.print(node.data + " ");
        inOrder(node.right);
    }
    
    public void postOrder(Node node){
    	if (node == null){
        	return;
        }
        
        postOrder(node.left);
        postOrder(node.right);
        System.out.print(node.data + " ");
    }
    
    public void levelOrder(Node node){
    	Queue<Node> queue = new LinkedList();
        queue.add(node);
        
        while (!queue.isEmpty()){
			Node cur = queue.poll();
            System.out.print(cur.data + " ");
            
            if (cur.left != null) {
            	queue.offer(cur.left);
            }
            
            if (cur.right != null) {
            	queue.offer(cur.right);
            }
        }
    }
}

public class Main{
   public static void main(String[] args){
      BinaryTree bt = new BinaryTree(new char[]{'A','B','C','D','E','F','G'});
      //     A
      //   B   C
      //  D E F G
   }
}

 

3. 가중치가 있는 포화이진트리의 루트~리프까지의 가중치 총합을 동일하게 만드는 최소합

BinaryTree{
    int h; //높이
    int sum; //가중치의합
    int[] w; //가중치 배열
    
    BinaryTree(int h, int[] w){
    	this.h = h;
        this.w = new int[(int)(Math.pow(2, (h+1)))];
        
        for (int i = 2; i < this.w.length; i++){
        	this.w[i] = w[i-2];
        }
    }
    
    // 하위 좌우 중 더 큰 가중치에 맞춘 후 -> 상위 좌우 중 더 큰 가중치 합에 맞춘다.
    public int dfs(int h, int idx){
    	if (h == this.h){
        	res += this.w[idx]; //리프 가중치 추가
        	return this.w[idx];
        }
        
        int leftW = dfs(h + 1, idx * 2 + 1); // 좌측 서브 트리의 가중치 합
        int rightW = dfs(h + 1, idx * 2 + 2); // 우측 서브 트리의 가중치 합
        res += this.w[idx] + Math.abs(leftW - rightW); 
        return this.w[idx] + Math.max(leftW, rightW); // 서브 트리에서 더 큰 가중치 합 반환
    }
}

public class Main{

	public static void solution(int h, int[] w) {
    	BinaryTree bt = new BinaryTree(h, w);
		bt.dfs(0, 1);
        System.out.println(bt.res);
    }

	public static void main(String[] args){
    	int h = 2;
        int[] w = new int[]{1, 1, 2, 1, 3, 1};
        solution(h, w);
    }
}

 

|  이진 탐색 트리(BST, Binary Search Tree)

이진 탐색 트리 : 루트부터 시작하여 좌측에는 더 작은 키가, 우측에는 더 큰 키가 들어간 형태

* 모든 서브 트리 또한 이진 탐색 트리이며,
* 중복된 키는 허용하지 않는다.

1. 이진 탐색 트리의 탐색 시간 복잡도

탐색 균형 상태 O(logN)
불균형 상태 O(N)   

2. 이진 탐색 트리의 구현 - 탐색, 삽입과 삭제 (▼ 더보기)

더보기
탐색* root에서부터 탐색.
찾으려는 key값이 현재 노드보다 작은 경우 좌측으로 이동, 큰 경우 우측으로 이동.

찾으려는 key값과 현재 노드의 키값이 같은 경우 탐색 완료
O(logN)
삽입 root에서부터 key값을 비교.
삽입하는 key값이 현재 노드보다 작은 경우 좌측으로 이동, 큰 경우 우측으로 이동.
이동한 현재 위치에 노드가 없는 경우(null), 그 위치에 노드를 추가
O(logN)
삭제 case1 삭제 노드가 리프인 경우 : 부모노드에서 리프방향을 null 처리 O(logN)
case2 삭제 노드의 자식이 하나 있는 경우 : 부모노드에서 내 방향(ex.left)에 자식을 연결
case2 삭제 노드의 자식이 둘인 경우 : 
(1) 부모노드에서 내 방향에 내 좌측자식에서 가장 큰 노드를 연결
(2) 부모노드에서 내 방향에 내 우측자식에서 가장 작은 노드를 연결

 * 여기서의 탐색은 루트에서부터의 탐색으로, 이진 탐색 트리를 실제로 조회할 때는 4가지 순회 중 하나를 선택하는 것이 더 유리하다.(ex. preOrder/InOrder/....)

class Node{
    int key;
    Node left;
    Node right;
    
    Node(int key, Node left, Node right){
    	this.key = key;
        this.left = left;
        this.right = right;
    }
}

class BinarySearchTree{

    Node root;
	
    BinatySearchTree(int key){
    	this.root = new Node(key, null, null);
    }
    
    // 삽입
    public void add(int key){
    	if (root == null){
        	root = new Node(key, null, null);
        } else {
        	Node cur = this.root;
            Node pre = cur;
            
            while (cur != null) {
            	pre = cur;
            
            	if (key < cur.key){
                	cur = cur.left;
                    
                    if (cur == null){
                    	pre.left = new Node(key, null, null);
                        break;
                    }
                }
                
                if (key > cur.key){
                	cur = cur.right;
                    
                    if (cur == null) {
                    	pre.right = new Node(key, null, null);
                        break;
                    }
                }
            }
        }
    }
    
    // 삭제
    public void remove(int key){
    	// 탐색 (해당 key값을 가진 노드 탐색)
        Node cur = this.root;
        Node parent = null;
        
        while (cur != null){
        	if (cur.key == key){
            	break;
            }
            
            parent = cur;
            if (key < cur.key) {
            	cur = cur.left;
            } else {
            	cur = cur.right;
            }
        }
        
        if (cur == null){ //없을 때
        	System.out.println("BinarySearchTree doesn't have key " + key);
        }
        
        // 세가지 case에 따라 삭제 진행
        Node child = null;
        Node leftRightMax = null;
        Node ex;
        
        // case 1 : 삭제 노드가 leaf 일 때
        if (cur.left == null && cur.right == null){
        	if (cur == this.root){
            	this.root = null;
            } else {
            	if (parent.left == cur){
                	parent.left = null;
                } else {
                	parent.right = null;
                }
            }
        } 
        // case 2 : 삭제 노드에 자식이 하나 있을 때
        else if (cur.left != null && cur.right == null || cur.left == null && cur.right != null) {
        	// 자식 위치를 먼저 확인
            if (cur.left != null) {
            	child = cur.left;
            } else {
            	child = cur.right;
            }
            
            // 자식 노드를 현재 위치로 올린다.
            if (parent == null){ // = cur 가 root
            	this.root = child;
            } else {
            	if (parent.left == cur){
                    parent.left = child;
                } else {
                    parent.right = child;
                }
            }
        }
        // case 3 : 삭제 노드에 자식이 둘 있을 때
        else {
        	// 나의 좌측 노드 중 가장 큰 노드를 찾는다.
            ex = cur;
            leftRightMax = cur.left;
            
            while (now.right != null){
            	ex = leftRightMax;
            	leftRightMax = leftRightMax.right;
            }
            
            // 나의 위치에 찾은 노드를 넣어준다.
            ex.right = leftRightMax.left; // 먼저, 찾은 노드에 좌측노드가 있는 경우
            leftRightMax.left = cur.left;
            leftRightMax.right = cur.right;
            
            if (parent == null){
            	this.root = leftRightMax;
            } else {
            	if (parent.left == cur) {
                	parent.left = leftRightMax;
                } else {
                	parent.right = leftRightMax;
                }
            }
        }
    }
    
}

 

|   균형 이진 트리 (추가 정리 예정)

(1) 이진 탐색 트리의 편향 발생

(2) 균형 이진 탐색 트리 종류

  AVL 트리 red-black 트리
공통점    
차이점    

 

■ 그래프 개요

- 그래프의 구조와 특징
- 그래프의 탐색
- 그래프의 구현

|  그래프의 구조와 종류

출처:https://medium.com/@sarinyaswift/intro-to-the-graph-data-structure-a8277c6a2ad9

Basic 정점(Vertex, 노드) 데이터를 담고 있는 노드
간선(Edge) 연결선 = link, brank
관계 인접 정점(Adjacent vertex) 간선 하나로 바로 연결된 정점
간선 갯수

정점의 차수(Degree) 무방향 그래프에서 하나의 정점에 인접한 정점의 수 (ex. a노드의 차수는 2)
* 무방향 그래프의 모든 정점 차수의 합 = 전체 간선의 수 x 2
진입 차수(In-degree) 들어오는 간선의 수 
진출 차수(Out-degree) 나가는 간선의 수
경로 경로 길이(Path length) 어떤 경로를 구성하는 데 사용된 간선의 수
단순 경로(Simple path) 어떤 경로를 가기 위해 특정 정점을 반복해서 가지 않아도 되는 경우
사이클 사이클(Cycle) 단순 경로의 시작과 끝 정점이 동일한 경우

 

출처:https://medium.com/@sarinyaswift/intro-to-the-graph-data-structure-a8277c6a2ad9
출처:위키백과(완전그래프)

종류 내용 정점 A와 B의 간선 표현 / 비고
무방향 그래프(Undirected) 간선에 방향이 없는 그래프 (A, B) = (B, A)
방향 그래프(Directed) 간선에 방향이 있는 그래프 <A, B> != <B, A>
가중치 그래프(Weighted) 간선에 값(이동 비용)이 있는 그래프 -
완전 그래프(Complete) 모든 정점이 서로 연결된 그래프 정점이 N개일 때, 간선의 수 n(n-1)/2

 

|  그래프의 탐색

DFS(깊이 우선 탐색) = depth first search, Stack 또는 재귀를 사용하여 깊이 탐색
BFS(너비 우선 탐색) = breath first search, Queue를 사용하여 같은 레벨의 노드를 탐색

 

■ 그래프 탐색의 핵심

(1) 정점(Vertex)
(2) 간선(Edge, 곧 연결상태) : 어느 방향으로, 몇 개의 데이터가 연결되어 있는가
(3) 요구사항이 무엇인가? 
ex.
- DFS
  : 만약 root~leaf까지의 가중치를 구해야하거나, 
  : 바둑판과 같은 2차원 배열 or x축과 y축 좌표의 특정 위치에서 사방위로 움직일때
  : 네트워크 방식으로 서로 복잡하게 연결된 정점들을 새로운 자료구조로 재구성할 경우
- BFS
  : 어떤 정점 X에서 파생되는 여러개의 정점들이 있고, 각 하위 정점이 같은 규칙으로 파생될때
    특정 타켓까지 구하기 위해서 얼만큼의 시도가 필요한가 (== level를 묻는 문제)

 

■ 인접 행렬 VS 인접리스트

- 간선 정보를 저장하는 자료구조로 인접 행렬과 인접 리스트를 주로 같이 설명하는데,

- 실제로 코딩테스트 문제를 풀다보면 HashSet를 쓰거나 HashMap을 통해서 간선 정보를 저장하기도 한다.

- DFS나 BFS를 써서 문제를 풀 때는 상황에 따라 가장 적절한 자료구조를 사용하는 것이 필요.

  인접 행렬 인접 리스트
내용 2차원 배열로 인접 간선을 저장 연결리스트(LinkedList[])로 인접 간선을 저장
장단점 간선 정보 확인/업데이트가 빠름
메모리 사용량이 높다(간선)
정점의 추가/삭제가 빠르다.
메모리 사용량이 적다.(간선)
간선 정보 확인이 오래걸린다.
언제 유리한가 정점의 갯수가 적고, 간선 갯수가 많을 때 정점의 갯수가 많고, 간선 갯수는 적을 때
시간복잡도





특정 간선 탐색 O(1) O(degree(V)) *V: 차수 (특정 정점의 간선들)
정점 차수 계산 O(N) O(degree(V)) 
전체 정점 탐색 O(N^2) *N : 정점 갯수 O(E) *E : 간선 갯수
공간복잡도 N x N N + E

 

|  그래프 문제

https://school.programmers.co.kr/learn/courses/30/parts/14393

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

https://school.programmers.co.kr/learn/courses/30/parts/12421

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

[ 참조 및 출처 ]

부트 캠프 수업을 들은 후 여러 참고 자료를 통해 내용을 정리하였습니다.

https://medium.com/@sarinyaswift/intro-to-the-graph-data-structure-a8277c6a2ad9

https://velog.io/@alstn3265/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%8A%B8%EB%A6%AC

https://velog.io/@jeewoo1025/%ED%8A%B8%EB%A6%AC-Tree

|  요약

종류 구분 내용 언제 쓰는 게 좋을까
HashTable 특징 - 해싱 함수를 통해 키 -> 해시로 변환한다
- key + value
- 특정 키와 값을 연결할 때(알파벳-단어, 사전, 전화번호부)
- 중복을 제거해야할 때(전체 명단 중 투표 여부 확인)
장점 - 해시 충돌이 없을 때 검색, 삽입, 삭제가 빠르다
  (시간복잡도 : 평균 O(1), 최악 O(n))
- 보안성이 높다 (Key와 Hash간의 연관성X)
- 데이터 캐싱에 자주 사용된다.
- 중복 제거에 유리하다.
단점 - 해시 충돌 : key1과 key2가 해시함수를 거쳐 동일한 해시값(공간)을 가지는 상태
- 공간 복잡도가 커진다
- 순서가 있는 경우에는 맞지 않는다
- 해시 함수 의존도가 높다

- 해시 테이블은 키와 값을 대응시켜 저장하는 구조로, 키를 통해 빠르게 값을 찾을 수 있다는 장점을 가진다.

- 단순히 [ 키 : 값 ] 로 대응시키는 것이 아니라 "해싱"을 통해 키를 변환시킨다.

- 일상생활에서 해시 테이블은 구매내역이나 To-do 체크리스트, 그리고 영어사전에서도 볼 수 있다. 

 


물품 - 가격이 매칭


체크박스 - 한 줄 문자열 매칭

단어 - 뜻이 매칭
출처 : https://witchform.com/ 출처 : https://www.yesform.com/ 출처 : https://gonggam.korea.kr/

 

|  해시함수와 해시값 그리고 해시 충돌

1. 해싱이란?

키를 해시 함수(어떤 계산식)를 통해서 바꾸어 값에 접근하도록 하는 과정

- 키 : 입력값 -> 해시 함수 : 키를 해시 값으로 매핑 -> 해시 값 : 해시 테이블의 인덱스 -> 해시 테이블 : 키-값 저장하는 구조

키(keys) 해시 함수(Hash Function) 해시 값(Hash Value) 해시 테이블(Hash Table)
int key1 = 0 key % table.length(3) 0 100
int key2 = 10 1 200
int key3 = 20 2 300

 

2. 해시 충돌이란? 

서로 다른 키에 대한 해시 값이 동일한 경우, 같은 공간에 값을 저장하게되는 충돌이 발생하는 것.

* 위의 표에 키를 하나 더 넣어서 해시 충돌을 임의로 일으켜 보자.

키(keys) 해시 함수(Hash Function) 해시 값(Hash Value) 해시 테이블(Hash Table)
int key1 = 0 key % table.length(3) 0 100
int key2 = 10 1 200
int key3 = 20 2 300
int key4 = 30 0 400

서로 다른 키 값 0과 30의 해시 값, 곧 인덱스가 0으로 동일하다. 이렇게 될 경우, 새로 들어간 400이 0자리에 덮어써진다.

 

|  해시 충돌을 해결하는 방법

- 해시 충돌을 해결하는 방법은 구글을 검색했을 때 정말 많이 나왔는데, 그 중에서도 대표적으로 크게 두 가지,

[1] 개방 주소법 [2] 분리연결법 를 이야기하려고 한다.

개방주소법(Open Address) 비어 있는 공간을 찾아서 데이터를 저장하는 방법

- 하나씩 찾는다면? 선형 탐사법
- +n^2씩 찾는다면? 제곱 탐사법
- 해시 함수를 하다 더 쓴다면? 이중 해싱
분리연결법(Separate Chaining) 해시 테이블을 연결 리스트로 구성하는 방식

- 배열 + 연결리스트의 느낌
- 키의 해시 값(인덱스) - 연결리스트

해시값 연결리스트
a 0 amanda avril agatha aeron
b 1 bob billy    
c 2 chris caeser carl  
d 3 dominic      

 

1. 개방주소법

- 해시테이블을 구현한 클래스 MyHashtable이 있다고 하자. 

- 나머지는 다 생략하고 해시 함수만 놓고 보았을 때 클래스 내 코드는 아래와 같은 상태일 것이다.

- 이 상태에서 개방주소법의 여러가지 종류, 선형탐사법, 제곱탐사법, 이중해싱을 다뤄보려 한다.

class MyHashTable{

    Integer table[];

    // 해시 함수
    public int getHash(int key) {
        return key % this.table.length;
    }
}

 

[1] 선형 탐사법 (Linear Probing)

: 충돌이 난 지점부터 +1를 하여 값이 없는 곳에 값을 넣는다.

- 코드를 보면 좀 더 이해하기 쉬울 것 같다.

- 아래 코드에서 보면 else 부문 안에 충돌 지점부터 배열의 인덱스를 하나씩 올리는 걸 볼 수 있다. 이처럼 선형탐사법은 사실상 배열의 인덱스값을 하나씩 올려주면서 배열의 빈 공간을 찾아 순회하는 걸 말한다.

class MyHashTable{

    Integer table[];

    // 해시 함수
    public int getHash(int key) {
        return key % this.table.length;
    }
    
    // 선형 탐사법 : 충돌난 지점부터 +1을 해서 비어있는 곳을 찾는다.
    public void put(int key, int value) {
        int idx = getHash(key);
    
    	if (this.isFull()){
            System.out.println("it is full!");
            return;
        } else if (this.table[idx] == null){
            this.table[idx] = value; // 키의 해시값(인덱스)에 값이 없으면 넣기
        } else {
            int newIdx = idx;
            while (true) { // 충돌지점부터 +1하여 값이 없는 곳 찾기 
               newIdx = (newIdx + 1) % this.table.length; 
               if (this.table[newIdx] == null) {
                  break;
               }
            }
            this.table[newIdx] = value;
        }
    }
}

- 단점 : 일차 군집화 문제가 발생할 수 있다.

- 일차 군집화는 뭘까?

충돌이 발생했던 지점에서 +1을 해서 비어있는 곳을 찾다보면, 충돌된 지점의 주위로 어느새 데이터들이 몰리는 현상이 발생한다. 이런 상황을 일차 군집화라고 한다. 

 

[2] 제곱 탐사법 (Quadratic Probing)

: 충돌이 난 지점부터 n^2만큼의 간격을 두고 탐사하는 방법이다.

- 단점 : 이차 군집화 문제

- 이차 군집화란, 특정 구간에 데이터가 몰리는 현상이 이차적으로 발생한다는 의미이다. 일차 군집화와 의미는 거의 비슷한데 발생 시점이 두번째라는 게 차이라 보인다.

* 위의 코드에서 else {} 지점만 빼서 수정한다면 아래와 같다.

class MyHashTable{

    /* 생략 */
    
    // 제곱 탐사법 : 충돌이 난 지점부터 n^2로 탐사
    public void put(int key, int value) {
        
        /* 생략 */
        
        } else {
            int newIdx = idx;
            int n = 1;
            while (true) { 
               newIdx = (newIdx + (int)Math.pow(n, 2)) % this.table.length; 
               if (this.table[newIdx] == null) {
                  break;
               }
               n++;
            }
            this.table[newIdx] = value;
        }
    }
}

 

[3] 이중 해싱 (Double Hashing)

- 말이 참 어려운데 풀이하면 해싱을 한 번 더 하겠다는 의미이다.

- 첫번째 해시함수로는 최초의 해시(인덱스)를 구하고, 충돌이나면 두번째 해시함수로 이동 간격을 잡고 빈 곳을 찾는다.

- 마찬가지로 아래 코드를 보면, 해싱을 두번 하는 코드가 나오는 걸 볼 수 있는데, 두번째의 해싱에서는 해시테이블의 크기보다 조금 작은 소수를 통해 해시값을 %해주고, 거기에 다시 *1, *2, *3..을 하는 방식으로 이중해싱을 하는 걸 볼 수 있다. 이러한 방식으로 하다보면, 선형 탐사나 제곱 탐사보다는 데이터가 골고루 분포된다.

class MyHashTable{

    char p; // Hashtable의 size 보다 조금 작은 소수
    
    /* p를 구하는 함수 생략 */
    
    // 해시 함수1 - 최초 해싱
    public int getHash(int key) {
        return key % this.table.length;
    }
    
    // 해시 함수2 - 충돌 시 2차 해싱
    public int getHash2(int key) {
        int hash = 1 + key % this.p;
        return hash % this.table.length;
    }
    
    // 이중 해싱 : 충돌이 난 지점에서 2차 해싱
    public void put(int key, int value) {
        
        /* 생략 */
        
        } else {
            int newIdx = idx;
            int cnt = 1;
            while (true) { 
               newIdx = (newIdx + getHash2(newIdx) * cnt) % this.table.length; 
               if (this.table[newIdx] == null) {
                  break;
               }
               cnt++;
            }
            this.table[newIdx] = value;
        }
    }
}

 

2. 분리 연결법

- 충돌이 발생했을 때, 동일한 버킷(Bucket - 지점)에서 연결리스트 형태로 데이터를 연결하는 방식

- 위에서도 표로 잠깐 보여주었던 것처럼 충돌지점에 데이터를 이어 붙이는 걸 볼 수 있다.

https://en.wikipedia.org/wiki/Hash_table#Separate_chaining

- 이번에는 구현 코드보다 자바 라이브러리로 분리 연결법을 사용할 때를 예를 들어보려고 한다.

// 분리연결법을 사용한 경우1 : 이름을 연결할때
// ex. a - amanda, avril, aeron...
Hashtable<String, LinkedList<String>> nameTable = new Hashtable();

// 분리연결법을 사용한 경우2 : (이름, 전화번호)를 담는 2차 배열을 연결할 때
// es. a - (amanda, 123-234-2342), (avril, 123-234-2213)...
Hashtable<String, LinkedList<ArrayList<String>>> phoneTable = new Hashtable();

 

|  Java 해시맵과 해시테이블의 차이점

  HashTable HashMap
공통점 두 클래스 모두 Map인터페이스를 구현했다.
차이점 key에 null 사용하지 않음 key에 null 사용
Thread-Safe (멀티 스레드 환경에서 우수) Thread-Safe X (싱글 스레드 환경에서 우수)

* 참조 : 멀티스레드용 HashMap이 별도로 있다 -- synchronizedMap, ConcurrentHashMap 

 

++ 유용하게 쓸만한 HashMap 메소드 - putIfAbsent(), getOrDefault()

// HashMap의 getOrDefault()
int[] nums = {3, 2, 1, 1, 2, 3, 5};
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
	// getOrDefault(key, defaultValue) - val가 있으면 val가져오고, 아니면 default로 세팅
    map.put(num, map.getOrDefault(num, 0) + 1); 
}

HashMap<Character, Node> map2 = new HashMap<>();
String word = "apple";
for (int i = 0; i < word.length(); i++) {
    char c = word.charAt(i);
    // putIfAbsent(key, val) : key가 없으면 val생성하고, 있으면 넘어간다.
    map2.putIfAbsent(c, new Node());
}

 

|  해시 관련 문제집

https://school.programmers.co.kr/learn/courses/30/parts/12077

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

 

[ 참고 ] 

https://hbase.tistory.com/185

위키백과 https://en.wikipedia.org/wiki/Hash_table#Separate_chaining

https://baeharam.github.io/posts/data-structure/hash-table/

https://velog.io/@roro/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94

|  스택

종류 구분 내용 언제 쓰는 게 좋을까
Stack 특징 - 후입선출(LIFO)
- top / bottom
- push, pop, peek 
- 데이터가 입력된 순서의 역순으로
  처리되어야 할 때
Ex.
- 함수 콜스택,역추적,인터럽트 처리
- 재귀, DFS
- 괄호 검사, 후위 연산, 문자열 역순출력, 실행취소
장점 - top을 통한 접근, 삽입, 삭제가 빠르다.
  (시간복잡도 O(1))
단점 - top 이외의 데이터에 접근 불가(탐색x)

- 스택은 마치 옷을 겹겹히 쌓듯 데이터를 쌓아 올렸다가 맨 위에서부터 꺼내는 구조이다.

- 스택에 대한 알고리즘 문제를 보면, "괄호 검사", "후위 연산", "문자열의 역순 출력", "실행 취소"와 같이 

  [ 넣었다가 위에서부터 빼는 행위 ]를 통해 문자열의 특정 문자를 검사하고, 어떤 행위를 역전 시키는 걸 볼 수 있다.

- 일상 생활에서 볼 수 있는 또다른 스택의 예시는, 제가 좋아하는 프링글스...위에서부터 뽑아먹는 재미가...

 

https://www.acmicpc.net/workbook/view/325

 

문제집: 스택 (kmg8280)

 

www.acmicpc.net

 

|  큐

종류 구분 내용 언제 쓰는 게 좋을까
Queue 특징 - 선입선출(FIFO)
- front / rear 
- enqueue(offer, add), dequeue(poll, remove) 
- 데이터를 순차적으로 삽입하고 꺼낼 때
Ex.
- 요세푸스 순열, 프로세스 관리, 대기 순서 관리(프린트 대기열)
- BFS
장점 - front/rear를 통한 접근, 삽입, 삭제가 빠르다.
  (시간복잡도 O(1))
단점 - 중간 위치의 데이터에 접근 불가

- 큐는 스타벅스 앞에 줄을 서고 있는 사람들처럼 먼저 들어간 데이터가 먼저 빠지는 구조이다.

- 원형 큐는 원형 연결리스트와 구조적으로 동일하다. 앞과 끝은 있으나 그걸 원형으로 벤딩해놓은 형태. 

  (자바에서는 그러나 원형 큐도 딱히 지원하지 않기 때문에 차라리 데크를 쓰는 편이 수월한 것 같다.)

- 자바에서는 큐는 인터페이스로 제공하고, 큐를 구현하는 객체를 쓸 수 있는데, LinkedList를 쓰면 된다.

- 한 가지 알아두어야 할 점은, 큐에 데이터를 삭제할 때

   poll()를 쓰면 데이터가 없어도 null을 반환하지만,

   remove()를 쓰면 데이터가 없을 때 예외가 발생하기 때문에, 안전성을 위해서 remove()를 쓰는 게 더 좋다는 것!

 

https://www.acmicpc.net/workbook/view/1504

 

문제집: Queue (jhlee1108)

 

www.acmicpc.net

 

|  데크

종류 구분 내용 언제 쓰는 게 좋을까
Deque 특징 = double-ended queue, stack + queue
- 양쪽에서 삽입 삭제가 가능
- front / rear
- add(offer), remove(poll) 
- addFirst, addLast, removeFirst, removeLast
- 데이터를 앞, 뒤쪽에서 삽입/삭제 처리 필요할 때

Ex.
팰린드롬 찾기, 카드묶음의 순서 변경하기 등
장점 - front / rear 양쪽을 통한 삽입, 삭제가 빠르다 
  (시간복잡도 O(1))
- Stack, Queue와 달리 idx를 통해 임의 원소에 접근 가능
- 새로운 원소 삽입 시 메모리를 재할당하지 않고 메모리 블록을 재할당하여 삽입한다.(chunk 사용)
단점 - 중간 위치에 데이터를 삽입, 삭제가 어렵다
Scroll - 입력제한 데크
- 한 쪽의 입력을 제한한 데크
Shelf - 출력제한 데크
- 한 쪽의 출력을 제한한 데크

- 데크는 마치 양 옆이 열린 파이프와 같은 형태라고 생각하면 쉬운 것 같다.

- 양쪽에서 데이터를 삽입하고 삭제할 수 있으며, 필요에 따라 양 옆을 제한 시켜 사용할 수도 있다.

- 데크 또한 큐와 마찬가지로 삭제를 할 때, remove()를 쓰는 것이 안전성의 측면에서 더 좋다. 

 

|  자바 컬렉션의 시간 복잡도

1. Queue

  offer() peak() poll() size()
LinkedList O(1) O(1) O(1) O(1)
ArrayDequeue O(1) O(1) O(1) O(1)
PriorityQueue O(log n) O(1) O(log n) O(1)

 

2. Stack

  push() peek() pop() size()
Stack O(1) O(1) O(1) O(1)

 

|  요약

종류 구분 내용 언제 쓰는 게 좋을까
LinkedList


특징 - 각 노드가 연결된 상태
- 각 노드의 메모리 주소는 항상 연속적이지 않다.
- head / tail (양방향 이상)
- 자료의 삽입과 삭제가 많은 경우
장점 - 데이터의 삽입 및 삭제가 잦을 때 유리
  * 삽입, 삭제 시 새로운 배열 생성 없이 링크만 연결한다.
단점 - 특정 인덱스를 조회하는 데 시간이 O(n)만큼 소요된다.
  * 처음~끝까지 인덱스를 돌기 때문에 Array보다 느리다.
양방향 - 각 노드가 prev / next 두 가지의 연결점을 가진다.
원형 - head -> tail -> head 식으로 원형으로 연결된 상태

- 연결리스트는 각 노드가 연결된 상태로, 삽입과 삭제가 용이하다.

- 다만, 연결리스트는 조회에는 취약한데 그 이유는 처음부터 노드를 순회해야 원하는 타켓을 조회할 수 있기 때문이다.

 

|  단방향 연결리스트, 양방향 연결리스트, 원형 연결리스트

- 연결리스트는 기본적으로 각각의 노드가 데이터와 포인터를 가진 구조로 되어있다.

- 단방향 연결리스트는 아래와 그림과 같이 각 노드가 next 포인터를 통해 한 방향으로만 연결된 구조인데

  이러한 구조에서는 head노드가 있고, tail은 없는 상태이다.

- 양방향 연결리스트는 아래의 두번째 그림과 같이 각 노드에 prev, next 포인터가 각각 있는 상태를 말한다.

  이 구조에서는 head 노드도 있으며, tail 노드도 존재한다.

  왜냐하면, 각각의 노드 뿐 아니라 전체적인 방향 자체가 양방향이기 때문에 tail이 필요하기 때문

- 원형 연결리스트는 tail 노드의 next가 head를 향하고 있는 상태로, 결과적으로 리스트를 순회하는 구조이다.

  만약, 양방향 연결까지 더해준다면 시계방향, 반시계방향 모두로 순회가 가능하다.

 

단방향 양방향 원형

* 이미지 출처 : 위키 백과

 

|  연결리스트의 시간 복잡도

- 연결리스트의 삽입과 삭제는 O(1) 의 시간 복잡도를 갖는다. 

  단순히 노드 사이에 연결만 이루어지면 되기 때문이다.

- 하지만 조회시에는 O(n)의 시간복잡도를 갖는다는 단점이 있다.

  배열과 같이 인덱스가 있는 것이 아니라 노드의 head부터 타겟지점까지 모두 순회해야 하기 때문이다.

  add() remove() get() contains()
ArrayList O(1) or O(n) O(1) or O(n) O(1) O(n)
LinkedList O(1) O(1) O(n) O(n)

 

|  연결리스트와 큐, 데크

- 연결리스트는 큐와 구조적으로 동일하다고 볼 수 있는데, 이유는 큐와 마찬가지로 선입선출이 가능하기 때문이다.

- 자바에서는 Java.util 라이브러리의 LinkedList를 통해 연결리스트를 사용할 수 있으며, 단방향연결리스트만 제공한다.

- 만약, 양방향 연결리스트가 필요한 경우, Doubly-LinkedList를 별도로 구현하거나 단방향을 활용하는 게 필요하다.

- 만약, 원형 연결리스트가 필요하다면, Deque를 대안적으로 사용할 수 있다.

 

|  연결리스트 문제집

https://www.acmicpc.net/workbook/view/1066

 

문제집: Linked List (DryType)

 

www.acmicpc.net

 

 

[ 참고 ] 

https://hbase.tistory.com/185

https://ko.wikipedia.org/wiki/%EC%97%B0%EA%B2%B0_%EB%A6%AC%EC%8A%A4%ED%8A%B8

|  요약

종류 구분 내용 언제 쓰는 게 좋을까
Array 특징 - 정해진 크기만큼 공간 할당
- 데이터가 연속적으로 하나씩 저장되어 있다.
- 데이터와 인덱스가 1 : 1로 매칭
- 많은 양의 데이터를 다룰때
- 특정 위치 자료를 빠르게 선택할때
장점 - 인덱스를 통해 빠르게 데이터에 접근할 수 있다.
단점 - 길이를 미리 설정해야 한다.
ArrayList 특징 - 가변 길이 배열
- 삽입 & 삭제 시, 배열을 새로 생성하고 복사한다.
장점 - 크기가 고정되어 있지 않다 (유동적)
단점 - 처음 또는 중간에 데이터를 삽입하거나 삭제할 때마다
  배열을 새로 생성&복사하거나,
  데이터를 모두 앞으로 미뤄야 한다. 

 

|  ArrayList은 어떻게 가변적으로 데이터를 관리하나

- ArrayList는 java.util 패키지 안에 들어있다.

- 예전에는 Vector라는 객체를 통해 ArrayList를 사용했는데, 버전이 높아지면서 향상된것이 ArrayList이다.

- ArrayList는 사실상 배열인데,

  공간 제약 없이 데이터를 삽입하고 추가하면서 배열 길이가 늘어나고 감소하도록 한 가변배열이다.

 

더보기

  1. ArrayList는 사실상 배열이고, 데이터가 늘어날 때마다 배열을 새로 만든다.  

자바 라이브러리에 들어있는 ArrayList 코드를 분석해보면 아래와 같았다.

[1] ArrayList의 디폴트 길이 10 - 그렇지만 중요한 건 실제 데이터의 양

처음 ArrayList를 생성할 때 디폴트 길이(size)는 10으로 되어 있는 걸 볼 수 있는데,

말만 디폴트이지, 생성과 동시에 데이터를 아무것도 넣지 않았다면 메모리상 실제 ArrayList의 길이는 0이다.

private static final int DEFAULT_CAPACITY = 10; //디폴트 길이

private static final Object[] EMPTY_ELEMENTDATA = {}; // 아무것도 안 들어가있다.

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 아무것도 안 들어가있다.

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA; // 아무것도 안 들어가있다.
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 아무것도 안 들어가있다.
}

>> 그렇다면 아래 코드에서 사이즈는 몇일까?

ArrayList<Integer> list = new ArrayList<>(10);
list.add(1);
list.add(2);
System.out.println(list.size());

>>> 답은 2.

-- 초기값을 지정해준다고 해도, 중요한 건 실제 데이터의 양이었다.

-- 결국 size는 지정해줘봤자 실제로 데이터가 들어간 만큼만 나온다.

-- 그런데도 지정해주는 이유는 뭘까? 이유는 ArrayList 크기를 미리 지정해두는 게, 계속해서 배열을 생성하고 복사하는 과정을 줄일 수 있기 때문이다. 

 

[2] 길이의 자동 변경? - 사실은 새로운 크기의 배열을 생성하고 기존걸 복사하는 것

- 아래 java.util.ArrayList의 add(E e, Object[] elementData, int s) 코드를 보면, 

- 배열 공간을 넘어설 때, Arrays.copyOf(T[] original, int newLength)로 결국 배열을 새로 생성하는 걸 볼 수 있다. 

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow(); //아래의 grow()를 보자
    elementData[s] = e;
    size = s + 1;
}
    
private Object[] grow(int minCapacity) { 
    return elementData = Arrays.copyOf(elementData, // 결국 배열을 또 만든다
                                       newCapacity(minCapacity));
}

 

  2. ArrayList의 데이터를 추가하고 삭제할때  

[1] 데이터를 추가할 때

- 1-[2]에서 본 것처럼 배열은 데이터를 추가할 때 공간(크기)을 벗어나면 새로운 배열을 생성하고, 복사한다.

위치 배열 길이 벗어나지 않을 때 배열 길이 벗어날 때
처음 또는 중간 우측의 2) - 3)만  1) 길이 + 1의 배열 생성
2) 처음 ~ 이전 끝까지 복사
3) idx + 1부터 이전 배열의 idx ~ 끝까지 복사  
우측의 2) - 3)만  1) 길이 + 1의 배열 생성
2) 처음 ~ 이전 끝까지 복사
3) 끝에 새로운 데이터 추가
// 끝에 삽입 시 : 배열 새로 생성 + 처음부터 끝까지 복사 + 새 데이터 추가하기
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow(); //배열 새로 생성 + 복사
    elementData[s] = e; //끝에 새로운 데이터 추가
    size = s + 1;
}

// 중간 삽입 시 : 배열 새로 생성 + 특정 index ~ 끝까지 복사 + 새 데이터 끼우기
public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow(); //배열 새로 생성 + 복사
    System.arraycopy(elementData, index, //넣을 자리부터 ~ 끝까지 데이터를 
                     elementData, index + 1, //넣을 자리 + 1 위치에 
                     s - index); //전체 사이즈에서 넣을 자리 뺀만큼 복사
    elementData[index] = element; //넣을 자리에 새로운 데이터 넣기
    size = s + 1;
}

 

[2] 데이터를 삭제할 때

시작하기 전에 잠시 전급하자면 ArrayList의 배열 타입은 Object[]이다.

- ArrayList를 쓸 때 정수형 데이터(int)를 boxing해야하는 경우들이 있었다.

- 예를 들어, 배열스트림-->List로 변경할때, Arrays.stream(arr).boxed().collect(Collectors.toList()) 

- 그 외에도, forEach문을 쓸 때 때때로 boxing을 해줄 때가 있었는데, 그 이유는 ArrayList의 배열 타입이 Object[]여서 였다.

 

이제 다시 데이터 삭제로 돌아가자.

ArrayList에서 "삭제"란 데이터를 실제로 "없앤다"기 보다는 "참조값(주소)"를 바꾸는 걸 얘기한다

>> 예를 들어, 중간 데이터를 삭제할 때는 중간 데이터 이후부터 데이터들을 앞으로 미루고,

>> 마지막 인덱스를 null로 바꿔주는 걸 볼 수 있다.

 

참고로 데이터를 삭제할 때에는 인자로 null 값까지도 확인을 하는데, null을 삭제해달라 요청하면 true를 반환한다.

위치 방법
처음 또는 중간 1) 삭제하려는 데이터가 처음으로 나타나는 지점(idx) 확인
2) idx + 1 ~ 끝까지를 idx위치로 이동
3) 끝을 null로 변경
1) 끝 인덱스를 null로 변경
// 실제로는 제거가 아니라, 마지막 인덱스를 null로 변경하는 것
private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i) //(처음포함)중간 인덱스를 제거하는 거라면
        System.arraycopy(es, i + 1, es, i, newSize - i); //인덱스 다음~끝을 인덱스자리에 복사
    es[size = newSize] = null; //끝 인덱스를 null로 변경
}

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: { //데이터가 있는지 없는지 확인
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i); //있으면 fastRemove()
    return true;
}

- 마찬가지로 clear도 같은 원리로 모든 데이터 위치를 null로 변경한다.

public void clear() {
    modCount++;
    final Object[] es = elementData;
    for (int to = size, i = size = 0; i < to; i++)
        es[i] = null;
}

 

  3. 배열이나 ArrayList를 스택을 옆으로 누인 형태로 쓸 수 있다.  

- 스택은 LIFO, 나중에 들어간 데이터를 빼주는 형태다 

- ArrayList는 추가할 때 (공간 내에서) 처음부터 순차적으로 추가하면 새로운 배열을 생성하고 카피하지 않는다. 

- ArrayList를 삭제할 때 끝부터 삭제하면 데이터를 카피하는 작업 없이 null처리만으로도 삭제가 가능하다. 

- 결국 배열이나 ArrayList도 스택처럼 사용이 가능하다는 의미이다.

 

  4. ArrayList 구현하기  

이제 위의 원리를 토대로 ArrayList를 다시 구현해보면 아래와 같다.

import java.util.Arrays;

class MyArray {
    int[] arr;
    
    int size; // 현재 저장된 실제 사이즈

//  배열의 초기 사이즈 설정
    MyArray(int initialSize) {
        arr = new int[initialSize];
    }

//  배열에 데이터 삽입
    // 끝에 삽입
    public void add(int data) {
        if (size == arr.length) {
            arr = Arrays.copyOf(arr, size + 1);
        }
        arr[size] = data;
        size++;
    }

    // 특정 인덱스에 삽입
    public void add(int idx, int data) {

        if (idx > arr.length - 1) {
            this.add(data);
        } else if (idx >= 0){

            int[] newArr = arr.clone();
            int newSize = size;

            if (size == arr.length){
                newArr = Arrays.copyOf(arr, size + 1);
                newSize++;
            }

            System.arraycopy(arr, idx, newArr, idx + 1, size - idx);
            newArr[idx] = data;
            arr = newArr;
            size = newSize;

        } else {
            System.out.println("Idx Error!");
        }
    }

//  배열에서 데이터 삭제
    // 특정 데이터 삭제
    public boolean remove(int data) {

        int idx = 0;
        boolean hasData = false;

        for (int i = 0; i < size; i++) {
            if (arr[i] == data) {
                idx = i;
                hasData = true;
                break;
            }
        }

        if (hasData) {
            fastRemove(idx);
        } else {
            System.out.println("해당 데이터가 없습니다.");
        }

        return hasData;
    }

    // 실질적인 삭제
    public void fastRemove(int idx) {

        int[] newArr = new int[size - 1];

        if (idx < size - 1) { //처음 또는 중간 인덱스 제거시

            int newIdx = 0;

            for (int i = 0; i < idx; i++){
                newArr[newIdx++] = arr[i];
            }

            for (int i = idx + 1; i < size; i++){
                newArr[newIdx++] = arr[i];
            }

        }else{ //마지막 인덳 제거시
            for (int i = 0; i < idx; i++){
                newArr[i] = arr[i];
            }
        }
        arr = newArr;
        size = size - 1;
    }


}

 

 

|  배열과 ArrayList 사용하기

배열과 ArrayList는 인덱스와 데이터를 1 : 1로 매칭하기 때문에 아래와 같을 때에 유리하다.

- (선형 구조에서)LIFO 방식으로 데이터를 삽입/삭제할 때(add, remove -- 이때는 O(1))

- 많은 양의 데이터에서 특정 index의 데이터를 선택해야할 때(get)

 

그럼 언제 배열을 쓰는 걸 지양해야 할까?

- (선형 구조에서)중간 인덱스에 삽입/삭제가 빈번할 때(add, remove -- 이때는 O(n))

- 상대적으로 많지 않은 데이터에서 특정 값의 데이터가 있는지 찾을 때(contains)

 

ex. 그래프의 경우, 많은 양의 데이터를 다루면서 간선 수가 적을 경우 배열 보다는 연결리스트를 사용한다.

ex. 중복된 데이터들이 아닌 수열에서 특정 값을 찾을 때 곧, contains()를 쓸 때, HashSet을 쓰는게 오히려 빠르다(O(1))

 

|  자바 ArrayList 의 시간 복잡도

  add() * remove() * get() contains()
ArrayList O(1) or O(n) O(1) or O(n) O(1) O(n)

* add() 또는 remove()의 경우, 사이즈를 지정하지 않고 중간에 데이터를 삽입/삭제할 경우,

  새롭게 배열을 만들고 데이터를 삽입하기 때문에 O(1)이 아니라 O(n)의 시간 복잡도를 갖는다.

 

 

[ 참고 ]

시간 복잡도 - https://hbase.tistory.com/185

삽입 삭제 - https://webprogramcustom.tistory.com/47

|  자릿수와 경우의 수

(1) X진수의 K자릿수에 대한 모든 경우의 수

어떤 연속된 10진수 수 3개에 대한 배열이 있다고 가정할 때,

각 자리에 올 수 있는 수의 범위는 0 ~ 9이다. 

0 0 0
0 0 1
0 0 2
0 0 3
0 0 4
0 0 5
0 0 6
0 0 7
0 0 8
0 0 9

..... (생략) 

 

10진수의 세 자릿수에 대한 모든 경우의 수는 아래와 같다 

--> 10 x 10 x 10 = 1000 (곧, 000 ~ 999)

 

결국 x 진수의 k자릿수에 대한 모든 경우의 수는

--> x * x * x.... = x^k (자릿수 하나당 0 ~ x-1)

 

(2) x진수의 각 자릿수 하나당 0 ~ x-1 범위를 가지며, x - 1를 넘어서면 올림이 이루어진다.

- 10진수일 때

1 1 8
1 1 9
1 2 0

- 8진수일 때

1 1 8
1 2 0
1 2 1

 

- 10진수인 세자리수 ㅁㅁㅁ 에 올 수 있는 모든 경우의 수는 10^3개였다.

- 8진수인 세자리수 ㅁㅁㅁ 에 올 수 있는 모든 경우의 수는 8^3개이다.

- N진수인 세자리수 ㅁㅁㅁ 에 올 수 있는 모든 경우의 수는 N^3개이다. 

- 복합진수(a,b,c)인 세자리수 ㅁㅁㅁ 에 올 수 있는 모든 경우의 수는 (a x b x c)개이다.

 

(3) 복합 진수의 전체 범위 : 0 ~ (a x b x c) - 1

- 8진수 세자릿수

전체 범위 :  0부터 8^3 - 1  

 

- 복합진수일 때

전체 범위 :  0부터 (a x b x c) - 1 

 

|  코드 구현

문제 :

세 개의 교실 a, b, c에 수용 가능 인원수가 4, 3, 4 일 때, 각 교실에 배치할 수 있는 인원수의 모든 경우의 수를 나열하시오.

public class Test06_05_2_jarik {
    public static void main(String[] args) {

        int[] digit = {4, 3, 4}; // 각 수용인원
        int allCases = Arrays.stream(digit).reduce(1, (x, y) -> x*(y+1)); // 각 자리에 올 수 있는 모든 경우의 수

        System.out.println("abc abc abc abc abc abc abc abc abc abc");
        for (int i = 0; i < allCases; i++) { // 000~allCases-1

            int[] buff = new int[3]; // 세자릿수 버퍼 역할

            long denominator = allCases; // 분모 : allCases
            long numerator = i; // 분자 : 000, 001, 002 .....

            for (int j = 0; j < 3; j++) { //주어진 수 i(0~allCases-1)에 대해
                denominator /= digit[j] + 1; // 자릿수를 맞추고
                buff[j] = (int) (numerator / denominator) ; // 자릿수의 수 구하기 ex. 999/100
                numerator %= denominator; // 자릿수 외 나머지 구하기 ex. 999%100
            }

            // 출력
            String result = String.format("%d%d%d", buff[0], buff[1], buff[2]);
            if ((i + 1) % 10 == 0) {
                System.out.println(result);
            }else {
                System.out.print(result + " ");
            }
        }
    }
}

>> 결과

abc abc abc abc abc abc abc abc abc abc
000 001 002 003 004 010 011 012 013 014
020 021 022 023 024 030 031 032 033 034
100 101 102 103 104 110 111 112 113 114
120 121 122 123 124 130 131 132 133 134
200 201 202 203 204 210 211 212 213 214
220 221 222 223 224 230 231 232 233 234
300 301 302 303 304 310 311 312 313 314
320 321 322 323 324 330 331 332 333 334
400 401 402 403 404 410 411 412 413 414
420 421 422 423 424 430 431 432 433 434

 

|  합성수와 소수

합성수 나누어떨어지는 수가 하나라도 존재하는 수 ex. 100 = 1 x 100, 2 x 50, 4 x 25, 5 x 20, 10 x 10... 
소수 오직 1과 자기 자신만으로 나누어떨어지는 수 ex. 3 = 1 x 3

 

소수에 관한 알고리즘 문제는 보통 아래의 세가지가 있다고 하는데요.

 

1. 자연수 N이 소수인지 판별하거나, 

2. 1~N까지 중 소수의 갯수를 구하거나

3. 1 ~ N 까지의 소수를 나열하는

 

이 중에서 먼저 1.자연수 N이 소수인지 판별하는 법을 이야기해 보려고 합니다.

 

|  N을 2부터 N - 1 까지 모두 나누기

[1] 소수는 1과 자기 자신을 제외한 다른 수로는 나누어떨어져서는 안됩니다. 

 

그것을 판별하기 위한 코드는 아래와 같은데요. 2부터 N - 1가 N의 약수인지 확인하는 과정입니다.

public static boolean solution(int n) {

    // 1은 소수가 아니다.
    if (n == 1) {
        return false;
    }

    for (int i = 2; i < n; i++) { 
        if ((n % i) == 0) {
           return false;
        }
    }

    return true;
}

 

[2] 여기에서 합성수의 성질을 이용해 2 ~ N - 1이 아닌 2 ~ 루트N까지로 소수 여부를 확인할 수 있어요.

 

- 예를 들어, N이 100이라고 할 때, 1과 자기자신인 100을 제외한 약수를 구하면 아래 표와 같아요.

- 보면 10 x 10 을 전후로 a x b 가 b x a로 바뀌는 걸 볼 수 있습니다.

- 우리는 N이 합성수가 아닌지를 보려는 것이므로, 루트N까지 a가 있는지 확인하면 됩니다.

 

2x50
4x25
5x20
10x10
20x5
25x4
50x2

 

- 그럼 코드는 아래와 같이 변경됩니다.

public static boolean solution(int n) {

    // 1은 소수가 아니다.
    if (n == 1) {
        return false;
    }

    for (int i = 2; i <= (int)Math.sqrt(n); i++) { 
        if ((n % i) == 0) {
           return false;
        }
    }

    return true;
}

 

이번에는 2. 1~N까지 중 소수의 갯수를 구하는 법을 얘기하려 합니다.

2.3. 1 ~ N까지 범위의 소수의 갯수나 소수 나열을 위한 알고리즘은 여러가지가 있으나

그 중에서도 가장 많이 사용되는 것이 에라토스테네스의 체라고 해요.

 

|  에라토스테네스의 체

- 소수는 2가 아닌 짝수가 나올 수가 없습니다.

- 그리고, 각 소수 2, 3, 5, 7의 배수면 소수가 아니죠.

- 에라토스테네스는 체에 거르듯이 소수가 아닌 수들을 거르는 알고리즘을 말합니다.

 

에라토스테네스의 체 알고리즘

  1. 2부터 소수를 구하고자 하는 구간의 모든 수를 나열한다. 그림에서 회색 사각형으로 두른 수들이 여기에 해당한다.
  2. 2는 소수이므로 오른쪽에 2를 쓴다. (빨간색)
  3. 자기 자신을 제외한 2의 배수를 모두 지운다.
  4. 남아있는 수 가운데 3은 소수이므로 오른쪽에 3을 쓴다. (초록색)
  5. 자기 자신을 제외한 3의 배수를 모두 지운다.
  6. 남아있는 수 가운데 5는 소수이므로 오른쪽에 5를 쓴다. (파란색)
  7. 자기 자신을 제외한 5의 배수를 모두 지운다.
  8. 남아있는 수 가운데 7은 소수이므로 오른쪽에 7을 쓴다. (노란색)
  9. 자기 자신을 제외한 7의 배수를 모두 지운다.
  10. 위의 과정을 반복하면 구하는 구간의 모든 소수가 남는다.

출처 : 위키백과

 

- 코드는 그러면 아래와 같이 나옵니다.

public static int solution(int n) {

    int[] numArr = new int[n];

    // 1로 초기화 (0, 1 제외)
    for (int i = 2; i < n; i++) {
        numArr[i] = 1;
    }

    // 2 ~ 루트n까지의 수들이 N의 약수인지 확인한다
    for (int i = 2; i <= (int)Math.sqrt(n); i++) {
        if (numArr[i] == 0) continue; // 이미 검사된 수 건너띄기

        // 자기자신을 제외한 모든 i의 배수 제외
        for (int j = i * 2; j < n; j+=i) { // i : 3, j : 6, 9, 12
            numArr[j] = 0;
        }
    }

    return Arrays.stream(numArr).sum();
}

 

 

[참조]

https://rebro.kr/46?category=449699 

https://ko.wikipedia.org/wiki/%EC%97%90%EB%9D%BC%ED%86%A0%EC%8A%A4%ED%85%8C%EB%84%A4%EC%8A%A4%EC%9D%98_%EC%B2%B4

|  자료구조란?

- 자료를 목적에 따라 효율적으로 관리하기 위한 구조이다.

- 데이터를 CRUD할 때 유용하다.

- 적절히 사용하면 속도를 높히고, 메모리 사용도 줄일 수 있다.

 

|  자료구조의 분류

분류 특징 종류
선형 자료구조 데이터가 1:1관계로 들어가있다. 배열, 연결리스트, 스택/큐/데크, 해시테이블
비선형 자료구조 데이터가 1:N, N:N 관계로 들어가 있다. 트리, 그래프, 힙/우선순위 큐, 트라이 

* 선형 자료구조는 기차처럼 길쭉하게 하나의 몸으로 구성되어 있다.

* 자료구조를 직접 구현하여(=추상 자료형) 사용할 수도 있다.

 

|  선형 자료 구조

종류 구분 내용 언제 쓰는 게 좋을까
Array 특징 - 정해진 크기만큼 공간 할당
- 데이터가 연속적으로 하나씩 저장되어 있다.
- 데이터와 인덱스가 1 : 1로 매칭
- 많은 양의 데이터를 다룰때
- 특정 위치 자료를 빠르게 선택할때
장점 - 인덱스를 통해 빠르게 데이터에 접근할 수 있다.
단점 - 길이를 미리 설정해야 한다.
ArrayList 특징 - 가변 길이 배열
- 삽입 & 삭제 시, 배열을 새로 생성하고 복사한다.
장점 - 크기가 고정되어 있지 않다 (유동적)
단점 - 처음 또는 중간에 데이터를 삽입하거나 삭제할 때마다
  배열을 새로 생성&복사하거나,
  데이터를 모두 앞으로 미뤄야 한다. 
LinkedList


특징 - 각 노드가 연결된 상태
- 각 노드의 메모리 주소는 항상 연속적이지 않다.
- head / tail (양방향 이상)
- 자료의 삽입과 삭제가 많은 경우
장점 - 데이터의 삽입 및 삭제가 잦을 때 유리
  * 삽입, 삭제 시 새로운 배열 생성 없이 링크만 연결한다.
단점 - 특정 인덱스를 조회하는 데 시간이 O(n)만큼 소요된다.
  * 처음~끝까지 인덱스를 돌기 때문에 Array보다 느리다.
양방향 - 각 노드가 prev / next 두 가지의 연결점을 가진다.
원형 - head -> tail -> head 식으로 원형으로 연결된 상태
Stack 특징 - 후입선출
- top / bottom
- push, pop, peek 
- 데이터가 입력된 순서의 역순으로
  처리되어야 할 때
Ex.
- 함수 콜스택,역추적,인터럽트 처리
- 재귀, DFS
- 괄호 검사, 후위 연산, 문자열 역순출력, 실행취소
장점 - top을 통한 접근, 삽입, 삭제가 빠르다.
  (시간복잡도 O(1))
단점 - top 이외의 데이터에 접근 불가(탐색x)
Queue 특징 - 선입선출
- front / rear 
- enqueue(offer), dequeue(poll) 
- 데이터를 순차적으로 삽입하고 꺼낼 때
Ex.
- 요세푸스 순열, 프로세스 관리, 대기 순서 관리(프린트 대기열)
- BFS
장점 - front/rear를 통한 접근, 삽입, 삭제가 빠르다.
  (시간복잡도 O(1))
단점 - 중간 위치의 데이터에 접근 불가
Deque 특징 = double-ended queue, stack + queue
- 양쪽에서 삽입 삭제가 가능
- front / rear
- add(offer), remove(poll) 
- addFirst, addLast, removeFirst, removeLast
- 데이터를 앞, 뒤쪽에서 삽입/삭제 처리 필요할 때

Ex.
팰린드롬 찾기, 카드묶음의 순서 변경하기 등
장점 - front / rear 양쪽을 통한 삽입, 삭제가 빠르다 
  (시간복잡도 O(1))
- Stack, Queue와 달리 idx를 통해 임의 원소에 접근 가능
- 새로운 원소 삽입 시 메모리를 재할당하지 않고 메모리 블록을 재할당하여 삽입한다.(chunk 사용)
단점 - 중간 위치에 데이터를 삽입, 삭제가 어렵다
Scroll - 입력제한 데크
- 한 쪽의 입력을 제한한 데크
Shelf - 출력제한 데크
- 한 쪽의 출력을 제한한 데크
HashTable 특징 - 해싱 함수를 통해 키 -> 해시로 변환한다
- key + value
- 특정 키와 값을 연결할 때(알파벳-단어, 사전, 전화번호부)
- 중복을 제거해야할 때(전체 명단 중 투표 여부 확인)
장점 - 해시 충돌이 없을 때 검색, 삽입, 삭제가 빠르다
  (시간복잡도 : 평균 O(1), 최악 O(n))
- 보안성이 높다 (Key와 Hash간의 연관성X)
- 데이터 캐싱에 자주 사용된다.
- 중복 제거에 유리하다.
단점 - 해시 충돌 : key1과 key2가 해시함수를 거쳐 동일한 해시값(공간)을 가지는 상태
- 공간 복잡도가 커진다
- 순서가 있는 경우에는 맞지 않는다
- 해시 함수 의존도가 높다

* queue, deque에서 poll을 사용하면 비어있는 공간일지라도 null을 반환한다. 그러나 remove를 사용하면 예외가 발생

  (offer / poll 보다는 add / remove 를 사용하는 것이 안정성면에서 더 좋다)

 

|  자바 컬렉션들의 시간 복잡도

1. List

  add() remove() get() contains()
ArrayList O(1) or O(n) O(1) or O(n) O(1) O(n)
LinkedList O(1) O(1) O(n) O(n)

2. Set

  add() contains() next()
HashSet O(1) O(1) O(h/n)
LinkedHashSet O(1) O(1) O(1)
TreeSet O(log n) O(log n) O(log n)

3. Map

  add() containsKey() next()
HashMap O(1) O(1) O(h/n)
LinkedHashMap O(1) O(1) O(1)
TreeMap O(1) O(1) O(h/n)

4. Queue

  offer() peak() poll() size()
LinkedList O(1) O(1) O(1) O(1)
ArrayDequeue O(1) O(1) O(1) O(1)
PriorityQueue O(log n) O(1) O(log n) O(1)

 

 

[ 참조 ] 

스택, 큐

- https://velog.io/@nnnyeong/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%8A%A4%ED%83%9D-Stack-%ED%81%90-Queue-%EB%8D%B1-Deque

데크

- https://hbase.tistory.com/128

- https://jee-young.tistory.com/31

해시테이블

- https://hee96-story.tistory.com/48

-https://velog.io/@roro/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%ED%95%B4%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94

- https://jtoday.tistory.com/73

자바 컬렉션의 시간 복잡도

- https://hbase.tistory.com/185

+ Recent posts