【データ構造 - C言語】双方向先行循環連結リストの実現

目次

1. 双方向先行循環リンクリストの導入

2. 双方向先行循環リンクリストのインターフェース

3. インターフェースの実装

3.1 ノードを開く

3.2 返されたリンクリストのヘッドノードを作成する

3.3 リンクされたリストが空かどうかを判断する

3.4 印刷

3.5 二重リンクリスト検索

3.6 二重リンクリストは pos の前に挿入されます

3.6.1 プラグ

3.6.2 後部プラグ

3.6.3 先頭挿入と末尾挿入の記述方法を更新

3.7 二重リンクリストは位置 pos のノードを削除します

3.7.1 ヘッダーの削除

3.7.2 末尾の削除

3.7.3 更新の先頭と末尾の削除

3.8 二重リンクリストの破壊

4. 完全なコード

5. 機能テスト


1. 双方向先行循環リンクリストの導入

このトピックを分割して、双方向、リーディング、ループという 3 つのキーワードを抽出します。まずは次の 3 つのキーワードから始めましょう。

1 つ目は双方向です。双方向とは、このノードがその先行者と後続者を見つけることができることを意味します。これは、単一リンクされたリストとは本質的に異なります。

2 番目はリードです: リードは、リンク リストにヘッド ノードがあることを示します。このノードは、センチネル位置のヘッド ノードとも呼ばれます。このノードは、データ フィールドがランダムな値で満たされることを除いて、他のノードと同じです。 (リンクされたリストの長さを保存するものもあります);

最後はループです。ループはその構造がリングであることを示し、先頭ノードの先行ノードには末尾ノードが格納され、末尾ノードの後続ノードには先頭ノードが格納されます。

2. 双方向先行循環リンクリストのインターフェース

双方向先行循環リンクリストの機能は単一リンクリストと同じで、どちらも追加、削除、確認、変更が可能ですが、双方向先行循環リンクリストの特性により効率が大幅に向上します。このような関数を書くとき。

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

// 创建返回链表的头结点
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

3. インターフェースの実装

3.1 ノードを開く

私たちのリンク リストは動的リンク リストであるため、ノードを挿入するたびにノードを malloc する必要があり、その後のコードの再利用を容易にするためにノードを関数にカプセル化します。

ListNode* BuyListNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (NULL == newnode)
	{
		perror("malloc fail:");
		return NULL;
	}
	
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

3.2 返されたリンクリストのヘッドノードを作成する

このステップでは、リンク リストのセンチネル ヘッド ノードを作成します。これは双方向の先行循環リンク リストであるため、prev ポインターと next ポインターの両方が自分自身を指すようにします。そのため、ヘッド ノードが 1 つしかない場合でも、双方向の循環構造になります。

ListNode* ListCreate()
{
	ListNode* phead = BuyListNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

3.3 リンクされたリストが空かどうかを判断する

このステップは、以降の削除のためにカプセル化する空判定関数ですが、リンクリストが存在する場合、リンクリストが空である可能性を排除できないため、以降のインターフェースを再利用できるようにこの関数をカプセル化します。

bool ListEmpty(ListNode* pHead)
{
	assert(pHead);

	return pHead->next == pHead;
}

3.4 印刷

印刷関数についてはすでによく理解していますが、両方向先行循環リンクリストの印刷終了条件に注意してください。ここでは、以前の cur == NULL ではなく、 cur != pHead になっています。cur が pHead に到達したときリンクされたリスト全体が消えています。

void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;

	printf("guard");
	while (cur != pHead)
	{
		printf("<==>%d", cur->data);
		cur = cur->next;
	}
	printf("<==>\n");
}

3.5 二重リンクリスト検索

見つけるのは難しくありませんが、ここでは、双方向の先頭リンク リストの最初のノードがセンチネル ノードであるため、センチネルの次のノードから検索をたどる必要があることに注意する必要があります (cur = pHead-> next)、ループの終わり 条件は依然として cur != pHead です。

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;

	while (cur != pHead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;
}

3.6 二重リンクリストは pos の前に挿入されます

pos 位置を見つけたら、それを pos 位置の前に挿入し、次のプロセスに従って実行します。

1. data4 を data3 の前に挿入したい場合は、まず構造体ポインター変数 prev を定義し、最初に data3->prev (data2) ノードを prev 変数に保存します。

2. 次に、prev->next を data4 に変更し、さらに data4->prev を prev に変更します。

3. 最後に、data4->next = data3 を設定し、次に data3->prev = data4 を設定します。

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);
	
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

3.6.1 プラグ

ヘッドの挿入については、一貫して pos 位置の前に挿入し、最初のノードを保存してから挿入します。

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newnode = BuyListNode(x);
	ListNode* first = pHead->next;//多写这一个变量下面的插入语句就可以无序写

	pHead->next = newnode;
	newnode->next = first;
	newnode->prev = pHead;
	first->prev = newnode;
}

3.6.2 後部プラグ

末尾の挿入の場合は、まずリンク リストの末尾ノードを保存してから、それを挿入します。これは、pos 位置の前に挿入するのと同じです。

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* tail = pHead->prev;
	ListNode* newnode = BuyListNode(x);

	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = pHead;
	pHead->prev = newnode;
}

3.6.3 先頭挿入と末尾挿入の記述方法を更新

挿入コードを比較すると、先頭挿入と末尾挿入のロジックは pos 位置の前に挿入したコードと同じであることがわかり、実装されている関数も基本的には変わっていないため、挿入時に ListInsert を直接再利用できます。頭と尻尾のインターフェースが実現できます。

1>プラグ

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead->next, x);
}

2> テールプラグ

これは pos 位置の前に挿入されるため、リンク リストは双方向のサイクルとなり、渡される最初のパラメータは pHead であり、センチネル ノードの前のノードが末尾ノードになります。

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListInsert(pHead, x);
}

3.7 二重リンクリストは位置 pos のノードを削除します

pos ノードを見つけたら、pos ノードを削除し、次のプロセスを実行します。

1. 2 つの構造体ポインタ変数 posPrev および posNext を定義し、pos 位置の前後のノードを posPrev および posNext ポインタ変数にそれぞれ格納します。

2. posPrev->next = posNext を設定し、次に posNext->prev = posPrev を設定します。

3. pos ノード free(pos) を解放します。

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* posPrev = pos->prev;
	ListNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
}

3.7.1 ヘッダーの削除

ヘッドが削除された場合は、まず pHead->next ノードと pHead->next->next ノードを保存し、センチネル ノードをヘッド ノードの次のノードに接続してから、ヘッド ノードを解放します。

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* first = pHead->next;
	ListNode* second = first->next;

	pHead->next = second;
	first->prev = pHead;
	free(first);
}

3.7.2 末尾の削除

テールが削除された場合は、まず pHead->prev ノードと pHead->prev->prev ノードを保存し、センチネル ノードをテール ノードの前のノードに接続してから、テール ノードを解放します。

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* tail = pHead->prev;
	ListNode* tailPrev = tail->prev;

	tailPrev->next = pHead;
	pHead->prev = tailPrev;
    free(tail);
}

要約:先頭と末尾を削除する前に、リンクされたリストが空かどうかを判断する必要があります。

3.7.3 更新の先頭と末尾の削除

削除コードを比較すると、先頭削除と末尾削除のコードロジックは位置ノードの削除と同じであり、実装された関数は基本的に変更されていないため、ListErase を直接再利用できます。先頭と末尾を削除した場合のインターフェースが実現できます。

1> 先頭削除

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListErase(pHead->next);
}

2>末尾削除

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListErase(pHead->prev);
}

3.8 二重リンクリストの破壊

リンク リストの破棄は、主に cur = pHead->next から開始してリンク リストを 1 回走査することです。ループ終了の条件は cur != pHead です。走査後、センチネル ノードが解放され、リンク リスト全体が解放されました。

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;

	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);

		cur = next;
	}
	free(pHead);
}

4. 完全なコード

完全なコードはコード ウェアハウスにあります。エントリ: C 言語: C 言語学習コード、詳細を確認 - Gitee.com

5. 機能テスト

おすすめ

転載: blog.csdn.net/Ljy_cx_21_4_3/article/details/130729905