스킵 리스트는 링크드 리스트의 단점을 개선하는 데서 시작한다. 여기서 설명하는 링크드 리스트는 정렬된 상태를 유지하는 리스트이다. 이런 링크드 리스트에서 n 번째 node를 찾으려면 n 번 비교를 해야 한다. 아래 그림에서 보는 것처럼 값이 '80' 인 노드를 찾으려면 비교를 9번 해야 한다. 최악의 경우 모든 노드를 비교해야 한다.
그림 1-a 정렬된 링크드 리스트
레벨을 갖는 스킵 리스트
탐색 시간을 단축하기 위해서는 비교 횟수를 줄여야 한다. 여기서 아이디어를 낸 것이 노드에 포인터를 두 개를 갖게 해서 두 번째 포인터는 하나 건너뛰어서 다음 노드를 가리키도록 한다.
그림 1-b 레벨 2 스킵 리스트
위 그림에서 보는 것처럼 홀수 번째 노드는 포인터 하나만 갖고, 짝수 번째 노드는 포인터를 두 개 갖은 것이다. 이렇게 구현하면 하나씩 건너 뛰어 비교할 수 있기 때문에 n 번째 노드를 찾는데 n/2 + 1 번 비교하면 된다. 비교 횟수를 반으로 줄이는 획기적인 아이디어이다. 값이 '80' 인 노드를 찾는데 비교를 6번으로 줄었다. 하지만 노드 수가 수십만, 수백만 개라면 여전히 비교 횟수는 많고 따라서 탐색 속도도 느릴 것이다.
레벨 3 갖는 스킵 리스트
한 발 더 나가서 그림 1-c 처럼 포인터를 세 개 가지는 노드를 두면, 비교 횟수는 더 줄어들게 된다.
그림 1-c 레벨 3 스킵 리스트
값 '80' 인 노드를 찾는 잠시 과정을 살펴보자. 이것이 스킵 리스트의 탐색 알고리즘을 설명한 것이므로 잘 기억해두자. 삽입, 삭제시에도 같은 방법으로 찾아간다.
맨 앞에 '레벨 3'이라고 쓰여있는 노드를 헤더라고 하자.
출발은 헤더 노드의 레벨 3 포인터에서 시작한다.
레벨 3이 가리키는 값이 '20'이다.
찾고자 하는 '80'이 '20' 보다 크기 때문에 다음 포인터로 진행해서 '70'과 비교한다.
역시 '80'이 크기 때문에 다음 포인터로 진행하려고 하지만 'Null' 이기 때문에 레벨을 하나 낮추어서(레벨 2) 비교한다.
'80'이 '90' 보다 작기 때문에 다시 레벨을 낮추어서(레벨 1) 비교한다.
'80'을 찾았다.
총 비교 횟수는 4회이다.
레벨 4 스킵 리스트
그림 1-d 레벨 4 스킵 리스트
위 그림을 보자. 값이 '70'인 8번째 노드에 포인터 4개를 갖도록 하면 비교 횟수는 더 줄어들게 된다. 이와 같이 구현하면 포인터 하나씩 증가할 때마다 2^n 개의 노드를 건너뛰어 비교해서 비교 횟수가 획기적으로 줄어들게 된다. 자 이제 스킵 리스트는 이해되었다. 같은 방식으로 '80'을 찾는데, 비교 횟수는 총 3회로 줄었다. 이제 탐색 시간 단축은 해결되었다. 16개 포인터를 가지면 65,536 번째 노드와 바로 비교할 수 있으므로, 건너뛰기(스킵) 하기 위해서 포인터를 늘려주면 된다.
자 이제, 용어를 좀 정리해서 포인터를 몇 개 가지는냐를 레벨(level)이라고 하자. 다음 노드를 가리키는 포인터를 레벨 1, 하나 건너뛰어 다음 노드를 가리키는 포인터를 레벨 2, 노드 세 개를 건너뛰어 네 번째 노드를 가리키는 포인터를 레벨 3라고 하자. 레벨은 계속 늘려나가면 된다.
그림 1-e 레벨 32 스킵 리스트
2의 거듭제곱으로 표시하면, (위 그림을 보면서) 레벨 1은 2^0=1 : 바로 다음 노드를 가리킨다. 레벨 2는 2^1=2 : 하나 건너뛴 다음 노드, 2 번째 노드를 가리킨다. 레벨 3은 2^2=4 : 4 번째 노드를 가리킨다. 레벨 4는 2^3=8 : 8 번째 노드를 가리킨다. ... 레벨 16은 2^15=32,768: 32,768번째 노드를 가리킨다. ... 레벨 32는 2^31=2,147,483,648: 대략 이십억 번째 노드를 가리킨다.
미리 정해진 레벨 문제
그림 2-a 미리 정해진 레벨을 갖는 스킵 리스트
이제까지 살펴본 스킵 리스트는 위의 그림처럼 노드 순서마다 레벨이 정해져 있다. 이렇게 미리 정해진 레벨을 사용하게 되면 무슨 문제가 생길까? 그렇다. 노드 삽입과 삭제 시에 이후 모든 노드의 순서가 바뀌므로 모두 레벨을 수정해주어야 한다. 이렇게 되면 삽입/삭제 시간이 굉장히 많이 걸릴 것이다. 레벨을 가지는 노드라는 매우 좋은 아이디어를 냈지만, 이 삽입/삭제 부하를 해결하지 못하면 스킵 리스트는 쓸 수 없을 것이다.
웹 소켓은 단일 소켓을 통해서 풀듀플렉스(full-duplex) 통신을 제공하는 프로토콜이다. 웹 브라우저오 서버간의 비동기 메시징을 활성화한다. 풀 듀플렉스는 서버가 브라우저에 메시지를 전송할 수 있고, 브라우저도 서버에 메시지를 전송할 수 있음을 의미한다. 스프링4.0은 웹 소켓 통신을 지원하며, 다음을 포함한다. 메시지 전송과 수신을 위한 하위 레벨 API 스프링 MVC 컨트롤러에서의 메시지 처리를 이한 상위 레벨 API 메시지 전송을 위한 메시징 템플릿 브라우저, 서버, 프록시에서 웹 소켓 지원 부족을 지원하기 위한 SockJS 1. 스프링의 하위 레벨 웹 소켓 API사용하기 간단한 형태로 웹 소켓은 두 애플리케이션 사이의 통신 채널이다. 풀 듀플렉스이므로 양쪽 종단은 메시지를 전송할 수 있고, 다른 쪽은 그 메시지를 받아서 처리한다. 웹 소켓 통신은 여러 종류의 애플리케이션 사이에서 사용될 수 있다. 그러나 웹 소켓의 대부분은 서버와 브라우저 기반 애플리케이션 사이의 통신을 용이하게 하기 위해서 사용된다. 브라우저에서의 자바스크립트 클라이언트는 서버에 대한 연결을 오픈하고, 서버는 그 연결을 통해 브라우저에 업데이트하라고 보낸다. 이러한 방법은 업데이트를 위해 서버를 폴링(Polling)하는 일반적인 방식보다 더 효율적이며, 좀 더 자연스럽다. 하위 레벨 웹 소켓을 지원하기 위해서 스프링에서 메시지를 처리하고, WebSocketHandler를 구현한 클래스를 작성한다. public interface WebSocketHandler{ void afterConnectionEstablished(WebSocketSession session)throws Exception; void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; void ...
*리얼타임 웹을 위한 기법으오 일정한 주기를 가지고 서버와 응답을 주고받는 방식이 폴링방식이다. 이는 Ajax Polling이라고도 불리는데 주로 Ajax호출을 사용하기 때문이다. 하지만 이 폴링기법은 두 가지 문제가 있는데, 폴링 주기에 관한 문제로 주기가 짧으면 서버의 성능에 부담이 가고, 주기가 길면 실시간 성능이 약간 떨어지는 문제가 있다. 그래서 이 폴링의 지연을 최대한 줄이기 위해 comet을 구현하는 방법이 있고 이 방법 중에 하나가 롱폴링기법이있다. 이 롱폴링 방식은 서버 측에서 접속을 열어두는 시간을 길게 하는 방식이다. 롱폴링 에서는 이벤트가 발생하면 바로 응답이 이루어지기 때문에 실시간성이 아주 높으며,스트리밍 방식과 달리 웹브라웆 환경에 관계없이 사용할 수 있기 떄문에 흔히 사용하는 방식이다. 롱폴링 방식외에 스트리밍 방식이 있는데 이 스트리밍 방식은 하나의 웹 요청에 대해 웹 접속을 계속 열어두고, 새로 이벤트가 발생하면 발생할 때마다 부분적인 응답으로 브라우저로 보내는 방식이다. -간단한 구현 - 쓸모없는 요청-응답 때문에 많은 트래픽이 낭비됨 - 반복하는 주기가 짧지 않은 경우 , 폴링기법 추천 (예: 페이스북 웹채팅에서 사용자 리스트 갱신주기 1분-폴링기법 사용) -서버에서 커넥션을 물고 기다리고 있으며, 이벤트의 업데이트가 있을 경우 클라이언트로 응답을 준다. - 구현기술 - iframe : 구현이 쉬우나 로딩이 표시되고 크로스 도메인을 지원하지 않음 - htmlFile : IE 에서만 동작함 - JSONP : 대중적인 사용방식, 크로스도메인 문제 해결 - XmlHttpRequest : 크로스 도메인을 지원하지 않음 <폴링방식과 롱폴링 방식 선택하기> Long-Polling 방식을 선택해야 하는 경우 약 3초간의 오차로 실시간 응답이 필...
처음 클래스를 배우면서 객체를 생성할 때 메모리를 동적으로 할당받지 않고(new를 사용하지 않고) 객체를 생성하였다. 클래스이름 객체이름; ex> Student kim; new 연산자를 사용한 동적할당. 클래스이름 *객체이름; 객체이름 = new 클래스이름; 객체이름 = new 클래스이름(매개변수 리스트); ex> Student *kim; kim = new Student(); kim->showData(); // 1) 객체 포인터를 선언한다. 2) new 연산자를 사용해 객체를 위한 메모리를 동적할당한다. 3) new 연산자에 의해 생성자가 자동으로 호출된다. 4) 객체 포인터를 사용해 멤버에 접근한다. ------------------------------------------------------------------------------------------------------------------------------------- 여기서 드는 의문점이 있다. 앞에서 객체를 생성할때 클래스 이름과 객체명만 적어주면 되었다. 그런데 굳이 new를 이용해서 동적할당을 받아 객체를 생성하는 이유는 무엇일까? 딱히 물어볼 곳이 없어 네이버 지식in을 통해 답을 구할 수 있었다. (나랑 비슷한 의문을 가진 사람은 항상있는듯 하다) 두 방법의 차이는 메모리의 어디에 올라가냐의 차이이다. 동적할당을 받지 않고 객체를 생성할 경우 스택영역에 올라가게 된다. 스택영역은 일반적인 변수가 올라가는 영역으로 범위를 벗어날 경우 메모리가 자동으로 해제된다. 하지만 동적할당을 할 경우 힙영역을 할당받고 delete를 이용하여 직접 해제를 해주지 않기 전까지는 메모리에 유지된다. 즉, 함수에서 잠깐 쓰고 자동으로 해제시키기 위해서는 스택 메모리를 이용하여 사용하도록 하고 계속 사용하기 위해서는 힙메모리를 이용해야 한다. 힙 메모리를 이용할 경우 메모리 누수가 발생할 수 있으므로 delete를 이용하여 해제를 해주어야 한다. view s...
댓글
댓글 쓰기