반응형
class Solution:
    def firstUniqChar(self, s: str) -> int:
        dict_ = dict()
        
        for char in s:
            dict_[char] = dict_.get(char, 0) + 1
            
        for i in list(dict_.keys()):
            if dict_[i] == 1:
                return s.index(i)
            
        return -1

한번에 풀긴했음.근데 이건 python 3.7+ 에만 적용 가능( dictionay input 키 순서가 유지된다는 전제 필요)

 

 

하지만 index, 알파벳, 각 알파벳이 나오는 횟수 3가지 정보를 저장하는 효율적인 방법은??

def firstUniqChar(s: str) -> int:
    letters = 'abcdefghijklmnopqrstuvwxyz'
    index = [s.index(l) for l in letters if s.count(l) == 1]
		# 하나만 있는 char의 index들을 저장 
    return min(index) if len(index) > 0 else -1

빠른 이유는?

  • string의 index 함수가 c함수이기 떄문
d = {}
    
    for l in s:
        if l not in d: d[l] = 1
        else: d[l] +=1
        
    index = -1
    for i in range(len(s)):
        if d[s[i]]==1:
            index=i
            break

이게 제일 범용적으로 사용할 수 있는 코드 같음

class Solution:
    def firstUniqChar(self, s: str) -> int:
        d = {}

        for l in s:
            if l not in d: d[l] = 1
            else: d[l] +=1

        index = -1
        for i in range(len(s)):
            if d[s[i]]==1:
                index=i
                break
                
        return index

반응형

'코딩테스트' 카테고리의 다른 글

Leetcode 125. Valid Palindrome  (0) 2022.10.02
반응형

02-3 허브

주소 개념이 없는 물리 계층

  • 물리 계층에는 주소 개념이 없어 송수신 되는 정보에 대한 어떠한 조작이나 판단을 하지 않음
  • 데이터 링크 계층에는 주소 개념이 있어 송수신지를 특정할 수 있고, 주소를 바탕으로 정보에 대한 조작과 판단을 할 수 있음

허브(Hub)

  • 여러 대의 호스트를 연결하는 장치로 리피터 허브(repeater hub)라 부르기도 함. 이더넷 네트워크의 허브는 이더넷 허브(Ethernet hub)라고도 부름

허브 (이미지출처:  https://dev-splin.github.io/cs (computer science)/network/Network-OSI-Layer1-Physical/

 

  • 커넥터를 연결할 수 있는 5개의 연결지점이 보이는데 이를 포트(Port)라고 함
  • 포트에 호스트와 연결된 통신 매체를 연결할 수 있음

허브의 특징

허브는 오늘날의 인터넷 환경에서 잘 사용되지 않지만 두가지 특징 때문에 설명

1. 전달받은 신호를 다른 모든 포트로 그대로 다시 보냄

  • 물리 계층에 속하는 장비로, 주소 개념이 없기에 허브는 수신지를 특정할 수 없음
  • 따라서 송신지를 제외한 모든 포트에 그저 내보내기만 함
  • 허브를 통해 신호를 받은 모든 호스트는 데이터 링크 계층에서 패킷의 MAC 주소를 확인하고 자신과 관련 없는 주소는 폐기

2. 반이중 모드로 통신

  • 반이중(half duplex) 모드는 마치 1차선 도로처럼 송수신을 번갈아가면서 하는 통신 방식
  • 전이중(full duplex)모드는 송수신을 동시에 양방향으로 할 수 있는 통신 바익으로 마치 2차선 도로와 같음

충돌 도메인

  • 허브는 반이중 통신을 지원하는데, 한 호스트가 허브에 신호를 보내는 동안 다른 호스트도 허브에 신호를 보낸다면? 충돌(collision)이 발생
  • 허브에 호스트가 많이 연결되어 있을수록 충돌 발생 가능성이 높음. 이런 춘동일 발생할 수 있는 영역을 콜리전 도메인(collision domain)이라고 함. 허브에 연결된 모든 호스트는 같은 콜리전 도메인에 속함
  • 이 문제를 해결하기 위해 CSMA/CD 라는 프로콜을 사용하거나 스위치 장비를 사용

CSMA/CD

  • Carrier Sense Multiple Access with Collision Detection

Carrier Sense

  • 캐리어 감지를 뜻함. 캐리어 감지는 이 프로토콜을 사용하는 반이중 이더넷 네트워크에서는 메시지를 보내기 전에 현재 네트워크 상에서 전송 중인 것이 있는지를 먼저 확인하는 검사하는 과정

Multiple Access

  • 캐리어 감지를 하더라도 두 대 이상의 호스트가 부득이하게 동시에 네트워크를 사용하려 할 때가 있음
  • 이런 상황을 다중 접근(multiple access)라고 하고 이때 충돌이 발생

Collision Detection

  • 충돌 검출이라고 하고, 충돌을 감지하면 전송이 중단되고 충돌을 검출한 호스트는 다른 이들에게 충돌이 발생했음을 알리고자 잼 신호(jam signal)라는 특별한 신호를 보냄. 그리고 임의의 시간이 지난 후 다시 전송

정리하면, 먼저 전송 가능한 상태인지 확인하고, 다른 호스트가 전송 중이지 않을 때 메시지 전송. 만일 충돌이 발생하면 임의의 시간만큼 대기한 후에 다시 전송

 
반응형
반응형

02-2 NIC와 케이블

NIC(Network Interface Controller)

  • 호스트와 통신 매체를 연결하고, MAC 주소가 부여되는 네트워크 장비
  • 통신 매체에는 전기, 빛 등 다양한 신호가 흐를 수 있는데 호스트가 제대로 이해하기 위해서는 전달되는 신호와 컴퓨터가 이해할 수 있는 정보 간의 변환이 이루어져야 함
  • 호스트와 유무선 통신 매체를 연결하고 이러한 변환을 담당하는 네트워크 장비를 NIC라고 함
 

케이블과 NIC (이미지 출처: https://stock.adobe.com/kr/images/connectivity-problem-concept-with-lan-cable-network-card/54429846)

  • NIC는 네트워크 인터페이스 카드, 네트워크 어댑터, LAN 카드, 네트워크 카드, 이더넷 카드 등 다양한 명칭으로 불림
  • ‘카드’라는 표현이 붙은 이유는 초기 NIC는 위 그림처럼 확장 카드 형태로 컴퓨터 본체에 따로 연결하여 사용했기 때문임

 

  • 요즘에는 아래 그림처럼 USB 로 연결하는 NIC도 있고 마더보드에 내장됨 형태도 있음

USB로 연결하는 NIC (이미지 출처: https://kr.element14.com/startech/usb31000s/usb-3-0-to-gigabit-enet-nic-n/dp/3765011)

  • 노트북의 경우 NIC 카드 형태나 USB 형태로 따로 연결하지 않지만 WIFI 를 사용할 수 있는 이유는 대부분 마더보드에 내장된 형태로 NIC를 사용중이기 때문

NIC의 역할

  • 통신 매체에 흐르는 신호를 호스트가 이해하는 프레임으로 변환하거나 반대로 신호로 변환
  • 즉, 호스트가 네트워크를 통해 송수신하는 정보는 NIC를 거치게 됨
  • 지원 속도는 10Mbps부터 100Gbps에 이르기까지 다양함

 

트위스티드 페어 케이블

  • 가장 대중적인 유선 통신 매체인 케이블
  • 구리선으로 전기신호를 주고 받고, 케이블 본체와 RJ-45라고 불리는 커넥터로 이루어짐

RJ-45 커넥터

 

케이블 본체 내부는 이름처럼 구리선이 두 가닥씩(pair) 꼬아져 있음(twisted)

트위스티드 페어 케이블 내부 (이미지 출처: https://www.linkedin.com/pulse/what-twisted-pair-cable-crxconec/)

 

  • 본체가 구리 선으로 이루어진 상태에서 전기 신호를 주고 받으면 구리선에 의해 전자적 간섭이 생기는 노이즈가 발생
  • 노이즈를 감소 시키기 위해 구리선을 그물 모양의 철사나 포일로 감싸는 경우가 많음

실드에 따른 트위스티드 페어 케이블의 분류

  • 그물 모양의 철사로 감싼 트위스티드 페어 케이블을 STP(Shield Twisted Pair)라고 함
  • 포일 실드로 노이즈를 감소시킨 트위스티드 페어 케이블은 FTP(Foil Twisted Pair)라고 함
  • 반면, 아무것도 감싸지 않은 경우는 UTP(Unshielded Twisted Pair)라고 함

실제 실드의 종류를 아래와 같이 세분화하여 명칭을 표기 (X와 Y에 U, S, F를 명시할 수 있음)

XX/YTP
  • XX에는 케이블 외부를 감싸는 실드의 종류(하나 혹은 두 개)
  • Y에는 꼬인 구리 선 쌍을 감싸는 실드의 종류

예시

  • S/FTP 케이블: 케이블 외부는 그물 모양의 브레이드 실드, 꼬인 각 구리 선 쌍은 포일 실드로 감싼 케이블
  • SF/FTP 케이블: 케이블 외부는 브레이드 실드와 포일 실드로 감싸고, 각 구리 선 쌍은 포일 실드로 감싼 케이블
  • U/UTP 케이블: 아무것도 감싸지 않은 케이블

카테고리에 따른 트위스티드 페어 케이블의 분류

  • 카테고리는 케이블 성능의 등급을 구분하는 역할로 높은 카테고리에 속한 케이블일수록 높은 성능을 보임
  • 카테고리가 높을수록 지원 가능한 대역폭이 높아지는데, 송수신 할 수 있는 데이터의 양이 더 많고, 더 빠른 전송이 가능함을 시사

 

특징 Cat5 Cat5e Cat6 Cat6a Cat7 Cat8
지원 대역폭 100MHz 100MHz 250MHz 500MHz 600MHz 2GHz
주요 대응 규격 100BASE-TX 100BASE-T 100BASE-TX 10GBASE-T 10GBASE-T 40GBASE-T
전송 속도 100Mbps 1Gbps 1Gbps 10Gbps 10Gbps 40Gbps

 

광섬유 케이블(Fiber Optic Cable)

  • 빛을 이용해 정보를 주고받는 케이블로 전기 신호를 이용하는 케이블에 비해 속도도 빠르고 먼 거리까지 전송이 가능하고 노이즈 간섭도 적어 대륙 간 네트워크 연결에도 사용됨
반응형
반응형

첫번째 자료구조 소개 전 '추상 자료형' 이라는 용어 소개→이해 필수

03-1 '추상 자료형'(Abstract Data Type) 이란?

  • 자료구조의 관점에서보면, 구체적인 기능의 완성과정을 언급하지 않고, 순수하게 기능이 무엇인지를 나열한 것'

그럼 위의 설명을 왜 추상 자료형이라고 부르는가?

  • 컴퓨터 공학적 측면에서 연산의 종류를 결정하는 것도 자료형 정의의 일부로 보아야 하고, 연산의 ****종류가 결정되었을 때 자료형의 정의는 완성이 된다.
  • 이 때 말하는 연산은 특정 언어(ex) c++, python) 에서 제공하는 기본 연산을 의미하는 것이 아니라, 제공할 수 있는 기능관련연산(함수)

ex) 아래와 같이 구조체를 기반으로 지갑을 의미하는 Wallet이라는 자료형을 정의

typedef struct _wallet
{
	int coin100Num; // 100원짜리 동전의 수
	int bill5000Num;// 5000원짜리 지폐의 수
} Wallet;

Wallet을 기반으로 제공할 수 있는 기능 관련 연산이 포함되어야 자료형의 정의가 완성됨

int TakeOutMoney(Wallet *pw, int coinNum, int billNum); //돈을 꺼내는 연산
int PutMoney(Wallet *pw, int coinNum, int billNum); //돈을 넣는 연산

결론

'자료형'의 정의에 '기능' 혹은 '연산'과 관련된 내용을 명시할 수 있다는 것

구조체 Wallet의 추상 자료형 정의

추상자료형(ADT) 에 대해 다시 설명하면,

구체적인 기능의 완성과정을 언급하지 않고, 순수하게 기능이 무엇인지를 나열한 것

결국 Wallet의 ADT를 아래처럼 정의할 수 있음

Operations:

int TakeOutMoney(Wallet *pw, int coinNum, int billNum)
**첫번째 인자로 전달된 주소의 지갑에서 돈을 꺼낸다
두번째 인자는 꺼낼 동전의 수, 세번째 인자는 꺼낼 지폐의 수를 전달한다.
꺼내고자 하는 돈의 총액을 반환된다. 그리고 그만큼 돈은 차감된다.**

void PutMoney(Wallet *pw, int coinNum, int billNum)
**첫번째 인자로 전달된 주소의 지갑에 돈을 넣는다.**
**두번째 인자는 꺼낼 동전의 수, 세번째 인자는 꺼낼 지폐의 수를 전달한다.
넣은 만큼 동전과 지폐의 수가 증가한다.**

위에서는 추상 자료형을 명시하는데 있어 특정 언어에 의존적(C or C++)이게 표기했지만, 특정 언어에 의존적이지 않게 별도의 표기법을 활용하는 것도 가능

Q. 최소한 구조체 Wallet의 정의는 ADT에 포함시켜야 하는 것 아닌가?

  • 필요하다면 가능, 제한은 없음
  • Q.Wallet의 정의는 필요한 정보인가? →이를 판단하기 위해 아래 main 함수를 보면,
int main()
{
	Wallet myWallet; //지갑 하나

	PutMoney(&myWallet, 5, 10); //돈을 넣음

	ret = TakeOutMoney(&myWallet, 2, 5); //돈을 꺼냄
}
  • 구조체 Wallet의 멤버가 어떻게 구성이 되어있는지 몰라도 Wallet을 기반으로 돈을 넣고 뺄 수 있음 →Wallet의 멤버 구성을 알 필요 없음 → Wallet의 정의를 ADT에 포함시키지 않는 것이 바람직

Q. 동전이나 지폐가 몇개씩 있는지 확인해야하는 경우에는? 이런 경우를 위해 Wallet의 멤버에 대해 알아야 하지 않나?

→ 동전이나 지폐의 수를 확인해야 한다면, 그에 관련된 연산을 위의 ADT에 추가하는 것이 더 바람직

→ 구초제 멤버에 직접 접근하는 것에 훨씬 익숙하지만 이것이 늘 옳은 것인지는 고민해봐야 함

자료구조의 학습에 ADT의 정의를 포함합니다.

이후의 자료구조에 대해서 아래와 같은 학습 순서를 요함.

  1. 자료구조의 ADT를 정의
  2. ADT를 근거로 리스트 자료구조를 활용하는 main 함수를 정의
  3. ADT를 근거로 리스트를 구현

위의 학습 순서는 "사용자에게 사용방법 이외에 불필요한 부분까지 알도록 부담주지 않기 위해"이고, 내부 구현을 알지 못해서 활용할 수 있도록 자료구조를 구현할 것

03-2 배열을 이용한 리스트의 구현

리스트의 이해

  • 리스트라는 자료구조는 구현방법에 따라 크게 2가지로 나뉨
    • 순차 리스트(Array list) : 배열을 기반으로 구현된 리스트
    • 연결 리스트(linked list) : 메모리의 동적 할당을 기반으로 구현된 리스트
  • 구현방법에 따라 나뉘기 때문에 둘의 ADT가 동일하다고 해서 문제될 것은 없음
    • 각종 자료구조들의 ADT는 표준이 있는게 아니라, 필요에 따라 달라짐
  • 리스트의 ADT 정의를 위해 리스트 자료 구조의 가장 기본적이고도 중요한 특성
    • 데이터를 나란히 저장
    • 중복된 데이터의 저장을 허용

리스트의 ADT

  • 특성에 대해(ex) 데이터를 어떻게 나란히 저장할지) 고민하는 게 아니라, 나란히 저장된다는 특성을 기반으로 제공해야 할 기능들을 정의해야 하는 것
void ListInit(List * plist);
**- 초기화할 리스트의 주소값을 인자로 전달
- 리스트 생성 후 제일 먼저 호출되어야 하는 함수**

void LInsert(List * plist, LData data); 
**- 리스트에 데이터를 저장한다. 매개변수 data에 전달된 값을 저장한다**

int LFirst(List * plist, LData * pdata);
**- 첫 번째 데이터가 pdata가 가리키는 메모리에 저장된다.
- 데이터의 참조를 위한 초기화가 진행된다.
- 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환**

int LNext(List * plist, LData * pdata);
**- 참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장된다.
- 순차적인 참조를 위해서 반복 호출이 가능하다.
- 참조를 새로 시작하려면 먼저 LFirst 함수를 호출해야 한다.
- 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환**

LData LRemove(List * plist);
**- LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제한다.
- 삭제된 데이터는 반환된다.
- 마지막 반환 데이터를 삭제하므로 연이은 반복 호출을 허용하지 않는다.**

int LCount(List * plist);
**- 리스트에 저장되어 있는 데이터의 수를 반환한다.**

리스트의 ADT를 기반으로 정의된 main 함수

  • 정의된 ADT를 기반으로 main 함수 작성
  • 아래의 main 함수를 보면서, 어떤 라이브러리에 포함되어 있는 리스트의 사용방법을 파악하는 상황이라고 생각
//main.cpp

#include "ArrayList.h"
using namespace std;

int main()
{
	//ArrayList의 생성 및 초기화
	List list;
	int data;
	ListInit(&list);

	// 5개의 데이터 저장
	LInsert(&list, 11);
	LInsert(&list, 11);
	LInsert(&list, 22);
	LInsert(&list, 22);
	LInsert(&list, 33);

	//저장된 데이터의 수 출력 
	cout << "현재 데이터의 수 : " << LCount(&list) << endl;

	if (LFirst(&list, &data)) //첫 번째 데이터 조회
	{
		cout << data << " ";

		while (LNext(&list, &data)) //두번째 이후 데이터 조회
			cout << data << " ";
	}
	cout << endl;
	cout << endl;

	//숫자 22를 탐색하여 모두 삭제 
	if (LFirst(&list, &data))
	{
		if (data == 22)
			LRemove(&list);

		while (LNext(&list, &data))
		{
			if (data == 22)
				LRemove(&list);
		}
	}

	//삭제 후 남은 데이터 전체 수 출력
	cout << "현재 데이터의 수 : " << LCount(&list) << endl;

	if (LFirst(&list, &data))
	{
		cout << data << " ";

		while (LNext(&list, &data))
			cout << data << " ";
	}
	cout << endl;
	cout << endl;

	return 0;
}
  • ArrayList.h
//#ifndef __ARRAY_LIST_H__
//#define __ARRAY_LIST_H__
#pragma once 

#define TRUE	1
#define FALSE	0

/*** ArrayList의 정의 ****/
#define LIST_LEN	100
typedef int LData;

typedef struct __ArrayList
{
	LData arr[LIST_LEN];
	int numOfData;
	int curPosition;
} ArrayList;


/*** ArrayList와 관련된 연산들 ****/
typedef ArrayList List;

void ListInit(List * plist);
void LInsert(List * plist, LData data);

int LFirst(List * plist, LData * pdata);
int LNext(List * plist, LData * pdata);

LData LRemove(List * plist);
int LCount(List * plist);

//#endif

ArrayList.cpp

#include <stdio.h>
#include "ArrayList.h"

void ListInit(List * plist)
{
	(plist->numOfData) = 0;
	(plist->curPosition) = -1;
}

void LInsert(List * plist, LData data)
{
	if(plist->numOfData > LIST_LEN) 
	{
		puts("저장이 불가능합니다.");
		return;
	}

	plist->arr[plist->numOfData] = data;
	(plist->numOfData)++;
}

int LFirst(List * plist, LData * pdata)
{
	if(plist->numOfData == 0)
		return FALSE;

	(plist->curPosition) = 0;
	*pdata = plist->arr[0];
	return TRUE;
}

int LNext(List * plist, LData * pdata)
{
	if(plist->curPosition >= (plist->numOfData)-1)
		return FALSE;

	(plist->curPosition)++;
	*pdata = plist->arr[plist->curPosition];
	return TRUE;
}

LData LRemove(List * plist)
{
	int rpos = plist->curPosition;
	int num = plist->numOfData;
	int i;
	LData rdata = plist->arr[rpos];

	for(i=rpos; i<num-1; i++)
		plist->arr[i] = plist->arr[i+1];

	(plist->numOfData)--;
	(plist->curPosition)--;
	return rdata;
}

int LCount(List * plist)
{
	return plist->numOfData;
}

현재 데이터의 수 : 5
11 11 22 22 33
현재 데이터의 수 : 3
11 11 33

 

리스트! 배열을 기반으로 구현하기1: 헤더파일의 정의

앞서 보인 main 함수에 리스트의 구현과 관련된 어떠한 코드도 등장하지 않았음

'자료구조의 구현'과 '구현된 자료구조의 활용'은 완전히 구분되도록 ADT를 정의해야 함

#pragma once
#include <iostream>

#define TRUE 1
#define FALSE 0

#define LIST_LEN 100
typedef int LData;

typedef struct __ArrayList
{
	LData arr[LIST_LEN];
	int numOfData;
	int curPosition;
} ArrayList;

typedef ArrayList List; //당장에는 큰 의미가 없어보이지만 향후에 다른 리스트의 종류 사용 가능

void ListInit(List *plist);
void LInsert(List *plist, LData data);

int LFirst(List *plist, LData *pdata);
int LNext(List *plist, LData *pdata);

LData LRemove(List *plist);
int LCount(List *plist);

리스트! 배열을 기반으로 구현하기2: 삽입과 조회

  • 헤더파일에 선언된 함수들을 정의해야 함
  • 먼저 아래의 두 함수
void ListInit(List *plist);            //초기화
void LInsert(List *plist, LData data); //데이터 저장
#include "ArrayList.h"

void ListInit(List * plist)
{
	plist->numOfData = 0; //리스트에 저장된 데이터의 수는 0
	plist->curPosition = -1; //배열의 index값. 현재 아무 위치도 가리키지 않음!
}

void LInsert(List * plist, LData data)
{
	//더이상 저장할 공간이 없는 경우 
	if (plist->numOfData >= LIST_LEN)
	{
		std::cout << "더 이상 데이터 저장 불가" << std::endl;
	}
	plist->arr[plist->numOfData] = data; //데이터 저장
	plist->numOfData++; //저장된 데이터 수 증가
}

다음 세 함수

int LFirst(List *plist, LData *pdata);
int LNext(List *plist, LData *pdata);
int LCount(List *plist);
int LFirst(List * plist, LData * pdata)
{
	//데이터가 없는 경우
	if (plist->numOfData == 0)
		return FALSE;
	//데이터가 있는 경우
	plist->curPosition = 0; //참조 위치 초기화
	*pdata = plist->arr[0]; //pdata가 가리키는 공간에 데이터 저장
	return TRUE;
}

int LNext(List * plist, LData * pdata)
{
	//데이터가 없는 경우
	if (plist->curPosition >= plist->numOfData - 1)
		return FALSE;
	
	plist->curPosition += 1;
	
	*pdata = plist->arr[plist->curPosition];
	return TRUE; 
}

int LCount(List * plist)
{
	return plist->numOfData;
}

리스트! 배열을 기반으로 구현하기3 : 삭제

  • LRemove 함수가 호출되면 리스트의 멤버 cur_pos를 확인해서 조회가 이뤄진 데이터의 위치를 확인한 다음 그 데이터를 삭제
  • 앞에서부터 데이터를 채우는 것이 원칙이니 중간에 데이터가 삭제되면, 뒤에 저장된 데이터들을 한칸씩 앞으로 이동시켜서 그 빈 공간을 메워야 함
LData LRemove(List * plist)
{
	// curposition 데이터 삭제하고
	// 뒤에 데이터들 한칸씩 앞으로 이동 
	// 그리고 참조 데이터는 이전 데이터
	// 삭제된 값 return 
	LData rdata = plist->arr[plist->curPosition];

	for (int i = plist->curPosition; i < plist->numOfData; i++)
	{
		plist->arr[i] = plist->arr[i + 1];
	}

	plist->curPosition--;
	plist->numOfData--;
	return plist->curPosition;
}

Q. 아래코드가 삽입된 이유?

plist->curPosition--; //참조위치를 하나 앞으로(배열 기준 왼쪽으로) 옮긴다.
  • curPosition 변수는 최근에 참조가 이뤄진 데이터의 인덱스 정보를 담아야 함
  • 삭제로 인해 비는 공간을 메우려 데이터를 한 칸씩 왼쪽으로 이동시키면, 참조가 이뤄지지 않는 데이터를 가리키게 되므로, cur_pos 변수가 가리키는 인덱스를 한칸 왼쪽으로 옮겨야 한다.

배열 기반의 리스트 구현 하나로 묶기

#include "ArrayList.h"

void ListInit(List * plist)
{
	plist->numOfData = 0; //리스트에 저장된 데이터의 수는 0
	plist->curPosition = -1; //배열의 index값. 현재 아무 위치도 가리키지 않음!
}

void LInsert(List * plist, LData data)
{
	//더이상 저장할 공간이 없는 경우 
	if (plist->numOfData >= LIST_LEN)
	{
		std::cout << "더 이상 데이터 저장 불가" << std::endl;
	}
	plist->arr[plist->numOfData] = data; //데이터 저장
	plist->numOfData++; //저장된 데이터 수 증가
}

int LFirst(List * plist, LData * pdata)
{
	//데이터가 없는 경우
	if (plist->numOfData == 0)
		return FALSE;
	//데이터가 있는 경우
	plist->curPosition = 0; //참조 위치 초기화
	*pdata = plist->arr[0]; //pdata가 가리키는 공간에 데이터 저장
	return TRUE;
}

int LNext(List * plist, LData * pdata)
{
	//데이터가 없는 경우
	if (plist->curPosition >= plist->numOfData - 1)
		return FALSE;
	
	plist->curPosition += 1;
	
	*pdata = plist->arr[plist->curPosition];
	return TRUE;
}

LData LRemove(List * plist)
{
	// curposition 데이터 삭제하고
	// 뒤에 데이터들 한칸씩 앞으로 이동 
	// 그리고 참조 데이터는 이전 데이터
	// 삭제된 값 return 
	LData rdata = plist->arr[plist->curPosition];

	for (int i = plist->curPosition; i < plist->numOfData; i++)
	{
		plist->arr[i] = plist->arr[i + 1];
	}

	plist->curPosition--;
	plist->numOfData--;
	return plist->curPosition;
}

int LCount(List * plist)
{
	return plist->numOfData;
}
  • 만든 리스트를 사용하기 위해서는 헤더에 선언된 함수의 기능을 숙지하면 됨
  • 실제 리스트가 어떻게 구현되어 있는지 확인할 필요는 없음

리스트에 구조체 변수 저장하기1: 구조체 Point와 관련 함수들의 정의

  • 앞의 예에서는 리스트에 간단한 정수만 저장하였지만 구조체 변수를 비롯하여 각종 데이터 저장 가능
  • 리스트에 '구조체 변수의 주소 값'을 저장해보고자 함
typedef struct _point
{
	int xpos;
	int ypos;
}Point;

구조체와 관련있는 함수들도 정의

void SetPointPos(Point * ppos, int xpos, int ypos); //값을 저장
void ShowPointPos(Point * ppos); //정보 출력
int PointComp(Point *pos1, Point *pos2); //비교
// 두 Point 변수의 멤버 xpos만 같으면 1 반환
// 두 Point 변수의 멤버 ypos만 같으면 2 반환
// 두 Point 변수의 멤버가 모두 같으면 0 반환
// 두 Point 변수의 멤버가 모두 다르면 -1 반환

함수들의 선언 과 정의를 아래와 같이 나누어 작성

//Point.h
#pragma once
#include <iostream>
typedef struct _point
{
	int xpos;
	int ypos;
}Point;

//Point 변수의 xpos, ypos 값 설정
void setPointPos(Point *ppos, int xpos, int ypos);

//Point 변수의 xpos, ypos 정보 출력
void ShowPointPos(Point *ppos);

//두 Point 변수의 비교 
int PointComp(Point * pos1, Point * pos2);
//Point.cpp 
#include "Point.h"

void setPointPos(Point * ppos, int xpos, int ypos)
{
	ppos->xpos = xpos;
	ppos->ypos = ypos;
}

void ShowPointPos(Point * ppos)
{
	std::cout << ppos->xpos << " " << ppos->ypos << std::endl;
}

int PointComp(Point * pos1, Point * pos2)
{
	if (pos1->xpos == pos2->xpos && pos1->ypos == pos2->ypos)
		return 0;
	else if (pos1->xpos == pos2->xpos)
		return 1;
	else if (pos1->ypos == pos2->ypos)
		return 2;
	else
		return -1;
}

리스트에 구조체 변수 저장하기2: 구조체 Point와 관련함수들의 정의

  • Point 구조체 변수의 주소 값을 저장할 수 있도록 변경
  • 헤더파일(ArrayList.h)은 일부 변경해야하지만, 소스파일(ArrayList.cpp)은 변경하지 않아도 됨
//ArrayList.h
typedef int LData; **//(typedef 선언변경) -> typedef Point *LData;**
  • List가 int형 data를 담는 것에서 Point 구조체의 포인터를 담는 것으로 변경되었기 때문에 main 내용은 변경됨
  • ArrayList.h에 Point 변수를 사용하므로 Point.h 파일 include 필요
//main.cpp

#include "ArrayList.h"
#include "Point.h"

using namespace std;

int main()
{
	//ArrayList의 생성 및 초기화
	List list;
	Point comPos;
	Point *ppos;
	ListInit(&list);

	// 4개의 데이터 저장
	ppos = new Point;
	SetPointPos(ppos, 2, 1);
	LInsert(&list, ppos);

	ppos = new Point;
	SetPointPos(ppos, 2, 2);
	LInsert(&list, ppos);

	ppos = new Point;
	SetPointPos(ppos, 3, 1);
	LInsert(&list, ppos);

	ppos = new Point;
	SetPointPos(ppos, 3, 2);
	LInsert(&list, ppos);

	//저장된 데이터의 수 출력 
	cout << "현재 데이터의 수 : " << LCount(&list) << endl;

	if (LFirst(&list, &ppos)) //첫 번째 데이터 조회
	{
		ShowPointPos(ppos);

		while (LNext(&list, &ppos)) //두번째 이후 데이터 조회
			ShowPointPos(ppos);
	}
	cout << endl;
	cout << endl;

	//xpos가 2인 모든 데이터 삭제
	if (LFirst(&list, &ppos))
	{
		if (ppos->xpos==2)
		{
			ppos=LRemove(&list);
			delete ppos;
		}

		while (LNext(&list, &ppos))
		{
			if (ppos->xpos == 2)
			{
				ppos = LRemove(&list);
				delete ppos;
			}
		}
	}

	//삭제 후 남은 데이터 전체 수 출력
	cout << "현재 데이터의 수 : " << LCount(&list) << endl;

	if (LFirst(&list, &ppos)) //첫 번째 데이터 조회
	{
		ShowPointPos(ppos);

		while (LNext(&list, &ppos)) //두번째 이후 데이터 조회
			ShowPointPos(ppos);
	}
	cout << endl;
	cout << endl;

	return 0;
}
  • LRemove 함수에서 왜 삭제된 데이터를 반환하도록 디자인했는지 알 수 있음
    • 리스트에 저장한 데이터는 Point 구조체 변수의 주소값으로 동적 할당한 메모리의 해제 과정을 거쳐야 메모리 누수를 막을 수 있다

문제 3-2 [리스트의 활용]

//NameCard.h
#pragma once
#define NAME_LEN  30
#define PHONE_LEN 30

typedef struct __namecard
{
	char name[NAME_LEN];
	char phone[PHONE_LEN];

}NameCard;

//NameCard 구조체 변수의 동적 할당 및 초기화 후 주소 값 반환
NameCard* MakeNameCard(const char *name, const char *phone);

//NameCard 구조체 변수의 정보출력
void ShowNameCardInfo(NameCard *pcard);

//이름이 같으면 0, 다르면 0이 아닌 값 반환
int NameCompare(NameCard *pcard, const char *name);

//전화번호 정보를 변경
void ChangePhoneNum(NameCard *pcard, const char *phone);
//NameCard.cpp
#include "NameCard.h"
#include <iostream>

NameCard * MakeNameCard(const char * name, const char * phone)
{
	NameCard *name_card = new NameCard;
	strcpy(name_card->name, name);
	strcpy(name_card->phone, phone);
	return name_card;
}

void ShowNameCardInfo(NameCard * pcard)
{
	std::cout << pcard->name << " " << pcard->phone << std::endl;
}

int NameCompare(NameCard * pcard, const char * name)
{
	return strcmp(pcard->name, name);
}

void ChangePhoneNum(NameCard * pcard, const char * phone)
{
	strcpy(pcard->phone, phone);
}
//main.cpp
#include <iostream>
#include "ArrayList.h"

using namespace std;

int main()
{
	List list;
	ListInit(&list);

	NameCard *p_namecard;

	p_namecard = MakeNameCard("최우성", "111");
	LInsert(&list, p_namecard);

	p_namecard = MakeNameCard("임은지", "222");
	LInsert(&list, p_namecard);

	p_namecard = MakeNameCard("최우승", "333");
	LInsert(&list, p_namecard);

	LCount(&list);

	//특정이름을 대상으로 탐색 후 그 사람의 정보 출력 
	if (LFirst(&list, &p_namecard))
	{
		//같으면 0 return 
		if (NameCompare(p_namecard, "최우성")==0)
			ShowNameCardInfo(p_namecard);
		else
		{
			while(LNext(&list, &p_namecard))
			{
				if (NameCompare(p_namecard, "최우성") == 0)
				{
					ShowNameCardInfo(p_namecard);
					break;
				}
					
			}
		}
		
	}

	//특정이름을 대상으로 탐색 후 그 사람의 전화번호 변경
	if (LFirst(&list, &p_namecard))
	{
		//같으면 0 return 
		if (NameCompare(p_namecard, "임은지") == 0)
		{
			ChangePhoneNum(p_namecard, "444");
			ShowNameCardInfo(p_namecard);
		}

		while (LNext(&list, &p_namecard))
		{
			if (NameCompare(p_namecard, "임은지") == 0)
			{
				ChangePhoneNum(p_namecard, "444");
				ShowNameCardInfo(p_namecard);
				break;
			}
		}
	}
	//특정이름 대상으로 탐색 후 그 사람의 정보 삭제
	if (LFirst(&list, &p_namecard))
	{
		if (NameCompare(p_namecard, "최우승") == 0)
		{
			LRemove(&list);
			delete p_namecard;
		}
		while(LNext(&list, &p_namecard))
		{
			if (NameCompare(p_namecard, "최우승") == 0)
			{
				LRemove(&list);
				delete p_namecard;
				break;
			}
		}
	}

	if (LFirst(&list, &p_namecard))
	{
		ShowNameCardInfo(p_namecard);
		while (LNext(&list, &p_namecard))
		{
			ShowNameCardInfo(p_namecard);
		}
	}
	
	return 0;
}

배열의 장점과 단점! 그리고 연결 기반 리스트

  • 배열 기반 리스트의 장점
    • 데이터의 참조가 쉽다. 인덱스 값을 기준으로 어디든 한 번에 참조가 가능
  • 배열 기반 리스트의 단점
    • 배열의 길이가 초기에 결정되어야 함→ 변경 불가
    • 삭제의 과정에서 데이터의 이동(복사)이 매우 빈번히 일어남
 
반응형
반응형

함수의 재귀적 호출의 의해

재귀함수의 기본적인 이해

void Recursive(void)
{
	cout<<"Recursive call!"<<endl;
	Recursive(); **//함수 나 자신을 재호출** 
}
  • 완료되지 않은 함수를 다시 호출하는 것 가능? → 가능
  • Recursive 함수가 호출되면, 원본으로부터 함수의 복사본이 만들어져서 복사본이 실행되는 구조로 이해

 

Recursive function은 한번 호출되면 계속 호출되는 문제때문에 탈출조건이 필요

재귀함수의 디자인 사례

재귀함수는 자료구조나 알고리즘의 어려운 문제를 단순화하는데 사용되는 중요한 무기

→ 재귀적인 수학 수식을 코드로 그대로 옮길 수 있음

ex) factorial

$$ f(n)= \begin{cases} n \times f(n-1), &n \ge 1 \\ 1, &n=0 \end{cases}
$$

#include <iostream>
using namespace std;

int factorial(int n)
{
	if (n == 0)
		return 1;
	else
		return n * factorial(n - 1);
}

int main()
{
	cout << "1! = " << factorial(1) << endl;
	cout << "2! = " << factorial(2) << endl;
	cout << "3! = " << factorial(3) << endl;
	cout << "4! = " << factorial(4) << endl;
	cout << "9! = " << factorial(9) << endl;
	return 0;
}

재귀의 활용

피보나치 수열: Fibonacci Sequence

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...

#include <iostream>
using namespace std;

int fibo(int num)
{
	cout << "function call param " << num << endl;
	if (num == 1)
		return 0;
	else if (num == 2)
		return 1;
	else
		return fibo(num - 1) + fibo(num - 2);
}

int main()
{
	fibo(7);
	return 0;
}

 

function call param 7
function call param 6
function call param 5
function call param 4
function call param 3
function call param 2
function call param 1
function call param 2
function call param 3
function call param 2
function call param 1
function call param 4
function call param 3
function call param 2
function call param 1
function call param 2
function call param 5
function call param 4
function call param 3
function call param 2
function call param 1
function call param 2
function call param 3
function call param 2
function call param 1

return fibo(n-1)+fibo(n-2);
  • 2개의 fibo 함수가 다시 호출되는데, + 연산자의 왼편에 있는 fibo 함수호출이 완료되어야 비로소 + 연산자의 오른편에 있는 fibo 함수호출이 진행이 된다.

이진 탐색 알고리즘의 재귀적 구현

Binary search 알고리즘도 패턴이 반복됨을 이용해서 recursive function 형태로 만들 수 있음

패턴

  • 탐색 범위의 중앙에 목표 값이 저장되었는지 확인
  • 저장되지 않았다면 탐색 범위를 반으로 줄여서 다시 탐색 시작

탈출조건

  • 탐색 범위의 시작을 의미하는 first가 탐색 범위의 끝을 의미하는 last보다 커지는 경우

함수

int BSearchRecur(int ar[], int first, int last, int target)

위의 패턴과 탈출조건, 그리고 함수 호출 형태만을 이용해 code 작성할 수 있어야 함

#include <iostream>
using namespace std;

int BSearchRecur(int arr[], int first, int last, int target)
{
	if (first > last)
		return -1; // 탐색의 실패를 의미

	int mid = (first + last) / 2;
	if (arr[mid] == target)
		return mid;
	else
	{
		if (arr[mid] > target)
			return BSearchRecur(arr, first, mid - 1, target);
		else
			return BSearchRecur(arr, mid + 1, last, target);
	}
}

int main()
{
	int arr[] = { 1, 3, 5, 7, 9};
	int idx;

	idx = BSearchRecur(arr, 0, sizeof(arr) / sizeof(int) - 1, 7);

	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	idx = BSearchRecur(arr, 0, sizeof(arr) / sizeof(int) - 1, 4);
	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	return 0;
}

타겟 저장 인덱스 : 3 탐색 실패

Q. Binary search 알고리즘을 재귀함수 기반으로 구현하는 이유는? 성능도 떨어지는데?

재귀함수에 익숙해지는게 목적

하노이 타워: The Tower of Hanoi

하노이 문제

  • 그림을 그려서 step by step 으로 이해
  • 코드를 재귀적으로 구성
  • 아래 코드와 비교
#include <iostream>
using namespace std;

void HanoiTowerMove(int num, char from, char by, char to)
{
	if (num == 1) //이동할 원반의 수가 1개라면
	{
		cout << "원반 1을 " << from << "에서 " << to << "로 이동" << endl;
	}
	else
	{
		HanoiTowerMove(num - 1, from, to, by);
		cout << "원반 " << num << "을(를) " << from << "에서 " << to << "로 이동" << endl;
		HanoiTowerMove(num - 1, by, from, to);
	}
}

int main()
{
	//막대 A의 원반 3개를 막대 B를 경유하여 막대C로 옮기기
	HanoiTowerMove(3, 'A', 'B', 'C');
	
	return 0;
}

원반 1을 A에서 C로 이동 원반 2을(를) A에서 B로 이동 원반 1을 C에서 B로 이동 원반 3을(를) A에서 C로 이동 원반 1을 B에서 A로 이동 원반 2을(를) B에서 C로 이동 원반 1을 A에서 C로 이동

 
반응형
반응형

01-1 자료구조와 알고리즘

"프로그램이란 데이터를 표현하고, 그렇게 표현된 데이터를 처리하는 것이다"

  • 자료구조 : 데이터의 표현 및 저장방법
  • 알고리즘 : 표현 및 저장된 데이터를 대상으로 하는 '문제의 해결 방법'

→ 자료구조에 따라서 알고리즘은 달라지고, 알고리즘은 자료구조에 의존적

본 책에서는 선형구조→비선형 구조까지 다룸 (파일구조x, 단순구조 x)

 

01-2 알고리즘의 성능분석 방법

시간 복잡도(Time complexity)와 공간 복잡도(Space complexity)

  • 잘 동작하는 것에 더하여 좋은 성능을 보장하기 위해 자료구조와 알고리즘을 분석하고 평가할 수 있어야 함 (만능은 존재하지 않음)
  • 시간복잡도 : 속도에 해당하는 알고리즘의 수행시간 분석 결과 (cpu에 얼마나 부하를 주는가?)
  • 공간복잡도 : 메모리 사용량에 대한 분석 결과

일반적으로 알고리즘을 평가할 때 메모리의 사용량보다 실행속도에 초점을 둠. 공간복잡도는 보조적인 역할

Q. 어떻게 속도를 평가하나?

  • 특정 상황에 대해서 속도를 재는 것은 무의미
  • $\because$ 처리해야 할 데이터 양의 변화에 따라 속도의 증가 및 감소의 정도가 달라지기 때문
  • 위의 이유로 알고리즘의 수행속도를 평가할 때 다음과 같은 방식을 취함
    1. 연산의 횟수를 셈
    2. 처리해야 할 데이터의 수 n에 대한 연산횟수의 함수 T(n)을 구성
  • 식을 구성하면 데이터 수의 증가에 따른 연산횟수의 변화 정도를 판단할 수 있고, 다른 알고리즘들의 시간복잡도를 비교할 수 있음

알고리즘은 상황에 맞게 종합적으로 사고하고 판단하여 선택해야 함

  • 데이터의 수가 많아짐에 따른 연산횟수의 증가 정도가 중요하므로 알고리즘 A가 B보다 훨씬 좋은 알고리즘. T(n)함수에 대한 패턴이 중요함
  • 그렇다고 알고리즘 B가 필요없는게 아님
    • B와 같은 성격의 알고리즘은 A보다 구현이 쉬워 데이터의 수가 많지 않고 성능에 덜 민감한 경우라면 구현의 편의를 위해 B를 선택하기도 함

순차 탐색(Linear Search) 알고리즘과 시간 복잡도 분석의 핵심요소

#include<iostream>
using namespace std;

int LSearch(int ar[], int len, int target) // 순차 탐색 알고리즘 적용된 함수
{
	int i;
	for (i = 0; i < len; i++)
	{
		if (ar[i] == target)
			return i; //찾은 대상의 인덱스 값 반환
	}
	return -1;
}

int main(void)
{
	int arr[] = { 3, 5, 2, 4, 9 };

	int idx;

	idx = LSearch(arr, sizeof(arr) / sizeof(int), 4);
	if (idx == -1)
		cout<<"탐색 실패"<<endl;
	else
		cout<<"타겟 저장 인덱스 : "<<idx<<endl;

	idx = LSearch(arr, sizeof(arr) / sizeof(int), 7);
	if (idx == 01)
		cout<<"탐색 실패"<<endl;
	else
		cout<<"타겟 저장 인덱스 : "<<idx<<endl;
	return 0;
}

타겟 저장 인덱스 : 3
타겟 저장 인덱스 : -1

  • 좋은 탐색 알고리즘에서는 비교하는 '==' 연산이 적게 수행되어야 함
    • '<'와 '++'는 '=='연산자가 true를 반환할 때까지 수행이 됨

최악의 경우와 최상의 경우

  • 탐색의 대상이 되는 요소의 수(ex) 배열길이)가 n인 경우,
    • 운이 좋아서 찾고자 하는 값이 배열의 맨 앞에 저장된 경우, 연산 횟수 1회 (best case)
    • 운이 없어서 찾고자 하는 값이 배열의 맨 뒤에 저장된 경우, 연산 횟수 n회 (worst case)
  • 알고리즘 평가에 있어 성능평가에서 중요한 것은 worst case

Q. 평균적인 경우를 따져야 하는 것 아닌가?

A. 계산하는 것이 쉽지 않음. 다양한 이론 및 가정이 포함되어야 함. 아래의 average case 참고

순차 탐색 알고리즘 시간복잡도 : worst case

데이터 수가 n개일 때, 최악의 경우에 해당하는 연산횟수는 n

$\therefore T(n)=n$

순차 탐색 알고리즘 시간복잡도 : average case

평균적인 경우의 연산 횟수 계산을 위한 두가지 가정

  • 가정 1. 탐색 대상이 배열에 존재하지 않을 확률이 50%
  • 가정 2. 배열의 첫 요소부터 마지막 요소까지, 탐색 대상이 존재할 확률은 동일

case1. 배열에 탐색대상이 존재하지 않는 경우의 연산횟수

  • 연산횟수 n

case2. 탐색 대상이 존재하는 경우의 연산횟수

  • 연산횟수 n/2
    • 길이가 7인 배열의 경우, 위의 가정 2에 따라 각 배열요소에 탐색 대상이 존재할 확률이 동일하므로 7번 탐색하면, 연산 횟수가 1+2+3+4+5+6+7 = 28, 평균 28/7 = 4회
    • n/2 = 3.5회이지만, 근사치 계산 (수식이 간결해지고 배열의 길이가 길어질수록 근사치 계산결과에 가까워짐)
  • 최종적으로는 가정 1에 따라, 1/2n + 1/2n/2 = 3/4*n
  • 최악의 경우에 비해서 상대적으로 average case에 대한 시간복잡도를 구하기 쉽지 않고 신뢰도도 높지 않음
    • 앞서 2가지 가정을 뒷받침할 근거가 부족
    • 프로그램, 데이터 성격에 따라 배열에 탐색 대상이 존재할 확률이 50%가 아닐 수 있는데 이런 부분이 고려되지 않음

→ 결론 : Worst case를 시간 복잡도의 기준으로 삼는다

이진 탐색(Binary Search) 알고리즘의 소개

  • 순차 탐색보다 높은 성능을 보이지만 아래의 조건을 만족해야 함
    • 배열에 저장된 데이터는 정렬되어 있어야 함

ex) 길이가 9인 배열

arr[] = {1, 2, 3, 7, 9, 12, 21, 23, 27};

이 배열을 대상으로 숫자 3이 저장되어 있는지 확인

첫번째 시도

  • 배열 인덱스의 시작과 끝은 각각 0과 8
  • 0과 8을 합하여 그 결과를 2로 나눔 → (0+8)/2 = 4
    • 2로 나눠서 얻은 결과 4를 인덱스 값으로 하여 arr[4]에 저장된 값이 3인지 확인 → NO

 

두번째 시도

  • arr[4] 에 저장된 값 9와 탐색 대상인 3의 대소를 비교
    • arr[4] = 9
    • 찾고자 하는 대상 3
    • 배열이 오름차순으로 정렬되었음을 전제하였기 때문에 탐색 범위를 수정 가능
  • arr[4] > 3이므로 탐색의 범위를 인덱스 기준 0~3으로 제한 (이전 : 0~8)
  • 0과 3을 더하여 그 결과를 2로 나눔. 나머지는 버림. (0+3)/2 = 1.5 ⇒ 1
  • 2로 나눠서 얻은 결과가 1이니, arr[1]에 저장된 값이 3인지 확인 → NO

→ 두번째 시도 만에 탐색의 대상을 반으로 줄임

세번째 시도

  • arr[1]에서 저장된 값 2와 탐색 대상인 3의 대소를 비교
  • 탐색 대상이 더 크므로 탐색의 범위를 인덱스 기준 2~3으로 제한(이전: 0~3)
  • 2와 3을 더하여 그 결과를 2로 나눔. 나머지는 버림. (2+3)/2 = 2.5 → 2
  • 2로 나눠서 얻은 결과가 2이니, arr[2]에 저장된 값이 3인지 확인 → YES

이진 탐색 알고리즘은 탐색의 대상을 반복하여 반씩 떨구어 내는 알고리즘

이진 탐색(Binary Search) 알고리즘의 구현

  • 탐색의 시작 인덱스(first)와 마지막 인덱스(last) 값을 포함하는 범위로, 탐색의 범위를 반으로 줄여나감.

Q. 언제까지 계속되어야 하나?

A. first 와 last가 같아질 때까지? NO. 같아진 경우 하나의 탐색 범위가 있으므로 이때도 탐색해야 함

#include <iostream>
using namespace std;

int BSearch(int ar[], int len, int targetValue)
{
	int first = 0;      // 탐색 대상의 시작 인덱스 값
	int last = len - 1; // 탐색 대상의 마지막 인덱스 값
	int mid;

	while (first <= last) **// 중요(등호 포함됨)** 
	{
		mid = (first + last) / 2;

		if (targetValue == ar[mid])
		{
			return mid;
		}
		else //타겟이 아니라면 탐색 대상을 반으로 줄인다. 
		{
			if (targetValue < ar[mid])
				last = mid - 1; //왜 -1을 하였을까?
			else
				first = mid + 1; //왜 +1을 하였을까? 
		}
	}
	return -1;
}

int main()
{
	int arr[] = { 1,2,3,7,9,12,21,23,27 };
	int idx;

	idx = BSearch(arr, sizeof(arr) / sizeof(int), 7);
	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	idx = BSearch(arr, sizeof(arr) / sizeof(int), 4);
	if (idx== -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	return 0;
}

타겟 저장 인덱스 : 3 탐색 실패

Q. 위의 코드 중 while 문 조건에서 등호 (=)를 뺀 후 결과는?

탐색 실패 탐색 실패

  • 값 7이 3번째 인덱스에 포함(arr[3] = 7)되어 있음에도 불구하고 탐색 실패의 결과를 얻음

→ "first가 last보다 큰 경우 탐색이 종료됨. 이렇게 종료되었다는 것은 탐색에 실패했음을 뜻함"

결론적으로 while문에서 '='를 빼면, worst case(탐색 범위가 하나만 남은 경우)에 index를 찾을 수 없게 됨

빅-오 표기법(Big-Oh Notation)

데이터의 수 n과 그에 따른 시간 복잡도 함수 T(n)을 정확히 그리고 오차 없이 구하는 것은 대부분의 경우 쉽지 않음

→ '빅-오'라는 것은 함수 T(n)에서 가장 영향력이 큰 부분이 어딘가를 따지는 것

ex) $T(n)=n^2+2n+1$ 은 $T(n)=n^2$ 로 간략화해서 진행할 수 있음

Q. 간략화 할 수 있는 이유는?

A. n의 값이 커질수록 T(n)에서 $n^2$이 차지하는 비율이 절대적으로 큼

표기는 $O(n^2)$로 하고, "빅-오 오브 n^2"로 읽음

단순하게 빅-오 구하기

"T(n)이 다항식으로 표현이 된 경우, 최고차항의 차수가 빅-오가 된다."

대표적인 빅-오

  • $O(1)$ : 상수형 빅-오. 데이터 수에 상관없이 연산횟수가 고정인 유형의 알고리즘
  • $O(logn)$ : 로그형 빅-오. '데이터 수의 증가율'에 비해서 '연산횟수의 증가율'이 훨씬 낮은 알고리즘
  • $O(n)$ : 선형 빅-오. 데이터수와 연산횟수가 비례
  • $O(nlogn)$ : 선형로그형 빅-오. 데이터의 수가 두배로 늘때, 연산횟수는 두배 조금 넘게 증가하는 알고리즘
  • $O(n^2)$ : 데이터 수의 제곱에 해당하는 연산횟수를 요구하는 알고리즘→데이터 양이 많은 경우 적용하기 부적절
  • $O(2^n)$ : 지수형 빅-오. 연산횟수가 지수적으로 증가

연산횟수 대소 정리

  • $O(1) < O(logn)<O(n)<O(nlogn)<O(n^2)<O(2^n)$

 

순차 탐색 알고리즘과 이진 탐색 알고리즘의 비교

#include <iostream>
using namespace std;

//Linear search vs Binary search 비교

int BSearch(int ar[], int length, int targetValue)
{
	int first = 0;
	int last = length - 1;
	int mid;
	int opCount = 0; //비교연산의 횟수를 기록

	while (first <= last)
	{
		mid = (first + last) / 2;

		if (targetValue == ar[mid])
		{
			return mid;
		}
		else
		{
			if (targetValue < ar[mid])
				last = mid - 1;
			else
				first = mid + 1;
		}
		opCount += 1;
	}
	cout << "데이터 수 "<<length<<"에 대한 "<<"비교연산횟수 : " << opCount << endl;
	return -1;
}

int main()
{
	int arr1[500] = { 0, };
	int arr2[5000] = { 0, };
	int arr3[50000] = { 0, };
	int idx;

	//배열 arr1을 대상으로, 저장되지 않는 정수 1을 찾으라고 명령 
	idx = BSearch(arr1, sizeof(arr1) / sizeof(int), 1);
	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	//배열 arr2을 대상으로, 저장되지 않는 정수 1을 찾으라고 명령 
	idx = BSearch(arr2, sizeof(arr2) / sizeof(int), 1);
	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	//배열 arr1을 대상으로, 저장되지 않는 정수 1을 찾으라고 명령 
	idx = BSearch(arr3, sizeof(arr3) / sizeof(int), 1);
	if (idx == -1)
		cout << "탐색 실패" << endl;
	else
		cout << "타겟 저장 인덱스 : " << idx << endl;

	return 0;
}

데이터 수 500에 대한 비교연산횟수 : 9
탐색 실패
데이터 수 5000에 대한 비교연산횟수 : 13
탐색 실패
데이터 수 50000에 대한 비교연산횟수 : 16
탐색 실패

순차 탐색 알고리즘의 Big Oh 가 O(n) 이므로, 비교해보면 $O(n)$ 과 $O(logn)$의 알고리즘 사이의 연산횟수에서 큰 차이를 보여줌

 
 
반응형
반응형
 

rate limit을 두는 이유?

  • 잠재적인 brute force attacks에 대비한 추가 보안 계층 역할
  • 트래픽 급증을 방지하여 안정적인 서버 확보
  • 사용자의 tier에 따라 service를 제한하는 역할 (ex) download )

 

Siege를 이용해 server에 load testing

$ apt-get install siege
# siege -v -r 2 -c 5 https:///thumb.png

c5 * r2 = 10 requests 
  • r: request test 수
  • c: concurrent connection

rate limit을 적용하지 않았고, 1.02초 안에 10번의 요청이 모두 성공

limit_req_zone $request_uri zone=MYZONE:10m rate=1r/s
  • limit_req_zone: rate limit을 적용할 zone을 정의
    • $server_name (서버에 들어오는 모든 요청)
    • $binary_remote_addr (user당)
    • $request_uri (URI 당)
  • zone: rate limit 을 적용할 zone의 이름과 memory에 저장할 zone의 size를 정의 (메모리는 뭘까)
  • rate: limit rate을 정의하고 초당 1개의 requests를 초과하지 않도록 제한

 

rate limit을 적용하면 처음 1개의 request만 성공, 나머지는 모두 503 (Service Unavailable)

 

burst

  • burst를 적용하면 rate limit을 초과하는 connect을 즉시 reject하지 않고 기다리게 만들 수 있음
limit_req_zone $request_uri zone=MYZONE:10m rate=10r/s burst=5
  • 1 + 5burst = 6 connection을 가질 수 있음. 하지만 5개는 바로 response하진 않음
  • 약간의 버퍼를 제공하고 요청 속도는 늦쳐짐
  • 내부적으로 간단한 queue를 사용 (https://www.nginx.com/blog/rate-limiting-nginx/)

  • 첫 요청은 바로 처리, 나머지는 4개는 순차적으로 1r/s에 맞춰 처리
  • 다음 5개의 연결이 전송되면 동일하게 처리됨, 하지만 9.33초 걸림

 

burst를 초과하도록 요청하면,

  • 한번에 15개를 요청하면, 1 + 5burst 이외에 9개는 즉시 503 error 발생
  • 나머지 5burst는 순차적으로 처리됨

nodelay : 허용된 버스트 요청을 가능한 빨리 처리

  • burst를 적용한 zone에 적용 가능
limit_req zone=MYZONE burst=5 nodelay;
  • burst 수 만큼의 connection은 rate limit 무시하고 즉시 처리
    • 즉시 response되었어도 다음 새로운 request에서는 여전히 동일한 rate limit 적용 받음

  • 첫 6개의 요청은 모두 성공
  • 2초 정도 머문 후 다음 요청을 했다고 가정하면, 2개의 요청만 처리가능한 시간이 지났으므로 나머지는 모두 503 error

 

Reference


반응형
반응형

02-1 이더넷

  • 물리 계층과 데이터 링크 계층은 이더넷이라는 공통된 기술이 사용되기 때문에 서로 밀접하게 관련되어 있음

이더넷

  • 유선 LAN 환경에서 가장 대중적으로 사용되는 기술로 케이블 등의 다양한 통신 매체의 규격들과 송수신되는 프레임의 형태, 프레임을 주고 받는 방법 등이 정의된 네트워크 기술

이더넷 표준

  • 유선 LAN 환경에서는 대부분 물리 계층에서는 이더넷 규격 케이블을 사용하고, 데이터 링크 계층에서 주고 받는 프레임은 이더넷 프레임의 형식을 따름
  • 이더넷은 국제적으로 표준화가 이루어져 IEEE 802.3이라는 이름으로 불림. 서로 다른 컴퓨터가 각기 다른 제조사의 네트워크 장비를 사용해도 이더넷 표준을 준수하면 서로 통일된 형태로 통신할 수 있음

통신 매체 표기 형태

  • 이더넷 표준에 따라 통신 매체의 종류과 전송 속도가 달라질 수 있는데 속도와 특성을 한눈에 파악하기 쉽도록 아래와 같은 형태로 표기

전송 속도 BASE-추가특성

1. 전송 속도(data rate)

  • 숫자만 표기되어 있으면 Mbps, 숫자 뒤에 G가 붙는 경우 Gbps 속도를 의미
    • 1000Base-T 케이블은 1000Mbps 속도를 지원하는 케이블
    • 10GBase-T 케이블은 10Gbps 속도를 지원하는 케이블

2. BASE

  • 베이스 밴드(BASEband)의 약자로, 변조 타입(modulation type)을 의미
    • 변조타입 : 비트 신호로 변환된 데이터를 통신 매체로 전송하는 방법으로 대부분 디지털 신호를 송수신하는 베이브밴드 방식을 사용

3. 추가특성

  • 다양한 추가적인 특성을 표현
  • 10BASE-2, 10BASE-5와 같이 전송 가능한 최대 거리가 명시
  • 데이터가 비트 신호로 변환되는 방식을 의미하는 물리 계층 인코딩 방식이 명시되기도 하고 (EX) 1000BASE-CX)
  • 비트 신호를 옮길 수 있는 전송로 수를 의미하는 레인 수가 명시되기도 함 (ex) 100GBASE-LR4)

통신 매체 종류

  • 가장 대중적인 통신 매체의 종류 예시로 추가 특성에 C, T, S, L이라는 글자가 있음
    • C: 동축 케이블
    • T: 트위스티드 페어 케이블
    • S: 단파장 광섬유 케이블
    • L: 장파장 광섬유 케이블

이더넷 프레임(Ethernet frame)

  • 이더넷 네트워크에서 주고받는 프레임 형식으로 상위 계층으로부터 받아들인 정보에 헤더와 트레일러를 추가하는 캡슐화 과정을 통해 만들어짐
  • 헤더는 기본적으로 프리앰블, 수신지 MAC 주소, 송신지 MAC 주소, 타입/길이로 구성되고, 페이로드는 데이터, 트레일러는 FCS로 구성

프리엠블 (preamble)

  • 서두를 뜻하고, 이더넷 프레임의 시작을 알리는 8바이트(64비트) 크기의 정보임
  • 첫 7바이트는 10101010 값을 가지고, 마지막 바이트는 10101011 값을 가짐. 수신지는 이 프리엠블을 통해 이더넷 프레임이 오고 있음을 알아차림

수신지 MAC 주소와 송신지 MAC 주소

  • MAC 주소란 Media Access Control address로 ‘물리적 주소’라고도 불림
  • 네트워크 인터페이스마다 부여되는 6바이트(48비트) 길이의 주소로 LAN 내의 수신지와 송신지를 특정할 수 있음
  • 보통 NIC(Network Interface Controller)라는 장치가 네트워크 인터페이스 역할을 하는데 한 컴퓨터에 NIC가 여러개 있다면 MAC주소도 여러개 있을 수 있음
  • 아래의 명령어로 컴퓨터의 MAC 주소를 직접 확인할 수 있음
    • Windows: ipconfig /all
    • 맥OS 나 리눅스 운영체제: ifconfig (결과 예시 bc:d0:74:61:fd:3c)
$ ifconfig 
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
	ether bc:d0:74:61:fd:3c
	inet6 fe80::1c94:978d:432:1cdf%en0 prefixlen 64 secured scopeid 0xf 
	inet 172.30.1.5 netmask 0xffffff00 broadcast 172.30.1.255
	nd6 options=201<PERFORMNUD,DAD>
	media: autoselect
	status: active

타입/길이

  • 필드에 명시된 크기가 1500(16진수 05DC) 이하인 경우 프레임의 크기(길이)를 나타냄
  • 크기가 1536(16진수 0600) 이상인 경우 타입을 나타냄
  • 타입이란 이더넷 프레임이 ‘어떤 정보를 캡슐화했는지’를 나타내는 정보로 이더타입(ethertype)이라고도 부름
  • 대표적으로 상위 계층에서 사용된 프로토콜의 이름이 명시됨
    • 0800: IPv4
    • 86DD: IPv6
    • 0806: ARP

데이터

  • 상위 계층에서 전달받거나 상위 계층으로 전달해야 할 내용으로 최대 크기는 1500 바이트
  • 항상 46 바이트 이상이어야 하며 그 이하의 데이터인 경우믄 크기를 맞추기 위해 패딩(padding)이라는 정보가 내부에 채워짐. 보통 0으로 채워짐

FCS

  • Frame Check Sequence로 수신한 이더넷 프레임에 오류가 있는지 체크하기 위한 필드
  • 이 필드에는 CRC(Cycle Redundancy Check) 라 불리는 오류 검출용 값이 들어가는데 송신지는 프리엠블을 제외한 나머지 필드 값들을 바탕으로 CRC 값을 계산 후 FCS에 명시
  • 수신지는 수신한 프레임에서 프리앰블과 FCS 필드를 제외한 나머지 필드 값을 바탕으로 CRC 값을 계산한 뒤, 이 값을 FCS와 비교하여 일치 하지 않으면 프레임을 폐기
 
 
 
반응형
반응형

pathlib 모듈은 파이썬 표준 라이브러리로, 파일 읽기/쓰기 작업이나 디렉토리에 있는 특정 유형 파일 나열, 특정 파일의 상위 디렉토리 찾기 등의 작업을 할 때 사용됨

The Problem With Representing Paths as Strings

  • python 3.4 버전부터 pathlib 모듈이 등장했는데 pathlib이 존재하기 전에는 전통적으로 string을 이용하여 파일 경로를 표현했음
  • 하지만 경로는 일반 문자열 이상이기 때문에 중요한 기능들이 os, glob, shutil과 같은 라이브러리를 포함한 표준 라이브러리 전체에 분산되어 있었음
  • 예를 들어 아래의 코드는 txt 파일을 하위의 archive 폴더로 이동시키는 내용
import glob
import os
import shutil

for file_name in glob.glob("*.txt"):
    new_path = os.path.join("archive", file_name)
    shutil.move(file_name, new_path)
  • glob, os, shutil 까지 3개의 import statement 필요
  • pathlib 모듈은 여러 운영체제에서 동일한 방식으로 작동하는 Path 클래스를 제공해 위 3가지 모듈은 임포트 하는 대신 pathlib 모듈만 사용하여 동일한 작업을 수행할 수 있음
from pathlib import Path

for file_path in Path.cwd().glob("*.txt"):
    new_path = Path("archive") / file_path.name
    file_path.replace(new_path)

Path Instantiation With Python’s pathlib

  • pathlib 모듈에 대해 한가지 강력한 동기는 string 대신 전용 객체로 파일 시스템을 표현하는 것
  • 객체 지향 접근 방식은 기존 os.path 방식과 대조할 때, pathlib 핵심이 Path 클래스 라는 점에 주목하면 더욱 분명함
>>> from pathlib import Path
>>> Path
<class 'pathlib.Path'>
  • Path 클래스로 작업하기 때문에 import pathlib; pathlib.Path 보다 from pathlib import Path로 작업하는게 더 효율적
  • Path 객체를 인스턴스화 하는 방법에는 몇가지가 있지만 이 글에서는 클래스 메소드, 문자열 전달, path 컴포넌트를 조인함으로써 path 객체를 생성하는 것을 살펴봄

Using Path Methods

  • Path를 import 한 후에 working directory나 home directory를 가져오기 위해 기존 메소드를 사용할 수 있음
>>> from pathlib import Path
>>> Path.cwd()
PosixPath('/Users/woo-seongchoi/Desktop/realpython')
  • pathlib.Path를 인스턴스로 만들면, OS에 따라 WindowsPath나 PosixPath 객체를 얻을 수 있음
  • 일반적으로 Path를 사용하면, 사용중인 플랫폼에 대한 구체적인 경로를 인스턴스화하는 동시에 코드가 플랫폼에 독립적으로 유지됨
>>> from pathlib import Path
>>> Path.home()
PosixPath('/Users/woo-seongchoi')
  • Path 객체의 cwd나 home 메소드를 통해 python script의 starting point를 쉽게 얻을 수 있음

Passing in a String

  • home directory나 current working directory 대신에 string을 Path 에 전달함으로써 directory나 file을 가리킬 수 있음
>>> from pathlib import Path
>>> Path("/Users/woo-seongchoi/Desktop/realpython/file.txt")
PosixPath('/Users/woo-seongchoi/Desktop/realpython/file.txt')
  • Path 객체를 생성하고 string을 다루는 대신 pathlib 모듈이 제공하는 유연성을 통해 작업 가능
  • POSIX는 Portable Operating System Interface 이고, path 표현 등을 포함하여 운영 체제간 호환성을 유지하기 위한 표준임

Joining Paths

  • 슬래시 (’/’) 를 이용하여 경로의 일부를 연결하도록 경로를 구성할 수 있음
from pathlib import Path

for file_path in Path.cwd().glob("*.txt"):
    new_path = Path("archive") / file_path.name
    file_path.rename(new_path)
  • 슬래시 연산자는 Path 객체를 포함하는 한 여러 경로 또는 경로와 문자열이 섞인 경우도 결합시킬 수 있음
  • 슬래시 연산자를 사용하지 않는다면 joinpath 메소드를 사용할 수 있음
>>> from pathlib import Path
>>> Path.home().joinpath("python", "scripts", "test.py")
PosixPath('/home/woo-seongchoi/python/scripts/test.py')

References

https://realpython.com/python-pathlib/

 

 

반응형
반응형

본 글은 인프런 강의 "대세는 쿠버네티스 [초급~중급]" 를 듣고 요약 및 실습 정리한 내용입니다.

 

Service

  • 기본적으로 자신의 cluster ip를 가짐

 

 

  • Service를 Pod에 연결하면 Service IP를 통해 Pod에 접근 가능
  • Pod는 재생성될 때 IP가 변경되기 때문에 신뢰성이 떨어짐

Service의 종류는 3가지가 있음

1) ClusterIP

  • 외부 (External)에서 접근 불가
  • Cluster 내에서 접근 가능
  • Service의 9000 포트로 요청이 들어오면 Pod의 8080 포트와 연결이 됨

 

  • 하나의 Service에 여러개의 Pod를 연결시킬 수 있고 서비스가 트래픽을 분산해서 Pod에 전달해줌
  • 활용: 인가된 사용자, 내부 대쉬보드, pod의 서비스상태 디버깅

Service yaml 파일

apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
    ports:
      - port: 9000 # Service port 
      - targetPort: 8080 # Service에 연결된 Pod port
		type: ClusterIP # default로 생략가능

 

Pod yaml 파일

apiVersion: v1
kind: Pod
metadata:
  name: pod-1
labels:
  app: pod
spec:
  containers:
  - name: container
    image: tmkube/app
    ports:
    - containerPort: 8080
  • Service yaml파일의 targetPort와 pod yaml파일의 containerPort가 같아야 함

 

2) NodePort

  • 서비스에 clusterIP가 할당됨
  • 쿠버네티스에 할당된 모든 Node에 똑같은 Port가 할당됨
  • 외부에서 어떤 노드든 간에 접속이 되면 Service에 연결이 됨
  • 서비스는 자신과 연결된 Pod에 트래픽을 전달

 

주의할점

  • pod가 있는 node만 port가 할당되는게 아니라 모든 Node에 포트가 할당됨
  • 활용: 내부망 연결, 데모나 임시 연결용
apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
    nodePort: 30000 # 30000 ~ 32767
  type: NodePort
  • externalTrafficPolicy: Local을 통해 각 Node에 속한 pod로 트래픽이 분산됨

3) Load Balancer (외부 시스템 노출용)

  • NodePort의 성격을 그대로 가지고 있고 추가적으로 Load Balancer가 생김 (트래픽 분산)

  • Load balancer에 접근하기 위한 외부 접속 ip주소는 별도로 할당하기 위한 플러그인 설치 필요
apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    app: pod
  ports:
  - port: 9000
    targetPort: 8080
  type: LoadBalancer
반응형

'Kubernetes' 카테고리의 다른 글

Node schedule  (0) 2024.08.04
Pod - Container  (0) 2024.08.04
Pod - Label  (0) 2024.08.04

+ Recent posts