データ構造 - 双方向リンクリスト
双方向リンクリスト(Doubly Linked List)は単方向リンクリストを拡張したもので、各ノードは次のノードを指すだけでなく、前のノードを指すポインタも追加で保持し、双方向に到達可能なチェーン構造を形成します。
双方向リンクリストの構造と特徴
双方向リンクリスト — ノード構造と双方向走査
→→
next ポインタ方向 (順方向走査)
prevNULL
data10
next→
prev←
data20
next→
prev←
data30
nextNULL
←←
prev ポインタ方向 (逆方向走査)
struct Node { int data; struct Node* prev; struct Node* next; };
prev は前のノードを指す | data はデータを格納 | next は後ろのノードを指す
中核的な利点:任意のノードを直接削除でき(前のノードを保存する必要がない)、双方向走査をサポート
青 = next ポインタ(順方向) | 紫 = prev ポインタ(逆方向) | 各ノード = prev + data + next
上の図のように、双方向リンクリストの各ノードはprevとnext2つのポインタを持ちます。
C言語では、双方向リンクリストのノードは次のように定義されます:
例
struct Node {
int data; /* データ領域 */
struct Node* prev; /* 前のノードを指すポインタ */
struct Node* next; /* 後ろのノードを指すポインタ */
};
int data; /* データ領域 */
struct Node* prev; /* 前のノードを指すポインタ */
struct Node* next; /* 後ろのノードを指すポインタ */
};
単方向リンクリストと比較して、双方向リンクリストの最大の利点は、任意のノードからその前のノードと後ろのノードに直接アクセスできることであり、これにより削除操作で前のノードの参照を追加で保存する必要がなくなります。
単方向リンクリストでは、指定したノードを削除する際に、その前のノードを知っている必要があり、それによってポインタ調整を完了できます。双方向リンクリストでは、各ノード自体が prev ポインタを保持しているため、削除操作を直接完了できます。
基本操作
挿入操作
双方向リンクリストの挿入では、新しいノードの prev/next ポインタ、およびその前のノードと後ろのノードのポインタを同時に調整する必要があり、合計4回のポインタ調整が必要です。
例
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node* prev;
struct Node* next;
};
/* 新しいノードを作成 */
struct Node* createNode(int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = value;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
/* 先頭にノードを挿入 手順:新しいノードの next が元の先頭ノードを指し、元の先頭ノードの prev が新しいノードを指す */
/* 新しいノードが新しい先頭ノードになる */
struct Node* insertAtHead(struct Node* head, int value) {
struct Node* newNode = createNode(value);
if (head != NULL) {
newNode->next = head;
head->prev = newNode;
}
return newNode; /* 指定したノードの後に新しいノードを挿入 4つのポインタを操作する必要があります: 1-2: 新しいノードの prev と next 3: 後ろのノードの prev(存在する場合) 4: 前のノードの next */
}
/* 新しいノードの next が元の後ろのノードを指す */
/* 新しいノードの prev が前のノードを指す */
/* 元の後ろのノードの prev が新しいノードを指す */
/* 前のノードの next が新しいノードを指す */
削除操作
void insertAfter(struct Node* prevNode, int value) {
if (prevNode == NULL) return;
struct Node* newNode = createNode(value);
newNode->next = prevNode->next; 例
newNode->prev = prevNode; /* 指定したノードを削除(前のノードを知る必要がない。これが双方向リンクリストの最大の利点) 手順:前後のノードを相互に接続し、現在のノードをスキップして、現在のノードを解放する */
if (prevNode->next != NULL) {
prevNode->next->prev = newNode; /* 削除するのが先頭ノードの場合、先頭ポインタを更新 */
}
prevNode->next = newNode; /* 前のノードの next を後ろのノードに向ける */
}
#include <stdlib.h>
struct Node {
int data;
struct Node* prev;
struct Node* next;
};
/* 新しいノードを作成 */
struct Node* createNode(int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = value;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
/* 先頭にノードを挿入 手順:新しいノードの next が元の先頭ノードを指し、元の先頭ノードの prev が新しいノードを指す */
/* 新しいノードが新しい先頭ノードになる */
struct Node* insertAtHead(struct Node* head, int value) {
struct Node* newNode = createNode(value);
if (head != NULL) {
newNode->next = head;
head->prev = newNode;
}
return newNode; /* 指定したノードの後に新しいノードを挿入 4つのポインタを操作する必要があります: 1-2: 新しいノードの prev と next 3: 後ろのノードの prev(存在する場合) 4: 前のノードの next */
}
/* 新しいノードの next が元の後ろのノードを指す */
/* 新しいノードの prev が前のノードを指す */
/* 元の後ろのノードの prev が新しいノードを指す */
/* 前のノードの next が新しいノードを指す */
削除操作
void insertAfter(struct Node* prevNode, int value) {
if (prevNode == NULL) return;
struct Node* newNode = createNode(value);
newNode->next = prevNode->next; 例
newNode->prev = prevNode; /* 指定したノードを削除(前のノードを知る必要がない。これが双方向リンクリストの最大の利点) 手順:前後のノードを相互に接続し、現在のノードをスキップして、現在のノードを解放する */
if (prevNode->next != NULL) {
prevNode->next->prev = newNode; /* 削除するのが先頭ノードの場合、先頭ポインタを更新 */
}
prevNode->next = newNode; /* 前のノードの next を後ろのノードに向ける */
}
削除操作
例
走査操作(双方向)
双方向リンクリストは、順方向と逆方向の2つの走査方法をサポートします。
void deleteNode(struct Node** headRef, struct Node* del) {
if (*headRef == NULL || del == NULL) return;
例
if (*headRef == del) {
*headRef = del->next;
}
/* 順方向走査:head から始めて、next 方向に進む */
if (del->prev != NULL) {
del->prev->next = del->next;
}
"順方向走査: "
if (del->next != NULL) {
del->next->prev = del->prev;
}
free(del); /* 逆方向走査:tail から始めて、prev 方向に進む まず末尾ノードを見つける必要がある */
}
双方向リンクリストは、順方向と逆方向の2つの走査方法をサポートします。
void deleteNode(struct Node** headRef, struct Node* del) {
if (*headRef == NULL || del == NULL) return;
例
if (*headRef == del) {
*headRef = del->next;
}
/* 順方向走査:head から始めて、next 方向に進む */
if (del->prev != NULL) {
del->prev->next = del->next;
}
"順方向走査: "
if (del->next != NULL) {
del->next->prev = del->prev;
}
free(del); /* 逆方向走査:tail から始めて、prev 方向に進む まず末尾ノードを見つける必要がある */
}
走査操作(双方向)
/* 末尾から prev 方向に沿って先頭に戻る */
例
#include <stdio.h>
/* リンクリストを構築: 10 <-> 20 <-> 30 */
void traverseForward(struct Node* head) {
printf(/* 出力: 順方向走査: 10 20 30 */);
struct Node* cur = head;
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
/* 出力: 逆方向走査: 30 20 10 */
双方向リンクリスト vs 単方向リンクリスト
void traverseBackward(struct Node* head) {
if (head == NULL) return;
比較項目
struct Node* cur = head;
while (cur->next != NULL) {
cur = cur->next;
}
単方向リンクリスト
printf(双方向リンクリスト);
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->prev;
}
printf("\n");
}
int main() {
struct Node* head = NULL;
ノード構造
head = insertAtHead(head, 30);
head = insertAtHead(head, 20);
head = insertAtHead(head, 10);
traverseForward(head); ノードあたりの追加メモリ
traverseBackward(head); 8 バイト(64ビットシステム)
return 0;
}
/* リンクリストを構築: 10 <-> 20 <-> 30 */
void traverseForward(struct Node* head) {
printf(/* 出力: 順方向走査: 10 20 30 */);
struct Node* cur = head;
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
/* 出力: 逆方向走査: 30 20 10 */
双方向リンクリスト vs 単方向リンクリスト
void traverseBackward(struct Node* head) {
if (head == NULL) return;
比較項目
struct Node* cur = head;
while (cur->next != NULL) {
cur = cur->next;
}
単方向リンクリスト
printf(双方向リンクリスト);
while (cur != NULL) {
printf("%d ", cur->data);
cur = cur->prev;
}
printf("\n");
}
int main() {
struct Node* head = NULL;
ノード構造
head = insertAtHead(head, 30);
head = insertAtHead(head, 20);
head = insertAtHead(head, 10);
traverseForward(head); ノードあたりの追加メモリ
traverseBackward(head); 8 バイト(64ビットシステム)
return 0;
}
双方向リンクリスト vs 単方向リンクリスト
| 順方向走査 | 対応 | 対応 |
|---|---|---|
| 逆方向走査 | data + next | data + prev + next |
| 非対応 | 対応 | ノードの削除 |
| 前のノードを知る必要がある | 前のノードは不要、直接削除可能 | 挿入操作 |
| 1〜2 個のポインタを調整 | 4 個のポインタを調整 | その他の拡張 |
| AI 思考中... | データ構造 – リンクリスト | データ構造 – 循環リンクリスト |
| ノート一覧 | クリックしてノートを共有 | ノートは本記事の内容を拡張したものである必要があります! ノートを共有する前にログインが必要です! |