해시테이블(HashTable)

simple_coding·2024년 1월 18일
0

해시테이블(HashTable)

키 값을 해시함수로 해싱하여 해시테이블의 특정 위치로 직접 엑세스하도록 만든 방식이다.
해시 : 임의의 길이를 가진 데이터를 고정된 길이를 가진 데이터로 매핑


    <해시테이블 구현>
    데이터를 담을 테이블을 이미 크게 확보해 놓은 후
    입력받은 키를 해싱하여 테이블 고유한 index를 계산하고 데이터를 담아 보관
    
              해싱
             ┌────┐
        27 ─→│    │─→  27
       385 ─→│해시│─→ 192
       581 ─→│함수│─→   2
      1031 ─→│    │─→  66
             └────┘
    [0]   [1]   [2]       [27]      [66]      [191] [192] [193]
    ┌─────┬─────┬─────┬─  ─┬────┬─  ─┬──────┬─  ─┬─────┬─────┬─────┐
    │    │     │ 582....27...1031....│    │ 385 │     │
    └─────┴─────┴─────┴─  ─┴────┴─  ─┴──────┴─  ─┴─────┴─────┴─────┘


     <해시함수>
     키값을 해싱하여 고유한 index를 만드는 함수
     하나의 키값을 해싱하는 경우 반드시 항상 같은 결과여야 함
     효율 : 해시의 결과가 분산적일수록 효율이 좋음
     
     나눗셈법   : 데이터 % 테이블크기
     자리수접기 : 데이터의 각 자리수의 합
     SHA-2      : 미국 국가안보국(NSA)이 설계한 암호화 해시 함수
     
     나눗셈법   예시 : 132031(132031 % 1000) = 31
     자리수접기 예시 : "hash"h(104) + a(97) + s(115) + h(104) = 420


     <해시테이블 주의점 - 충돌>
     해시함수가 서로 다른 입력 값에 대해 동일한 해시테이블 주소를 반환하는 것
     모든 입력 값에 대해 고유한 해시 값을 만드는 것은 불가능하며 충돌은 피할 수 없음
     
               해싱
              ┌────┐
        274 ─→│해시│─→  81
        660 ─→│함수│─→  81
              └────┘
     
      [0]   [1]   [2]        [81]        [191] [192] [193]
     ┌─────┬─────┬─────┬─  ─┬─────────┬─  ─┬─────┬─────┬─────┐
     │    │     │     │...274 660...│     │     │     │
     └─────┴─────┴─────┴─  ─┴─────────┴─  ─┴─────┴─────┴─────┘


      <충돌해결방안 - 체이닝>
      해시 충돌이 발생하면 연결리스트로 데이터들을 연결하는 방식
      장점 : 해시테이블에 자료가 많아지더라도 성능저하가 적음
      단점 : 해시테이블 외 추가적인 저장공간이 필요
      
                해싱
               ┌────┐
         274 ─→│해시│─→  81
         660 ─→│함수│─→  81
               └────┘
      
       [0]   [1]   [2]      [81]     [191] [192] [193]
      ┌─────┬─────┬─────┬─  ─┬─────┬─  ─┬─────┬─────┬─────┐
      │     │    │     │...│  │  │...│     │     │     │
      └─────┴─────┴─────┴─  ─┴──│──┴─  ─┴─────┴─────┴─────┘
                              ↓
                      ┌─────┬─┐ ┌─────┬─┐
                      │ 274 │──→│ 660 │ │
                      └─────┴─┘ └─────┴─┘


       <충돌해결방안 - 개방주소법> C#에서 사용
       해시 충돌이 발생하면 다른 빈 공간에 데이터를 삽입하는 방식
       해시 충돌시 선형탐색, 제곱탐색, 이중해시 등을 통해 다른 빈 공간을 선정
       장점 : 추가적인 저장공간이 필요하지 않음, 삽입삭제시 오버헤드가 적음
       단점 : 해시테이블에 자료가 많아질수록 성능저하가 많음
       
       개방주소법 해시테이블의 공간 사용률이 높을 경우(통계적으로 70% 이상) 급격한 성능저하가 발생
       이런 경우 재해싱을 통해 공간 사용률을 낮추어 다시 효율을 확보함
       재해싱 : 해시테이블의 크기를 늘리고 테이블 내의 모든 데이터를 다시 해싱하여 보관
       
                 해싱
                ┌────┐
          274 ─→│해시│─→  81
          660 ─→│함수│─→  81
                └────┘
                                
        [0]   [1]   [2]      [81]  [82]      [191] [192] [193]
       ┌─────┬─────┬─────┬─  ─┬─────┬─────┬─  ─┬─────┬─────┬─────┐
       │     │     │    │....274 │    │....│    │     │     │
       └─────┴─────┴─────┴─  ─┴─────┴─────┴─  ─┴─────┴─────┴─────┘
                               ↑660(충돌)
      
        [0]   [1]   [2]      [81]  [82]     [191] [192] [193]
      ┌─────┬─────┬─────┬─  ─┬─────┬─────┬─  ─┬─────┬─────┬─────┐
      │     │    │    │....274660....│    │     │     │
      └─────┴─────┴─────┴─  ─┴─────┴─────┴─  ─┴─────┴─────┴─────┘
                                   ↑(다음위치에 저장)


        <해시테이블의 시간복잡도>
 접근       탐색       삽입       삭제(*삭제된 위치 재사용불가)
  X         O(1)       O(1)       O(1)
충돌하는 특성으로 인해 삽입, 삭제에 불리하다.



        static void Main(string[] args)
        {
            // 해시테이블 기반의 HashSet 자료구조
            // 중복이 없는 해시기반의 저장소
            //HashSet 0(1) 입력순서 출력// SortedSet(logN) 정렬 출력
            //둘 다 값을 중복 없이 모아두는 저장소로 활용
            HashSet<string> set = new HashSet<string>();

            // 삽입
            set.Add("D");
            set.Add("B");
            set.Add("B");   //중복추가 무시
            set.Add("C");
            set.Add("A");
            set.Add("A");
            set.Add("A");
            set.Add("E");

            // 삭제
            set.Remove("B");

            // 탐색
            set.Contains("A");      // 포함 확인

            // 순서대로 출력시 포함된 순서대로 결과 확인
            foreach (string value in set)
            {
                Console.Write(value);   // output : DCAE
            }
            Console.WriteLine();


            // 해시테이블 기반의 Dictionary 자료구조
            // 중복을 허용하지 않는 key를 기준으로 해시기반의 value 저장소
            // Dictionary 0(1) 입력순서출력 // SortedDictionary(logN)정렬출력
            Dictionary<int, string> dictionary = new Dictionary<int, string>();

            // 삽입
            dictionary.Add(2, "A");
            dictionary.Add(1, "B");
            dictionary.Add(4, "C");
            dictionary.Add(3, "D");
            dictionary.Add(5, "E");
            // dictionary.Add(4, "F");    // error : Dictionary는 중복을 허용하지 않음
            dictionary.TryAdd(4, "E"); // SortedDictionary와 같은 명령어들 사용가능
            // 삭제
            dictionary.Remove(5);

            // 탐색
            dictionary.ContainsKey(3);                        // 포함 확인
            dictionary.TryGetValue(3, out string dicValue);   // 탐색 시도
            dictionary[4] = "F";
           
            // 순서대로 출력시 정렬된 결과 확인
            foreach (string value in dictionary.Values)
            {
                Console.Write(value);     // output : ABCD
            }
            Console.WriteLine();
            
            dictionary.TryGetValue(4,out string ivalue);
Console.WriteLine(ivalue); // output: F
        }
        

<해시테이블(HashTable)기능 구현>

namespace DataStructure
{   //Dictionary 클래스 생성<key,value 자료형 일반화>
    //key 값을 비교할 필요가 없기 때문에 IComparable이 아닌
    //키 값이 같은지 다른지 비교하는 where TKey : IEquatable<TKey>
    public class Dictionary<TKey, TValue> where TKey : IEquatable<TKey>
    {
        private const int DefaultCapacity = 1000; //기본 크기 설정(소수로 설정 시 효율 증가)

        private struct Entry //구조체 Entry
        {
            public enum State { None, Using, Deleted } //Entry의 상태를 구별하는 열거형 

            public State state; //state값
            public TKey key; //key값
            public TValue value; //value값         
        }

        private Entry[] table; //key와 value, state를 가진 테이블 배열 생성
        private int usedCount; //사용중인 갯수

        public Dictionary()
        {
            table = new Entry[DefaultCapacity]; //테이블 기본크기 
            usedCount = 0; //갯수 0
        }

        public TValue this[TKey key] //this 인덱서, dictionary[4] = "F"; 구현
        {
            get //가져오기
            {
                if (Find(key, out int index)) //키값 찾았을 때
                {
                    return table[index].value; //값 확인
                }
                else //키값 못 찾았을 때
                {
                    throw new KeyNotFoundException(); //예외처리
                }
            }
            set //갱신하기
            {
                if (Find(key, out int index)) //키값 찾았을 때
                {
                    table[index].value = value; //value값 인덱스 위치에 입력
                }
                else //키값 못 찾았을 때
                {
                    Add(key, value); //비어있는 위치에 키,데이터 입력
                    //table[index].key = key;
                    //table[index].value = value;
                    table[index].state = Entry.State.Using; //인덱스위치를 사용중으로 상태변경
                    usedCount++; //갯수 증가
                }
            }
        }

        public void Add(TKey key, TValue value) //값 입력 함수 Add
        {
            if (Find(key, out int index))  //입력한 키값이 있다면 
            {
                throw new InvalidOperationException("Already exist key"); //예외처리
            }
            else // 입력한 키값이 없다면
            {
                if (usedCount > table.Length * 0.7f) // 테이블의 70%이상 사용중이라면
                {
                    ReHashing(); //테이블 크기 증가 
                }

                table[index].key = key; //지금 key값 현재인덱스key값에 입력
                table[index].value = value; // 지금 value값 현재인덱스 value값에 입력
                table[index].state = Entry.State.Using; //값을 추가했으니 사용중으로 상태변경
                usedCount++; //갯수 증가
            }
        }

        public void Clear()
        {
            table = new Entry[DefaultCapacity];
            usedCount = 0;
        }

        public bool ContainsKey(TKey key) //키값으로 데이터 확인하는 함수
        {
            if (Find(key, out int index)) //찾는 키값이 있을때
            {
                return true;
            }
            else //찾는 키값이 없을때
            {
                return false;
            }
        }

        public bool Remove(TKey key) //키값을 찾아 삭제하는 함수 구현
        {
            if (Find(key, out int index)) //찾는 키값이 있다면
            {
                table[index].state = Entry.State.Deleted; //삭제된 것으로 상태변경
                return true; //완료
            }
            else //찾는 키값이 없다면
            {
                return false; //실패
            }
        }

        private bool Find(TKey key, out int index) //key값이 있는지 확인하는 bool함수
        {
            if (key == null) //key값이 없으면
                throw new ArgumentNullException(); //예외처리

            index = Hash(key); //key값을 해싱하여 index입력

            for (int i = 0; i < table.Length; i++) //빈자리를 발견할 때까지 반복
            {
                if (table[index].state == Entry.State.None) //테이블 인덱스위치가 비어있는 경우
                {
                    return false; //찾지 못했으니 false
                }
                else if (table[index].state == Entry.State.Using) //테이블 인덱스위치가 사용중인 경우
                {
                    if (key.Equals(table[index].key)) //현재 키 값이 테이블에 있는 키 값과 같은지 확인
                    {
                        return true; //같으면 true
                    }
                    else // 같지 않다면
                    {
                        // 다음칸으로
                        index = Hash2(index);
                    }
                }
                else // if(table[index].state==Entry.state.Deleted) 테이블 인덱스 위치가 지워진 상태일 때
                {
                    //다음칸으로
                    index = Hash2(index);
                }
            }
            index = -1; //빈칸이 없을경우, 인덱스를 찾을 수 없다는 표현
            throw new InvalidOperationException();
        }

        private int Hash(TKey key) //key값으로 해싱하는 함수
        {
            
            return Math.Abs(key.GetHashCode() % table.Length); //key값을 받아서 테이블크기로 나눠서 나머지, 절댓값으로 고정, 나눗셈법 구현
        }

        private int Hash2(int index) //충돌했을 떄 선형탐사하는 함수
        {
            // 선형 탐사
            return (index + 1) % table.Length; //다음 인덱스에 나눗셈법하여 입력

            // 제곱 탐사
            // return (index + 1) * (index + 1) % table.Length;

            // 이중 해싱
            // return Math.Abs((index + 1).GetHashCode() % table.Length);
        }

        private void ReHashing() //데이터가 일정 부분 채워지면 크기 변경
        {
            Entry[] oldTable = table; //기존 테이블을 변수에 저장
            table = new Entry[table.Length * 2]; //기존테이블의 2배 크기로 인스턴스 생성
            usedCount = 0; //갯수 초기화

            for (int i = 0; i < oldTable.Length; i++) //기존테이블을 전부 확인
            {
                Entry entry = oldTable[i]; //인덱스 위치를 순서대로 넣고
                if (entry.state == Entry.State.Using) // 사용중인 위치가 있다면
                {
                    Add(entry.key, entry.value); // 입력해준다.
                }
            }
        }
    }
}
profile
기록하는 것이 기억된다.

0개의 댓글