Linked List


링크드리스트란 짧게 이야기해서 노드를 연결시킨 자료구조입니다. 노드는 무엇일까요?


링크드리스트에서 데이터를 갖고 있는 데이터의 묶음입니다. 그림으로 보는 것이 편할 것 같네요.





데이터들을 갖고 있는 하나의 요소가 보이시나요? 이것이 노드입니다. 노드 속에 다음 노드를 가리키고 있습니다.

화살표 모양으로 보아하니, 포인터군요!

특히, 제일 앞에 있는 노드는 헤드(head), 제일 끝 노드는 테일(tail)이라고 부릅니다.




head와 tail은 데이터 필드는 있지만 쓰지 않을 겁니다. 구현의 용이함을 위해선데요. 만약 head와 tail의 데이터 필드를 쓰게 되면  추가, 삭제시 3가지를 고려해야합니다.


1) 추가, 삭제할 노드가 맨 앞 노드인가

2) 추가, 삭제할 노드가 맨 뒤 노드인가

3) 추가, 삭제할 노드가 중간 노드인가


하지만 head와 tail의 data를 쓰지 않는다면 3)번 조건만 고려하면 되기 때문입니다.


이처럼 아무 데이터가 없는 노드를 더미노드라고 합니다.


이딴걸 왜 쓰지?

링크드리스트의 가장 큰 장점은 리스트의 길이가 가변적이라는 겁니다. 배열의 단점을 링크드리스트가 커버 할 수 있습니다.

배열은 크기가 가변적이지 않죠. 우선 크기를 정해준 다음에 모자라면 메모리를 더 할당하고, 배열의 데이터를 복사해야 되죠. 오래걸리고 비효율적이라는 것을 알 수 있겠죠?

배열을 사용해서 다시 사이즈를 늘리는 코드를 이런식으로 짤 수 있겠죠.

data *newList = (data*)malloc(sizeof(data)*(2*size));
for (int i = 0; i < size; size++)
	list[i] = newList[i];
free(list);


위 코드에서 보는 것과 같이 메모리를 할당한 후 for루프로 기존의 있던 값을 복사합니다.


하지만 링크드리스트는 어떨까요?

링크드리스트는 다음 노드만 추가하면 되기 때문에 리스트의 사이즈를 조정하는데, 그리 큰 비용을 들이지 않습니다.


아 좋은거네! 이것만 쓰면 되겠구만

이라고 생각할 수도 있겠지만, 링크드리스트는 어떤 노드를 search하거나 데이터를 변경할때 바로 찾아낼 수가 없습니다.

링크드리스트를 전부 탐색해야할 수도 있습니다.


그러니까 데이터가 자주 추가되거나 가변적으로 자주 변하게 될 프로그램이라면 링크드리스트를 쓰는 것이 좋겠고, 주로 데이터의 변경이나 탐색을 위한 것이라면 배열을 쓰는 것이 좋습니다. case-by-case죠.




구현

포인터나 구조체를 제대로 배우지 않은 사람이라면 약간 어려울 수 있습니다.


우리는 다음과 같은 연산을 정의할 겁니다.



1. addFirst(list, data)

링크드리스트의 새로운 노드를 추가합니다. 가장 앞 있는 노드(head의 다음)에 새로운 노드를 추가하는 연산입니다.


2. addLast(list, data)

addFirst와 반대로 가장 뒤에 노드를 추가합니다.


3. removeNode(list, data)

링크드리스트가 갖고 있는 노드 중에 data값을 갖고 있는 노드를 삭제합니다. 


4. searchNode(list,data)

링크드 리스트에서 data의 값과 일치하는 노드를 검색합니다.


5. printList()

링크드리스트를 전부 탐색합니다. 리스트의 데이터를 전부 보여줍니다.


각각의 연산을 어떻게 구현하면 될 지 그림으로 설명하도록 하지요.


1. addFirst(list, data)

가장 첫번째에 노드를 추가합니다. 가장 첫번째라고 해서 head 앞에 추가해서는 안됩니다. head의 다음 노드에 추가해야합니다.

head의 다음 노드를 새로운 노드로 가리키게 만들고, 그 새로운 노드는 이전에 head가 가리키고 있던 노드를 가리키면 됩니다.


코드는 아래와 같습니다.


void addFirst(List *list,int data) {
	Node *newNode = (Node*)malloc(sizeof(Node));
	newNode->data = data;
	newNode->next = list->head->next;
	list->head->next = newNode;
	list->size++;
}


2. addLast(list, data)

가장 마지막(tail 앞)에 노드를 추가하는 연산입니다. 일단 tail앞까지 가야하지만, 그 전의 노드를 기억해야합니다. 그러니까 살짝 까다로울 수 있지요.


아래는 노드 삭제연산을 구현한 코드입니다.


void addLast(List* list, int data) {
	Node *last = list->head;
	
	while (last->next != list->tail) 
		last = last->next;
	
	Node *newNode = (Node*)malloc(sizeof(Node));
	newNode->data = data;
	newNode->next = list->tail;
	last->next = newNode;
	list->size++;
}




3. removeNode(list, data)

리스트의 노드를 하나씩 돌면서 data가 일치하면 그 노드를 삭제하는 겁니다.

주의할 점은 그 노드 다음 노드를 이전의 노드가 가리키는 작업이 우선적으로 이루어져야한다는 겁니다.



void removeNode(List *list, int data) {
	Node *node = list->head;
	while (node->next != list->tail) {
		if (node->next->data == data) {
			Node *delNode = node->next;
			node->next = delNode->next;
			free(delNode);
			list->size--;
			return;
		}
		node = node->next;
	}
	printf("데이터를 찾지 못했습니다.\n");
}

4. searchNode(list, data)

삭제 연산보다 쉽습니다. list를 돌면서 data와 값이 일치하면 그 노드를 반환하면 되니까요.


void removeNode(List *list, int data) {
	Node *node = list->head;
	while (node->next != list->tail) {
		if (node->next->data == data) {
			Node *delNode = node->next;
			node->next = delNode->next;
			free(delNode);
			list->size--;
			return;
		}
		node = node->next;
	}
	printf("데이터를 찾지 못했습니다.\n");
}


5. printList(list)

이 함수 역시 정말 쉽습니다. search 연산과 별 다를 것이 없죠. 그냥 list돌면서 하나하나 출력해주기만 하면 됩니다.




전체 코드


#include <stdio.h>
#include <stdlib.h>


typedef struct node {
	int data;
	struct node *next;
} Node;
typedef struct list {
	Node *head;
	Node *tail;
	int size;
} List;

void createlist(List *list) {
	
	list->head = (Node*)malloc(sizeof(Node));
	list->tail = (Node*)malloc(sizeof(Node));
	list->head->next = list->tail;
	list->tail->next = list->tail;
	list->size = 0;
}
void addFirst(List *list,int data) {
	Node *newNode = (Node*)malloc(sizeof(Node));
	newNode->data = data;
	newNode->next = list->head->next;
	list->head->next = newNode;
	list->size++;
}
void addLast(List* list, int data) {
	Node *last = list->head;
	
	while (last->next != list->tail) 
		last = last->next;
	
	Node *newNode = (Node*)malloc(sizeof(Node));
	newNode->data = data;
	newNode->next = list->tail;
	last->next = newNode;
	list->size++;
}

Node* searchNode(List *list, int data) {
	Node *node = list->head->next;
	while (node != list->tail) {
		if (node->data == data)
			return node;
		node = node->next;
	}
	printf("데이터를 찾지 못했습니다.\n");

	return NULL;
}

void removeNode(List *list, int data) {
	Node *node = list->head;
	while (node->next != list->tail) {
		if (node->next->data == data) {
			Node *delNode = node->next;
			node->next = delNode->next;
			free(delNode);
			list->size--;
			return;
		}
		node = node->next;
	}
	printf("데이터를 찾지 못했습니다.\n");
}



void printList(List *list) {
	Node *node = list->head->next;
	int i = 1;
	while (node != list->tail) {
		printf("%d 번째 노드 데이터 :%d\n", i++, node->data);
		node = node->next;
	}
}
void distroyList(List *list) {
	Node *node = list->head;
	while (node != list->tail) {
		Node *delNode = node;
		node = delNode->next;
		free(delNode);
	}
	free(list->head);
	free(list->tail);
}

int main() {

	int i;
	List list;
	createlist(&list);
	
	for (i = 1; i <= 5; i++)
		addLast(&list, i);
	for (i = 11; i <= 15; i++)
		addFirst(&list, i);
	removeNode(&list, 11);
	removeNode(&list, 15);
	removeNode(&list, 5);
	removeNode(&list, 4);
	removeNode(&list, 50);

	Node *node = searchNode(&list, 14);
	printf("search :%d\n", node->data);

	node=searchNode(&list,12);
	printf("search :%d\n", node->data);

	node = searchNode(&list, 3);
	printf("search :%d\n", node->data);

	printList(&list);
	return 0;
}


제가 구현한 코드가 삑사리가 있을지도 모릅니다. 그럴땐 여러분들이 고쳐보세요! 그러면서 실력이 느는거 아니겠습니까?! 하하


반응형
블로그 이미지

REAKWON

와나진짜

,

스택(Stack) 개념


자료구조에서 스택은 어떤 자료구조일까요? 스택은 영어 단어 자체에서 힌트를 얻을 수 있습니다.


stack

1. (보통 깔끔하게 정돈된) 무더기   2. 많음, 다량   3. (특히 공장의 높은) 굴뚝
1. 쌓다


stack은 쌓다, 무더기 이런 의미로 쓰이죠.


이 의미가 자료구조에 그대로 반영이 됩니다. 

우리는 어떤 물건을 쌓는다면 가장 먼저 쌓은 물건은 아래에 깔리고 가장 최근 쌓은 물건은 위에 쌓이게 되지요.


꺼낼때는 어떨까요?

나중에 쌓은 물건을 먼저 꺼내겠죠?


이처럼 나중에 쌓은 것이 먼저 나오고 먼저 쌓은 물건은 더 나중에 나온다 라는 개념이 LIFO(Last In First Out)이라고 합니다. 그와 반대, 먼저 들어온 것이 먼저 나간다의 개념은 FIFO(First In First Out)라고 합니다.




종류

스택은 두 가지 정도의 종류가 존재합니다.


● 배열형태의 스택

● 연결리스트 형태의 스택


두 형태는 스택을 구현하는 기능은 같더라도 효율성에서 다릅니다. 


배열형 스택은 우선 접근 속도가 빠릅니다. 하지만 다시 크기를 확장하거나 줄일때 효율성이 떨어지게 되죠. 생각해보세요. 스택 크기가 10인 스택이 데이터를 더 저장하기 위해 스택의 길이를 늘리게 되면 우선 배열의 크기를 다시 할당하고 난 후에 값을 복사해야합니다. 길이가 10인 스택이라 그렇지 크기가 크면 클 수록 성능이 저하되는 것을 알 수 있습니다.


그리고 사용자가 얼마만큼의 스택 크기가 필요한지 예측을 할 수도 없습니다.


반면 연결리스트형 스택은 가변적으로 스택의 크기를 줄였다 늘였다 할 수 있습니다. 반면 값에 접근하려면 그 효율성이 떨어지게 됩니다. 연결리스트 형 스택을 구현하려면 linked list 자료구조를 우선 알아야합니다.


스택의 사용처

스택은 함수의 호출 뿐만 아니라 수식에서도 사용되며 사용처는 많습니다. 우리가 즐겨쓰는 인터넷의 뒤로가기 역시 스택 자료구조가 쓰이게 됩니다. 심지어 미로를 찾는데에도 스택을 쓸 수 있습니다.


구현

기본적으로 스택은 여러 언어에서 표준 라이브러리로 제공이 되긴 합니다만 어떻게 구현이 되어있는지 정도는 이해하고 있어야합니다.


아래의 코드는 배열형 스택을 c++ 코드로 구현한 것입니다. 



#include <iostream>
#define stack_size 100
using namespace std;

struct stack {
	int top = -1;
	int arr[stack_size];
	void push(int data) {
		if (top == stack_size-1) {
			printf("stack is full\n");
			return;
		}
		arr[++top] = data;
	}
	int pop() {
		if (empty()) {
			printf("stack is empty\n");
			return -1;
		}
		return arr[top--];
	}
	int peek() {
		if (empty()) {
			printf("stack is empty\n");
			return -1;
		}
		return arr[top];
	}
	bool empty() {
		return top <= -1;
	}
};
int main() {
	stack st;
	//현재 스택은 비워져있는 상태
	cout << "is stack empty? "<<st.empty() << endl;
	cout << st.pop() << endl;
	cout << st.peek() << endl;

        //for 루프가 돌면 스택은 1개만 저장 가능한 상태
	for (int i = 0; i < stack_size-1; i++)
		st.push(i + 1);

        cout << endl;
        cout << "after for loop"<<endl;
	st.push(15);
        //스택은 전부 채워져 있는 상태
	st.push(120);
        //스택에서 한 개가 비워짐, 1개 채울 수 있는 상태
	st.pop();

	st.push(120);

	cout<<endl;
        //스택이 비워지면 while루프 종료
	while(!st.empty())
		cout << st.pop() << endl;
	
}




stack은 다음과 같은 연산을 하게 됩니다.

push : 데이터를 쌓습니다.
pop : 데이터를 하나 꺼내옵니다.
peek : 데이터를 하나 봅니다. 꺼내오지는 않습니다.
empty : 스택이 비어있는 지 확인합니다.

위의 코드를 이해하기 쉽게 그림으로 표현해보겠습니다. 


1. 초기 스택의 상태는 이런 그림입니다. top은 맨 아래, -1이라는 배열의 인덱스로 쓸 수 없는 위치에 있지요.

 

그래서 stack.empty()는 true(1)입니다. stack에서 peek이나 push 연산을 해도 데이터가 없기 때문에 stack is empty라는 메시지를 보게 되고 -1이 반환됩니다.

2. 그 이후 for 루프로 스택의 최상위 위치를 빼고 전부 push연산으로 채웁니다.

top은 아래의 위치에 있습니다.



3. 이제 15을 push하게 되면 스택은 전부 채워졌고, 다음 120은 모두 채워져있기 때문에 스택에 쌓이지 못합니다.





4. 이후 스택에서 pop연산을 했으니 공간이 하나 남고, 다시 120을 push하면 스택에 들어가게 됩니다.


5. 이제 while루프를 돌며 pop연산을 하게 됩니다. 스택에 데이터가 남아있는 동안 출력하게 됩니다. 최종적으로 스택은 초기상태가 됩니다.




코드를 잘 따라가 보면 이해하실 겁니다.


배열 스택을 조금 이해하셨나요?

반응형
블로그 이미지

REAKWON

와나진짜

,

이름공간(namespace)


C언어에서 한 단계 더 발전한 언어, 바로 C++이 C언어와 차이점을 두고 있는 것은 무엇일까요?


우리는 그 차이점 중에서 한가지를 이야기해보려 합니다.

우선 바로 코드를 보면서 어떤 주제에 대해서 설명할 지 감을 잡아보겠습니다.



#include <iostream>

int main() {
        char input[50];
	std::cin >> input;
	std::cout << input << std::endl;
}



이 짧은 코드를 보며 C언어에서 보지 못한 것이 보이시나요?


몇가지 보입니다. 우선 iostream이라는 파일을 include하고, std::in과 std::cout이 보이네요. std::endl라는 것도 보입니다.




하나씩 보도록 하지요.


● #include <iostream>

원래는 iostream.h 헤더 파일 입니다만 c++에서는 .h확장자를 붙이지 않아도 됩니다. 그래서 파일명만 써주도록 하는 거죠. 우리는 이 헤더파일을 include 해주어야 콘솔 출력, 입력을 할 수 있습니다(물론 다른 헤더파일을 써도 할 수는 있습니다..). (아, iostream은 input, output stream이라는 뜻이라는 거~)



● std::cin

cin은 콘솔 입력을 담당하는 객체입니다. >>은 오퍼레이터로 연산자를 의미합니다. cin에서 >>은 콘솔로 입력할때 쓰이는 연산자입니다. 음 그냥 cin>>은 이 후에 데이터에 콘솔로 입력하여라라는 것입니다(in앞에 c는 console의 c입니다.).  C언어에서 scanf와 아주 비슷한 역할을 하는 녀석입니다.

그 앞의 std는 우리가 오늘 이야기할 네임스페이스라고 합니다.


● std::cout

cin을 알았으니 cout도 대충 눈치채셨겠죠? <<역시 연산자를 의미합니다. 

cout은 콘솔출력을 담당하는 객체입니다. 누군가가 cout 보고 카우트라고 하는데 그럼 cin은 킨이냐?


● std::endl

endl은  endline으로 줄을 바꾸어 줍니다. 


namespace

여기서 자주 등장하는 키워드는 무엇일까요? 바로 std입니다. 

std는 네임스페이스의 이름입니다. 네임스페이스는 이름 공간으로 그 변수를 구분해주는 역할을 합니다. 


왜 쓰일까요?

어떤 개발자 A, B 두명이 있습니다. 서로 협동하여 프로젝트를 진행하고 있지요. 

프로젝트를 진행하던 중 개발자A는 함수를 func(int a,int b)해서 함수를 선언했습니다. 개발자 B는 생각없이 func(int a,int b)로 다른 기능을 하지만 함수 이름은 A가 정한것과 같이 정의해버렸습니다.


나중에 프로젝트가 진행될때 같은 함수이름과 같은 매개변수 때문에 오류가 나게 되지요.


그렇게 등장한게 네임스페이스입니다. 개발자 A는 namespace의 이름을 namespaceA, 그리고 개발자 B는 namespaceB로 자신의 함수를 선언합니다. 이렇게 하면 충돌할 확률이 줄어들겠죠??


와 그런데도 namespace이름마저 같으면 답없다 너네 둘은


네임스페이스를 정의하는 것은 너무 쉽습니다. 그저 namespace 키워드를 써주고 namespace의 이름을 정해 중괄호({, })로 묶어주기만 하면 끝입니다.


namespace 네임스페이스 이름{

...

변수나 함수

...

}


어때요? 참 쉽지 않나요? 그래서 네임스페이스에 존재하는 함수와 이름을 사용하려면 네임스페이스이름::함수 또는 변수 로 써주면 됩니다. 


예를 한번 볼까요?





#include <iostream>

namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}
int main() {
	myNamespace::a = 30;
	myNamespace::b = 40;
	std::cout << "30+40="<< myNamespace::sum(myNamespace::a, myNamespace::b) << std::endl;
}


myNamespace라는 네임스페이스에서는 a,b라는 변수, sum이라는 함수가 있네요. 얘네들을 써먹어서 a+b를 sum함수로 값을 도출해냅니다. 그러니 myNamespace::가 앞에 붙어있죠.



원하는 결과가 나오죠?


네임스페이스안에 네임스페이스를 지정할 수도 있습니다. 그렇게 되면 충돌확률은 현저히 줄어들겠네요. 하지만 쓸 일이 거의 없습니다. 이름도 길어지고.


namespace 네임스페이스 이름{

namespace 네임스페이스 이름1{

...

변수나 함수

...

}

namespace 네임스페이스 이름2{

...

변수나 함수

...

}

namespace 네임스페이스 이름3{

...

변수나 함수

...

}

}



접근하는 방법도 역시 똑같습니다.

네임스페이스::네임스페이스이름n::함수나 변수명



좋은것 같은 데 귀찮아

근데요. 사실 우리는 공부하는데 일일이 std::붙어주는 게 여간 귀찮은게 아니죠.


그런 경우에는 using namespace 네임스페이스이름 을 써주기만 하면 namespace의 이름으로 구분할 것 없이 바로 변수, 함수 등의 데이터를 사용할 수 있습니다.


이렇게 말입니다.


#include <iostream>

using namespace std;
namespace myNamespace {
	int a, b;
	int sum(int a, int b) {
		return a + b;
	}
}

using namespace myNamespace;
int main() {
	a = 30;
	b = 40;
	cout << "30+40="<< sum(a, b) << endl;
}


바로 위의 코드를 네임스페이스를 사용하지 않고 구현했습니다. 코드가 확 줄고 편해졌지요?


이제까지 네임스페이스를 알아보았습니다.


다음에 봐요~



반응형
블로그 이미지

REAKWON

와나진짜

,