본문 바로가기
Python/Data Structure & Algorithm in Python

[Python] 자료구조와 알고리즘 기초 - 재귀 알고리즘, 동적 계획법, 연결리스트, 스택, 큐, 이진트리

by Air’s Big Data 2020. 9. 12.

Contents

  • 선형 배열(Linear Array)
  • 정렬(Sort)
  • 탐색(Search)
  • 재귀 알고리즘
  • 동적계획법
  • 연결리스트
  • 양방향 연결리스트
  • 스택
  • 환형큐
  • 우선순위 큐
  • 이진 트리
  • 이진 탐색 트리

 


선형 배열(Linear Array)

 

#Python 리스트에 활용할 수 있는 연산들

(1) 리스트 길이과 관계 없이 빠르게 실행 결과를 보게되는 연산들

  • 원소 덧붙이기 .append()
  • 원소 하나를 꺼내기 .pop()

(2) 리스트의 길이에 비례해서 실행 시간이 걸리는 연산들

  • 원소 삽입하기 .insert()
  • 원소 삭제하기 .del()

 

#리스트에서 원소 찾아내기

 

Example 1:

Input: 
L = [64, 72, 83, 72, 54] 
x = 72 

Output:
[1, 3]

 

Example 2:

Input: 
L = [64, 72, 83, 72, 54] 
x = 83

Output:
[2]

 

Example 3:

Input: 
L = [64, 72, 83, 72, 54] 
x = 49

Output:
[-1]

 

Solution 1:

def solution(L,x):
    result = list()
    for i in range(len(L)):
        if L[i] == x:
            result.append(i)
    if result == []:
        result = [-1]
    return result

 


정렬 (Sort)

 

#알파벳, 숫자 크기 순으로 정렬

L=['abcd','xyz','spam']
sorted(L)
#Out: ['abcd', 'spam', 'xyz']
L=[3,7,2,7,1,7]
L.sort()
L
#Out: [1, 2, 3, 7, 7, 7]

 

 

#lambda : 익명함수

(lambda x,y: x + y)(5, 6)
#Out: 11

 

 

 

#정렬의 순서를 반대로

L=[3,4,2,0,1,8]
L2 = sorted(L,reverse=True)
L2
#Out: [8, 4, 3, 2, 1, 0]
L=[3,4,2,0,1,8]
L.sort(reverse=True)
L

#Out: [8, 4, 3, 2, 1, 0]

 

 

#튜플의 정렬

a = [(1, 2), (0, 1), (5, 1), (5, 2), (3, 0)]
sorted(a, key = lambda x : x[0])#튜플 앞에 숫자의 크기가 작은 순서대로 정렬
#Out: [(0, 1), (1, 2), (3, 0), (5, 1), (5, 2)]

 

a = [(1, 2), (0, 1), (5, 1), (5, 2), (3, 0)]
sorted(a, key = lambda x : x[1]) #튜플 뒤에 숫자의 크기가 작은 순서대로 정렬
#[(3, 0), (0, 1), (5, 1), (1, 2), (5, 2)]

 

 

#길이 순서대로 정렬

L = ['abcd','xyz','spam']
sorted(L,key=lambda x: len(x)) #길이순서대로 정렬
#Out: ['xyz', 'abcd', 'spam']

 

 

#키를 지정하여 딕셔너리 정렬 

L = [{'name':'John','score':83},{'name':'Paul', 'score':92}]
L.sort(key=lambda x:x['name']) #이름 순으로 정렬
L
#Out: [{'name': 'John', 'score': 83}, {'name': 'Paul', 'score': 92}]
L = [{'name':'John','score':90},{'name':'Paul', 'score':50}]
L.sort(key=lambda x:x['score']) #점수 순으로 정렬
L
#Out: [{'name': 'Paul', 'score': 50}, {'name': 'John', 'score': 90}]

 


탐색 (Search)

 

#선형탐색 (Linear Search)

def linear_search(L,x):
    i=0 #index i를 0으로 준다
    while i < len(L) and L[i] != x: #i가 L리스트의 길이보다 작고, L의 i번째 원소가 x와 같지 않을때
        i += 1  #i를 1씩 증가시켜나간다.
#원소가 발견되면 i값을 가지고 loop가 종료하게 될 것이기 때문에 
#i가 L리스트의 길이보다 짧으면 리스트 안에서 원소를 발견했다는 의미
    if i < len(L): 
        return i  #발견된 x를 리턴하고
    else:
        return -1 #그렇지 않으면 -1을 리턴한다. 

 

S = [3,8,2,7,6,7,6,10,9]
linear_search(S,6)

#Out: 4

 

S = [3,8,2,7,6,7,6,10,9]
linear_search(S,1)
#Out: -1

 

 

 

 

#이진 탐색 (Binary Search)

-Divide & Conquer : 정렬되어 있을때만 가능 → O(log n)

 

#이미 정렬된 리스트 L에서 x를 이진 탐색으로 찾기
#리스트에 없을 경우 -1 반환
def solution(L, x):
    answer = -1
    lt=0 
    rt=len(L)-1 
    while lt<=rt:
        mid = (lt+rt)//2 
        if L[mid] == x: 
            answer = mid
            break
        elif L[mid]>x:
            rt=mid-1
        else: 
            lt=mid+1 
    return answer

 

 

#L 리스트 내 n개 숫자 중 target 위치를 이분 검색으로 찾기
n, target = map(int,input().split()) 
L = list(map(int,input().split()))
L.sort() #L을 순차로 정렬
lt=0 #lt는 가장 왼쪽 위치
rt=n-1 #lt는 가장 오른쪽 위치
while lt<=rt:
    mid = (lt+rt)//2 #중간 값을 구함 : 몫을 구함
    if L[mid] == target: #mid가 target이면 답을 찾은 것
        print(mid+1) #mid는 index(0번 부터 시작)이기 때문에 +1
        break
    elif L[mid]>target:
         #mid가 target보다 크면 lt...target..mid ...rt 
         #mid 왼쪽 탐색
        rt=mid-1 #mid index에서 -1함으로써 mid보다 큰 쪽을 다 날림. rt가 mid-1값으로 조정됨
    else: 
       #mid가 target보다 작면 lt.....mid ..target..rt
       #mid 오른쪽 탐색
        lt=mid+1 #mid위치에서 +1함으로써 mid+1위치부터 오른쪽으로 탐색
#Iput: 8 32
#Iput: 23 87 65 12 57 32 99 81
#Out: 3 (정렬 후의 결과이므로 작은 수로부터 3번째 큰 자리가 출력됨) 

 


재귀 알고리즘 (recursive algorithms)

 

 

#종결조건의 중요성 알아보기

#재귀의 간단한 예시 - 1부터 n까지 sum

def sum(n):
    print(n)
    if n<=1: #이 종결 조건이 없으면 에러 발생 (계속 작아짐)
        return n
    else:
        return n+sum(n-1)

    
a = int(input("Numer:"))
print(sum(a)) 

#In: 4
#Out:
#Numer:4
#4
#3
#2
#1
#10

 

 

#Recursive vs Iterative 복잡도

#recursive
def sum(n):
    if n<=1:
        return n
    else:
        return n+sum(n-1)
#iterative
def sum(n):
	s = 0
    while n >=0:
    	s+=n
        n-=1
	return s

- Recursive와 Iterative 모두 n에 비례하는 복잡도를 가짐 → O(n)

- 그러나 효율성 측면에서는 Recursive가 더 떨어질 수 있음

def sum(n):
    return n*(n+1)//2
#O(1) 복잡도를 가짐

 

 

#Recursive 예제 - n!

#n!을 구하는 재귀함수
def factorial(n):
    if n<=1:
        return 1
    else:
        return n*factorial(n-1)

 

 

 

#Recursive vs Iterative 예제 - 피보나치(Fibonacci) 순열

 

#Recursive 
def solution(n):
    '''
    F0 = 0
    F1 = 1
    Fn = Fn - 1 + Fn - 2, n >= 2
    '''
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else: 
        return solution(n-1)+solution(n-2)
        
solution(7) 
#[1]1 - [2]1 - [3]2 - [4]3 - [5]5 - [6]8 - "[7]13"
#[5]8 + [6]8 = "[7]13"
#Out: 13

solution(8) 
#[1]1 - [2]1 - [3]2 - [4]3 - [5]5 - [6]8 - [7]13 - "[8]21"
#[6]8 +[7]13 = "[8]21"
#Out: 21

 

#Iteratvie 
def solution(n):
    if n < 2:
        return n
    a, b = 0, 1
    for i in range(n): 
        a, b = b, a+b 
    return a
    
solution (4)
solution (8)
#[1]a,b=1,1 
#[2]a,b=1,2 
#[3]a,b=2,3 
#[4]a,b=3,5 
#[5]a,b=5,8
#[6]a,b=8,13
#[7]a,b=13,21
#[8]a,b=21,34
#a = 21
#Out: 21

#Recursive 예제 - 조합의 수 계산

#재귀적 방법으로 조합의 수 계산
#n개의 다른 원소에서 m개를 택하는 경우의 수
def combi(n, m):
    #if와 elif를 이용해 trivial case 
    if n == m:    #선택하는 원소의 개수가 전체 원소의 개수와 같으면 
        return 1 #1로 반환
    elif m == 0: #선택하는 원소 개수가 없으면 
        return 1 #1로 반환
    else: 
        return combi(n-1,m)+combi(n-1,m-1)

 

#Recursive 예제 - 이진탐색

#L은 리스트
#x는 찾으려는 원소
#l부터 u까지 리스트 내 범위 인덱스

def solution(L, x, l, u): 
    if l > u: 
        return -1
    mid = (l + u) // 2
    if x == L[mid]:
        return mid
    elif x < L[mid]: x가 mid보다 작을 경우
        return solution(L, x, l, mid-1) #mid의 오른쪽 날림
    else:
        return solution(L, x, mid+1, u) #mid의 왼쪽 날림

 


동적계획법(Dynamic programming)

#동적계획법(Dynamic programming) 

 - 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 방법 (wikipedia)

 - Bottom-Up 적 방법

 

 

#동적계획법 예제 - nm선 자르기

#nm의 선을 1m나 2m 자르는 방법의 수

#1m 선의 경우
#[1,1,1] => 1가지
#dy[1]=1

#2m 선의 경우
#[1,1,1],[2] => 2가지
#dy[2]=2

#3m 선의 경우
#[1,1,1] => 1가지
#[1,1,1],[2] => 2가지
#1가지(1m선의 경우) + 2가지(2m선의 경우)  = 3가지
#dy[3]=3

#4m 선의 경우
#2가지(2m선의 경우) + 3가지(3m선의 경우)  = 5가지
#dy[4]=5

n = int(input())
dy=[0]*(n+1) #배열을 만듦
dy[1]=1
dy[2]=2

for i in range(3, n+1): #3부터 n+1 범위 계산
    dy[i] = dy[i-1]+dy[i-2]

print(dy[n])

 

#Brute Force 접근법

: 가능한 부분 배열들을 모두 살펴본 후 합을 구해서 가장 큰 값을 찾는 방식

 

 

#카데인 알고리즘 (Kadane’s Algorithm)

: A[0]이 아닌 A[n-1]에서(뒤에서 부터) 시작하는 방식

 

 

#카데인 알고리즘 (Kadane’s Algorithm) 예제 - Maximum Subarray Problem(최대 부분 합 구하기)

 

 

Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

(출처 :  https://sustainable-dev.tistory.com/23)

 

 

A[5]의 최대 부분합 = A[4]의 최대 부분합+A[5] = 3+2 = 5

A[6]의 최대 부분합 = A[5]의 최대 부분합+A[6] = 5+1 = 6

A[7]의 최대 부분합 = A[6]의 최대 부분합+A[7] = 5-5 = 0

A[8]의 최대 부분합 = A[7]의 최대 부분합+A[8] = 0+4 = 4

 

localMaximum[i] = max(A[i], A[i] + lacalMaximum[i-1])
#i번째에서는 A의 i번째 수와 i-1번째의 최대 부분합을 더한다.

 

 

 


연결리스트 (Linked List)

 

#자료 구조 정의

#노드
class Node:
	def __init__(self.item):
    	self.data = item
        self.next = None
        

#비어있는 연결리스트 
class LinkedList:
	def __init(self)
    		self.nodeCount = 0
            self.head = None
            self.tail = None
            

 

#연산 정의

(1) 특정 원소 참조 (k번째)

#K번째 노드 찾기

#[1][67] -> [2][34] -> [3][58]
# 노드의 개수: 3
#Head: 67
#Tail: 58


def getAt(self, pos):
	if pos <=0 or pos > self.nodeCount: #pos 번째에 있는 노드를 반환하는 것이 목표
    	retrun None #pos가 0보다 작거나 노드 개수보다 작으면 없다.

	i = 1
    curr = self.head
    while i < pos:
            curr = curr.next
            i += 1
    return curr

 

(2) 리스트 순회

    def traverse(self):
        if not self.head:
            return []

        returnList = []
        curr = self.head # 1 

        while curr is not None:
            returnList.append(curr.data) # 67
            curr = curr.next
            
        return returnList

 

(3) 길이 얻어내기

# Node class 
class Node: 
    # Function to initialise the node object 
    def __init__(self, data): 
        self.data = data # Assign data 
        self.next = None # Initialize next as null 
  
  
# Linked List class contains a Node object 
class LinkedList: 
  
    # Function to initialize head 
    def __init__(self): 
        self.head = None
  
  
    # This function is in LinkedList class. It inserts 
    # a new node at the beginning of Linked List. 
    def push(self, new_data): 
  
        # 1 & 2: Allocate the Node & 
        #     Put in the data 
        new_node = Node(new_data) 
  
        # 3. Make next of new Node as head 
        new_node.next = self.head 
  
        # 4. Move the head to point to new Node 
        self.head = new_node 
  
  
    # This function counts number of nodes in Linked List 
    # iteratively, given 'node' as starting node. 
    def getCount(self): 
        temp = self.head # Initialise temp 
        count = 0 # Initialise count 
  
        # Loop while end of linked list is not reached 
        while (temp): 
            count += 1
            temp = temp.next
        return count 

 

(4) 원소 삽입

#원소 삽입
def insertAt(self, pos, newNode):
    if pos < 1 or pos > self.nodeCount + 1:#pos가 올바른 값을 가지는 범위에 없으면
        return False #False를 반환함
    
    if pos == 1: #삽입하려는 위치가 맨 앞일 때 prev가 없음
        newNode.next = self.head #head 조정 필요
        self.head =newNode
        
    else: #삽입하려는 위치가 리스트의 처음이 아니라면
        prev = self.getAt(pos - 1) #직전의 노드를 얻고
        newNode.next = prev.next #삽입되는 노드의 next는 pos - 1 노드의 next로 보냄
        prev.next = newNode #prev의 next링크는 newNode를 가르키도록 조정
        
    if pos == self.nodeCount +1:#삽입하려는 위치가 맨 끝일때 
        self.tail = newNode #tail을 새로운 노드에 가르키도록 조정
        
    self.nodeCount += 1 #self에 +1한 뒤 True를 반환
    return True



#삽입하려는 위치가 맨 끝일때
def insertAt(self, pos, newNode):
    if pos < 1 or pos > self.nodeCount + 1:#pos가 올바른 값을 가지는 범위에 없으면
        return False #False를 반환함
    
    if pos == 1: #삽입하려는 위치가 맨 앞일 때 prev가 없음
        newNode.next = self.head #head 조정 필요
        self.head =newNode
        
    else: 
        if prev = self.getAt + 1:
            prev = self.tail #prev가 self의 tail로 
        else:
            prev = self.getAt(pos -1) #그렇지 않으면 앞에서부터
        newNode.next = prev.next
        prev.next = newNode
        
    if pos == self.nodeCount +1:
        self.tail = newNode 
        
    self.nodeCount += 1 #self에 +1한 뒤 True를 반환
    return True

- 원소 삽입 실습

class Node:

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


class LinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = None
        self.tail = None


    def __repr__(self):
        if self.nodeCount == 0:
            return 'LinkedList: empty'

        s = ''
        curr = self.head
        while curr is not None:
            s += repr(curr.data)
            if curr.next is not None:
                s += ' -> '
            curr = curr.next
        return s


    def getAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            return None

        i = 1
        curr = self.head
        while i < pos:
            curr = curr.next
            i += 1

        return curr


    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        if pos == 1:
            newNode.next = self.head
            self.head = newNode

        else:
            if pos == self.nodeCount + 1:
                prev = self.tail
            else:
                prev = self.getAt(pos - 1)
            newNode.next = prev.next
            prev.next = newNode

        if pos == self.nodeCount + 1:
            self.tail = newNode

        self.nodeCount += 1
        return True


    def getLength(self):
        return self.nodeCount


    def traverse(self):
        result = []
        curr = self.head
        while curr is not None:
            result.append(curr.data)
            curr = curr.next
        return result


    def concat(self, L):
        self.tail.next = L.head
        if L.tail:
            self.tail = L.tail
        self.nodeCount += L.nodeCount

 

a = Node(67)
b = Node(34)
c = Node(28)
L = LinkedList()
L

#Out: LinkedList: empty

 

 

L.insertAt(1,a)
#Out: True


L.insertAt(2,b)
#Out: True


L
#Out: 67 -> 34


L.insertAt(1,c)
#Out: True


L
#Out: 28 -> 67 -> 34

- 원소 삽입의 복잡도

 맨 앞에 삽입하는 경우 : O(1)

 중간에 삽입하는 경우 : O(n)

 맨 끝에 삽입하는 경우 : O(1)

 

(5) 원소 삭제

- 원소 삭제의 복잡도

 맨 앞에 삭제하는 경우 : O(1)

 중간에 삭제하는 경우 : O(n)

 맨 끝에 삭제하는 경우 : O(n)

 

#연결 리스트 순회 예제

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

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

    def getAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            return None
        i = 1
        curr = self.head
        while i < pos:
            curr = curr.next
            i += 1
        return curr

    def traverse(self):
        if not self.head:
            return []

        returnList = []
        curr = self.head 

        while curr is not None:
            returnList.append(curr.data)
            curr = curr.next
            
        return returnList

    
# 이 solution 함수는 그대로 두어야 합니다.
def solution(x):
    return 0

 

 

 

(6) 두 리스트 합치기

#두 리스트의 연결
def concat(self,L):
    self.tail.next = L.head #내 끝이 이어 붙이려는 처음
    #내 tail이 이어붙이려는 tail
    #L.tail이 유효한 경우에만 붙임
    if L.tail
        self.tail = L.tail
    self.nodeCount +=L.nodeCount

 

 

 

#Dummy node를 추가한 형태

class Node:

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


class LinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = None
        self.head.next = self.tail


    def __repr__(self):
        if self.nodeCount == 0:
        return 'LinkedList: empty'

        s = ''
        curr = self.head
        while curr.next:
            curr = curr.next
            s += repr(curr.data)
            if curr.next is not None:
                s += ' -> '
        return s


    def getLength(self):
        return self.nodeCount

#리스트 순회
    def traverse(self): 
        result = []
        curr = self.head
        while curr.next: #next 링크가 살아있는 한
            curr = curr.next 
            result.append(curr.data) #그 결과를 result리스트에 담는다.
        return result


#K번째 원소 얻어내기
    def getAt(self, pos):
		if pos < 0 or pos > self.nodeCount:
			return None #pos가 0보다 작으면 head 리턴

		i = 0
		curr = self.head
		while i < pos:
			curr = curr.next
			i += 1

		return curr


#원소의 삽입
#[prev]->[prev.next] 
#[prev]->[newNode]->[prev.next] 
	def insertAfter(self, prev, newNode):
		newNode.next = prev.next #newNode의 next링크는 prev.next
		if prev.next is None: #prev.next가 없다면
			self.tail = newNode #tail이 newNode로 옮겨짐
		prev.next = newNode #prev가 가리키는 node의 다음에 newNode 삽입
		self.nodeCount += 1 
		return True #성공/실패에 따라 True/False를 리턴


	def insertAt(self, pos, newNode):
		if pos < 1 or pos > self.nodeCount + 1:
			return False

		if pos != 1 and pos == self.nodeCount + 1:
			prev = self.tail
		else:
			prev = self.getAt(pos - 1)
		return self.insertAfter(prev, newNode)


	def concat(self, L):
		self.tail.next = L.head.next
		if L.tail:
			self.tail = L.tail
		self.nodeCount += L.nodeCount

 

 

- 리스트 순회

#리스트 순회
    def traverse(self): 
        result = []
        curr = self.head
        while curr.next: #next 링크가 살아있는 한
            curr = curr.next 
            result.append(curr.data) #그 결과를 result리스트에 담는다.
        return result

 

 - 특정 원소 참조 (k번째)

#K번째 원소 얻어내기
    def getAt(self, pos):
		if pos < 0 or pos > self.nodeCount:
			return None #pos가 0보다 작으면 head 리턴

		i = 0
		curr = self.head
		while i < pos:
			curr = curr.next
			i += 1

		return curr
#pos범위 조건 확인
	def insertAt(self, pos, newNode):
		if pos < 1 or pos > self.nodeCount + 1:
			return False
        
#pos == 1인 경우에는 head 뒤에 새 node 삽입
#pos == nodeCount +1인 경우에는 prev <- tail
		if pos != 1 and pos == self.nodeCount + 1:
			prev = self.tail
#그렇지 않은 경우에는 prev <- getAt(...)
		else:
			prev = self.getAt(pos - 1)
		return self.insertAfter(prev, newNode)

 

- 원소 삽입

#원소의 삽입
#[prev]->[prev.next] 
#[prev]->[newNode]->[prev.next] 
	def insertAfter(self, prev, newNode):
		newNode.next = prev.next #newNode의 next링크는 prev.next
		if prev.next is None: #prev.next가 없다면
			self.tail = newNode #tail이 newNode로 옮겨짐
		prev.next = newNode #prev가 가리키는 node의 다음에 newNode 삽입
		self.nodeCount += 1 
		return True #성공/실패에 따라 True/False를 리턴

 

- 원소 삭제

            def popAt(self, pos):
                data = 0
                if pos < 1 or pos > self.nodeCount:
                    raise IndexError
                    
                if self.nodeCount == 1: 
                    data = self.head.data
                    self.head = None
                    self.tail = None
                    
                else:
                    if pos == 1:
                        data = self.head.data
                        self.head = self.head.next
                        
                    if pos == self.nodeCount:
                        prev = self.getAt(pos-1)
                        data = prev.next.data
                        prev.next = None
                        self.tail = prev
                        
                    else:
                        prev = self.getAt(pos-1)
                        data = prev.next.data
                        prev.next = prev.next.next
                        
                    self.nodeCount -= 1
                    return data

 

- 두 리스트 합치기

#두 리스트의 연결
	def concat(self, L):
		self.tail.next = L.head.next
		if L.tail:
			self.tail = L.tail
		self.nodeCount += L.nodeCount

 

 


연결리스트 (Linked List)

 

#Node의 구조 확장

#<-[]->
class Node:
    def __init__(self, item):
        self.data = item
        self.prev = None
        self.next = None

 

 

#Dummy node

class DoublyLinkedList:
    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = Node(None)
        self.head.prev = None
        self.head.next = self.tail
        self.tail.prev = self.head
        self.tail.next = None

 

 

#리스트 순회

    def traverse(self):
        result = []
        curr = self.head
        while curr.next.next: #Dummy Node가 head와 tail에 모두 있음
            curr = curr.next  #빈 리스트라도 순회
            result.append(curr.data)
        return result

 

 

#리스트 역순회

    def revese(self):
        result = []
        curr = self.head
        while curr.prev.prev: #prev > prev
            curr = curr.prev  #빈 리스트라도 반복하지 않고 빠져 나옴> 유효함
            result.append(curr.data)
        return result

 

 

#원소의 삽입

def insertBefore(self, next, newNode):
        prev = next.prev
        newNode.next = prev.next
        newNode.prev = prev
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        
        return True
#링크만 조정하면 됨
	def insertAfter(self, prev, newNode):
        next = prev.next
        newNode.prev = prev
        newNode.next = next
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        return True

 

 

#특정 원소 얻어내기

    def getAt(self, pos):
        if pos < 0 or pos > self.nodeCount:
            return None

        if pos > self.nodeCount // 2:
            i = 0
            curr = self.tail  #뒤에서 하나하나 세도록
            while i < self.nodeCount - pos + 1:
                curr = curr.prev
                i += 1
        else:
            i = 0
            curr = self.head
            while i < pos:
                curr = curr.next
                i += 1

 

 

#양방향 연결 리스트 노드 삭제

def popAfter(self, prev):
        curr = prev.next
        prev.next = curr.next
        curr.next.prev = prev
        self.nodeCount -= 1
        
        return curr.data

    def popBefore(self, next):
        curr = next.prev
        prev = curr.prev
        prev.next = next
        next.prev = prev
        self.nodeCount -= 1
        
        return curr.data

    def popAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            raise IndexError

        prev = self.getAt(pos - 1)
        
        return self.popAfter(prev)

 

 

#양방향 연결 리스트의 병합

def concat(self, L):
        self.tail.prev.next = L.head.next
        L.head.next.prev = self.tail.prev
        self.tail = L.tail
        self.nodeCount += L.nodeCount

 


스택(Stacks)

(스택의 구조, wikipedia)

 

#연산의 정의

  • size(): 현재 스택에 들어 있는 데이터 원소의 수를 구함
  • isEmpty(): 현재 스택이 비어 있는지를 판단 (size() == 0?)
  • push(x): 데이터 원소 x 를 스택에 추가
  • pop(): 스택에 가장 나중에 저장된 데이터 원소를 제거 (또한, 반환)
  • peek(): 스택에 가장 나중에 저장된 데이터 원소를 참조 (반환), 그러나 제거하지는 않음

 

#배열로 구현한 스택

class ArrayStack:

	def __init__(self):
		self.data = []

	def size(self): #크기 리턴
		return len(self.data)

	def isEmpty(self):
		return self.size() == 0

	def push(self, item):
		self.data.append(item)

	def pop(self): #원소 삭제(리턴)
		return self.data.pop()

	def peek(self): #꼭대기 원소 반환
		return self.data[-1]

 

 

#양방향연결리스트로 구현한 스택

from doublylinkedlist import Node
from doublylinkedlist import DoublyLinkedList


class LinkedListStack:

	def __init__(self):
		self.data = DoublyLinkedList()

	def size(self):
		return self.data.getLength()

	def isEmpty(self):
		return self.size() == 0

	def push(self, item):
		node = Node(item)
		self.data.insertAt(self.size() + 1, node) #1만큼 사이즈가 늘어난 뒤 추가

	def pop(self):
		return self.data.popAt(self.size())

	def peek(self):
		return self.data.getAt(self.size()).data

 

 

#수식의 후위 표기법 (Postfix Notation)

-  연산자가 피연산자들의 뒤에 위치

중위 후위
A*B+C A+B*C
AB*C+ ABC*+
A+B+C AB+C+
(A+B)*C AB+C*
여는 괄호는 스택에 push
닫는 괄호를 만나면 여는 괄호가 나올 때까지 pop
A*(B+C) ABC+**
연산자를 만났을 떄 여는 괄호 너머까지 pop하지 않도록
여는 괄호의 우선순위는 가장 낮게 설정
(A+B) * (C+D) AB+CD+*
(A+(B-C))*D ABC-+D*
A*(B-(C+D)) ABCD+-*

- 중위 표현식 왼쪽부터 한 글자씩 읽어서 피연산자이면 그냥 출럭

- '('이면 스택에 push

- ')'이면 '('이 나올 때까지 스택에서 pop, 출력

- 연산자이면 스택에서 이보다 높거나 같은 우선순위 것들을 pop, 출력

- 이 연산자는 스택에 push하고 스택에 남아 있는 연산자는 모두 pop, 출력중위표현 수식 --> 후위표현 수식

 

 

#중위표현 수식 → 후위표현 수식 예제

class ArrayStack:

    def __init__(self):
        self.data = []

    def size(self):
        return len(self.data)

    def isEmpty(self):
        return self.size() == 0

    def push(self, item):
        self.data.append(item)

    def pop(self):
        return self.data.pop()

    def peek(self):
        return self.data[-1]

prec = {
    '*': 3, '/': 3,
    '+': 2, '-': 2,
    '(': 1
}

def solution(S):
    stack = []
    answer = ''
    for c in S:
        if c not in '()+-*/':
            answer += c
        elif  c == '(':
            stack.append(c)
        elif c == ')':
            while stack[-1] != '(':
                answer += stack.pop()
            stack.pop()
        elif stack and prec[c] <= prec[stack[-1]]:
            answer += stack.pop()
            stack.append(c)
        else:
            stack.append(c)
            
    while stack:
        answer += stack.pop()
                
    return answer

 

 

 

#후위 표기 수식 계산

def postfixEval(tokenList):
    valStack = ArrayStack()
    
    for token in tokenList:
        if type(token) is int:
            valStack.push(token)
            
        elif token == '+':
            n1 = valStack.pop()
            n2 = valStack.pop()
            valStack.push(n2+n1)
        
        elif token == '-':
            n1 = valStack.pop()
            n2 = valStack.pop()
            valStack.push(n2-n1)
            
        elif token == '*':
            n1 = valStack.pop()
            n2 = valStack.pop()
            valStack.push(n2*n1)
            
        elif token == '/':
            n1 = valStack.pop()
            n2 = valStack.pop()
            valStack.push(int(n2/n1))
        
    return valStack.pop()

 


큐 (Queue)

 

(Representation of a  FIFO  (first in, first out) queue, Wikipedia)

#연산의 정의

  • size() : 현재 큐에 들어있는 데이터 원소의 수를 구함
  • isEmpty() : 현재 큐가 비어 있는지를 판단
  • enqueue(x) : 데이터 원소 x를 큐에 추가
  • dequeue() : 큐의 맨 앞에 저장된 데이터 원소를 제거(또는 반환)
  • peek() : 큐의 맨 앞에 저장된 데이터 원소를 반환(제거하지 않음)

 

#배열을 이용하여 구현

class ArrayQueue:
    def __init__(self):
        self.data = []
        
    def size(self):
        return len(self.data)

    def isEmpty(self):
        return self.size() == 0
    
    def enqueue(self, item):
        self.data.append(item)
        
    def dequeue(self):
        return self.data.pop(0)
    
    def peek(self):
        return self.data[0]

 

연산 복잡도
size() O(1)
isEmpty() O(1)
enqueue() O(1)
dequeue() O(n)
peek()  O(1)

 

 

#이중 연결리스트을 이용하여 구현

class Node:

    def __init__(self, item):
        self.data = item
        self.prev = None
        self.next = None

class DoublyLinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = Node(None)
        self.head.prev = None
        self.head.next = self.tail
        self.tail.prev = self.head
        self.tail.next = None

    def __repr__(self):
        if self.nodeCount == 0:
            return 'LinkedList: empty'

        s = ''
        curr = self.head
        while curr.next.next:
            curr = curr.next
            s += repr(curr.data)
            if curr.next.next is not None:
                s += ' -> '
        return s

    def getLength(self):
        return self.nodeCount

    def traverse(self):
        result = []
        curr = self.head
        while curr.next.next:
            curr = curr.next
            result.append(curr.data)
        return result

    def reverse(self):
        result = []
        curr = self.tail
        while curr.prev.prev:
            curr = curr.prev
            result.append(curr.data)
        return result

    def getAt(self, pos):
        if pos < 0 or pos > self.nodeCount:
            return None

        if pos > self.nodeCount // 2:
            i = 0
            curr = self.tail
            while i < self.nodeCount - pos + 1:
                curr = curr.prev
                i += 1
        else:
            i = 0
            curr = self.head
            while i < pos:
                curr = curr.next
                i += 1

        return curr

    def insertAfter(self, prev, newNode):
        next = prev.next
        newNode.prev = prev
        newNode.next = next
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        return True

    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        prev = self.getAt(pos - 1)
        return self.insertAfter(prev, newNode)

    def popAfter(self, prev):
        curr = prev.next
        next = curr.next
        prev.next = next
        next.prev = prev
        self.nodeCount -= 1
        return curr.data

    def popAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            raise IndexError('Index out of range')

        prev = self.getAt(pos - 1)
        return self.popAfter(prev)

    def concat(self, L):
        self.tail.prev.next = L.head.next
        L.head.next.prev = self.tail.prev
        self.tail = L.tail

        self.nodeCount += L.nodeCount
class LinkedListQueue:

    def __init__(self):
        self.data = DoublyLinkedList()

    def size(self):
        return self.data.nodeCount

    def isEmpty(self):
        return self.data.nodeCount==0

    def enqueue(self, item):
        node = Node(item)
        self.data.insertAt(self.size()+1,node)

    def dequeue(self):
        return self.data.popAt(1)

    def peek(self):
        return self.data.head.next.data
def solution(x):
    return 0

 

#양방향 연결리스트을 이용한 큐 구현 예제

class Node:

    def __init__(self, item):
        self.data = item
        self.prev = None
        self.next = None

class DoublyLinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = Node(None)
        self.head.prev = None
        self.head.next = self.tail
        self.tail.prev = self.head
        self.tail.next = None

    def __repr__(self):
        if self.nodeCount == 0:
            return 'LinkedList: empty'

        s = ''
        curr = self.head
        while curr.next.next:
            curr = curr.next
            s += repr(curr.data)
            if curr.next.next is not None:
                s += ' -> '
        return s

    def getLength(self):
        return self.nodeCount

    def traverse(self):
        result = []
        curr = self.head
        while curr.next.next:
            curr = curr.next
            result.append(curr.data)
        return result

    def reverse(self):
        result = []
        curr = self.tail
        while curr.prev.prev:
            curr = curr.prev
            result.append(curr.data)
        return result

    def getAt(self, pos):
        if pos < 0 or pos > self.nodeCount:
            return None

        if pos > self.nodeCount // 2:
            i = 0
            curr = self.tail
            while i < self.nodeCount - pos + 1:
                curr = curr.prev
                i += 1
        else:
            i = 0
            curr = self.head
            while i < pos:
                curr = curr.next
                i += 1

        return curr

    def insertAfter(self, prev, newNode):
        next = prev.next
        newNode.prev = prev
        newNode.next = next
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        return True

    def insertAt(self, pos, newNode):
        if pos < 1 or pos > self.nodeCount + 1:
            return False

        prev = self.getAt(pos - 1)
        return self.insertAfter(prev, newNode)

    def popAfter(self, prev):
        curr = prev.next
        next = curr.next
        prev.next = next
        next.prev = prev
        self.nodeCount -= 1
        return curr.data

    def popAt(self, pos):
        if pos < 1 or pos > self.nodeCount:
            raise IndexError('Index out of range')

        prev = self.getAt(pos - 1)
        return self.popAfter(prev)

    def concat(self, L):
        self.tail.prev.next = L.head.next
        L.head.next.prev = self.tail.prev
        self.tail = L.tail

        self.nodeCount += L.nodeCount

class LinkedListQueue:

    def __init__(self):
        self.data = DoublyLinkedList()

    def size(self):
        return self.data.nodeCount

    def isEmpty(self):
        return self.data.nodeCount==0

    def enqueue(self, item):
        node = Node(item)
        self.data.insertAt(self.size()+1,node)

    def dequeue(self):
        return self.data.popAt(1)

    def peek(self):
        return self.data.head.next.data

def solution(x):
    return 0

 


환형 큐(Circular Queue)

 

 

(A 24-byte keyboard circular buffer, wikipedia)

 

 #환형 큐

 - '정해진' 개수의 저장 공간을 빙 돌려가며 이용

 - 큐가 가득 차면 더이상 원소를 넣을 수 없음

 

 

#연산의 정의

  • size() : 현재 큐에 들어 있는 데이터 원소의 수를 구함
  • isEmpty() : 현재 큐가 비어 있는지를 판단
  • isFull() : 큐에 데이터 원소가 꼭 차 있는지를 판단
  • qneueue(x) : 데이터 원소 x를 큐에 추가
  • dequeue() : 큐의 맨 앞에 저장된 데이터 원소를 제거(또한, 반환)
  • peek() : 큐의 맨 앞에 저장된 데이터 원소를 반환(제거하지 않음)

 

#배열로 구현한 환형 큐

 

class CircularQueue:

#큐 초기화
    def __init__(self, n):
        self.maxCount = n
        self.data = [None] * n
        self.count = 0
        self.front = -1
        self.rear = -1

 

    # 현재 큐 길이를 반환
    def size(self):
        return self.count

 

    # 큐가 비어있는지 
    def isEmpty(self):
        return self.count == 0

 

    # 큐가 꽉 차있는지
    def isFull(self):
        return self.count == self.maxCount

 

# 데이터 원소 추가
    def enqueue(self, x):
        if self.isFull():
            raise IndexError('Queue full')
        
        self.rear = (self.rear + 1) % self.maxCount
        self.data[self.rear] = x
        self.count += 1

 

    #데이터 원소 제거
    def dequeue(self):
        if self.isEmpty():
            raise IndexError('Queue empty')

        self.front = (self.front + 1) % self.maxCount
        x = self.data[self.front]
        self.count -= 1
        return x

 

    # 큐의 맨 앞 원소 반환
    def peek(self):
        if self.isEmpty():
            raise IndexError('Queue empty')
        return self.data[(self.front + 1) % self.maxCount]

 


우선순위 큐 (Priority Queues)

 

(Removing Highest Priority Element, programiz)

 

 

#우선순위 큐 (Priority Queues)

 - 큐가 FIFO(First-In-First-Out) 방식을 따르지 않고 원소들의 우선순위에 따라 큐에서 빠져나오는 방식

 

 

#우선순위 큐 구현 방식

 - (1) Enqueue할 때 우선순위 순서를 유지하도록 vs (2) Dequeue할 때 우선순위 높은 것을 선택

 - (1) 가 더 유리, (2)는 모든 원소를 다 살펴보아야 함

 

 

#우선순위 큐 연상 구현

class PriorityQueue:
    # 양방향 연결리스트를 이용하여 초기화
    def __init__(self, x):
        self.queue = DoublyLinkedList()

 

    # 크기
    def size(self):
        return self.queue.getLength()

 

    # 비어있는지
    def isEmpty(self):
        return self.size() == 0

 

    # 데이터 삽입 연산
    def enqueue(self, x):
        newNode = Node(x)
        
        # 처음 시작하는 위치 head에서 시작
        curr = self.queue.head
        
        # 끝까지 가지 않을 조건 && 우선순위를 비교하는 조건
        while curr.next != self.queue.tail and x < curr.next.data :
            curr = curr.next
        
        # 양방향 연결리스트를 이용해 삽입
        self.queue.insertAfter(curr, newNode)

 

    # 데이터 삭제 연산
    def dequeue(self):
        return self.queue.popAt(self.queue.getLength())

    # 첫번째 데이터 반환
    def peek(self):
        return self.queue.getAt(self.queue.getLength()).data

 

 

 

#우선순위 큐 연상 구현 예제

class PriorityQueue:

    def __init__(self):
        self.queue = DoublyLinkedList()

    def size(self):
        return self.queue.getLength()

    def isEmpty(self):
        return self.size() == 0

    def enqueue(self, x):
        newNode = Node(x)
        curr = 
self.queue.head

        while 
curr.next != self.queue.tail
 and 
x < curr.next.data
:
            curr = curr.next
        self.queue.
insertAfter
(curr, newNode)

    def dequeue(self):
        return self.queue.popAt(self.queue.getLength())

    def peek(self):
        return self.queue.getAt(self.queue.getLength()).data

def solution(x):
    return 0

 

 


이진 트리 (Binary Trees)

 

#연산의 정의

  • size() : 현재 트리에 포함되어 있는 노드의 수를 구함

  • depth() : 현재 트리의 깊이(또는 높이;height)를 구함

  • 순회(traversal)

 

#이진 트리의 구현 - size()

 

# 이진트리의 구현 - 노드(node)
class Node:
    def __init__(self, item):
        self.data = item
        self.left = None
        self.right = None

 

# 이진 트리의 구현 - 트리(tree)
class BinaryTree:
    def __init__(self, r):
        self.root = r

 

class Node:

	# 이진 트리의 크기 - 재귀함수 이용
    def size(self):
        l = self.left.size() if self.left else 0 # 왼쪽 서브트리
        r = self.right.size() if self.right else 0 # 오른쪽 서브트리
        return l + r + 1 # 왼쪽 + 오른쪽 + 자기자신(+1)

 

class BinaryTree:
	
    # 크기 구하기
    def size(self):
        if self.root:
            return self.root.size()
        else:
            return 0

 

#이진 트리의 구현 - depth()

    # depth
    def depth(self):
        l = self.left.depth() if self.left else 0
        r = self.right.depth() if self.right else 0
        return max(l,r) + 1

 

 

 

#깊이 우선 순회 (depth first traveral)

(1) 중위 순회 (in-order traversal) : Left subtree → 자기 자신  Right subtree

    # 중위 순회
    def inorder(self):
        traversal = []
        
        # 왼쪽 서브트리가 존재한다면
        if self.left:
            traversal += self.left.inorder()
        
        traversal.append(self.data)
        
        # 오른쪽 서브트리가 있다면
        if self.right:
            traversal += self.right.inorder()
            
        return traversal

 

 

(2) 전위 순회 (pre-order traversal) :  자기 자신  Left subtree → Right subtree

 

    def preorder(self):
        traversal = []
        traversal.append(self.data)
        if self.left:
            traversal += self.left.preorder()
        if self.right:
            traversal += self.right.preorder()
        return traversal

 

(3) 후위 순회 (post-order traversal):   Left subtree → Right subtree  자기 자신

    def postorder(self):
        traversal = []
        if self.left:
            traversal += self.left.postorder()
        if self.right:
            traversal += self.right.postorder()
        traversal.append(self.data)
        return traversal

 

 

#넓이 우선 순회 (breadth first traversal)

(Breadth-first search, wikipedia)

 - 수준(Level)이 낮은 노드를 우선으로 방문

 - 같은 수준의 노드들 사이에서는 부모 노드의 방문 순서에 따라 방문, 왼쪽 자식 노드를 오른쪽 자식보다 먼저 방문

 - 한 노드를 방문했을 때, 나중에 방문할 노드들을 순서대로 기록해 두어야함

 → 선입선출 성질을 가지고 있기 때문에 자료구조는 큐를 사용

 

    def bft(self):
        # 빈 리스트 초기화
        traversal = []
        # 빈 큐 초기화
        visitQueue = ArrayQueue()

        # 빈 트리가 아니면 루트 노드가 있는 것 -> 큐에 루트 노드를 인큐
        if self.root:
            visitQueue.enqueue(self.root)

        # 큐가 비어있지 않는 동안 반복
        while visitQueue.isEmpty()==False:
            # 큐에서 꺼내서 node에 저장
            node = visitQueue.dequeue()
            # 꺼낸 노드를 방문 리스트에 저장
            traversal.append(node.data)

            # 노드에 왼쪽 자식 노드가 존재하는경우 큐에 저장
            if node.left:
                visitQueue.enqueue(node.left)
            # 노드에 오른쪽 자식 노드가 존재하는 경우 큐에 저장
            if node.right:
                visitQueue.enqueue(node.right)
            
        return traversal

 


이진 탐색 트리 (Binary Search Trees)

 

#이진 탐색 트리이진 탐색 트리 (Binary Searcah Trees)

- 왼쪽 서브트리에 있는 데이터는 모두 현재 노드의 값보다 작고, 오른쪽 서브트리에 있는 데이터는 모두 현재 노드의 값보다 큰 성질을 만족하는 이진트리 (중복되는 데이터 원소는 없는 것으로 가정)

 

#연산의 정의

  • insert(key, data) : 트리에 주어진 데이터 원소를 추가
  • remove(key) : 특정 원소를 트리로 부터 삭제
  • lookup(key) : 특정 원소를 검색
  • inorder() : 키의 순서대로 데이터 원소를 나열
  • min(), max() : 최소 키, 최대 키를 가지는 원소를 각각 탐색

 

#insert(key, data) 

class Node:
  def insert(self, key, data):
          if key < self.key:
              if self.left:
                  self.left.insert(key, data)
              else:
                  self.left = Node(key, data)

          elif key > self.key:
              if self.right:
                  self.right.insert(key, data)
              else:
                  self.right = Node(key, data)

          else:
              print("중복된 노드가 존재")

          return True
          
          
class BinSearchTree:
    def insert(self, key, data):
        if self.root:
            self.root.insert(key,data)
        
        else:
            self.root = Node(key, data)

 

 

#remove(key)

    def remove(self, key):
        node, parent = self.lookup(key)

        if node:
            nChildren = node.countChildren()

            if nChildren == 0:
                if parent:
                    if parent.left == node:
                        parent.left = None
                    if parent.right == node:
                        parent.right = None

                else:
                    self.root = None

 

 

#lookup()

    def lookup(self, key, parent=None):
        if key < self.key:
            if self.left:
                return self.left.lookup(key, self)
            else:
                return None, None
        
        elif key > self.key:
            if self.right:
                return self.right.lookup(key, self)
            else:
                return None, None
        
        else:
            return self, parent

 

 

#inorder()

    def inorder(self):
        traversal = []
        if self.left:
            traversal += self.left.inorder()
        traversal.append(self)
        if self.right:
            traversal += self.right.inorder()
        return traversal

 

 

#min(), max()

    def min(self):
        if self.root:
            return self.root.min()
        else:
            return None

    def max(self):
        if self.root:
            return self.root.max()
        else:
            return None

 

 

#이진 탐색 트리의 원소 삽입 연산 구현

class Node:

    def __init__(self, key, data):
        self.key = key
        self.data = data
        self.left = None
        self.right = None

    def insert(self, key, data):
        if key < self.key:
            if self.left:
                self.left.insert(key, data)
            else:
                self.left = Node(key,data)
        
        elif key > self.key:
            if self.right:
                self.right.insert(key, data)
            else:
                self.right = Node(key, data)
        
        else:
            raise KeyError
        
        return True

    def inorder(self):
        traversal = []
        if self.left:
            traversal += self.left.inorder()
        traversal.append(self)
        if self.right:
            traversal += self.right.inorder()
        return traversal

class BinSearchTree:

    def __init__(self):
        self.root = None

    def insert(self, key, data):
        if self.root:
            self.root.insert(key, data)
        else:
            self.root = Node(key, data)

    def inorder(self):
        if self.root:
            return self.root.inorder()
        else:
            return []

def solution(x):
    return 0

 

 

 

#원소 삭제

(1) 방법

 - 키(key)를 이용해서 노드를 찾는다

 - 해당 키의 노드가 없으면 삭제할 것도 없음

 - 노드가 있으면 찾은 노드의 부모 노드도 알고 있어야 함

 - 찾은 노드를 제거하고도 이진 탐색 트리 성질을 만족하도록 트리의 구조를 정리함

(2) 인터페이스 설계

 - 입력: 키(key)

 - 출력: 삭제한 경우 True, 해당 키의 노드가 없는 경우 False

 def remove(self, key):
        node, parent = self.lookup(key)
        if node:
			...
            return True
        else:
            return False

(3) 이진 탐색 트리 구조의 유지

 - 삭제되는 노드가 말단(leaf) 노드인 경우

    : 그냥 그 노드를 없애면 됨

    → 부모 노드의 링크를 조정 

 - 자식을 하나 가지고 있는 경우

    : 삭제되는 노드 자리에 그 자식을 대신 배치

    → 자식이 왼쪽인지 오른쪽인지

    → 부모 노드의 링크를 조정 (좌/우 알고 있어야 함)

 

- 자식을 둘 가지고 있는 경우

    : 삭제되는 노드보다 바로 다음 큰 키를 가지는 노드를 찾아 그 노드를 삭제되는 노드 자리에 대신 배치

    → 해당 노드를 삭제

#이진 탐색 트리가 효율적이지 못한 경우

 - 순서대로 삽입하는 경우 효율적이지 못하다

 - 한쪽으로 치우치는 모양


힙 (Heaps)

 

(Example of a  binary  max-heap, Wikipedia)

 

#힙과 이진 탐색 트리 비교

구분 이진 탐색 트리 
원소들은 완전히 크기 순으로 정렬되어 있는가? X O
특정 키 값을 가지는 원소를 빠르게 검색할 수 있는가? X O
부가의 제약 조건은 어떤 것인가? 완전 이진 트리이어야 한다.  

 

#연산의 정의

  • init() : 비어 있는 최대 힙을 생성
  • insert(item) : 새로운 원소를 삽입
  • remove() : 최대 원소(root node)를 반환하고 삭제

 

#배열을 이용한 이진 트리의 표현

class MaxHeap:
    def __init__(self):
        self.data = [None]

 

 

#최대 힙에 원소 삽입

(1) 방법

 - 트리의 마지막 자리에 새로운 원소를 임시로 저장

 - 부모 노드의 키 값을 비교하여 위로, 비교하고 위로 이동

(2) 복잡도

 - 원소의 개수가 n인 최대 힙에 새로운 원소 삽입

 → 부모 노드와 대소 비교 최대 회수: log2n

 → 최악 복잡도 O(logn)의 삽입 연산

 

 

#삽입 연산의 구현 - insert(item) 메서드

class MaxHeap:
    def insert(self, item):
        # 흰트 - python에서 두 변수의 값 바꾸기
        # a = 3, b = 5
        # a, b  = b, a
        self.data.append(item)
        
        index = len(self.data) -1
    
        while index != 1:
            numOfParentNode = index//2
            print(numOfParentNode)
            
            if self.data[numOfParentNode] < self.data[index]:
                self.data[numOfParentNode], self.data[index] = self.data[index], self.data[numOfParentNode]
                index = numOfParentNode
            else:
                break

 

 

#최대 힙에서 원소의 삭제

(1) 방법

 - 루트 노드의 제거 - 이것이 원소들 중 최대 값

 - 트리 마지막 자리 노드를 임시로 루트 노드의 자리에 배치

 - 자식 노드들과의 값 비교로 아래로, 비교하여 아래로 이동
   자식은 둘이 있을 수 있는데 더 큰 값을 기준으로 이동

(2) 복잡도

 - 원소의 개수가 n인 최대 힙에서 최대 원소 삭제

   자식 노드들과 대소 비교 최대 횟수: 2 X log2n

 - 최악 복잡도 O(logn)의 삭제 연산

 

 

#삭제 연산의 구현

 (1) remove() 메서드

class MaxHeap:

    def remove(self):
        if len(self.data) > 1:
            self.data[1], self.data[-1] = self.data[-1], self.data[1]
            data = self.data.pop(-1)
            self.maxHeapify(1)
        else:
            data = None
        return data

 (2) maxHeapify() 메서드

class MaxHeap:
	def maxHeapify(self, i):
        left = i * 2
        right = i * 2 + 1
        greatest = i
        
        if left < len(self.data) and self.data[left] > self.data[greatest]:
            greatest = left

        if right < len(self.data) and self.data[right] > self.data[greatest]:
            greatest = right
            
        if greatest != i:
            self.data[i], self.data[greatest] = self.data[greatest], self.data[i]
            self.maxHeapify(greatest)

 

 

#최대/최소 힙의 응용

(1) 우선 순위 큐

 - Enqueue할 때 "느슨한 정력"을 이루고 있도록 함: O(logN)

 - Dequeue할 때 최댓값을 순서대로 추출: O(logN)

(2) 힙 정렬 (heap sort)

 - 정렬되지 않은 원소들은 아무 순서로나 최대 힙에 삽입 : O(logN)

 - 삽입이 끝나면, 힙이 비어지게 될 때까지 하나씩 삭제 : O(logN)

 - 원소들이 삭제되는 순서가 원소들의 정렬 순서

 - 정렬 알고리즘 복잡도 : O(NlogN)

 

 

#힙 정렬(heap sort)의 코드 구현

def heapSort(unsorted):
    H = MaxHeap()
    for item in unsorted:
        H.insert(item)
    sorted = []
    d = H.remove()
    while d:
        sorted.append(d)
        d = H.remove()
    return sorted

 

댓글