퀵 정렬

  • 데이터의 갯수가 적을 때는 괜찮지만 데이터의 갯수가 10만 개만 넘어가도 보통의 상황에서
    선택,버블,삽입 정렬 알고리즘을 사용하기가 힘들다. 이런 경우 퀵 정렬을 사용한다면
    아무리 많은 양의 데이터라도 빠르게 정렬을 할 수 있다.
  • 대표적인 분할 정복 알고리즘
  • c 라이브러리로 제공하는 sort() 함수가 퀵 정렬을 기반으로 구현되어 있다.
  • 이미 정렬이 되어있거나 거의 정렬되어 있는 데이터에 사용할 때는 성능이 매우 떨어짐

 

퀵 정렬의 시간 복잡도

평균  :  O(N * logN)    -> 반씩 쪼개서 정렬하기 떄문에 logN인 것이다.

최악  :  O(N^2)

 

퀵 정렬이 최악인 경우에도 O(N * logN)을 보장하는 방법

- STL 라이브러리에서 제공하는 sort() 함수는 퀵 정렬을 기반으로 하되 별도의 처리를 거쳐
  최악의 경우에도 O(N * logN)을 보장한다.

 

 

퀵 정렬 동작 방식

1. 데이터들 중 특정한 값을 피벗(Pivot)으로 정한다.  =>     기준값 설정

2. 피벗을 기준으로 왼쪽에는 피벗보다 작은 값들 오른쪽에는 피벗보다 큰 값이 모이도록 한다.

3. 왼쪽과 오른쪽을 각각 퀵 정렬을 수행한다.           =>      재귀 호출

4. 최종적으로 전체 값들이 정렬된다.  

 

 

배열의 중앙을 피벗(Pivot)으로 잡고 하는 법

 

 

  (1)  left, right, pivot 설정

 

 

(2) left : pivot보다 큰 값을 찾을 때까지 이동(left++) / right : pivot보다 작은 값을 찾을 때까지 이동(right--)

   - pivot보다 큰 값(left)과 작은 값(right)을 찾고, 서로의 값을 교환

   - 교환한 후 left++ , right--

 

 

(3) 위와 같은 과정을 반복해서 피봇보다 큰 값(left)과 작은 값(right) 찾아서 교환

    - 교환한 후 left++, right--;

 

 

(4) 값을 교환한 후 left와 right가 엇갈림 ( ☆☆ )

- left와 right가 엇갈려 left가 더 커지게 된 경우 

   => right와 left 두 부분으로 분할해서 따로 퀵 정렬 수행!!

 

 

(5) 분할된 두 부분을 각각 퀵 정렬 수행 ( 재귀 호출 )

 

 

퀵 정렬 구현 코드

#include<iostream>
using namespace std;

int data[] = {5,1,6,3,4,2,7};

void quickSort(int start, int end){
	if(start >= end) return;
    
    int left = start;
    int right = end;
    int pivot = (start + end) / 2;
    
    while(left < right){
    	while(data[left] < data[pivot]) left++;
        while(data[right] > data[pivot]) right--;
        
        if(left <= right){
        	swap(data[left], data[right]);
            left++;
            right--;
        }
    }
	
    quickSort(start,right);
    quickSort(left,end);
}

int main(){
	int i;
    quickSort(0,6);
    for(i = 0; i < 7; i++){
    	printf("%d ",data[i]);	
    }    
}

'알고리즘' 카테고리의 다른 글

힙 정렬 ( Heap sort )  (0) 2022.02.11
C++ STL sort() 함수 다루기  (0) 2022.02.10
병합 정렬 ( Merge sort )  (0) 2022.02.09
버블 정렬 ( Bubble sort )  (0) 2022.02.09
삽입 정렬(insertion sort)  (0) 2022.02.08

c언어를 처음 접했을 때 문자열을 char* 로 저장할 때와 char[]로 저장할 때 많이 헷갈렸다.

그래서 차이점을 정리해본다.

 

배열로 선언된 문자열

정상적으로 실행되는 경우

1. char s1[10] = "hello";

2. char s1[] ="hello";

 

3. char s1[10] = "Hello";

   s1[0] = 'A';

   printf("%s");  // Aello 

 

4. char s1[10] = "Hello";

   char s2[10];

   scanf("%s",s1);          => 기존에 있던 문자열은 사라지고 입력한 값으로 바뀜

   scanf("%s",s2);     

 

5. char buffer[100] = {0,};

   FILE* fp = fopen("a.txt","r");

   fread(buffer, sizeof(char), 4, fp);       

 

에러 발생하는 경우

1. char s1[10];

   s1 = "hello";            => 이미 선언된 배열에는 문자열 할당 X

 

   char s1[10] = {0,};

   s1 = "hello";            => X

 

   => 이미 선언된 문자열 배열에 값을 넣어주고 싶을 때 strcpy()로 넣는 것은 가능

         ( 단, scanf()로는 넣을 수 있음 )

 


포인터로 선언된 문자열

- 포인터는 단지 문자열의 주소를 가리키고 있을 뿐이라서 문자열의 값을 변경할 수 없음

  ( 즉 상수 취급 )

 

정상적으로 실행되는 경우

1. char* s1;

   s1 = "Hello";               => 되는데 되도록이면 const char* 로 선언해야 경고 안 뜸

 

2. const char* s1 = "hello";

   s1 = "bye";                 

   => const char*라서 상수는 맞지만 이건 포인터라서 가리키는 주소만

        바꿔주는 것일 뿐 원래 "hello"를 가리키는 주소의 값인 "hello"를 바꿔주는 게

        아니라 가능하다. 즉, 아예 다른 주소를 가리키게 하는 것일뿐이다!!

 

3. char* buffer = (char*)malloc(sizeof(char) * 100);

   FILE* fp = fopen("a.txt","r");

   fread(buffer, sizeof(char), 4, fp);

 

세그멘테이션 에러 발생하는 경우

1. char* s1;

   scanf("%s",s1);

 

2. char* s1 = "Hello";

   s1[0] = 'A'               => 문자열 포인터에 문자열을 저장하면 상수 취급됨

                                      그래서 값 변경 불가능

 

3. char* s1;

   FILE* fp = fopen("a.txt","r");

   fread(s1,sizeof(char),4,fp);

 

 

C언어로 PostgreSQL과 연동해 CLI에서 음식을 주문하는 대화형 프로그램을 구현할 때 이중포인터 때문에

고생했던 기억이 난다. 이 당시 PGresult 구조체의 주소값을 받아왔어야 됐는데, 받을 때 이중포인터가 아닌

그냥 포인터로 받아와서 함수 내에서 받아온 값을 핸들링한다고 해도 실제 main에 있는 원본은 변화가

없었다.

 

아무래도 이중포인터는 평생 나를 괴롭힐 것 같아 확실히 정리해두고 가야 할 것 같아서 정리한다.

#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;

void setToNull(int**);

int main(){
  int five = 5;
  int* ptr = &five;

  printf("&ptr:%p | ptr(변수 five의 주소값 ): %p\n",&ptr,ptr); 
  setToNull(&ptr); 

  if(ptr != nullptr){ 
    cout <<*ptr << endl;
  } else{
    cout << "ptr is null";
  }

  return 0;
}

void setToNull(int** tempPtr){
  printf("tempPtr:%p | &tempPtr:%p | *tempPtr :%p\n",tempPtr,&tempPtr,*tempPtr);
  *tempPtr = nullptr;
}

실행 결과

&ptr         =>  0061FF08

ptr           =>  0061FF0C


tempPtr     =>  0061FF0C

&tempPtr   =>  0061FEF0

*tempPtr    =>  0061FF0C

main에 있는 ptr이 nullptr로 바뀌어서 "ptr is null" 이 출력된다.

 

 

실행 결과 ( 그림을 통한 이해 )

 

 

 

 

 

TCP의 특징

1) Full duplex data transfer

           - 데이터 통신을 할 때 양방향으로 데이터가 흐른다.

 

2) Connection-oriented

           - 통신을 하기 앞서 Handshaking 과정을 통해 sender receiver의 상태를 확인한다.

 

3) Flow controlled

           - senderreceiver의 상태를 살펴보며 전송량을 조절한다.

 

4) Reliable, in-order byte stream

           - 데이터는 순서대로 application layer로 넘겨지며, 데이터를 byte로 처리하기 때문에

             application message의 맥락을 신경쓰지 않는다.

 

5) Pipelined

           - TCP congestion, flow controlpipeline을 위한 윈도우 사이즈를 결정한다.

 

 

TCP 입출력버퍼

  • TCP 소켓의 데이터 송수신에는 경계가 없다.
    아래와 같은 데이터의 처리를 하기 위해 tcp 통신에서는 입력 버퍼와 출력 버퍼가 필요하다.
    1. 송신측에서 write()로 여러 데이터를 몇 번에 걸쳐 보내도 수신 측에서는 read() 한 번으로 그 데이터들을
       한번에 수신한다.
    2. 송신측에서 용량이 큰 데이터를 write()로 보내도 수신 측에서는 read()로 데이터를 여러번 나누어 수신할 수 있다.


TCP 입출력 버퍼의 특징

  • 입출력버퍼는 tcp 소켓 각각에 별도로 존재한다.
  • 입출력버퍼는 소켓 생성시 자동으로 생성된다.
  • 소켓을 닫아도 출력버퍼에 남아 있는 데이터는 계속 전송된다.
  • 소켓을 닫으면 입력버퍼에 남아 있는 데이터는 소멸된다.
    -> 소켓을 닫았을 때 read()로 입력버퍼에 있는 값을 가져오려고 하면 read()는 -1을 반환한다.
  • 수신자가 데이터를 수신하는 시점 == 송신자가 write()를 호출했을 때가 아닌, 수신자가 read()를 호출했을 때!!

 

TCPpipelining

           - TCP는 패킷을 보낼 때 pipeline을 통해 전송한다.

            -> pipeline방식으로는 Go-Back-NSelective repeat 방식이 있는데,

                TCP는 두가지 방법을 혼합하여 사용한다.

            ( GBN방식처럼 중간에 패킷 하나가 빠지면 빠진 패킷에 대한 ack을 계속 전송하며,

              retransmission timer는 하나만 설정한다. 또한 selective repeat 방식처럼

              순서에 맞지 않게 온 패킷들도 버퍼에 다 보관해 놓는다. )

 

 

TCPsequence number

           - TCP에서 sequence numberapplication data의 양만큼 증가한다.

             ex) 하나의 패킷의 크기가 8bytes라면, sequence number1,9,17,25 …

                 이런 식으로 증가한다.

 

 

TCPack number

           - TCP에서 ACK번호 == “내가 너로부터 받기로 예상하는 sequence number

           - ACK번호를 받으면 바로 다음 턴에 ack에 해당하는 패킷을 보내줘야 한다.

 

TCP Header

           - UDP에 비해 굉장히 복잡한 형태를 가지고 있음.

 

             acknowledgement numberwindow size

                 => receiver가 필요한 정보를 실어 보내는 것

 

             나머지 부분

                 => sender가 필요한 정보를 실어 보내는 것

 

 

TCP round trip time ( RTT )

           - RTT : 특정 패킷을 전송하고 다시 응답을 받기까지 걸린 시간

           - TCP에서는 기본적으로 retransmission timer를 사용하므로, 예상 RTT를 계산하는 것

             이 매우 중요함!!

               

             retransmission timer < RTT 인 경우

                => 패킷이 유실되지도 않았는데 재전송하는 경우 발생

 

             retransmission timer > RTT 인 경우

                => 패킷이 유실되는 경우에 늦게 반응하는 속도 저하 발생

 

TCP fast retransmit

           TCP에서 패킷이 유실된 것을 의심해볼 수 있는 경우 2가지

           1. 중복된 ack이 계속해서 오는 경우        

           2. timeout이 발생한 경우

 

           -> 유실이 된 것으로 의심이 되면 빠르게 재전송을 해야 하는데 타이머의 timeout을 기

              다리는 것은 굉장히 답답하다. 한번에 여러 패킷을 보냈다고 하더라도 가장 첫 패킷

              에 대한 ack이 오고 나서야 다음 패킷에 대한 타이머가 시작되어 굉장히 느리다.

              

           위 문제점 해결 방법

                      - TCP fast retrasmit 방식 사용

                                 -> 재전송을 timeout에 의지하지 않고 중복된 ack메시지가 올 경우

                                      패킷이 유실된 것으로 추정하고 바로 재전송하는 방식

 

 

TCP Flow control

           - receiverbuffer 눈치를 보고 senderwindow size를 조절하는 것

           - TCP에서 flow controlreceiver측의 소켓에 존재하는 버퍼를 기준으로 삼는다.

 

       동작방식

           - 소켓의 버퍼에 데이터가 계속 쌓이면, application layer에서 데이터를 계속 가져간다.

             이 때 application layer에서 데이터를 가져가는 속도보다 버퍼에 데이터가 쌓이는 속도

             가 큰 경우 데이터가 유실될 수 있다

 

             => 이럴 경우 receiver측의 소켓 버퍼 상황을 고려하여 window size를 조절하는 것이

                 바로 TCP flow control이다.

             => flow controlsender가 조절하는게 아닌 receiversender에게 명령하는 것

                 ( 나 상황 이러니까 너가 보내는 속도 조절해라 sender~~^^ )

 

       TCP persist Timer

            - senderreceiver버퍼에 빈 공간이 있는지 알아보려고 주기적으로

              1byte짜리 메시지로 찔러보기 위한 타이머

      

 

결론

     1. pipelined된 패킷 통신을 할 때는 window가 존재하는데, TCP header에 수용 가능한

        window크기가 적혀있다.

    

     2. TCP flow controlreceiver의 소켓 버퍼가 수용 가능한지를 따져 sender가 전송량을

        조절하는 것이다.

     

    3. receiver의 소켓 버퍼가 가득 차있다가 여유가 생기면 sender에게 알려준다.

       동시에 senderreceiver에게 여유가 있는지 주기적으로 계속 찔러보는데

       이를 TCP persist Timer라고 한다.

'TCP, IP 네트워크 프로그래밍' 카테고리의 다른 글

TCP, IP, Ethernet 헤더  (0) 2022.02.18

헤더 : <sys/socket.h>

 

<서버>

socket() : 소켓 생성 함수

           int socket(int domain, int type, int protocol);

                      -첫번째 매개변수  :  어떤 영역에서 통신할 건지 영역 지정 ( protocol family )

                                                 - AF_INET : ipv4 / AF_INET6 : ipv6

                      -두번째 매개변수 :  어떤 서비스 타입의 소켓을 생성할건지 지정( socket type )

                                                 - SOCK_STREAM : TCP

                                                 - SOCK_DGRAM : UDP

                                                 - SOCK_RAW : TCP,UDP를 거치지 않고 바로 IP 계층 사용

                      -세번째 매개변수 :  소켓에서 사용할 프로토콜 지정

                                                 - IPPROTO_TCP : TCP / IPPROTO_UDP : UDP

                                                 - 0 : socket type에서 이미 정해진 경우

          

           성공     =>       소켓 디스크립터(0 이상)

           실패     =>         -1

 

bind() : 서버의 ip주소와 port를 소켓에 할당해주는 함수

           - clientserver의 소켓 디스크립터번호를 알아야 하기 때문에 사용

 

           int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);

                      -첫번째 매개변수  :  ip,port를 할당해줄 서버 소켓 디스크립터 번호

                      -두번째 매개변수  :  서버의 ip를 담고있는 sockaddr 구조체의 주소값

                      -세번째 매개변수  :  서버정보가 담긴 serv_addr 구조체 길이

          

           성공     =>       0

           실패     =>       -1

 

listen() : client에서 요청이 오면 수락할 수 있도록 대기상태에 들어가는 함수

           int listen(int sockfd, int backlog);

                      -첫번째 매개변수 :  서버 소켓 디스크립터

                      -두번째 매개변수 :  client가 대기할 수 있는 대기열의 크기를 지정

          

           성공     =>       0

           실패     =>       -1

 

accept() : 서버에 client를 연결하는 함수

           int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

              - 첫번째 매개변수 :  서버 소켓 디스크립터

              - 두번째 매개변수 :  연결된 클라이언트의 ip주소 정보를 담을 sockaddr 구조체 주소

              - 세번째 매개변수 :  연결된 클라이언트 정보가 담길 sockaddr 구조체 길이

 

<클라이언트>

connect() : 서버로 연결요청하는 함수

           int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);

              - 첫번째 매개변수 :  서버와 통신하기 위한 소켓 디스크립터

              - 두번째 매개변수 :  연결할 서버의 ip,port정보를 담고 있는 sockaddr 구조체 주소

              - 세번째 매개변수 :  연결할 서버의 정보가 담긴 sockaddr 구조체 길이

          

           성공     =>       0

           실패     =>       -1

 

close() : 소켓을 닫고 통신을 종료하는 함수

           -<unistd.h>

           int close(int sockfd);

          

           성공     =>       0

           실패     =>       -1

 

server 구현        =>       client, server소켓 둘 다 생성

client 구현         =>       소켓 1개만 생성

                                 ( 서버의 정보를 담고있는 sockaddr 구조체를 이 소켓에 담음).

                                 ( connect()로 서버와 연결해서 그 때 담는다. )

 

 

<소켓 통신에서 주의사항>

- 소켓을 보낼 때 내가 fgets100까지 읽어오라고 해도, 정확히 100까지 읽는다는 보장 X.

- tcp는 그나마 신뢰성 통신이라 드물긴 하지만, udp인 경우에는 더 드라마틱하게 이상하게 보내질 때가 많음.

  그렇기 때문에 fgets(req_line,100,clnt_read) 같은 형태로 소켓으로부터 데이터를 읽어오도록 해도 100만큼 정확히 읽어 온단 보장이 없기 때문에 조건을 걸어서

  (EX : \r\n을 만나기 전까진 버퍼에서 보내지 말고 버퍼 뒤에 이어붙여라)

  완전히 원하는 만큼 데이터가 보내지도록 개발자가 보장을 직접 해줘야 함.

 

<소켓 통신시 발생하는 문제점>

문제점

      - 전송측은 빅 엔디안으로 전송 / 받는 쪽은 리틀 엔디안으로 처리

         =>서버와 클라이언트의 바이트 정렬 방식이 다르기 때문

 

해결방법

      - 받는 측(리틀 엔디안)에서 전송받은 데이터(빅 엔디안)를 뒤집어서 처리!!

         => 개발자가 해야 할 것 : port번호, ip주소만 직접 처리해주면 됨.

         => 기본적으로 pc는 리틀 엔디안이므로 이를 빅 엔디안으로 변경해야함.

          

        포트  :  2byte로 이루어짐

        IP     :  4byte로 이루어짐

 

컴퓨터공학과 공학도로써 제대 후부터 정말 열심히 공부한 것 같다.

지금까지 전공 공부, 인턴 생활, 프로젝트 등을 경험하며 개발 지식들을 word나 한글 파일에 저장하고 프린트해서

종이로 들고 다녔다. 

 

지금부터는 공부했던 개발 지식들을 이 곳에 올려 다른 사람들도 필요한 정보들을 사용할 수 있고, 나도 찾기 편하게

정리해볼 생각이다. ( 뭐든지 기록하는 습관을 가지자 )

2022/02/02 졸업을 앞두고...

+ Recent posts