더블 링크드 리스트(Double Linked List)

 

더블 링크드 리스트란?

  • 노드를 연결해서 만든 리스트다.
  • 데이터와 포인터를 이용하여 노드를 만들고 이것을 기차처럼 연결시켜준다.
  • 머리(헤드)와 꼬리(테일)을 가지고있다.
  • 싱글 링크드 리스트 보다 이전노드를 가지고있어야 하기때문에 메모리를 더 차지하게된다.
  • 싱글리스트는 한쪽으로만 탐색이 가능하지만 더블 링크트 리스트는 양쪽에서 탐색이 가능하게 된다.

 

싱글 링크드 리스트 구현

  • 노드 추가
  • 노드 탐색
  • 노드 삭제
  • 노드 삽입

노드 추가

테일 뒤쪽에 노드를 추가하는 그림이다.

싱글 링크드 리스트의 경우 이전 노드를 알 수가 없어 노드를 추가할땐 무조건 끝까지 탐색 후 넣어줘야 했지만

더블 링크드 리스트의 경우 이전 노드를 알 수 있어 단순히 테일의 이전노드를 참조하여 간단히 추가 할 수 있다.

void LinkedList::Add(const int& NewData)
{
	Node* NewNode = new Node();

	if (NewNode != nullptr)
	{
		NewNode->Data = NewData;
	}
	if (Tail != nullptr)
	{
		NewNode->Prev = Tail->Prev;
		NewNode->Next = Tail;
		Tail->Prev->Next = NewNode;
		Tail->Prev = NewNode;
	}
}

노드 탐색

테일 전까지의 노드를 순회하면서 같은 데이터를 찾아야한다.

싱글 링크트 리스트와 코드차이는 없다. 다만 뒤에서 탐색하는걸 조건에 넣어 만들면 양방향 탐색이 가능해진다.

Node* LinkedList::Search(const int& Data)
{
	Node* CurrentNode = Head;

	if (CurrentNode != nullptr)
	{
		while (CurrentNode->Next != Tail)
		{
			if (CurrentNode->Next->Data == Data)
			{
				return CurrentNode->Next;
			}

			CurrentNode = CurrentNode->Next;
		}
		std::cout << "검색 할 노드가 없습니다" << std::endl;
		return nullptr;
	}
	else
	{
		std::cout << "헤드가 없습니다" << std::endl;
		return nullptr;
	}
}

 

노드 삽입

헤드의 다음노드를 첫번째 노드라고 생각하고 첫 번째 노드와 두 번째 노드사이에 새로운 노드를 넣는다.

그림에선 첫 번째 뒤에 삽입 하기 위해 1이라는 인덱스를 필요로 한다.

 

void LinkedList::Insert(const int& NewData, const int& Index)
{
	Node* CurrentNode = Head;

	if (CurrentNode != nullptr)
	{
		for (int i = 0; i < Index; ++i)
		{
			if (CurrentNode != nullptr)
			{
				CurrentNode = CurrentNode->Next;
			}
			else
			{
				std::cout << Index << "번째의 노드가 없습니다" << std::endl;
				return;
			}
		}
	}

	if (CurrentNode == nullptr)
	{
		std::cout << Index << "번째의 노드가 없습니다" << std::endl;
		return;
	}

	Node* NewNode = new Node();
	if (NewNode != nullptr)
	{
		NewNode->Data = NewData;

		CurrentNode->Next->Prev = NewNode;
		NewNode->Next = CurrentNode->Next;
		NewNode->Prev = CurrentNode;
		CurrentNode->Next = NewNode;
	}
}

노드 제거

테일 전까지 노드를 순회하면서 같은 데이터를 찾고 그 데이터가 있으면 제거를 할 수 있다.

void LinkedList::Remove(Node* InRemoveNode)
{
	Node* CurrentNode = Head->Next;
	if (CurrentNode != nullptr && InRemoveNode != nullptr)
	{
		if (CurrentNode == InRemoveNode)
		{
			Head->Next = InRemoveNode->Next;
			InRemoveNode->Next->Prev = Head;
		}
		else
		{
			InRemoveNode->Prev->Next = InRemoveNode->Next;
			InRemoveNode->Next->Prev = InRemoveNode->Prev;
		}
		delete InRemoveNode;
	}
}

메인 코드

int main()
{
	LinkedList* List = new LinkedList();

	if (List != nullptr)
	{
		List->Add(5);
		List->Add(9);
		List->Add(7);
		List->Insert(3, 1);
		std::cout << "==제거 하기전 출력==" << std::endl;
		List->Show();
		Node* RemoveNode = List->Search(9);
		if (RemoveNode != nullptr)
		{
			List->Remove(RemoveNode);
		}
		std::cout << "==제거 후 출력==" << std::endl;
		List->Show();
	}
}

Add함수를 통해 5,9,7을 추가 해주고

Insert함수를 통해 3의 데이터를 1번째에 삽입시켜주면 5,3,9,7이 출력되며

Search함수로 9의 데이터를 찾은 후 제거해준다

그럼 5,3,7 이 출력된다.

 

싱글 링크드 리스트(Single Linked List)

 

싱글 링크드 리스트란?

  • 노드를 연결해서 만든 리스트다.
  • 데이터와 포인터를 이용하여 노드를 만들고 이것을 기차처럼 연결시켜준다.
  • 머리(헤드)와 꼬리(테일)을 가지고있다.
  • 데이터의 추가, 삽입, 삭제는 용이하나(가르키는 노드만 바꿔주면 됨) 그 과정까지 탐색 속도가 떨어진다.

데이터와 다음 노드에 대한 포인터로 이루어져있다.
헤드와 테일이 있으며 노드끼리 연결 되어 있다.

싱글 링크드 리스트 구현

  • 노드 추가
  • 노드 탐색
  • 노드 삭제
  • 노드 삽입

싱글 링크드 리스트 클래스

class Node
{
public:
	Node();
private:
	int Data;
	Node* Next;
	friend class LinkedList;

public:

	int GetData()
	{
		return Data;
	}
};

class LinkedList
{
public:
	LinkedList();
private:
	Node* Head;
	Node* Tail;

public:

	void Add(const int& NewData);
	bool Search(const int& Data);
	void Insert(const int& NewData, const int& Index);
	void Remove(const int& Data);

	Node* GetNodeAt(const int& Index);
	void Show();
};

노드 추가

새로운 노드를 추가하는 그림
11의 데이터를 가진 노드의 Next는 테일을 가리키는데 이걸 지우고 새로운 노드를 가르키게 해준뒤 새로운 노드는 테일을 가르키게 만든다

먼저 테일전까지의 노드에 접근하기 위해선 선형으로 연결되어있는 노드들을 통해 접근을 해야한다.

테일전까지 접근을 하고 기존노드를 지운 후 새로운 노드와 연결을 시켜주고 새로운 노드는 테일에 연결 시켜준다.

void LinkedList::Add(const int& NewData)
{
	Node* NewNode = new Node();

	if (NewNode != nullptr)
	{
		NewNode->Data = NewData;
	}

	if (Head->Next != nullptr)
	{
		Node* CurrentNode = Head;

		if (CurrentNode != nullptr)
		{
			while (CurrentNode->Next != Tail)
			{
				CurrentNode = CurrentNode->Next;
			}

			CurrentNode->Next = NewNode;
			NewNode->Next = Tail;
		}
	}
	else
	{
		Head->Next = NewNode;
		NewNode->Next = Tail;
	}
}

 

노드 탐색

테일 전까지의 노드를 순회하면서 같은 데이터를 찾아야한다.

bool LinkedList::Search(const int& Data)
{
	Node* CurrentNode = Head;

	if (CurrentNode != nullptr)
	{
		while (CurrentNode->Next != Tail)
		{
			if (CurrentNode->Next->Data == Data)
			{
				return true;
			}

			CurrentNode = CurrentNode->Next;
		}
		std::cout << "검색 할 노드가 없습니다" << std::endl;
		return false;
	}
	else
	{
		std::cout << "헤드가 없습니다" << std::endl;
		return false;
	}
}

노드 삽입

데이터5와 데이터11사이에 삽입을 해준다.
첫번째 노드와 두번째 노드의 사이에 새로운 노드를 사입하는 과정

헤드의 다음노드를 첫번째 노드라고 생각하고 첫 번째 노드와 두 번째 노드사이에 새로운 노드를 넣는다.

그림에선 첫 번째 뒤에 삽입 하기 위해 1이라는 인덱스를 필요로 한다.

void LinkedList::Insert(const int& NewData, const int& Index)
{
	Node* CurrentNode = Head;

	if (CurrentNode != nullptr)
	{
		for (int i = 0; i < Index; ++i)
		{	
			if (CurrentNode != nullptr)
			{
				CurrentNode = CurrentNode->Next;
			}
			else
			{
				std::cout << Index << "번째의 노드가 없습니다" << std::endl;
				return;
			}
		}
	}

	if (CurrentNode == nullptr)
	{
		std::cout << Index << "번째의 노드가 없습니다" << std::endl;
		return;
	}

	Node* NewNode = new Node();
	if (NewNode != nullptr)
	{
		NewNode->Data = NewData;
		NewNode->Next = CurrentNode->Next;
		CurrentNode->Next = NewNode;
	}
}

노드 제거

20과 6사이에 있는 노드를 지우는 그림이다.

테일 전까지 노드를 순회하면서 같은 데이터를 찾고 그 데이터가 있으면 제거를 할 수 있다.

여기서 중요한 점은 다음 노드를 연결할때 순서가 중요한다.

제거 하기전에 제거 해야하는 노드의 전 노드를 찾고 그림에선 데이터 20의 노드 이다.

데이터 20의 노드를 제거 해야하는 노드의 다음 노드를 연결 시켜주고 노드를 제거해야 한다.

제거해야할 노드를 먼저 제거하면 제거하는 다음 노드를 알 수 없기 때문이다.

void LinkedList::Remove(const int& Data)
{
	Node* CurrentNode = Head;

	if (CurrentNode != nullptr)
	{
		while (CurrentNode->Next != Tail)
		{
			if (CurrentNode->Next->GetData() == Data)
			{
				Node* RemoveNode = CurrentNode->Next;
				CurrentNode->Next = CurrentNode->Next->Next;
				std::cout << RemoveNode->GetData() << std::endl;
				delete RemoveNode;
				return;
			}
			CurrentNode = CurrentNode->Next;
		}
		std::cout << Data << "가 리스트에 존재하지 않습니다" << std::endl;
	}
}

메인 코드

int main()
{
	LinkedList* List = new LinkedList();

	if (List != nullptr)
	{
		List->Add(5);
		List->Add(8);
		List->Add(1);
		List->Insert(10, 2);
		List->Search(11);
		List->Remove(8);
		List->Remove(55);
		List->Show();
	}
}

Add함수를 통해 5, 8, 1 을 추가해주고

Insert함수를 통해 10의 데이터를 2번째에 삽입시켜주면 5, 8, 10, 1이 저장되며

Search함수로 검색을 하면 없는 데이터이기에 검색에 실패하고

Remove함수를 8을 제거 해서 5, 10, 1 이 되고 55를 제거할려 하지만 없기때문애 실패한다.

선택 정렬(Selection Sort)

 

선택 정렬 이란?

  • 주어진 리스트 중에 최소값을 찾는다.
  • 그 값을 맨 앞에 위치한 값과 교체한다.
  • 맨 처음 위치를 뺀 나머지 리스트를 같은 방법으로 교체한다.
  • 시간복잡도는 O(n^2) 이다.

선택 정렬 과정

리스트의 배열 0~4번까지 순차적으로 돌면서 제일 작은 숫자의 찾아 인덱스를 저장한다.

이때 3의 인덱스를 가지게 된다.


위에서 찾은 3번의 인덱스를 0번의 인덱스를 참조해 교체한다.


0번의 인덱스는 제외하고 1~4번까지 다시 순차적으로 돌면서 제일 작은 숫자의 인덱스를 저장한다.

이때 2의 인덱스를 가지게 된다.


위에서 찾은 2번의 인덱스를 1번의 인덱스를 참조해 교체한다.


0,1번의 인덱스는 제외하고 2~4번까지 다시 순차적으로 돌면서 제일 작은 숫자의 인덱스를 저장한다.

이때 3번의 인덱스를 가지게 된다.

위에서 찾은 3번의 인덱스를 2번의 인덱스를 참조해 교체한다.

이런 과정을 통해 정렬을 할 수 있다.

 

선택 정렬 코드

#include <iostream>

int main()
{
    int Array[5] = { 4,3,2,1,5 };

    int Count = 5;

    int Index = 0;

    for (int i = 0; i < Count; ++i)
    {
        Index = i;
        for (int j = i + 1; j < Count; ++j)
        {
            if (Array[Index] > Array[j])
            {
                Index = j;
            }
        }
        int Temp = Array[Index];
        Array[Index] = Array[i];
        Array[i] = Temp;
    }


    for (int i = 0; i < Count; ++i)
    {
        std::cout << Array[i] << " ";
    }
}

삽입 정렬(Insertion Sort)

 

삽입 정렬이란?

  • 모든 요소를 앞에서부터 차례대로 이미 정렬된 부분과 비교하여, 위치를 찾아 삽입을 하는 알고리즘 이다.
  • 시간복잡도는 평균 or 최악의 경우 O(n^2)를 가지게 된다.
  • 정렬된 데이터 기준으로 시간복잡도는 O(1)을 가지게 된다.
  • 데이터가 많아지면 효율이 떨어지고 구현이 간단하다.
  • 선택 정렬이나 버블 정렬과 같은 O(n^2) 알고리즘에 비해 빠르다

삽입 정렬 과정

오름차순 기준이다.

앞에서 부터 배열의 인덱스 0이 아닌 1로 기준을 잡고 기준에서 왼쪽과 비교하여 삽입을 해준다.

 

먼저 인덱스1의 키 값인 3을 가져온다.


3 과 4를 비교 후 3보다 4가 크니 4를 한칸 오른쪽으로 이동 시킨다.


배열의 -1의 인덱스가 존재 하지 않기 때문에 키 값은 0번째 배열에 삽입을 해준다.


이번엔 세 번째의 키 값을 가져 온다.


2와 4를 비교 후 4가 더 크니 오른쪽으로 한칸 이동 시킨다.


2와 3을 비교 후 3이 더 크니 오른쪽으로 한칸 이동 시킨다.


배열의 -1의 인덱스가 존재 하지 않기 때문에 키 값은 0번째 배열에 삽입을 해준다.

이런식으로 배열의 네 번째까지 똑같은 방식으로 비교하여 정렬을 시켜준다.

 

삽입 정렬 코드

#include <iostream>

int main()
{
    int Array[5] = { 4,3,2,1,5 };

    int Count = 5;

    for (int i = 1; i < Count; ++i)
    {
        int Key = Array[i];
        int j = i - 1;
        while (j >= 0 && Key < Array[j])
        {
            Array[j + 1] = Array[j];
            --j;
        }
        Array[j + 1] = Key;
    }


    for (int i = 0; i < Count; ++i)
    {
        std::cout << Array[i] << " ";
    }
}

버블 정렬(Bubble Sort)

 

버블 정렬이란?

  • 서로 인접한 두 원소를 검사하여 정렬하는 알고리즘 이다
  • 시간복잡도는 O(n^2)를 가지게 된다.
  • 정렬 알고리즘 중에 연산 수가 많아 가장 느리고 효율성이 떨어진다.

버블 정렬 과정

 

- 첫번 째 루프 -

  • 첫 번째 4 와 두 번째 3 을 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  4 3 2 1 5 -> 3 4 2 1 5
  • 두 번째 4 와 세 번째 2 를 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  3 4 2 1 5 -> 3 2 4 1 5
  • 세 번째 4 와 네 번째 1 을 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  3 2 4 1 5 -> 3 2 1 4 5
  • 네 번째 4 와 다섯 번째 5 를 비교 후 왼쪽의 숫자가 작으니 그대로 둔다. 3 2 1 4 5

- 두번 째 루프 -

  • 첫 번째 3 과 두 번째 2 를 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  3 2 1 4 5 -> 2 3 1 4 5
  • 두 번째 3 과 세 번째 1 을 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  2 3 1 4 5 -> 2 1 3 4 5
  • 세 번째 3 과 네 번째 4 를 비교 후 왼쪽의 숫자가 작으니 그대로 둔다. 2 1 3 4 5

- 세번 째 루프 -

  • 첫 번째 2 와 두 번째 1 을 비교 후 왼쪽의 숫자가 크니 서로 바꾼다.  2 1 3 4 5 -> 1 2 3 4 5
  • 두 번째 2와 세 번째 3 을 비교 후 왼쪽의 숫자가 작으니 그대로 둔다. 1 2 3 4 5

버블 정렬 정리

1. 위에 과정을 통해 오름차순 정렬을 하였다.

2. 첫 번째 숫자와 인접한 오른쪽의 두 번째 숫자를 비교 후 왼쪽의 숫자가 크면 서로 바꾼다.

3. 그렇게 제일 큰 숫자를 오른쪽 끝에 배치하게 된 후 그 숫자는 다음부턴 검사를 안하게 된다. 이때는 O(n)

4. 한번의 루프가 끝난 후 다시 처음부터 제일 큰 숫자 전까지 검사를 하게 된다.  이때부터 O(n^2)이 된다

 

 

버블 정렬 코드

#include <iostream>

int main()
{
    int Array[5] = { 4,3,2,1,5 };

    int Count = 5;

    for (int i = 0; i < Count - 1; ++i)
    {
        for (int j = 0; j < Count - 1; ++j)
        {
            if (Array[j] > Array[j + 1])
            {
                int Temp = Array[j + 1];
                Array[j + 1] = Array[j];
                Array[j] = Temp;
            }
        }
    }

    for (int i = 0; i < Count; ++i)
    {
        std::cout << Array[i] << " ";
    }
}

구간 합(Prefix Sum)

 

구간 합이란?

  • 구간 합은 한 배열을 이용하여 시간 복잡도를 더 줄이기 위해 사용하는 특수한 목적의 알고리즘 이다.
  • 구간 합 알고리즘을 활용하기 위해 합 배열을 미리 구해야 한다.
  • 기존 배열의 범위 합의 구하는 시간 복잡도는 O(N)인데  배열을 통해 구하는 시간 복잡도가 O(1)로 감소한다.

합 배열 공식

  • S[ i - 1 ] + A[ i ]

구간 합 공식

  • S[ j ] + S[ i - 1 ]

구간 합 코드 C++

#include<iostream>

int main()
{
	int A[10] = { 1,2,3,4,5,6,7,8,9,10 };

	int S[10] = {};

	S[0] = A[0];
	std::cout << S[0] << " ";
	for (int i = 1; i < 10; ++i)
	{
		S[i] = S[i  - 1] + A[i];
		std::cout << S[i] << " ";
	}

	std::cout << std::endl;
	// 1 ~ 4 에서 부분합
	// PrefixSum = 14
	int PrefixSum = S[4] - S[1 - 1];
	std::cout << PrefixSum << std::endl;

	// 2 ~ 7 에서 부분합
	// PrefixSum = 33
	PrefixSum = S[7] - S[2 - 1];
	std::cout << PrefixSum;
}

+ Recent posts