인프런의 영리한 프로그래밍을 위한 알고리즘 강좌를 보고 작성한 문서입니다.


좋은 해쉬 함수란?

  • 현실에서는 키들이 랜덤하지 않음

  • 만약 키들으 티오계적 분포에 대해 알고 있담녀 이를 이용해서 해쉬 함수를 고안하는 것이 가능하겠지만 현실적으로 어려움

  • 키들이 어떤 특정한 (가시적인) 패턴을 가지더라도 해쉬함수값이 불규칙적이 되도록 하는게 바람직

    • 해쉬함수값이 키의 특정 부분에 의해서만 결정되지 않아야 한다.(학번처럼 앞 부분이 많이 중복되는 경우 문제가 생길 수 있다.)

해쉬 함수

Division 기법

  • h(k) = k mod m

  • 예: m = 20 and k = 91 => h(k) = 11

  • 장점: 한번의 mod연산으로 계산. 따라서 빠름

  • 단점: 어떤 m값에 대해서는 해쉬 함수값이 키값의 특정 부분에 의해서 결정되는 경우가 있음. 가령 m=2^p이면 키의 하위 p비트가 해쉬 함수값이 됨

multiplication 기법 :

  • 0에서 1사이의 상수 A를 선택: 0<A<1

  • kA의 소수부분만을 택한다.

  • 소수 부분에 m을 곱한 후 소수점 아래를 버린다.

예: m=8, word size = w = 5, k = 21

  • A = 13/32를 선택

  • kA = 21*13/32 = 273/32 = 8 + 17/32

  • m (kA mod 1) = 8 * 17/32 = 17/4 = 4.~

  • 즉, h(21) = 4


Hash code in Java

  • Java의 Object 클래스는 hashCode() 메서드를 가짐. 따라서 모든 클래스는 hashCode() 메서드를 상속받는다. 이 메서드는 하나의 32비트 정수(음수 포함)를 반환한다.

  • 만약 x.equals(y)이면 x.hashCode()==y.hashCode()이다. 하지만 역은 성립하지 않는다.

  • Object 클래스의 hashCode() 메서드는 객체의 메모리 주소를 반환하는 것으로 알려져 있음 (but it’s implementation-dependent)

  • 필요에 따라 각 클래스마다 이 메서드를 override하여 사용한다.

    • 예) Integer 클래스는 정수값을 hashCode로 사용

해쉬함수의 예: hashCode() for Strings in Java

public final class String
{
  private final char[] s;
  ...

  public int hashCode()
  {
      int hash = 0;
      for ( int i = 0; i < length(); i++)
        hash = s[i] + (31 * hash);
        return hash;
  }
}

string_hash

사용자 정의 클래스의 예

모든 멤버들을 사용하여 hashCode를 생성한다.

public class Record
{
  private String name;
  private int id;
  private double value;
  ...
  public int hashCode() {
    int hash = 17;    //nonzero constant, 프라임넘버로 꼭 17일 필요는 없다. 17은 일종의 가이드 라인
    hash = 31*hash + name.hashCode();
    hash = 31*hash + Integer.valueOf(id).hashCode();
    hash = 31*hash + Double.valueOf(value).hashCode();    
    return hash;
  }
}

hashCode와 hash 함수

  • Hash code: -2^31에서 2^31사이의 정수

  • Hash 함수: 0에서 M-1까지의 정수 (배열 인덱스)

    private int hash(Key key)
    {
      //나머지 연산에서 음수면 문제가 생기니 "& 0x7fffffff"로 양수로 만든다.
      return (key.hashCode() & 0x7fffffff) % M;
    }
    

HashMap in Java

  • 4장에서 다룬 TreeMap 클래스와 유사한 인터페이스를 제공(둘 다 java.util.Map 인터페이스를 구현)

  • 내부적으로 하나의 배열을 해쉬 테이블로 사용

  • 해쉬 함수는 24페이지의 것과 유사함

  • chaining으로 충돌 해결

  • load factor를 지정할 수 있음(0~1 사이의 실수)

  • 저장된 키의 개수가 load factor를 초과하면 더 큰 배열을 할당하고 저장된 키들을 재배치(re-hashing)

HashSet in Java

HashSet<MyKey> set = new HashSet<MyKey>();
set.add(MyKey);
if (set.contains(theKey))
  ...
int k = set.size();
set.remove(theKey);
Iterator<MyKey> it = set.iterator();
while (it.hasNext()) {
  MyKey key = it.next();
  if (key.equals(aKey))
  it.remove();
}

데몬 쓰레드

  • 데몬 쓰레드가 아닌 다른 일반 쓰레드의 작업으 돕는 보조적인 역할을 수행

  • 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료

  • 예로 가비지 컬렉터와 자동 저장 기능

  • 쓰레드 생성 후 실행 전에 setDaemon(true) 호출

실습1

public class TestDaemonThread1 extends Thread {
	public void run() {
		if (Thread.currentThread().isDaemon()) {// checking for daemon thread
			System.out.println("daemon thread work");
		} else {
			System.out.println("user thread work");
		}
	}

	public static void main(String[] args) {
		TestDaemonThread1 t1 = new TestDaemonThread1();// creating thread
		TestDaemonThread1 t2 = new TestDaemonThread1();
		TestDaemonThread1 t3 = new TestDaemonThread1();

		t1.setDaemon(true);// now t1 is daemon thread

		t1.start();// starting threads
		t2.start();
		t3.start();
	}
}

결과

user thread work
daemon thread work
user thread work

실습2

이미 실행 시킨 쓰레드를 데몬 쓰레드로 만들면 IllegalThreadStateException 발생 한다.

public class TestDaemonThread2 extends Thread {
	public void run() {
		System.out.println("Name: " + Thread.currentThread().getName());
		System.out.println("Daemon: " + Thread.currentThread().isDaemon());
	}

	public static void main(String[] args) {
		TestDaemonThread2 t1 = new TestDaemonThread2();
		TestDaemonThread2 t2 = new TestDaemonThread2();
		t1.start();
		t1.setDaemon(true);// will throw exception here
		t2.start();
	}
}

결과

쓰레드이기 때문에 출력 순서는 변경될 수 있다.

Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.setDaemon(Thread.java:1359)
	at thread.TestDaemonThread2.main(TestDaemonThread2.java:13)
Name: Thread-0
Daemon: false

관련자료

인프런의 영리한 프로그래밍을 위한 알고리즘 강좌를 보고 작성한 문서입니다.

Open Addressing에 의한충돌 해결

  • 모든 키를 해쉬 테이블 자체에 저장

  • 테이블의 각 칸(slot)에는 1개의 키만 저장

  • 충돌 해결 기법

    • Linear probing

    • Quadratic probing

    • Double hashing


Linear probing

Linear_probing

h(k), h(k)+1, h(k)+2, … 순서로 검사하여 처음으로 빈 슬롯에 젖아 테이블의 끝에 도달하면 다시 처음으로 circular하게 돌아감

  • Linear probing의 단점

    • primary cluster : 키에 의해서 채워진 연속된 슬롯들을 의미

    • 이런 cluster가 생성되면 이 cluster는 점점 더 커지는 경향이 생김

  • Quadratic probing
    • 충돌 발생시 h(k), h(k)+1^2, h(k)+2^2, h(k)+3^2, … 순서로 시도
      (꼭 n^2가 2일 필요는 없고 임의의 숫자면 된다)
  • Double hashing

    • 서로 다른 두 해쉬 함수 h1과 h2를 이용하여
      h(k, i) = (h1(k) + i*h2(k)) mod m

Quadratic probing

Quadratic_probing

충돌 발생시 h(k), h(k)+1^2, h(k)+2^2, h(k)+3^2, … 순서로 시도

Double hashing

Double_hashing

(두번째 해쉬 함수는 0이면 안된다)


Open Addressing - 키의 삭제

  • 단순히 키를 삭제하는 경우 문제가 발생

    • 가령 A2, B2, C2가 순서대로 모두 동일한 해쉬함수값을 가져서 Linear probing으로 충돌 해결(왼쪽 그림)

    • B2를 삭제한 후(가운데 그림) C2를 검색

키의_삭제

인프런의 영리한 프로그래밍을 위한 알고리즘 강좌를 보고 작성한 문서입니다.

hashing

해쉬 테이블은 dynamic set을 구현하는 효과적인 방법의 하나

  • 적절한 가정하에서 평균 탐색, 삽입, 삭제시간 O(1)

  • 보통 최악의 경우 θ(n)

    hash_table1

해쉬 함수(hash function) h를 사용하여 키 k를 T[h(k)]에 저장


  • 모든 키들을 자연수라고 가정. 어떤 데이터든지 자연수로 해석하는 것이 가능

예)문자열

  • ASCII 코드: C=67, L=76, R=82, S=83

해쉬 함수의 간단한 예:

  • h(k) = k % m, 즉 key를 하나의 자연수로 해석한 후 테이블의 크기 m으로 나눈 나머지
  • 항상 0~m-1 사이의 정수가 됨

충돌

  • 두 개 이상의 키가 동일한 위치로 해슁되는 경우

  • 즉, 서로 다른 두 키 k1과 k2에 대해서 h(k1)=h(k2)인 상황

  • 일반적으로 |U|»m이므로 항상 발생 가능 (즉 단사함수(일대일 함수)가 아님)

  • 만약 |K|>m라면 당연히 발생, 여기서 K는 실제로 저장된 키들의 집합

  • 충돌이 발생할 경우 대처 방법이 필요

  • 대표적인 두 가지 충돌 해결 방법: chainingopen addressing

    해쉬_충돌

Chaining에 의한 충돌 해결

  • 동일한 장소로 해슁된 모든 키들을 하나의 연결리스트(Linked List)로 저장

    해슁_chaining

  • 키의 삽입(Insertion)

    • 키 k를 리스트 T[h(k)]의 맨 앞에 삽입: 시간복잡도 O(1)

    • 중복된 키가 들어올 수 있고 중복 저장이 허용되지 않는다면 삽입시 리스트를 검색해야 함. 따라서 시간복잡도는 리스트의 길이에 비례

  • 키의 검색(Search)

    • 리스트 T[h(k)]에서 순차검색

    • 시간복잡도는 키가 저장된 리스트의 길이에 비례

  • 키의 삭제(Deletion)

    • 리스트 T[h(k)]로 부터 키를 검색 후 삭제

    • 일단 키를 검색해서 찾은 후에는 O(1)시간에 삭제 가능

최악의 경우

  • 최악의 경우는 모든 키가 하나의 슬롯으로 해슁되는 경우

    • 길이가 n인 하나의 연결리스트가 만들어짐

    • 따라서 최악의 경우 탐색시간은 θ(n)+해쉬함수 계산시간

  • 평균시간복잡도는 키들이 여러 슬롯에 얼마나 잘 분배되느냐에 의해서 결정

SUHA(Simple Uniform Hashing Assumption)

  • 각각의 키가 모든 슬롯들에 균등한 확률로(equally likely) 독립적으로(independently) 해슁된다는 가정

    • 성능분석을 위해서 주로 하는 가정임
    • hash함수는 deterministic하므로 현실에서는 불가능
  • Load factor a = n/m:
    • n: 테이블에 저장될 키의 개수
    • m: 해쉬테이블의 크기, 즉 연결리스트이 개수
    • 각 슬롯에 저장된 키의 평균 개수
  • 연결리스트 T[j]의 길이를 nj라고 하면 E[nj] = a

  • 만약 n=O(m)이면 평균검색시간은 O(1)

프로세스와 쓰레드

쓰레드는 왜 만들었을까?

프로세스가 하나 시작하려면 많은 자원이 필요하다. 만약 하나의 작업을 동시에 수행하려고 할 때 여러 개의 프로세스를 띄워서 실행하면 각각 메모리를 할당하여 주어야만 한다. JVM은 기본적으로 아무런 옵션 없이 실행하면 OS마다 다르지만, 적어도 32MB~64MB의 물리 메모리를 점유한다. 그에 반해서, 쓰레드를 하나 추가하면 1MB 이내의 메모리를 점유한다. 그래서, 쓰레드를 “경량 프로세스”라고도 부른다. (출처: 자바의 신)

Process

  • 주소 공간, 메모리 등 정상적인 실행을 위해 필요한 환경을 시스템으로부터 부여받은 존재

  • 리소스와 쓰레드로 구성된다.

  • 모든 프로세스는 하나 이상의 쓰레드를 가지고 있다.

Thread

  • 프로세스 내 실행 단위이다.

  • 프로세스 내에서 실제 작업을 수행한다.


프로세스와 쓰레드의 차이

프로세스

프로세스_쓰레드

Process

  • Code, Data, Heap, Stack 영역으로 이루어져 있으며 프로세스간에 독립된 메모리 영역을 할당 받는다.

  • 각각의 Memory space를 차지한다.

Thread

  • 프로세스 안에서 동작하며 Code, Data, Heap 영역을 공유하고 별도의 Stack만 가지고 있다.

  • Context switching시 Stack영역만 switching하면 되므로 프로세스 스위칭 보다 빠르다.

  • 쓰레드 간 자원 공유가 가능하여 편리하지만 자원 동기화 의 문제가 있다.

관련 좋은 요약들(출처: OS? Oh Yes! 도서, 브런치 글)

  • 큰 틀은 프로레스로, 세분된 작은 일 하나하나는 스레드라고 부른다.

  • 프로세스는 부여된 자원의 소유자로서, 스레드는 스케줄링의 단위로서 존재한다.

  • 프로세스는 운영체제로부터 자원을 할당받는 작업의 단위이고, 스레드는 프로세스가 할당받은 자원을 이용하는 실행의 단위다.


쓰레드의 장단점

장점

  • 메모리 공유로 인한 시스템 자원 소모가 줄어든다.

  • 응답시간이 단축 된다.

  • Context switching에 대한 오버헤드가 줄어든다.

단점

  • 서로 데이터를 사용하다가 충돌이 일어날 가능성이 있다.

  • 디버깅이 다소 까다로워진다.