1. 연결 리스트 기본 구조
연결 리스트는 노드(Node)라는 단위로 구성되며, 각 노드는 데이터와 다음 노드의 주소를 담고 있는 포인터로 구성됩니다. 연결 리스트는 첫 번째 노드부터 순차적으로 링크를 따라가면서 다음 노드로 이동할 수 있습니다. C언어에서는 구조체와 포인터를 활용하여 이러한 연결 구조를 쉽게 구현할 수 있습니다.
- 데이터를 동적으로 관리하는 자료구조
- 각 노드는 데이터와 다음 노드를 가리키는 포인터(링크)로 구성
- [데이터, 링크]의 형태의 노드를 기본 단위로 연결
- 데이터 필드(data field): 표현하려는 값을 저장
- 링크 필드(link field): 다음 노드의 주소를 저장
- 노드의 구조 정의: 자기참조구조 사용
- 노드 생성: malloc()함수 사용
- 노드 삭제: free()함수 사용
typedef struct list_node *list_pointer;
struct list_node {
int data; // 데이터 필드
list_pointer link; // 다음 노드를 가리키는 포인터
};
위 구조에서 list_pointer는 list_node 구조체에 대한 포인터 타입으로, 연결 리스트 노드를 가리키는 데 사용됩니다.
2. 단순 연결 리스트 (Singly Linked List)
단순 연결 리스트는 각 노드가 하나의 링크 필드를 통해 다음 노드와 연결된 형태입니다. 마지막 노드의 링크는 NULL을 가리켜 리스트의 끝을 표시합니다. 노드의 삽입과 삭제가 용이하며, 한 방향으로만 이동이 가능합니다.
1) 단순 연결리스트 노드 삽입
- 기본 구조체 정의
먼저, 연결 리스트의 구조체와 포인터를 정의합니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct list_node *list_pointer;
struct list_node {
int data; // 데이터 필드
list_pointer link; // 다음 노드를 가리키는 포인터
};
- 리스트의 처음에 삽입
리스트의 처음에 새로운 노드를 삽입하려면, 새 노드를 생성하고 기존 첫 번째 노드를 새 노드의 link로 설정한 후, 헤드 포인터를 새 노드로 갱신해야 합니다. (새로운 노드가 리스트의 첫 번째 노드가 되고, 기존의 첫 번째 노드는 이제 두 번째 노드로 바뀜)- 새로운 노드를 동적으로 할당한다.
- 새로운 노드의 링크 필드를 현재 첫 번째 노드의 주소로 설정한다.
- 리스트의 머리 포인터(head)를 새로운 노드로 변경한다.
void insert_first(list_pointer *head, int value) {
list_pointer new_node = (list_pointer)malloc(sizeof(struct list_node));
new_node->data = value; // new_node를 생성하여 메모리를 할당받고, 데이터 필드에 value를 저장한다.
new_node->link = *head; // new_node->link를 현재 headr가 가리키고 있는 첫 번째 노드로 설정한다.
*head = new_node; // head를 new_node로 갱신하여 새 노드를 리스트의 처음에 추가한다.
}
- 리스트의 중간에 삽입
리스트의 중간에 삽입하려면, 삽입하려는 위치 바로 이전의 노드를 찾아서 그 노드의 link를 새 노드로 가리키도록 설정하고, 새 노도의 link는 삽입 위치의 다음 노드를 가리키도록 설정합니다.- 삽입할 위치 확인
- 새 노드의 링크를 이전 노드의 다음 노드로 설정
- 이전 노드의 링크를 새 노드로 연결
void insert_after(list_pointer prev, int value) {
//prev는 새 노드를 삽입하고자 하는 위치의 이전 노드를 가리키는 포인터
if (prev == NULL) {
printf("이전 노드가 NULL입니다. 삽입할 수 없습니다.\n");
return;
}
list_pointer new_node = (list_pointer)malloc(sizeof(struct list_node)); //new_node 생성
new_node->data = value; //value를 데이터로 설정
new_node->link = prev->link; // new_node->link를 prev->link로 설정하여, 새 노드가 이전 노드의 다음 노드를 가리키게 만든다.
prev->link = new_node; // prev->link를 new_node로 갱신하여 새 노드가 이전 노드 다음에 위치하게 한다.
}
- 리스트의 끝에 삽입
리스트의 끝에 노드를 삽입하려면, 리스트의 마지막 노드를 찾아서 그 노드의 link를 새 노드로 설정하고, 새 노드의 link를 NULL로 설정하여 끝임을 표시합니다.
- 리스트의 끝까지 이동하여 마지막 노드를 찾음
- 새로운 노드를 동적으로 생성하고 데이터 필드를 설정
- 마지막 노드의 링크 필드를 새로운 노드를 가리키도록 설정
- 새로운 노드의 링크 필드는 NULL로 설정하여 리스트의 끝임을 나타냄
void insert_last(list_pointer *head, int value) {
list_pointer new_node = (list_pointer)malloc(sizeof(struct list_node)); //new_node를 생성하고
new_node->data = value; //value를 데이터로 저장하며
new_node->link = NULL; //link를 NULL로 설정하여 리스트의 끝임을 나타낸다.
if (*head == NULL) {
*head = new_node; // 리스트가 비어 있으면 (*head가 NULL), head를 new_node로 설정하여 새 노드를 첫 번째 노드로 삽입
} else {
list_pointer temp = *head;
while (temp->link != NULL) {
temp = temp->link; // 마지막 노드를 찾음
}
temp->link = new_node; // 마지막 노드의 링크를 새 노드로 연결
//그렇지 않으면, temp를 사용하여 리스트의 마지막 노드까지 이동한 후,
//마지막 노드의 link를 new_node로 설정하여 새 노드를 끝에 추가한다.
}
}
단순 연결리스트 전체 코드 예제
아래는 리스트의 처음, 중간, 끝에 노드를 삽입하고 출력하는 예제 프로그램입니다.
int main() {
list_pointer head = NULL; // 초기에는 리스트가 비어 있음
// 리스트의 처음에 노드 삽입
insert_first(&head, 10);
insert_first(&head, 20);
// 리스트의 끝에 노드 삽입
insert_last(&head, 30);
// 중간에 노드 삽입 (두 번째 노드 뒤에 삽입)
insert_after(head->link, 25);
// 리스트 출력
print_list(head); // 예: "The list contains: 20 10 25 30"
return 0;
}
2) 단순 연결리스트 노드 삭제
단순 연결 리스트에서 노드를 삭제하기 위해서는 특정 위치의 노드를 찾고, 그 노드를 연결에서 제거하여 이전 노드와 다음 노드를 연결하는 방식으로 이루어집니다. 이때, 삭제된 노드의 메모리는 free( )를 통해 해제해줍니다.
- 첫 번째 노드를 삭제할 경우
- 첫 번째 노드의 주소를 임시 포인터에 저장
- 헤드 포인터를 두 번째 노드로 이동
- 임시 포인터가 가리키는 첫 번째 노드를 메모리에서 해제
(삭제할 노드의 메모리를 해제: 메모리 누수를 방지)
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data; // 데이터 필드
struct Node* next; // 다음 노드를 가리키는 포인터
} Node;
typedef Node* NodePointer;
// 첫 번째 노드 삭제 함수
void delete_first_node(NodePointer *head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
NodePointer temp = *head; // 첫 번째 노드를 임시로 저장
*head = (*head)->next; // 헤드를 두 번째 노드로 이동
free(temp); // 첫 번째 노드 메모리 해제
printf("첫 번째 노드가 삭제되었습니다.\n");
}
// 리스트 출력 함수
void print_list(NodePointer head) {
while (head != NULL) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
int main() {
NodePointer head = (NodePointer)malloc(sizeof(Node));
head->data = 10;
head->next = (NodePointer)malloc(sizeof(Node));
head->next->data = 20;
head->next->next = (NodePointer)malloc(sizeof(Node));
head->next->next->data = 30;
head->next->next->next = NULL;
printf("삭제 전 리스트: ");
print_list(head);
// 첫 번째 노드 삭제
delete_first_node(&head);
printf("삭제 후 리스트: ");
print_list(head);
// 남은 노드 메모리 해제
while (head != NULL) {
NodePointer temp = head;
head = head->next;
free(temp);
}
return 0;
}
삭제 전 리스트: 10 -> 20 -> 30 -> NULL
첫 번째 노드가 삭제되었습니다.
삭제 후 리스트: 20 -> 30 -> NULL
- 리스트의 중간 부분에 삭제
- 삭제할 노드의 이전 노드를 찾기
- 중간에 있는 노드를 삭제하기 위해서는 삭제할 노드의 이전 노드를 알아야 합니다. 이전 노드의 next 필드를 삭제할 노드의 next로 변경하여 리스트에서 제거합니다.
- 이전 노드의 next 필드를 삭제할 노드의 next로 변경
- 삭제할 노드의 다음 노드를 연결하여, 중간에 있는 노드를 리스트에서 제외합니다.
- 삭제할 노드의 메모리를 해제
- 삭제할 노드가 더 이상 리스트와 연결되어 있지 않으므로 free() 함수를 사용하여 메모리를 해제합니다.
- 삭제할 노드의 이전 노드를 찾기
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef Node* NodePointer;
// 중간 노드 삭제 함수 (특정 값을 가진 노드 삭제)
void delete_node(NodePointer *head, int target) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
NodePointer temp = *head;
NodePointer prev = NULL;
// 첫 번째 노드가 삭제 대상인 경우
if (temp != NULL && temp->data == target) {
*head = temp->next; // 헤드를 두 번째 노드로 변경
free(temp); // 첫 번째 노드 삭제
printf("노드 %d가 삭제되었습니다.\n", target);
return;
}
// 삭제할 노드 찾기
while (temp != NULL && temp->data != target) {
prev = temp;
temp = temp->next;
}
// 대상 노드가 없는 경우
if (temp == NULL) {
printf("노드 %d를 찾을 수 없습니다.\n", target);
return;
}
// 이전 노드의 next를 삭제할 노드의 next로 변경
prev->next = temp->next;
free(temp); // 대상 노드 삭제
printf("노드 %d가 삭제되었습니다.\n", target);
}
// 리스트 출력 함수
void print_list(NodePointer head) {
printf("리스트: ");
while (head != NULL) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
int main() {
NodePointer head = (NodePointer)malloc(sizeof(Node));
head->data = 10;
head->next = (NodePointer)malloc(sizeof(Node));
head->next->data = 20;
head->next->next = (NodePointer)malloc(sizeof(Node));
head->next->next->data = 30;
head->next->next->next = (NodePointer)malloc(sizeof(Node));
head->next->next->next->data = 40;
head->next->next->next->next = NULL;
printf("삭제 전 리스트:\n");
print_list(head);
// 중간에 있는 노드 삭제 (예: 값이 20인 노드)
delete_node(&head, 20);
printf("노드 20 삭제 후 리스트:\n");
print_list(head);
// 마지막 노드 삭제 (예: 값이 40인 노드)
delete_node(&head, 40);
printf("노드 40 삭제 후 리스트:\n");
print_list(head);
// 첫 번째 노드 삭제 (예: 값이 10인 노드)
delete_node(&head, 10);
printf("노드 10 삭제 후 리스트:\n");
print_list(head);
// 남은 노드 메모리 해제
while (head != NULL) {
NodePointer temp = head;
head = head->next;
free(temp);
}
return 0;
}
삭제 전 리스트:
리스트: 10 -> 20 -> 30 -> 40 -> NULL
노드 20가 삭제되었습니다.
노드 20 삭제 후 리스트:
리스트: 10 -> 30 -> 40 -> NULL
노드 40가 삭제되었습니다.
노드 40 삭제 후 리스트:
리스트: 10 -> 30 -> NULL
노드 10가 삭제되었습니다.
노드 10 삭제 후 리스트:
리스트: 30 -> NULL
- 맨 뒤의 노드를 삭제
- 리스트가 비어 있는지 확인
- 리스트가 비어 있으면(즉, head가 NULL이면) 아무 작업도 할 필요가 없습니다.
- 리스트에 노드가 하나뿐인지 확인
- head 노드가 곧 삭제할 노드라면, 리스트가 하나의 노드로만 구성되어 있다는 뜻입니다. 이 경우 head를 NULL로 설정하고 해당 노드를 삭제합니다.
- 리스트에 두 개 이상의 노드가 있는 경우
- 마지막 노드 이전의 노드(즉, 두 번째 마지막 노드)를 찾아서 해당 노드의 next 필드를 NULL로 설정합니다.
- 이후 마지막 노드의 메모리를 해제합니다.
- 리스트가 비어 있는지 확인
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* next;
} Node;
typedef Node* NodePointer;
// 맨 뒤 노드 삭제 함수
void delete_last_node(NodePointer *head) {
if (*head == NULL) {
printf("리스트가 비어 있습니다.\n");
return;
}
// 리스트에 노드가 하나만 있는 경우
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
printf("리스트에 있던 유일한 노드를 삭제했습니다.\n");
return;
}
// 리스트에 두 개 이상의 노드가 있는 경우
NodePointer temp = *head;
while (temp->next->next != NULL) { // 마지막에서 두 번째 노드까지 이동
temp = temp->next;
}
free(temp->next); // 마지막 노드 삭제
temp->next = NULL; // 마지막 노드의 이전 노드의 링크를 NULL로 설정
printf("마지막 노드를 삭제했습니다.\n");
}
// 리스트 출력 함수
void print_list(NodePointer head) {
printf("리스트: ");
while (head != NULL) {
printf("%d -> ", head->data);
head = head->next;
}
printf("NULL\n");
}
int main() {
NodePointer head = (NodePointer)malloc(sizeof(Node));
head->data = 10;
head->next = (NodePointer)malloc(sizeof(Node));
head->next->data = 20;
head->next->next = (NodePointer)malloc(sizeof(Node));
head->next->next->data = 30;
head->next->next->next = NULL;
printf("삭제 전 리스트:\n");
print_list(head);
// 마지막 노드 삭제
delete_last_node(&head);
printf("마지막 노드 삭제 후 리스트:\n");
print_list(head);
// 다시 마지막 노드 삭제
delete_last_node(&head);
printf("마지막 노드 삭제 후 리스트:\n");
print_list(head);
// 모든 노드 삭제
delete_last_node(&head);
printf("모든 노드를 삭제한 후 리스트:\n");
print_list(head);
return 0;
}
삭제 전 리스트:
리스트: 10 -> 20 -> 30 -> NULL
마지막 노드를 삭제했습니다.
마지막 노드 삭제 후 리스트:
리스트: 10 -> 20 -> NULL
마지막 노드를 삭제했습니다.
마지막 노드 삭제 후 리스트:
리스트: 10 -> NULL
리스트에 있던 유일한 노드를 삭제했습니다.
모든 노드를 삭제한 후 리스트:
리스트: NULL
3. 원형 연결 리스트 (Circular Linked List)
원형 연결 리스트는 마지막 노드가 다시 첫 번째 노드를 가리키도록 연결되어 원형 형태를 이룹니다. 이러한 구조 덕분에 리스트의 끝에서 다시 처음으로 돌아갈 수 있어, 순환형 자료를 구현할 때 유리합니다. 다만, 원형 연결 리스트는 연결을 관리하기 복잡하며, 탐색 시 끝을 표시하는 NULL이 없어 주의가 필요합니다.
- 마지막 노드의 링크가 첫 번째 노드(head node)를 가리키는 리스트
- 한 노드에서 다른 모든 노드로의 접근이 가능
- 검색할 때 무한루프에 빠지지 않도록 검색종료 시킬 조건을 정해야 함
- 단점: 앞에 삽입하려면, 맨 뒤 원소까지 찾아가야 함
- 보완: 시작노드를 맨 뒤 노드로 설정 -> 맨 앞에 삽입 시 편리하다.
원형 연결 리스트 생성 예시
void make_circular_list(list_pointer *head, int data) {
list_pointer temp = (list_pointer) malloc(sizeof(struct list_node));
temp->data = data;
if (*head == NULL) {
*head = temp;
temp->link = *head;
} else {
list_pointer last = *head;
while (last->link != *head) {
last = last->link;
}
last->link = temp;
temp->link = *head;
}
}
위 함수는 원형 연결 리스트의 마지막 노드를 첫 번째 노드로 다시 연결하여 순환 구조를 만듭니다.
4. 이중 연결 리스트 (Doubly Linked List)
이중 연결 리스트는 각 노드가 이전 노드와 다음 노드를 가리키는 두 개의 포인터 필드를 가진 구조입니다. 양방향 탐색이 가능하며, 중간 노드의 삽입 및 삭제 시 더 빠르게 작업할 수 있습니다.
- 헤드노드를 사용하여 연산을 쉽게 한다.
- 헤드노드: 아무 정보 없이 프로그래밍을 편리하게하기 위해 존재하며 맨 앞 노드보다 앞에, 맨 뒤 노드보다 뒤에 있다고 가정
- 원형 구조일 수도 있고 아닐 수도 있음
- 각 노드는 적어도 세 개의 필드를 가짐
- 왼쪽 링크 필드(llink), 데이터 필드(item), 오른쪽 링크 필드(rlink)
typedef struct d_node {
int data; // 데이터 필드
struct d_node *prev; // 이전 노드를 가리키는 포인터
struct d_node *next; // 다음 노드를 가리키는 포인터
} d_node;
1) 이중 연결 리스트에서 노드 삽입
이중 연결 리스트에 노드를 삽입하는 경우, 위치에 따라 다음과 같은 경우로 나뉩니다.
- 리스트의 맨 앞에 삽입
맨 앞에 새로운 노드를 삽입하는 과정은 다음과 같습니다.
void insert_front(d_node **head, int data) {
d_node *new_node = (d_node *)malloc(sizeof(d_node));
new_node->data = data;
new_node->prev = NULL;
new_node->next = *head;
if (*head != NULL) {
(*head)->prev = new_node;
}
*head = new_node;
}
- 설명:
- new_node를 생성하고 data를 저장합니다.
- 새 노드의 next를 현재의 head로 설정하고, prev는 NULL로 설정합니다.
- 기존 첫 번째 노드의 prev를 new_node로 변경합니다(기존 head가 있을 경우).
- 마지막으로 head를 new_node로 갱신합니다.
- 리스트의 특정 위치에 삽입
리스트의 중간에 새로운 노드를 삽입하는 과정입니다.
void insert_after(d_node *node, int data) {
if (node == NULL) return;
d_node *new_node = (d_node *)malloc(sizeof(d_node));
new_node->data = data;
new_node->next = node->next;
new_node->prev = node;
if (node->next != NULL) {
node->next->prev = new_node;
}
node->next = new_node;
}
- 설명:
- node의 다음에 new_node를 삽입합니다.
- new_node->next는 node->next를 가리키고, new_node->prev는 node를 가리킵니다.
- 기존 node->next가 있으면, 그 노드의 prev를 new_node로 연결합니다.
- 마지막으로 node->next를 new_node로 설정하여 연결을 완성합니다.
2) 이중 연결 리스트에서 노드 삭제
이중 연결 리스트에서 노드를 삭제할 때는 연결을 끊어주고, 메모리를 해제하는 작업이 필요합니다.
- 특정 노드를 삭제
삭제할 노드 node가 주어졌을 때, 해당 노드를 삭제하는 함수입니다.
void delete_node(d_node **head, d_node *node) {
if (*head == NULL || node == NULL) return;
if (*head == node) {
*head = node->next;
}
if (node->next != NULL) {
node->next->prev = node->prev;
}
if (node->prev != NULL) {
node->prev->next = node->next;
}
free(node);
}
- 설명:
- node가 head라면, head를 node->next로 변경합니다.
- node->next가 NULL이 아니면, node->next->prev를 node->prev로 설정하여 node를 건너뛰도록 합니다.
- node->prev가 NULL이 아니면, node->prev->next를 node->next로 설정하여 node를 건너뛰도록 합니다.
- 마지막으로 node의 메모리를 해제합니다.
- 리스트의 맨 뒤 노드를 삭제
맨 마지막 노드를 삭제하는 함수입니다.
void delete_last(d_node **head) {
if (*head == NULL) return;
d_node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
if (temp->prev != NULL) {
temp->prev->next = NULL;
} else {
*head = NULL; // 리스트에 노드가 하나만 있는 경우
}
free(temp);
}
- 설명:
- temp를 통해 마지막 노드까지 이동합니다(temp->next가 NULL인 노드).
- 마지막 노드의 prev가 NULL이 아니라면, temp->prev->next를 NULL로 설정하여 temp를 리스트에서 제거합니다.
- 리스트에 노드가 하나뿐이라면 *head를 NULL로 설정합니다.
- 마지막으로 temp의 메모리를 해제하여 노드를 삭제합니다.
예제: 이중 연결 리스트 사용
다음은 이중 연결 리스트를 생성하고, 노드를 삽입 및 삭제하는 예제입니다.
#include <stdio.h>
#include <stdlib.h>
typedef struct d_node {
int data;
struct d_node *prev;
struct d_node *next;
} d_node;
// 함수 선언
void insert_front(d_node **head, int data);
void insert_after(d_node *node, int data);
void delete_node(d_node **head, d_node *node);
void delete_last(d_node **head);
void print_list(d_node *head);
int main() {
d_node *head = NULL;
insert_front(&head, 10);
insert_front(&head, 20);
insert_front(&head, 30); // 리스트: 30 -> 20 -> 10
printf("리스트 초기 상태: ");
print_list(head);
delete_last(&head); // 맨 마지막 노드 삭제
printf("마지막 노드 삭제 후: ");
print_list(head);
delete_node(&head, head->next); // 중간 노드(20) 삭제
printf("중간 노드 삭제 후: ");
print_list(head);
return 0;
}
// 각 함수의 구현은 위 설명과 동일합니다.
5. 연결 리스트 기본 기능 구현
연결 리스트의 주요 기능으로는 노드 생성, 노드 삽입, 노드 삭제, 리스트 출력이 있습니다.
노드 생성 예제
list_pointer make_node(int data) {
list_pointer new_node = (list_pointer) malloc(sizeof(struct list_node));
new_node->data = data;
new_node->link = NULL;
return new_node;
}
리스트 출력 예제
void print_list(list_pointer ptr) {
printf("The list contains: ");
while (ptr != NULL) {
printf("%4d", ptr->data);
ptr = ptr->link;
}
printf("\n");
}
노드 합계와 개수 계산
연결 리스트의 모든 노드를 탐색하며 합계와 개수를 계산할 수 있습니다.
int nodesum(list_pointer ptr) {
int sum = 0;
while (ptr != NULL) {
sum += ptr->data;
ptr = ptr->link;
}
return sum;
}
int nodenumber(list_pointer ptr) {
int count = 0;
while (ptr != NULL) {
count++;
ptr = ptr->link;
}
return count;
}
6. 연결 리스트 실습 예제 프로그램
아래 프로그램은 연결 리스트를 생성하고, 리스트의 데이터를 출력하며, 노드의 합계와 개수를 계산하는 코드입니다.
int main() {
list_pointer ptr;
// 연결 리스트 생성
ptr = make_node(100);
ptr->link = make_node(200);
ptr->link->link = make_node(300);
// 연결 리스트 출력
print_list(ptr);
// 노드 합계와 개수 출력
printf("list안의 data 합: %d\n", nodesum(ptr));
printf("list안의 node 수: %d\n", nodenumber(ptr));
return 0;
}
이 프로그램은 연결 리스트의 생성, 출력, 합계 계산 및 노드 수를 세는 과정을 통해 연결 리스트의 기본 동작을 연습할 수 있도록 구성되어 있습니다.
'IT개발및프로그래밍 > 자료구조' 카테고리의 다른 글
포인터와 연결 리스트: 동적 자료 구조의 핵심 이해하기 (0) | 2024.10.21 |
---|---|
C 언어의 배열과 구조체: 데이터 관리의 효율성 (0) | 2024.10.21 |
C 프로그래밍 기초: 명령어 제어 구조 이해하기 (0) | 2024.10.21 |
프로그래밍 기초와 프로그램 번역 과정 (0) | 2024.10.21 |
프로그램과 알고리즘: 문제 해결을 위한 단계적 절차 (0) | 2024.10.21 |