연결리스트로 스택을 구현은 위처럼 topNode를 두고 새로운 데이터가 추가될 때마다 해당 노드를 topNode로 설정한 뒤 이전 노드를 참조하는 방식으로 이루어진다. 삭제할 땐 topNode.next를 topNode로 설정해주면 된다.
2. Stack 구현하기
구현할 내용은 아래와 같다.
push(E x) - 원소 x를 스택에 입력한다.
pop() - 스택 맨 위의 원소를 삭제한다.
top() - 스택 맨 위의 원소 값을 리턴한다.
isEmpty() - 스택이 비었는지 여부를 리턴한다.
popAll() - 스택의 내부 데이터를 모두 삭제한다.
printAll() - 스택의 내부의 모든 데이터를 출력한다.
** 원래 스택에서는 맨 위의 값에만 접근이 가능하나, 테스트 편의상 printAll() 메서드를 만들어 사용하였다.
1) 노드 클래스
public class Node<E> {
public E item;
public Node<E> next;
public Node (E newItem) {
item = newItem;
next = null;
}
public Node (E newItem, Node<E> nextNode) {
item = newItem;
next = nextNode;
}
}
item 변수에 데이터를 저장한다.
next 변수에 다음 노드를 저장한다. next는 다음 노드의 위치를 가리키는 포인터가 된다.
첫 번째 생성자는 데이터를 입력받아 데이터를 item에 저장하고, 포인터 값은 null로 저장한다.
두 번째 생성자는 데이터와 다음 노드를 입력받아 데이터를 item에 저장하고, 포인터 값에 다음 노드를 저장한다.
2) 인터페이스
public interface StackInterface<E> {
public void push (E x);
public void pop();
public E top();
public boolean isEmpty();
public void popAll();
public void printAll();
}
3) LinkedStack 클래스
<push 메서드 구현>
@Override
public void push(E x) {
topNode = new Node<> (x, topNode);
}
기존 topNode를 다음 노드로 참조하고 입력 받은 값을 item 값으로 하는 새 노드를 만든다.
<pop 메서드 구현>
@Override
public void pop() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
topNode = topNode.next;
}
}
스택이 비었다면 에러를 출력한다.
그렇지 않다면 기존 topNode 자리에 기존 topNode가 참조하던 다음 노드가 들어오게 한다.
<top 메서드 구현>
@Override
public E top() {
if (isEmpty()) {
return null;
} else {
return topNode.item;
}
}
스택이 비었다면 null 값을 리턴한다.
그렇지 않다면 topNode의 item값을 리턴한다.
<isEmpty 메서드 구현>
@Override
public boolean isEmpty() {
return topNode==null;
}
topNode가 null값인지 여부를 리턴한다.
<popAll 메서드 구현>
@Override
public void popAll() {
topNode = null;
}
topNode를 null로 초기화한다.
<printAll 메서드 구현>
@Override
public void printAll() {
Node<E> currNode = topNode;
while (currNode!=null) {
System.out.print(currNode.item+" ");
currNode=currNode.next;
}
for문이 topNode부터 스택의 전체 노드를 순회하며 값을 프린트한다.
- 전체 코드
public class LinkedStack<E> implements StackInterface<E>{
private Node<E> topNode;
public LinkedStack() {
topNode=null;
}
@Override
public void push(E x) {
topNode = new Node<> (x, topNode);
}
@Override
public void pop() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
topNode = topNode.next;
}
}
@Override
public E top() {
if (isEmpty()) {
return null;
} else {
return topNode.item;
}
}
@Override
public boolean isEmpty() {
return topNode==null;
}
@Override
public void popAll() {
topNode = null;
}
@Override
public void printAll() {
Node<E> currNode = topNode;
while (currNode!=null) {
System.out.print(currNode.item+" ");
currNode=currNode.next;
}
}
}
** 배열 리스트로 구현한 스택과 달리 정해진 용량이 없기 때문에 isFull 메서드는 필요하지 않다.
3) 결과 확인하기
<코드>
public class Test {
public static void main(String[] args) {
LinkedStack<Integer> stack = new LinkedStack<>();
stack.push(100);
stack.push(200);
stack.push(300);
stack.push(400);
System.out.println(stack.top());
stack.printAll();
System.out.println();
for (int i=0; i<5; i++) {
stack.pop();
}
stack.printAll();
//스택에서 pop을 5번 수행한 뒤 출력합니다. - 원소가 4개인데 5번 pop하였으므로 에러가 발생하고, 스택이 비어있으므로 내용물은 출력되지 않음.
System.out.println(stack.isEmpty());
// 스택이 비었는지 여부를 출력합니다.
stack.push(100);
stack.printAll();
System.out.println();
//stack에 100을 입력하고 출력합니다.
stack.popAll();
stack.printAll();
//stack에 - stack을 popAll 했으므로 내용물이 출력되지 않음.
}
}
** 원래 스택에서는 맨 위의 값에만 접근이 가능하나, 테스트 편의상 printAll() 메서드를 만들어 사용하였다.
1) 인터페이스
public interface StackInterface<E> {
public void push (E x);
public void pop();
public E top();
public boolean isEmpty();
public void popAll();
public void printAll();
}
2) 클래스
- 필드 영역
//필드 영역
private E stack[];
private int topIndex;
private static final int DEFAULT_CAPACITY = 64;
stack - 원소를 입력 받을 배열 리스트이다.
topIndex - 스택의 맨 윗 원소의 인덱스를 저장한다.
DEFAULT_CAPACITY - 스택 생성 시 용량을 따로 선언하지 않을 경우 설정할 기본 용량이다.
- 생성자 영역
//생성자 영역
public ArrayStack() {
stack = (E[]) new Object[DEFAULT_CAPACITY];
topIndex = -1;
}
public ArrayStack(int n) {
stack = (E[]) new Object[n];
topIndex = -1;
}
생성 시 입력 값으로 정수를 받으면 해당 정수 크기의 스택을, 그렇지 않으면 DEFAULT_CAPACITY 크기의 스택을 생성한다.
-메서드 영역
<push 메서드 구현>
//메서드 영역
@Override
public void push(E x) {
if (isFull()) {
System.out.println("ERROR");
} else {
topIndex++;
stack[topIndex]=x;
}
}
만약 stack에 남은 공간이 없는데 push를 시도하면 에러를 출력한다.
그렇지 않으면 topIndex를 늘려준 뒤 해당 인덱스에 입력받은 데이터를 입력한다.
<pop 메서드 구현>
@Override
public void pop() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
stack[topIndex]=null;
topIndex--;
}
}
만약 stack이 비어 있는데 pop을 시도하면 에러를 출력한다.
그렇지 않다면 현재 topIndex를 null로 바꾼 후 topIndex를 1 감소시킨다.
** 사실 add시 해당 인덱스를 덮어 쓸 것이기 때문에 topIndex만 1 감소시키고 null 처리는 해주지 않아도 된다.
<top 메서드 구현>
@Override
public E top() {
if (isEmpty()) {
return null;
} else {
return stack[topIndex];
}
}
만약 스택이 비어있는데 top을 시도하면 null 값을 반환한다.
그렇지 않다면 stack의 topIndex값을 반환한다.
<isEmpty&isFull 메서드 구현>
@Override
public boolean isEmpty() {
return (topIndex<0);
}
//isFull메서드는 인터페이스를 오버라이딩 한 것이 아님에 주의!
public boolean isFull() {
return (topIndex==stack.length-1);
}
isEmpty - topIndex가 음수이면 true, 아니면 false.
isFull - topIndex와 스택의 크기를 비교하여 스택이 다 찼는지를 판별.
<popAll 메서드 구현>
@Override
public void popAll() {
stack = (E[]) new Object[stack.length];
topIndex=-1;
}
stack을 기존 길이의 빈 배열 리스트로 덮어쓴다.
topIndex를 초기화한다.
<printAll 메서드 구현>
@Override
public void printAll() {
for (int i=0; i<=topIndex; i++) {
System.out.print(stack[i]+" ");
}
}
for문이 전체 스택의 인덱스를 순회하며 해당 인덱스의 값들을 프린트한다.
- 전체 코드
public class ArrayStack<E> implements StackInterface<E> {
//필드 영역
private E stack[];
private int topIndex;
private static final int DEFAULT_CAPACITY = 64;
//생성자 영역
public ArrayStack() {
stack = (E[]) new Object[DEFAULT_CAPACITY];
topIndex = -1;
}
public ArrayStack(int n) {
stack = (E[]) new Object[n];
topIndex = -1;
}
//메서드 영역
@Override
public void push(E x) {
if (isFull()) {
System.out.println("ERROR");
} else {
topIndex++;
stack[topIndex]=x;
}
}
@Override
public void pop() {
if (isEmpty()) {
System.out.println("ERROR");
} else {
stack[topIndex]=null; //안해도 됨.
topIndex--;
}
}
@Override
public E top() {
if (isEmpty()) {
return null;
} else {
return stack[topIndex];
}
}
@Override
public boolean isEmpty() {
return (topIndex<0);
}
public boolean isFull() {
return (topIndex==stack.length-1);
}
@Override
public void popAll() {
stack = (E[]) new Object[stack.length];
topIndex=-1;
}
@Override
public void printAll() {
for (int i=0; i<=topIndex; i++) {
System.out.print(stack[i]+" ");
}
}
}
3) 결과 확인하기
<코드>
public class Test {
public static void main(String[] args) {
ArrayStack<Integer> stack = new ArrayStack<>();
stack.push(100);
stack.push(200);
stack.push(300);
stack.push(400);
System.out.println(stack.top());
stack.printAll();
System.out.println();
for (int i=0; i<5; i++) {
stack.pop();
}
stack.printAll();
//스택에서 pop을 5번 수행한 뒤 출력합니다. - 원소가 4개인데 5번 pop하였으므로 에러가 발생하고, 스택이 비어있으므로 내용물은 출력되지 않음.
System.out.println(stack.isEmpty());
// 스택이 비었는지 여부를 출력합니다.
stack.push(100);
stack.printAll();
System.out.println();
//stack에 100을 입력하고 출력합니다.
stack.popAll();
stack.printAll();
//stack에 - stack을 popAll 했으므로 내용물이 출력되지 않음.
}
}
단일 방향 연결 리스트와 다르게 어떤 노드를 한 번 지나도 반대 방향으로 돌아갈 수 있다.
마지막 노드의 next 값이 헤드 노드를 가리키고 헤드 노드의 prev 값이 마지막 노드를 가리킨다.
2. CircularDoublyLinkedList 구현하기
구현할 내용은 아래와 같다.
add (int i, E x) - i번째 인덱스에 원소 x를 입력한다.
append (E x) - 배열 리스트의 끝에 원소 x를 입력한다.
remove (int i) - i번째 원소를 삭제한다.
removeItem (E x) - 배열 리스트에서 원소 x를 삭제한다. 중복 원소가 있을 경우 먼저 나오는 원소를 삭제한다.
get (int i) - i번째 원소를 리턴한다.
set (int i, E x) - i번째 인덱스의 원소를 x로 교체한다.
len() - 배열 리스트의 원소 개수를 리턴한다.
isEmpty() - 배열 리스트가 비었는지 여부를 boolean으로 리턴한다.
clear() - 인덱스를 비운다.
indexOf(E x) - 배열 리스트 원소 x의 인덱스를 리턴한다. 중복 원소가 있을 경우 먼저 나오는 원소 인덱스를 리턴한다.
printAll() - 배열 리스트의 모든 원소를 리턴한다.
1) 노드 클래스
public class BidirectionalNode<E> {
public BidirectionalNode<E> prev;
public E item;
public BidirectionalNode<E> next;
public BidirectionalNode() {
prev=next=null;
item=null;
}
public BidirectionalNode (E newItem) {
prev=next=null;
item = newItem;
}
public BidirectionalNode (BidirectionalNode<E> prevNode, E newItem, BidirectionalNode<E> nextNode) {
prev = prevNode;
item = newItem;
next = nextNode;
}
}
prev 변수에 해당 노드의 이전 노드를 저장한다.
item 변수에 해당 노드에 입력할 데이터 값을 저장한다.
next 변수에 해당 노드의 다음 노드를 저장한다.
첫 번째 생성자 - 클래스 호출 시 입력값이 없으면 prev, next, item 변수를 모두 null로 저장한다.
두 번째 생성자 - 클래스 호출 시 노드에 저장할 데이터를 입력받아 item 변수에 저장한다.
세 번째 생성자 - 클래스 호출 시 이전 노드, 저장할 값, 다음 노드를 입력받아 각각 prev, item, next 변수에 저장한다.
2) 인터페이스
public interface ListInterface<E>{
public void add(int i, E x);
public void append (E x);
public void remove (int i);
public void removeItem (E x);
public E get(int i);
public void set(int i, E x);
public int len();
public boolean isEmpty();
public void clear();
public int IndexOf (E x);
public void printAll ();
}
3) CircularDoublyLinkedList 클래스
- 필드 영역
//필드 영역
private BidirectionalNode<E> head;
private int size;
더미 헤드를 저장할 head 변수를 선언한다.
원형 양방향 연결 리스트의 원소 개수를 저장할 size 변수를 선언한다.
- 생성자 영역
//생성자 영역
public CircularDoublyLinkedList() {
head = new BidirectionalNode<>(null);
head.next = head.prev = head;
}
head 변수에 item 값이 null인 노드를 입력합니다.
처음 원형 양방향 연결 리스트가 생성 되었을 때 head 노드는 next와 prev로 자기 자신을 참조합니다.
- 메서드 영역
<getNode 메서드 구현>
public BidirectionalNode<E> getNode (int i) {
if (i>size||i<-1) throw new IllegalArgumentException();
BidirectionalNode<E> currNode = head;
if (i<size/2) {
for (int j=0; j<=i; j++) {
currNode=currNode.next;
}
} else {
for (int j=size-1; j>=i; j--) {
currNode=currNode.prev;
}
}
return currNode;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우 (-1일 경우 더미헤드를 반환)
currNode 변수에 head 노드를 저장한다.
입력 받은 인덱스가 앞쪽에 있으면 head 노드부터 입력 받은 인덱스까지 next로 이동하고 currNode를 리턴한다.
입력 받은 인덱스가 뒷쪽에 있으면 head 노드부터 입력 받은 인덱스까지 prev로 이동하고 currNode를 리턴한다.
<add 메서드 구현>
@Override
public void add(int i, E x) {
if (i>size||i<0) throw new IllegalArgumentException();
Node<E> prevNode = getNode(i-1);
Node<E> currNode = new Node(x,prevNode.next);
prevNode.next=currNode;
if (i==size) {
tail = currNode;
}
size++;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
prevNode 변수에 입력 받은 인덱스의 이전 인덱스 노드를 저장한다.
prevNode는 새로 들어올 노드의 이전 노드가 될 것이다.
nextNode 변수에 입력 받은 인덱스의 노드를 저장한다.
nextNode는 새로 들어올 노드의 다음 노드가 될 것이다.
newNode 변수에 입력 받은 데이터를 item 값으로 하고 prevNode를 이전 노드, nextNode를 다음 노드로 하는 노드를 저장한다.
prevNode의 다음 노드를 newNode로, nextNode의 이전 노드를 newNode로 갱신해준다.
size 변수를 1 증가시킨다.
<append 메서드 구현>
@Override
public void append(E x) {
BidirectionalNode<E> prevNode = getNode(size-1);
BidirectionalNode<E> newNode = new BidirectionalNode<>(prevNode,x,head);
prevNode.next=newNode;
head.prev=newNode;
size++;
}
prevNode에 현재 원형 양방향 연결 리스트의 마지막 인덱스 노드를 저장한다.
NewNode 변수에 prevNode를 이전 노드로 하고 head 노드를 다음 노드로 하며 입력 받은 데이터를 item 값으로 하는 노드를 저장한다.
prevNode의 다음 노드를 newNode로 저장한다.
head 노드의 이전 도르르 newNode로 저장한다.
size 변수를 1 증가시킨다.
<remove 메서드 구현>
@Override
public void remove(int i) {
if (i>size-1||i<0) throw new IllegalArgumentException();
BidirectionalNode<E> prevNode = getNode(i-1);
BidirectionalNode<E> nextNode = getNode(i+1);
prevNode.next=nextNode;
nextNode.prev=prevNode;
size--;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
prevNode 변수에 입력 받은 인덱스의 이전 인덱스 노드를 저장한다.
nextNode 변수에 입력 받은 인덱스의 다음 인덱스 노드를 저장한다.
prevNode가 next로 i번째 노드를 생략하고 nextNode를 참조하게 한다.
nextNode가 prev로 i번째 노드를 생략하고 prevNode를 참조하게 한다.
size 변수를 1 감소시킨다.
<removeItem 메서드 구현>
@Override
public void removeItem(E x) {
BidirectionalNode<E> currNode = head;
for (int i=0; i<size; i++) {
currNode=currNode.next;
if (currNode.item.equals(x)) {
currNode.prev.next=currNode.next;
currNode.next.prev=currNode.prev;
break;
}
}
size--;
}
currNode 변수에 head 노드를 저장한다.
0부터 마지막 인덱스까지 순회하면서 입력받은 x와 item 값이 같은 노드가 있는지 검사한다.
currNode와 x 값이 일치하면 해당 노드의 이전 노드가 next로 currNode를 생략하고 그 다음 노드를 참조하게 한다. 해당 노드의 다음 노드는 prev 값으로 currNode를 생략하고 그 이전 노드를 참조하게 한다.
size 변수를 1 감소시킨다.
<get,set,len,isEmpty 메서드 구현>
@Override
public E get(int i) {
if (i>size-1||i<0) throw new IllegalArgumentException();
return getNode(i).item;
}
@Override
public void set(int i, E x) {
if (i>size-1||i<0) throw new IllegalArgumentException();
getNode(i).item=x;
}
@Override
public int len() {
return size;
}
@Override
public boolean isEmpty() {
return (size==0);
}
get - 입력받은 인덱스에 해당하는 값을 리턴한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
set - 입력받은 인덱스에 해당하는 값을 입력받은 값으로 교체한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
len - size 변수를 리턴한다.
isEmpty - size==0 여부를 리턴한다.
<clear 메서드 구현>
@Override
public void clear() {
size=0;
head.next=head;
head.prev=head;
}
size 변수를 0으로 초기화한다.
head 노드가 next, prev로 자기 자신을 참조하게 한다.
<indexOf 메서드 구현>
@Override
public int IndexOf(E x) {
int idx = -1;
BidirectionalNode<E> currNode = head;
for (int i=0; i<size; i++) {
currNode=currNode.next;
if (currNode.item.equals(x)) {
idx=i;
break;
}
}
return idx;
}
스토어드 프로시저는 SQL에 프로그래밍 기능을 추가해서 일반 프로그래밍 언어와 비슷한 효과를 내는 것임.
일반 프로그래밍에서 함수를 구현하여 사용하는 것과 비슷함.
1) 생성
<입력 매개변수가 없을 경우>
DELIMITER $$
CREATE PROCEDURE stored_procedure_name ()
BEGIN
-- 여기에 stored_procedure_name에서 실행할 내용을 입력합니다.
END$$
DELIMITER ;
<입력 매개변수가 있을 경우>
DELIMITER $$
CREATE PROCEDURE stored_procedure_name (IN data_name data_type)
BEGIN
-- 여기에 stored_procedure_name에서 실행할 내용을 입력합니다.
END$$
DELIMITER ;
<출력 매개변수가 있을 경우>
DELIMITER $$
CREATE PROCEDURE stored_procedure_name (OUT data_name data_type)
BEGIN
-- 여기에 stored_procedure_name에서 실행할 내용을 입력합니다.
-- data_name에 저장할 값을 여기에서 설정해줍니다.
END$$
DELIMITER ;
2) 삭제
DROP PROCEDURE stored_procedure_name();
2. 스토어드 함수
MySQL의 내장 함수 외에 직접 함수를 만드는 것,
스토어드 프로시저와 다르게 RETURNS 예약어를 통해 하나의 값을 반환해야 함.
1) 생성
DELIMITER $$
CREATE FUNCTION stored_function_name(var1 INT) -- 변수와 입력 데이터 타입을 지정
RETURNS INT -- 리턴 데이터 타입
BEGIN
-- 이 부분에 코딩
RETURN var1+10; -- 리턴값
END $$
DELIMITER ;
2) 삭제
DROP FUNCTION stored_function_name;
3) 커서
테이블에서 한 행씩 처리하기 위한 방법
첫 행부터 마지막 행까지 겁근해서 값을 처리
커서 선언 → 반복 조건 선언 → 커서 열기 → 데이터 가져오기 → 데이터 처리하기 → 커서 닫기 순으로 작동
<커서 선언>
DECLARE cursor1 CURSOR FOR
SELECT column1 FROM table1;
cursor1이 table1의 column1 열을 순회하면서 데이터에 접근함.
<반복 조건 선언>
DECLARE CONTINUE HANDLER
FOR NOT FOUND SET endOfRow=TRUE;
DECLARE CONTINUE HANDLER - 반복 조건을 준비하는 예약어
FOR NOT FOUND - 더 이상 행이 없을 때 이어진 문장을 수행 (endOfRow=TRUE;)
<커서 열기>
OPEN cursor1;
<데이터 가져오기 & 데이터 처리하기>
cursor_loop : LOOP
FETCH cursor1 INTO var;
IF endOfRow THEN
LEAVE cursor_loop;
END IF;
-- 실행할 내용을 입력합니다.
END LOOP cursor_loop;
cursor_loop은 반복 구간에 대해 임의로 정한 이름임.
FETCH - 한 행씩 읽어옴. cursor에서 column1 행을 조회했으므로 var에 column1의 값이 순서대로 저장됨.
cursor_loop : LOOP & END LOOP cursor_loop - 해당 예약어 사이에 있는 코드를 반복함.
LEAVE cursor_loop - 해당 반복 구간을 종료하고 빠져나감.
<커서 닫기>
CLOSE cursor1;
3. AFTER 트리거
트리거는 자동 수행을 통해 사용자가 추가 작업을 잊어버리는 실수를 방지함.
예를 들어 한 테이블에서 데이터를 삭제하기 전 다른 테이블에 해당 데이터를 입력하는 작업을 트리거 하여 수동으로 백업하지 않아도 됨.
테이블에 INSERT, UPDATE, DELETE 등의의 이벤트가 발생할 때 작동함.
1) 생성
DELIMITER $$
CREATE TRIGGER trigger_name
AFTER DELETE
ON table1
FOR EACH ROW
BEGIN
INSERT INTO backup_table1 VALUES
(OLD.column1, OLD.column2, OLD.column3);
END $$
DELIMITER;
LinkedList는 리스트의 맨 끝에 데이터를 입력/삭제시 head부터 끝까지 이동해야 하므로 O(n)의 시간이 든다.
CircularLinkedList는 맨 끝의 노드가 헤드 노드를 참조하도록 하여 시간을 단축한다.
tail에는 유효한 값이 들어갈 수 있지만 tail.next는 item에 null값을 유지한다.
2. CircularLinkedList 구현하기
구현할 내용은 아래와 같다.
add (int i, E x) - i번째 인덱스에 원소 x를 입력한다.
append (E x) - 배열 리스트의 끝에 원소 x를 입력한다.
remove (int i) - i번째 원소를 삭제한다.
removeItem (E x) - 배열 리스트에서 원소 x를 삭제한다. 중복 원소가 있을 경우 먼저 나오는 원소를 삭제한다.
get (int i) - i번째 원소를 리턴한다.
set (int i, E x) - i번째 인덱스의 원소를 x로 교체한다.
len() - 배열 리스트의 원소 개수를 리턴한다.
isEmpty() - 배열 리스트가 비었는지 여부를 boolean으로 리턴한다.
clear() - 인덱스를 비운다.
indexOf(E x) - 배열 리스트 원소 x의 인덱스를 리턴한다. 중복 원소가 있을 경우 먼저 나오는 원소 인덱스를 리턴한다.
printAll() - 배열 리스트의 모든 원소를 리턴한다.
1) 노드 클래스
public class Node<E> {
public E item;
public Node<E> next;
public Node (E newItem) {
item = newItem;
next = null;
}
public Node (E newItem, Node<E> nextNode) {
item = newItem;
next = nextNode;
}
}
item 변수에 데이터를 저장한다.
next 변수에 다음 노드를 저장한다. next는 다음 노드의 위치를 가리키는 포인터가 된다.
첫 번째 생성자는 데이터를 입력받아 데이터를 item에 저장하고, 포인터 값은 null로 저장한다.
두 번째 생성자는 데이터와 다음 노드를 입력받아 데이터를 item에 저장하고, 포인터 값에 다음 노드를 저장한다.
2) 인터페이스
public interface ListInterface<E>{
public void add(int i, E x);
public void append (E x);
public void remove (int i);
public void removeItem (E x);
public E get(int i);
public void set(int i, E x);
public int len();
public boolean isEmpty();
public void clear();
public int IndexOf (E x);
public void printAll ();
}
3) CircularLinkedList 클래스
- 필드 영역
//필드 영역
private Node<E> tail;
private int size;
tail 변수에 tail로 설정할 노드를 저장한다.
size 변수에 원형 연결 리스트의 크기를 저장한다.
- 생성자 영역
//생성자 영역
public CircularLinkedList() {
size = 0;
tail = new Node(-1);
tail.next=tail;
}
초기 원형 연결 리스트의 크기는 0이다.
초기 tail의 저장 데이터는 -1이며, 다음 노드로 자기 자신을 참조한다.
-메서드 영역
<getNode 메서드 구현>
public Node<E> getNode (int i) {
if (i>size||i<-1) throw new IllegalArgumentException();
Node<E> currNode = tail.next;
for (int j=0; j<=i; j++) {
currNode=currNode.next;
}
return currNode;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우 ( -1일 경우 더미헤드를 반환)
변수 currNode에 head(tail.next) 노드를 저장한다.
입력받은 인덱스까지 for문으로 원형 연결 리스트를 순회한 뒤 currNode를 리턴한다.
<add 메서드 구현>
@Override
public void add(int i, E x) {
if (i>size||i<0) throw new IllegalArgumentException();
Node<E> prevNode = getNode(i-1);
Node<E> currNode = new Node(x,prevNode.next);
prevNode.next=currNode;
if (i==size) {
tail = currNode;
}
size++;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
변수 prevNode에 입력받은 인덱스 전 노드를 저장한다.
변수 currNode에 item이 x고 next로 prevNode의 다음 노드를 참조하는 새 노드를 만들어 저장한다.
prevNode가 next로 currNode를 참조하게 한다.
만약 원형 연결 리스트의 끝부분에 currNode를 저장하는 것이라면 tail을 currNode로 교체한다.
size 변수를 1 증가시킨다.
<append 메서드 구현>
public void append(Object x) {
Node<E> prevNode = tail;
Node<E> newNode = new Node(x,prevNode.next);
tail.next=newNode;
tail = newNode;
size++;
}
변수 prevNode에 tail 노드를 저장한다. 마지막 자리에 덧붙일 것이므로 현재의 tail이 새로운 노드의 이전 노드가 된다.
newNode에 item이 x고 tail.next(head)를 next로 참조하는 새 노드를 만든다.
기존 tail노드가 next로 newNode를 참조하게 한다.
newNode가 tail이 된다.
size 변수를 1 증가시킨다.
<remove 메서드 구현 - 단일 연결 리스트와 동일>
@Override
public void remove(int i) {
if (i>size-1||i<0) throw new IllegalArgumentException();
Node<E> prevNode = getNode(i-1);
prevNode.next=getNode(i+1);
size--;
}
currNode의 데이터가 x와 일치하면 prevNode가 next값으로 currNode를 생략하고 currNode.next를 참조하게 한다.
<get, set, len, isEmpty 메서드 구현>
@Override
public E get(int i) {
if (i>size-1||i<0) throw new IllegalArgumentException();
return getNode(i).item;
}
@Override
public void set(int i, E x) {
if (i>size-1||i<0) throw new IllegalArgumentException();
Node<E> currNode = getNode(i);
currNode.item=x;
}
@Override
public int len() {
return size;
}
@Override
public boolean isEmpty() {
return (size==0);
}
get - 입력받은 인덱스에 해당하는 값을 리턴한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
set - 입력받은 인덱스에 해당하는 값을 입력받은 값으로 교체한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
len - size 변수를 리턴한다.
isEmpty - size==0 여부를 리턴한다.
<clear 메서드 구현>
@Override
public void clear() {
size=0;
tail=new Node(-1);
tail.next=tail;
}
size 변수를 0으로 초기화한다.
tail 변수를 item 값이 -1인 새 노드로 초기화한다.
tail 변수가 다음 노드로 자기 자신을 참조하게 한다. (tail.next==head)
<IndexOf 메서드 구현>
@Override
public int IndexOf(E x) {
int idx=-1;
Node<E> currNode = tail.next;
for (int i=0; i<size; i++) {
currNode=currNode.next;
if (currNode.item.equals(x)) {
idx=i;
break;
}
}
return idx;
}
연결 리스트(LinkedList)란 각 노드가 데이터(item)와 포인터(next)를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조이다. 연결리스트의 대표적 종류에는 단일 연결 리스트, 양방향 연결 리스트, 원형 연결 리스트가 있다.
선언시 크기가 고정되어 있지 않다.
데이터 접근시 처음부터 노드를 순회해야 하므로 배열 리스트에 비해 속도가 느리다.
데이터 추가/삭제시 전후 노드의 포인터만 수정하면 되므로 배열 리스트에 비해 속도가 빠르다.
2. LinkedList 구현하기
구현할 내용은 아래와 같다.
add (int i, E x) - i번째 인덱스에 원소 x를 입력한다.
append (E x) - 배열 리스트의 끝에 원소 x를 입력한다.
remove (int i) - i번째 원소를 삭제한다.
removeItem (E x) - 배열 리스트에서 원소 x를 삭제한다. 중복 원소가 있을 경우 먼저 나오는 원소를 삭제한다.
get (int i) - i번째 원소를 리턴한다.
set (int i, E x) - i번째 인덱스의 원소를 x로 교체한다.
len() - 배열 리스트의 원소 개수를 리턴한다.
isEmpty() - 배열 리스트가 비었는지 여부를 boolean으로 리턴한다.
clear() - 인덱스를 비운다.
indexOf(E x) - 배열 리스트 원소 x의 인덱스를 리턴한다. 중복 원소가 있을 경우 먼저 나오는 원소 인덱스를 리턴한다.
printAll() - 배열 리스트의 모든 원소를 리턴한다.
1) 노드 클래스
public class Node<E> {
public E item;
public Node<E> next;
public Node (E newItem) {
item = newItem;
next = null;
}
public Node (E newItem, Node<E> nextNode) {
item = newItem;
next = nextNode;
}
}
item 변수에 데이터를 저장한다.
next 변수에 다음 노드를 저장한다. next는 다음 노드의 위치를 가리키는 포인터가 된다.
첫 번째 생성자는 데이터를 입력받아 데이터를 item에 저장하고, 포인터 값은 null로 저장한다.
두 번째 생성자는 데이터와 다음 노드를 입력받아 데이터를 item에 저장하고, 포인터 값에 다음 노드를 저장한다.
2) 인터페이스
public interface ListInterface<E>{
public void add(int i, E x);
public void append (E x);
public void remove (int i);
public void removeItem (E x);
public E get(int i);
public void set(int i, E x);
public int len();
public boolean isEmpty();
public void clear();
public int IndexOf (E x);
public void printAll ();
}
3) LinkedList 클래스
- 필드 영역
//필드 영역
private Node<E> head;
private int size;
head 변수에는 연결 리스트의 시작 노드를 저장한다.
size에는 연결 리스트의 길이를 저장한다.
- 생성자 영역
//생성자 영역
public LinkedList() {
size=0;
head = new Node<>(null,null);
}
새 연결 리스트를 생성하면 필드 영역의 head에 데이터와 포인터값이 모두 null인 새 노드를 저장한다.
연결 리스트의 크기인 size는 0이다.
- 메서드 영역
<getNode 메서드 구현>
public Node<E> getNode (int i) {
if (i<-1||i>size) throw new IllegalArgumentException();
Node<E> currNode = head;
for (int j=0; j<=i; j++) {
currNode=currNode.next;
}
return currNode;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우 (-1인 경우 더미 헤드를 반환)
인터페이스의 메서드를 오버라이딩 한 것은 아니다.
head 노드를 currNode 변수에 저장한다.
입력받은 인덱스에 도달할 때까지 head부터 i만큼 포인터를 통해 다음 노드로 이동한 뒤 currNode를 리턴한다.
<add 메서드 구현>
@Override
public void add(int i, E x) {
if (i<0||i>size) throw new IllegalArgumentException();
Node<E> prevNode = getNode(i-1);
Node<E> newNode = new Node<> (x,prevNode.next);
prevNode.next=newNode;
size++;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
x를 item으로 하고 prevNode.next를 next로하는 newNode를 생성한다.
prevNode가 next로 newNode를 참조하게 한다.
size 변수를 1 증가시킨다.
<append 메서드 구현>
@Override
public void append(E x) {
Node<E> prevNode = getNode(size-1);
Node<E> newNode = new Node<>(x,null);
prevNode.next=newNode;
size++;
prevNode에 연결 리스트의 마지막 인덱스 노드를 저장한다.
item에 입력 받은 데이터를, next에 null값을 입력한 newNode 객체를 생성한다.
prevNode가 next로 newNode를 참조하게 한다.
<remove 메서드 구현>
@Override
public void remove(int i) {
if (i<0||i>size-1) throw new IllegalArgumentException();
Node<E> prevNode = getNode(i-1);
prevNode.next=getNode(i+1);
size--;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
prevNode가 next로 prevNode.next를 생략하고 prevNode.next.next를 참조하게 한다.
<removeItem 메서드 구현>
@Override
public void removeItem(E x) {
Node<E> currNode = head;
Node<E> prevNode;
for (int i=0; i<size; i++) {
prevNode=currNode;
currNode=prevNode.next;
if (currNode.item.equals(x)) {
prevNode.next=currNode.next;
size--;
return;
}
}
}
for문으로 연결 리스트를 순회한다.
currNode의 데이터가 x와 일치하면 prevNode가 next값으로 currNode를 생략하고 currNode.next를 참조하게 한다.
<get,set,len,isEmpty 메서드 구현>
@Override
public E get(int i) {
if (i<0||i>size-1) throw new IllegalArgumentException();
Node<E> currNode = getNode(i);
return currNode.item;
}
@Override
public void set(int i, E x) {
if (i<0||i>size-1) throw new IllegalArgumentException();
Node<E> currNode = getNode(i);
currNode.item=x;
}
@Override
public int len() {
return size;
}
@Override
public boolean isEmpty() {
return (size==0);
}
get - 입력받은 인덱스에 해당하는 값을 리턴한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
set - 입력받은 인덱스에 해당하는 값을 입력받은 값으로 교체한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
len - size 변수를 리턴한다.
isEmpty - size==0 여부를 리턴한다.
<clear 메서드 구현>
@Override
public void clear() {
size = 0;
head = new Node<>(null,null);
}
size 변수를 0으로 초기화한다.
head 변수를 새로운 노드로 초기화한다.
<indexOf 메서드 구현>
@Override
public int IndexOf(E x) {
int idx=-1;
Node<E> currNode = head;
for (int i=0; i<size; i++) {
currNode=currNode.next;
if (currNode.item.equals(x)) {
idx=i;
break;
}
}
return idx;
}
removeItem (E x) - 배열 리스트에서 원소 x를 삭제한다. 중복 원소가 있을 경우 먼저 나오는 원소를 삭제한다.
get (int i) - i번째 원소를 리턴한다.
set (int i, E x) - i번째 인덱스의 원소를 x로 교체한다.
len() - 배열 리스트의 원소 개수를 리턴한다.
isEmpty() - 배열 리스트가 비었는지 여부를 boolean으로 리턴한다.
clear() - 인덱스를 비운다.
indexOf(E x) - 배열 리스트 원소 x의 인덱스를 리턴한다. 중복 원소가 있을 경우 먼저 나오는 원소 인덱스를 리턴한다.
printAll() - 배열 리스트의 모든 원소를 리턴한다.
1) 인터페이스
public interface ListInterface<E>{
public void add(int i, E x);
public void append (E x);
public void remove (int i);
public void removeItem (E x);
public E get(int i);
public void set(int i, E x);
public int len();
public boolean isEmpty();
public void clear();
public int IndexOf (E x);
public void printAll ();
}
2) 클래스
- 필드 영역
//필드 영역
private E item[];
private int size;
private static final int DEFAULT_CAPACITY = 64;
item[] - 원소를 입력 받을 배열 리스트이다.
size - 원소의 개수를 저장할 정수형 변수이다.
DEFAULT_CAPACITY - 배열 리스트에 용량을 따로 선언하지 않을 경우 설정할 기본 용량이다.
- 생성자 영역
//생성자 영역
public ArrayList() {
item = (E[]) new Object [DEFAULT_CAPACITY];
size=0;
}
public ArrayList(int n) {
item = (E[]) new Object [n];
size=0;
}
- 메서드 영역
<add 메서드 구현>
@Override
public void add(int i, E x) {
if (size>=item.length||i<0|| i>item.length) throw new IllegalArgumentException();
for (int j=size-1; j>=i; i--) {
item[j+1]=item[j];
}
item[i]=x;
size++;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우, 배열 리스트에 빈 자리가 없을 경우
for문이 배열 리스트의 마지막 원소부터 i번째 인덱스의 원소까지의 모든 원소들을 뒤로 한 칸씩 밀어낸다.
빈 공간이 된 i번째 인덱스에 원소 x를 입력한다.
size 변수를 1 증가시킨다.
<append 메서드 구현>
@Override
public void append(E x) {
if (size>=item.length) throw new IllegalArgumentException();
item[size]=x;
size++;
}
예외처리 - 배열 리스트에 빈 자리가 없을 경우
배열 리스트의 끝에 원소 x를 입력한다.
size 변수를 1 증가시킨다.
<remove 메서드 구현>
@Override
public void remove(int i) {
if (size==0||i>size-1||i<0) throw new IllegalArgumentException();
for (int j=i; j<size-1; j++) {
item[j]=item[j+1];
}
item[size-1]=null;
size--;
}
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우, 배열 리스트가 비어 있을 경우
for문이 입력받은 인덱스부터 마지막 원소의 이전 원소까지 순회하며 해당 원소의 다음 원소로 덮어쓴다.
for문 종료 후 마지막 원소를 null 처리해준다.
size 변수를 1 감소시킨다.
<removeItem 메서드 구현>
@Override
public void removeItem(E x) {
int idx=-1;
for (int i=0; i<size; i++) {
if (item[i].equals(x)) {
idx=i;
break;
}
}
for (int i=idx; i<size-1; i++) {
item[i]=item[i+1];
}
item[size-1]=null;
size--;
}
첫 번째 for문은 배열 리스트의 전체 인덱스를 순회하며 해당 인덱스의 원소가 x와 일치하는지 검사한다.
x와 일치하는 원소가 존재하면 해당 인덱스를 변수 idx에 저장하고 for문을 빠져나온다.
두 번째 for문은 idx부터 마지막 원소의 이전 원소까지 순회하며 해당 원소의 다음 원소로 덮어쓴다.
for문 종료 후 마지막 원소를 null 처리해준다.
size 변수를 1 감소시킨다.
만약 x와 일치하는 원소가 없다면 idx 변수에 -1 이 저장되어 있을 것이므로 인덱스 에러가 발생할 것이다.
<get, set, len, isEmpty 메서드 구현>
@Override
public E get(int i) {
if (i<0||i>size-1||size==0) throw new IllegalArgumentException();
return item[i];
}
@Override
public void set(int i, E x) {
if (i<0||i>size-1) throw new IllegalArgumentException();
item[i]=x;
}
@Override
public int len() {
return size;
}
@Override
public boolean isEmpty() {
return (size==0);
}
get - 입력받은 인덱스에 해당하는 값을 리턴한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우, 배열 리스트가 비어있을 경우
set - 입력받은 인덱스에 해당하는 값을 입력받은 값으로 교체한다.
예외처리 - 입력받은 인덱스가 범위를 벗어나는 경우
size 변수를 리턴한다.
size==0 여부를 리턴한다.
<clear 메서드 구현>
@Override
public void clear() {
item = (E[]) new Object [item.length];
size=0;
}
item 리스트를 기존 길이의 빈 배열 리스트로 덮어쓴다.
size 변수를 초기화한다.
<indexOf 메서드 구현>
@Override
public int IndexOf(E x) {
int idx=-1;
for (int i=0; i<size; i++) {
if (item[i].equals(x)) {
idx=i;
break;
}
}
return idx;
}
for문이 배열 리스트의 전체 인덱스를 순회하며 해당 인덱스의 원소가 x와 일치하는지 검사한다.
x와 일치하는 원소가 존재하면 해당 인덱스를 변수 idx에 저장하고 for문을 빠져나온다.
만약 x와 일치하는 원소가 없다면 idx 변수에 -1 이 저장되어 있을 것이므로 -1을 리턴한다.
<printAll 메서드 구현>
@Override
public void printAll() {
for (int i=0; i<size; i++) {
System.out.print(item[i]+" ");
}
System.out.println();
}
for문이 전체 배열 리스트의 인덱스를 순회하며 해당 인덱스의 값들을 프린트한다.
-전체 코드
public class ArrayList<E> implements ListInterface<E> {
//필드 영역
private E item[];
private int size;
private static final int DEFAULT_CAPACITY = 64;
//생성자 영역
public ArrayList() {
item = (E[]) new Object [DEFAULT_CAPACITY];
size=0;
}
public ArrayList(int n) {
item = (E[]) new Object [n];
size=0;
}
//메서드 영역
@Override
public void add(int i, E x) {
if (size>=item.length||i<0|| i>item.length) throw new IllegalArgumentException();
for (int j=size-1; j>=i; i--) {
item[j+1]=item[j];
}
item[i]=x;
size++;
}
@Override
public void append(E x) {
if (size>=item.length) throw new IllegalArgumentException();
item[size]=x;
size++;
}
@Override
public void remove(int i) {
if (size==0||i>size-1||i<0) throw new IllegalArgumentException();
for (int j=i; j<size-1; j++) {
item[j]=item[j+1];
}
item[size-1]=null;
size--;
}
@Override
public void removeItem(E x) {
int idx=-1;
for (int i=0; i<size; i++) {
if (item[i].equals(x)) {
idx=i;
break;
}
}
for (int i=idx; i<size-1; i++) {
item[i]=item[i+1];
}
item[size-1]=null;
size--;
}
@Override
public E get(int i) {
if (i<0||i>size-1||size==0) throw new IllegalArgumentException();
return item[i];
}
@Override
public void set(int i, E x) {
if (i<0||i>size-1) throw new IllegalArgumentException();
item[i]=x;
}
@Override
public int len() {
return size;
}
@Override
public boolean isEmpty() {
return (size==0);
}
@Override
public void clear() {
item = (E[]) new Object [item.length];
size=0;
}
@Override
public int IndexOf(E x) {
int idx=-1;
for (int i=0; i<size; i++) {
if (item[i].equals(x)) {
idx=i;
break;
}
}
return idx;
}
@Override
public void printAll() {
for (int i=0; i<size; i++) {
System.out.print(item[i]+" ");
}
System.out.println();
}
}
3) 결과 확인하기
<코드>
public class Test {
public static void main(String[] args) {
ArrayList<Integer> lst = new ArrayList<>();
lst.add(0, 100);
lst.add(1, 200);
lst.add(2, 300);
lst.append(400);
lst.printAll();
//100,200,300,400을 차례로 입력하고 출력합니다.
lst.remove(0);
lst.printAll();
lst.removeItem(400);
lst.printAll();
//0번째 인덱스의 값과 내용물이 400인 값을 삭제하고 출력합니다.
lst.IndexOf(300);
//300의 인덱스를 출력합니다.
System.out.println(lst.isEmpty());
System.out.println(lst.len());
//lst가 비었는지 여부와 lst의 길이를 출력합니다.
System.out.println(lst.get(0));
//인덱스 0의 값을 출력합니다.
lst.set(0, 1000);
lst.printAll();
//인덱스 0의 값을 1000으로 바꾸고 출력합니다.
}
}
특정 열을 PRIMARY KEY로 지정하면 자동으로 해당 열에 클러스터형 인덱스가 생성됨.
보조 인덱스
고유 보조 인덱스
특정 열을 UNIQUE 지정하면 자동으로 해당 열에 고유 보조 인덱스가 생성됨. 특정 열에 인덱스를 생성할 때 UNIQUE를 지정하면 고유 보조 인덱스가 생성됨.
단순 보조 인덱스
특정 열에 인덱스를 생성할 때 UNIQUE를 지정하지 않으면 단순 보조 인덱스가 생성됨.
cf) 불필요하게 인덱스를 만들면 데이터베이스의 공간만 낭비하게 될 수도 있음.
2. 자동 지정되는 인덱스 생성하기
1) PRIMARY KEY로 클러스터형 인덱스 생성하기
<코드>
CREATE TABLE table1 (
column1 INT,
column2 INT,
column3 INT
);
ALTER TABLE table1 ADD CONSTRAINT PRIMARY KEY (column1);
SHOW INDEX FROM table1;
<조회화면>
중복 허용하지 않음. (Non_unique false)
NULL값 허용하지 않음. (Null false)
2) UNIQUE 속성으로 고유 보조 인덱스 생성하기
<코드>
CREATE TABLE table2 (
column1 INT PRIMARY KEY,
column2 INT UNIQUE,
column3 INT UNIQUE
);
SHOW INDEX FROM table2;
<조회화면>
중복 허용하지 않음, (Non_unique false)
NULL값 허용 (NULL true)
3. 인덱스 생성하기
CREATE [UNIQUE] INDEX index_name ON table_name (column_name);
ANALYZE TABLE table_name;
-- UNIQUE 붙이면 고유 보조 인덱스, 그렇지 않으면 단순 보조 인덱스.
-- 인덱스 생성 후 ANALYZE TABLE 해줘야 함.
PRIMARY KEY, UNIQUE를 사용하면 자동으로 인덱스가 생성됨.
자동 생성이 아니라 직접 인덱스를 생성하려면 CREATE INDEX 문을 사용해야함.
CREATE INDEX로 생성되는 인덱스는 보조 인덱스임.
CREATE문 끝에 ASC, DESC를 입력하여 오름차순 혹은 내림차순으로 정렬할 수 있음.
DROP INDEX index_name ON table_name; -- 보조 인덱스의 삭제
ALTER TABLE member DROP PRIMARY KEY; -- 클러스터형 인덱스의 삭제
보조 인덱스의 경우 DROP INDEX를 이용하여 삭제함.
클러스터형 인덱스의 경우 ALTER TABLE DROP을 이용하여 삭제함.
PRIMARY KEY의 경우 FOREIGN KEY와 연동되어 있을 시 FOREGIN KEY를 먼저 삭제해줘야 함.
4. 인덱스의 원리
인덱스는 데이터를 트리 형식으로 정리함.
트리의 노드 부분을 페이지 (page) 라고 부름.
페이지는 최소한의 저장 단위로 16Kbyte의 크기를 가짐.
SELECT의 경우 탐색 횟수가 전제 탐색보다 줄어들어 속도가 빨라지지만, INSERT, DELETE, UPDATE는 페이지 분할로 인하여 오히려 성능이 저하될 수 있음.
1) 클러스터형 인덱스의 원리
클러스터형 인덱스 구조
루트페이지부터 리프페이지까지 따라 내려가면서 탐색을 진행함.
리프페이지가 곧 데이터페이지임.
데이터페이지도 인덱스에 포함됨. (정렬 되어 있음.)
2) 보조 인덱스의 원리
보조 인덱스 구조
루트 페이지부터 리프 페이지까지 따라 내려가면서 탐색을 진행함.
리프 페이지에는 데이터마다 페이지번호와 페이지번호 내부 위치가 기록되어 있음.
해당하는 페이지로 이동하여 데이터를 가져옴.
클러스터형 인덱스와 다르게 인덱스가 데이터페이지와 별도의 공간에 만들어짐,
5. 인덱스를 통한 탐색
WHERE을 사용하지 않을 경우 모든 테이블을 탐색함.
WHERE을 사용한 경우 MySQL 프로그램이 인덱스 탐색과 모든 테이블 탐색 중 어느것이 더 나을지 판단해 탐색함.