탐색이란?
우리의 일상생활에서 많이 사용되는데
전화번호부에서 전화번호를 찾거나,
사전에서 단어를 검색하고,
인터넷 쇼핑몰에서 내가 원하는 물건을 찾는 작업
이진탐색트리(BST, Binary Search Tree)란?
이진트리 기반의 탐새글 위한 자료구조로 효율적인 탐색 작업을 위한 자료구조이다.
key(왼쪽서브트리) ≤ key(루트노드) ≤ key(오른쪽서브트리)
이진탐색를 중위순회하면 오름차순으로 정렬된 값을 가진다.
search(key) : 키 값이 key인 노드를 찾아 반환한다.
순환적인 탐색 함수
BinaryNode* searchRecur( BinaryNode *n, int key ) { //루트노드 n과 서브트리에서 키값이 key인 노드를 찾아서 반환하는 함수 if( n == NULL ) return NULL; if( key == n->getData() ) return n; else if (key < n->getData() ) return search( n->getLeft(), key ); else return search( n->getRight(), key ); }
반복적인 탐색 함수
BinaryNode* searchIter( BinaryNode *n, int key ) { //키값으로 노드를 탐색하는 함수로 일반 함수로 BinarySearchTree의 멤버 함수로 구현한다. while(n != NULL){ if( key == n->getData() ) return n; else if (key < n->getData() ) n = n->getLeft(); else n = n->getRight(); } return NULL; }
순환적인 탐색 함수
BinaryNode* BinaryNode::search( int key ) { //키값으로 노드를 탐색하는 함수로써 노드 클래스의 멤버로 구현된다. 일반 함수가 아니다. if( key == data ) return this; else if (key < data && left!=NULL) return left->search(key); else if (key > data && right!=NULL) return right->search(key); else return NULL; }
insert(n) : 이진 탐색 트리의 특성을 유지하면서 새로운 노드 n을 이진 탐색 트리에 삽입한다.
void insertRecur( BinaryNode* r, BinaryNode* n ) { //서브트리의 루트 노드 r, 삽입하고자 하는 노드 n if( n->getData() == r->getData() ) return; else if( n->getData() < r->getData() ) { if( r->getLeft() == NULL ) r->setLeft(n); else insertRecur( r->getLeft(), n ); //순환호출 } else { if( r->getRight() == NULL ) r->setRight(n); else insertRecur( r->getRight(), n ); //순환호출 } }
remove(n) : 이진 탐색 트리의 특성을 유지하면서 노드 n을 트리에서 삭제한다.
삭제하려는 노드가 단말 노드일 경우
알아야할 정보 => (삭제할 노드, 삭제할 노드의 부모 노드)
void remove (BinaryNode *parent, BinaryNode *node) { // case 1 if( node->isLeaf() ) { if( parent == NULL ) root = NULL; else { if( parent->getLeft() == node ) parent->setLeft(NULL); else parent->setRight(NULL); } }```
삭제하려는 노드가 하나의 서브트리만 가지고 있는 경우
알아야 할 정보 => (삭제할 노드, 삭제할 노드의 부모노드, 삭제할 노드의 유일한 자식노드)
// case 2 else if( node->getLeft()== NULL|| node->getRight()==NULL ) { BinaryNode *child = (node->getLeft() != NULL ) ? node->getLeft(): node->getRight(); if( node == root ) root = child; else { if( parent->getLeft() == node ) parent->setLeft(child); else parent->setRight(child); } }
삭제하려는 노드가 두개의 서브트리를 가지고 있는 경우
알아야 할 정보 => (삭제할 노드, 삭제할 노드의 부모노드, 후계노드, 후계노드의 부모노드)
// case 3 else { BinaryNode* succp = node; BinaryNode* succ = node->getRight(); while (succ->getLeft() != NULL) { succp = succ; succ = succ->getLeft(); } if( succp->getLeft() == succ ) succp->setLeft(succ->getRight()); else succp->setRight(succ->getRight()); node->setData(succ->getData()); node = succ; } delete node; }
알고리즘의 복잡도를 판단하는 척도는 공간복잡도와 시간복잡도로 이루어져 있다. Big-O표기법은 시간복잡도를 다룬다. 시간복잡도는 알고리즘 내의 연산 횟수와 관련이 있다.
알고리즘이 해당 차수이거나 그보다 낮은 차수의 시간복잡도를 나타내는 표기법이다.
O(1) : 입력값에 상관없이 일정한 실행시간을 가지는 알고리즘이다. 하지만 상수값이 상상 이상으로 클 경우 사실상 일정한 시간의 의미가 없다.
O(log n) : 로그는 매우 큰 입력값에서도 크게 영향을 받지 않아 매우 견고한 알고리즘으로 이진 탐색의 경우가 이에 해당한다. input size의 값이 큰 경우에 사용한다.
O(n) : 알고리즘을 수행하는데 걸리는 시간은 입력값에 비례한다. 이러한 알고리즘을 선형 시간 알고리즘이라 부른다. element의 값을 하나하나 다 보는 경우에 사용한다.
O (n log n) : 병합 정렬등의 대부분 효율이 좋은 알고리즘이 이에 해당 한다. 아무리 좋은 알고리즘이라 할지라도 n log n 보다 빠를 수 없다. 입력값이 최선일 경우, 비교를 건너 뛰어 O(n)이 될 수 있다.
O(n^2) : 버블 정렬 알고리즘이 이에 해당 한다.
O(2^n) : 피보나치의 수를 재귀로 계산하는 알고리즘이 이에 해당 한다. n^2와 혼동되는 경우가 있는데 2^n이 훨씬 더 크다.
O(n!) : 가장 느린 알고리즘으로 입력값이 조금만 커져도 계산이 어렵다.
문제설명
- 이진 트리의 루트가 주어지면 유효한 이진 검색 트리(BST)인지 확인한다.
- 노드의 왼쪽 하위 트리에는 노드 키보다 작은 키가 있는 노드만 포함된다.
- 노드의 오른쪽 하위 트리에는 노드 키보다 큰 키가 있는 노드만 포함된다.
- 왼쪽 및 오른쪽 하위 트리도 모두 이진 검색 트리여야 한다.
문제풀이
해당 트리가 이진 탐색 트리인지 확인하는 문제이다.
두 번째 예제에서 노드의 오른쪽 하위 트리에는 노드 키보다 큰 키가 있는 노드만 포함한다는 조건을 만족하지 않는다.
그러므로 부모 노드의 오른쪽 자식이면 부모 노드보다 커야 하고 부모 노드보다 작은 노드가 없는지 확인해야 한다.class Solution { public: // 재귀적으로 반복하기 위한 매개변수 // 현재 노드, 노드가 가져야하는 최소값, 노드가 가져야하는 최대값 bool isValid(TreeNode* root, TreeNode* low, TreeNode* high) { if(!root) return true; if(low && root->val <= low->val) return false; if(high && root->val >= high->val) return false; return isValid(root->right, root, high) && isValid(root->left, low, root); } bool isValidBST(TreeNode* root) { return isValid(root, nullptr, nullptr); } };
<다른 코드>
중위탐색을 이용한 이진 트리 탐색
이진 탐색 트리에서 중위 탐색을 하게 되면 탐색한 노드들의 값들이 오름차순이 된다는 특징을 이용한다. 최근에 탐색한 값보다 값이 큰 경우에 이진 탐색 트리에 해당된다.class Solution { func isValidBST(_ root: TreeNode?) -> Bool { var isValid = true var ans = Int.min func find(_ node: TreeNode?) { if node == nil { return } find(node?.left) if ans < node!.val { ans = node!.val } else { isValid = false return } find(node?.right) } find(root) return isValid } }
https://m42-orion.tistory.com/38
https://vapor3965.tistory.com/76?category=975115
문제설명
- BST(Binary Search Tree)의 루트가 주어졌는데, 여기서 트리의 정확히 두 노드의 값이 실수로 스왑되었다. 구조를 변경하지 않고 트리를 복구한다.
문제풀이
중위탐색을 이용하여 이진 탐색 트리를 오름차순으로 정리한해 나열한 뒤에 수정이 필요한 부분을 찾아 구조를 유지하면서 해당 두 값을 스왑해준다.
class Solution { private: TreeNode *first; TreeNode *second; TreeNode *pre; void inorder(TreeNode *root){ if(root==nullptr) return; inorder(root->left); if(first==nullptr && (pre==nullptr || pre->val >= root->val)){ first = pre; } if(first!=nullptr && pre->val >= root->val){ second = root; } pre = root; inorder(root->right); } public: void recoverTree(TreeNode* root) { if(root==nullptr) return; first = nullptr; second = nullptr; pre = nullptr; inorder(root); int temp = first->val; first->val = second->val; second->val = temp; } };
문제설명
이진 탐색 트리(BST)의 루트와 정수 val이 제공된다.
BST에서 노드의 값이 val과 같은 노드를 찾고 해당 노드를 기반으로 하는 하위 트리를 반환한다. 그러한 노드가 존재하지 않으면 null을 반환한다.문제풀이
루트 노드를 기준으로 정렬되었을 경우
public TreeNode searchBST(TreeNode root, int val) { if(root == null) return null; if(root.val == val) return root; // 조건 ? 참일 경우 실행 : 거짓일 경우 실행 return root.val > val? searchBST(root.left,val) : searchBST(root.right,val); }
루트 노드를 기준으로 정렬되지 않았을 경우
깊이 우선 탐색을 이용하여 재귀호출을 통해 찾고자 하는 노드를 찾아간다.class Solution { public: TreeNode* searchBST(TreeNode* root, int val) { if(root == nullptr) return nullptr; if(root->val == val) return root; TreeNode *left = searchBST(root->left, val); if(left != nullptr){ return left; } else{ return searchBST(root->right, val); } } };
너비 우선탐색을 이용한 경우
반복적으로 큐에 노드를 넣고 모든 노드를 검사해가면서 찾고자하는 값을 찾는다.public TreeNode searchBST4(TreeNode root, int val) { if(root == null) return null; Queue<TreeNode> q = new LinkedList<>(); q.offer(root); while(!q.isEmpty()){ TreeNode cur = q.poll(); if(cur.val == val) return cur; if(cur.left != null) q.offer(cur.left); if(cur.right != null) q.offer(cur.right); } return null; }
문제설명
- 이진 검색 트리(BST)의 루트 노드와 트리에 삽입할 값이 제공됩니다. 삽입 후 BST의 루트 노드를 반환합니다. 새로운 값이 원래 BST에 존재하지 않음을 보장합니다.
- 삽입 후 트리가 BST를 유지하는 한 삽입을 위한 여러 유효한 방법이 있을 수 있습니다. 당신은 그들 중 하나를 반환할 수 있습니다.
문제풀이
재귀호출을 이용하여 해당 노드보다 클 경우 오른쪽 노드에 재귀호출, 작을 경우 왼쪽에 재귀호출을 한다.
class Solution { public: TreeNode* insertIntoBST(TreeNode* root, int val) { if(root==nullptr) return new TreeNode(val); if(root->val > val){ root->left = insertIntoBST(root->left,val); }else{ root->right = insertIntoBST(root->right,val); } return root; } };
추가
재귀호출을 사용하지 않고 반복하는 경우class Solution { public: TreeNode* insertIntoBST(TreeNode* root, int val) { if(root==nullptr) return new TreeNode(val); TreeNode* cur = root; while(cur != nullptr){ if(cur->val > val){ if(cur->left == nullptr){ cur->left = new TreeNode(val); break; }else{ cur = cur->left; } }else{ if(cur->right == nullptr){ cur->right = new TreeNode(val); break; }else{ cur = cur->right; } } } return root; } };