반응형

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

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)$의 알고리즘 사이의 연산횟수에서 큰 차이를 보여줌

 
 
반응형
반응형
  • Linked List를 이용해서 stack 구현
class Node:
    def __init__(self, data, next: "Node" = None): 
        self.data = data
        self.next = next

string "Node"로 type hint를 사용한 이유는? https://wschoi.tistory.com/36 참고

class Stack:
    def __init__(self):
        self.last = None

stack = Stack()
  • self.last 속성을 이용해 stack의 push, pop () 메소드 구현
 

 

Push 메소드

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data) # here
        new_node.next = self.last
        self.last = new_node

stack = Stack()
stack.push(1)

new_node = Node(1)

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last # here
        self.last = new_node

stack = Stack()
stack.push(1)

new_node.next = self.last

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node # here

stack = Stack()
stack.push(1)

self.last = new_node

  • last 포인터가 새로운 node를 가리킴

push() 메소드를 한번 더 호출하면?

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data) # here
        new_node.next = self.last
        self.last = new_node

stack = Stack()
stack.push(1)
stack.push(2) # push 한번 더 호출

new_node = Node(2)

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last # here
        self.last = new_node

stack = Stack()
stack.push(1)
stack.push(2)

new_node.next = self.last

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node # here

stack = Stack()
stack.push(1)
stack.push(2)

self.last = new_node

  • last 포인터를 새로 추가된 노드를 가리키도록 업데이트

 

Pop 메소드

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node

    def pop(self):
        data = self.last.data # here
        self.last = self.last.next
        return data

stack = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # here

data = self.last.data

class Stack:
    def __init__(self):
        self.last = None

    def push(self, data):
        new_node = Node(data)
        new_node.next = self.last
        self.last = new_node

    def pop(self):
        data = self.last.data
        self.last = self.last.next # here
        return data

stack = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2

self.last = self.last.next

  • last 포인터를 업데이트
  • 마지막으로 data를 반환
 
반응형
반응형

2022.05.25 - [자료구조] - [자료구조] Linked List - (1) 개념

2022.06.05 - [자료구조] - [자료구조] Linked List - (2) 간단 개념 구현

2022.06.07 - [자료구조] - [자료구조] Linked List - (3) 노드 삽입 (at the end)

2022.06.08 - [자료구조] - [자료구조] Linked List - (4) 노드 삽입 (at the front)

2022.06.12 - [자료구조] - [자료구조] Linked List - (5) 데이터 조회

본 글에서는 Linked List에서 노드 삭제에 대해 다룸. cur이라는 변수를 이용하여 head가 가리키는 노드부터 삭제할 노드를 찾음.

노드 삭제에 대해서 크게 3가지 경우로 나눠서 생각

1) Linked list가 비어있는 경우 : 메소드 바로 종료

cur = self.head

if cur is None:
	return

Linked List가 비어있는 상태

2) head가 가리키는 노드의 값이 삭제하고자하는 값인 경우 (아래 그림에서 data 변수가 3인 경우)

cur = self.head

 

head가 가리키는 노드를 삭제하는 경우

head는 현재 가리키는 노드의 next가 가리키는 노드를 참조해야 함. 그리고 cur에는 None 할당

if cur.data == data: # 첫 노드 삭제
    self.head = cur.next
    cur = None
    return

head가 가리키는 노드 업데이트

3) head가 가리키는 노드가 아닌 다른 노드를 삭제하는 경우 (data에 2가 담긴 노드를 삭제한다고 가정)

cur = self.head

prev 변수를 도입하여 cur이 가리키는 노드를 참조하고, cur 변수는 현재 가리키는 노드의 next가 가리키는 노드 참조.

prev = cur
cur = cur.next

그리고 cur 변수가 가리키는 노드의 data가 삭제할 데이터인지 확인하는 과정을 반복하고 맞으면 반복과정을 종료

while True:
    prev = cur
    cur = cur.next
        
    if cur.data == data:
        break

 

Linked List가 끊기지 않기 위해서는 prev가 가리키는 노드의 next가 cur노드가 가리키는 노드의 next가 가리키는 노드를 참조해야 함 (아래 그림에서 빨간선)

prev.next = cur.next

data에 3이 담긴 노드의 next가 data에 1이 담긴 노드를 가리킴

 

위 3가지 경우가 포함된 delete 메소드는 아래와 같음

    def delete(self, data): # data가 포함된 노드를 linked list에서 삭제
        cur = self.head
        
        if cur == None: # Linked List가 비어있는 경우
            return 

        if cur.data == data: # 첫 노드 삭제
            self.head = cur.next
            cur = None
            return

        while True:
            prev = cur
            cur = cur.next
        
            if cur.data == data: # 중간 노드 삭제 
                break
        
        prev.next = cur.next
        cur = None

전체 코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    # linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.head = new_node
        else:
            self.tail.next = new_node
            
        self.tail = new_node

    # linked list 처음에 노드 추가
    def push(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.tail = new_node
        else:
            new_node.next = self.head
        
        self.head = new_node

    def delete_all(self):
        while self.head is not None:
            cur = self.head
            self.head = self.head.next
            cur = None

    # data가 포함된 노드를 linked list에서 삭제
    def delete(self, data):
        cur = self.head
        
        if cur == None: # Linked List가 비어있는 경우
            return 

        if cur.data == data: # 첫 노드 삭제
            self.head = cur.next
            cur = None
            return

        while True:
            prev = cur
            cur = cur.next
        
            if cur.data == data: # 중간 노드 삭제
                break
        
        prev.next = cur.next
        cur = None

    def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

if __name__ == "__main__":
    linked_list = LinkedList()
    linked_list.print() # 출력 없음
    linked_list.push(1)
    linked_list.print() # 1
    linked_list.push(2)
    linked_list.print() # 2 1
    linked_list.push(3)
    linked_list.print() # 3 2 1

    linked_list.delete(2)
    linked_list.print() # 3 1

 

 

 
 
 
반응형
반응형

2022.06.08 - [자료구조] - [자료구조] Linked List - (4) 노드 삽입 (at the front)

 

[자료구조] Linked List - (4) 노드 삽입 (at the front)

2022.06.07 - [자료구조] - [자료구조] Linked List - (3) 노드 삽입 (at the end) 본 글에서는 위의 글에 이어서 새로운 노드를 Linked List 맨 앞에 추가하는 방법을 다룸. (1) 새로운 노드를 Linked List 맨 앞..

wschoi.tistory.com

본 글에서는 Linked List에 삽입된 노드들의 데이터를 조회하는 메소드를 다룸.

Linked list에 연결된 노드들의 data를 조회 

1) head가 가리키는 노드부터 순차적으로 출력하기 위해 cur 변수를 사용해 head가 참조하는 노드를 같이 참조

cur = self.head

2) cur 변수가 가리키는 노드의 data 출력

print(cur.data, end=" ") # 3

3) cur이 가리키는 노드를 연결된 노드로 이동

cur = cur->next

4) cur이 가리키는 노드의 data 출력 

print(cur.data, end=" ") # 2

위 과정을 print 메소드로 묶어서 표현하면 아래와 같음

def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

최종 코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

	# linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.head = new_node
        else:
            self.tail.next = new_node
            
        self.tail = new_node

	# linked list 처음에 노드 추가
    def push(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.tail = new_node
        else:
            new_node.next = self.head
        
        self.head = new_node

    def print(self):
        cur = self.head

        while cur:
            print(cur.data, end=" ")
            cur = cur.next
        print()

if __name__ == "__main__":
    linked_list = LinkedList()
    linked_list.print() # 출력 없음
    linked_list.push(1)
    linked_list.print() # 1
    linked_list.push(2)
    linked_list.print() # 2 1
    linked_list.push(3)
    linked_list.print() # 3 2 1
반응형
반응형

2022.06.07 - [자료구조] - [자료구조] Linked List - (3) 노드 삽입 (at the end)

본 글에서는 위의 글에 이어서 새로운 노드를 Linked List 맨 앞에 추가하는 방법을 다룸. 

(1) 새로운 노드를 Linked List 맨 앞에 추가
(2) 새로운 노드를 Linked List 맨 뒤에 추가 

1) LinkedList 클래스를 정의

  • 클래스의 instance 변수로 head와 tail이 있고 모두 None값으로 초기화
class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

LinkedList의 초기 상태

 

head가 None을 가리키면, LinkedList에 아직 Node가 하나도 없는 상태.

2) 빈 Linked List에 새로운 node 추가

node가 새롭게 추가되면 head가 새로 추가된 node를 가리킴.

총 1개의 node만 있으므로 tail로 새로 추가된 node를 가리킴.

new_node = Node(1) # 새로운 노드 추가

if self.head is None: # head가 None을 가리키면,
    self.head = new_node
    self.tail = new_node

Linked List에 노드가 하나도 없는 상황에서 노드가 추가되는 과정

3) Node가 있는 Linked List에 새로운 node 추가

여기에 다시 새로운 node를 추가.

new_node = Node(2)

새로운 노드 추가

 

Linked List로 연결되기 위해서는 새로운 노드의 next가 head가 가리키는 노드를 가리켜야 함

new_node.next = self.head

새로운 노드의 next가 기존 노드를 가리킴

그리고 head는 새로 추가된 노드를 가리켜야함. 

self.head = new_node

head가 참조하는 노드 업데이트

 

마지막으로 새로운 노드 하나 더 추가.

new_node = Node(3)

세번째 노드 추가

새로운 노드의 next가 head가 가리키는 노드를 가리켜야 함.

new_node.next = self.head

 

마지막으로 head가 새로 추가된 노드를 가리키도록 업데이트

self.head = new_node

 

위의 과정을 'push' 라는 이름의 method로 변환하면 아래와 같음.

# linked list 처음에 노드 추가
def push(self, new_data):
    new_node = Node(new_data)

    if self.head is None:
        self.tail = new_node
    else:
        new_node.next = self.head
        
    self.head = new_node
  • push 메소드는 항상 처음에 노드를 추가하기 때문에 Time complexity가 항상 O(1)이다.

최종코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

	# linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.head = new_node
        else:
            self.tail.next = new_node
            
        self.tail = new_node

	# linked list 처음에 노드 추가
    def push(self, new_data):
        new_node = Node(new_data)

        if self.head is None:
            self.tail = new_node
        else:
            new_node.next = self.head
        
        self.head = new_node

 

 
 
 
 
 
 
반응형
반응형

2022.06.05 - [자료구조] - [자료구조] Linked List - (2) 간단 개념 구현

 

[자료구조] Linked List - (2) 간단 개념 구현

본 글에서는 이전 Linked List 글에서 언급한 Node를 구현. 최대한 간단히 Node 클래스만을 이용해서 어떻게 node들이 연결될 수 있는지에 대해 설명. Node 클래스 정의 class Node: def __init__(self, data=None..

wschoi.tistory.com

 

본 글에서는 위의 글에 이어서 Linked List에 노드를 추가하는 방법을 다룸. 노드를 추가하는 방법이 크게 2가지가 있음.

(1) 새로운 노드를 Linked List 맨 앞에 추가
(2) 새로운 노드를 Linked List 맨 뒤에 추가 (본 글에서 다룸)

1) LinkedList 클래스를 정의

  • 클래스의 instance 변수로 head와 tail이 있고 모두 None값으로 초기화
class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

LinkedList의 초기 상태

 

head가 None을 가리키면, LinkedList에 아직 Node가 하나도 없는 상태.

2) 빈 Linked List에 새로운 node 추가

node가 새롭게 추가되면 head가 새로 추가된 node를 가리킴.

총 1개의 node만 있으므로 tail로 새로 추가된 node를 가리킴.

new_node = Node(1) # 새로운 노드 추가

if self.head is None: # head가 None을 가리키면,
    self.head = new_node
    self.tail = new_node

Linked List에 노드가 하나도 없는 상황에서 노드가 추가되는 과정

3) Node가 있는 Linked List에 새로운 node 추가

여기에 다시 새로운 node를 추가.

new_node = Node(2)

새로운 노드 추가

Linked List로 연결되기 위해서는 head와 tail이 가리키는 노드의 next가 새로운 노드를 가리켜야함. 

self.tail.next = new_node

기존의 노드의 next가 새로운 노드를 가리킴

그리고 tail은 새로 추가된 노드를 가리켜야함. 

self.tail = new_node

tail이 참조하는 노드 업데이트

마지막으로 새로운 노드 하나 더 추가.

new_node = Node(3)

세번째 노드 추가

 

tail이 가리키는 노드의 next가 새로운 노드를 가리켜야함.

self.tail.next = new_node

두번재 node의 next가 새로운 노드를 가리킴

 

마지막으로 tail이 새로 추가된 노드를 가리키도록 업데이트

self.tail = new_node

 

위의 과정을 'append' 라는 이름의 method로 변환하면 아래와 같음.

# linked list 끝에 노드 추가
def append(self, new_data):

    new_node = Node(new_data)

    if self.head is None:# 기존에 node가 없는 경우
        self.head = new_node
    else: # 기존에 node가 있는 경우
        self.tail.next = new_node
    
    self.tail = new_node

 

  • self.tail = new_node는 if-else 문에서 공통이므로 바깥으로 빼줌

최종코드

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    # linked list 끝에 노드 추가
    def append(self, new_data):
        new_node = Node(new_data)

        if self.head is None:# 기존에 node가 없는 경우
            self.head = new_node
        else: # 기존에 node가 있는 경우
            self.tail.next = new_node
    
        self.tail = new_node

 

참고)

1. Linked List 클래스에서 tail 이라는 인스턴스 변수를 사용함으로써, append 메소드의 time complexity가 O(1)을 유지한다. 변수를 사용하지 않는 경우는 head 노드부터 끝 노드까지 loop를 이용해 찾은 후 node를 추가해야하기 때문에 time complexity가 O(n)이 된다. (n은 노드 수)

 
 
반응형
반응형

본 글에서는 이전 Linked List 글에서 언급한 Node를 구현. 

 

최대한 간단히 Node 클래스만을 이용해서 어떻게 node들이 연결될 수 있는지에 대해 설명.

 

Node 클래스 정의

class Node:
	def __init__(self, data=None):
		self.data = data
		self.next = None

다음의 2가지 인스턴스 속성을 가짐

(1) 값을 저장하는 data

(2) 다음 Node를 가리키는 next

 

여러 개의 노드를 생성 후 서로 연결

1. 3개의 Node 인스턴스 생성

first  = Node(1)
second = Node(2)
third  = Node(3)

3개의 Node 인스턴스들

 

2. first 노드의 next에 second 노드 인스턴스 할당

first.next = second
 

first.next 에 second 를 할당하면 진짜 first 노드가 second 노드와 연결된게 맞는지 확인하는 방법

print(type(first.next))  # <class '__main__.Node>

# first노드의 next를 통해서 second 노드의 값에 접근 가능
print(first.next.data)  # 2
 
 

first 노드의 next를 통해 second 노드의 data에 접근 가능

 

3. second 노드의 next에 third 노드 할당

second.next = third

연결된 3개의 Node 들

 
 
 

 

본 글에서는 Node들의 연결을 통해 한 노드에서 다른 노드에 접근가능함을 보여주는 내용이었음. 

배열 대비 Linked List의 장점은 1) dynamic size 2) 삽입/삭제가 쉬움 인데 위의 내용을 통해 node를 계속 추가함으로써 dynamic size의 장점은 보여주었고 다음 글에서 삽입/삭제에 대한 내용을 다룸.

반응형
반응형

리스트는 데이터에 순서를 매겨 늘어놓은 자료구조.

리스트라는 자료구조는 구현 방법에 따라 크게 2가지로 나뉨

1. 순차 리스트 (Array list) : 배열을 기반으로 구현된 리스트
2. 연결 리스트 (Linked list) : 메모리의 동적 할당을 기반으로 구현된 리스트

본 글에서는 연결 리스트에 대해 다룸.

 

Array List와 Linked List의 메모리 사용 비교(출처 : 생활코딩)

미리 정해진 크기(위 그림에서는 노란색 9칸) 에 순차적으로 저장되는 Array List와 달리,
Linked List는 미리 공간의 크기가 정해져있지 않고, 물리적으로 떨어져 있음.


그렇기 떄문에 서로를 연결할 수 있는 방법이 필요.

떨어져 있는 공간을 연결할 수 있는 방법 필요 (출처: 생활코딩)

위 그림에서 노란색 점 하나하나가 아래 그림 linked list 의 Node와 대응됨. Node는 linked list에서 각각의 원소를 의미.

그럼 아래 그림에서 Node1과 Node2는 어떻게 연결할까?

Node 간의 연결 어떻게??

 

각각의 Node는 data와 next 라는 속성을 가짐.

data에는 값이 저장되고, next는 다음에 가리키는 Node에 대한 정보가 들어가 있음. 이를 참조하면 다음 Node에 접근 가능.

 

특별히 맨 앞에 있는 노드를 head node, 맨 끝에 있는 노드를 tail node라고 함.

반응형

+ Recent posts