連結リスト - 双方向循環連結リスト [C言語]

        双方向循環連結リストは最適な連結リストであり、単一連結リストの短所を補うことができ、構造が複雑で操作が簡単な連結リストです。ノードの挿入と削除の時間計算量は O(1) です。


二重にリンクされたリスト:

        片方向リストとは異なり、双方向リストには 2 つのポインターがあり、1 つは次のノードを指し、もう 1 つは前のノードを指します。

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

 

PS: 一般に、このリンク リストを作成するときは、操作を簡単にするためにセンチネル ビットが使用されます. リンク リストのセンチネル ビットの _pre はテール ノードを指し、テール ノードの _next はセンチネル ビットを指します.

リンクされたリストを印刷:

        この連結リストの末尾ノードは空ではないため、末尾ノードを判断することはできません。ただし、センチネル ビットにはデータが格納されないため、そのノードがセンチネル ビットであるかどうかを判断して、ループを終了することができます。

void ListPrint(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->_next;    //这里传入哨兵位的下一个
	while (cur!=pHead) {
		printf("%d -> ",cur->_data);
		cur = cur->_next;
	}
	printf("head\n");
}

データ挿入:

         センチネル位置を使用するため、センチネル位置の次のノードがヘッド ノードであり、ヘッド ノードを変更する必要がないため、セカンダリ ポインターを使用する必要はありません。なのでここにノードを入れる場合は、まずセンチネル職に応募して連結リストを作成します。

ListNode* ListCreate()
{
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	if (!newNode)
	{
		perror("申请失败\n");
		return NULL;
	}
	newNode->_next = newNode;
	newNode->_prev = newNode;
	return newNode;
}

PS: ノードが 1 つしかない場合は、自分自身を指す必要があります。これにより、後続の操作が容易になります。

ノードを挿入:

        すでにリンク リストが設定されており、新しいノード newNode を P ノードの前に挿入する必要があります。 

        ここに二重連結リストの利点が反映されている.ノードの先頭に挿入する場合、前のノードはpの前に接続されているため(p->_pre)、連結リストをトラバースする必要はなく、直接接続します。

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	newNode->_data = x;
	ListNode* cur = pos;
	ListNode* pre = pos->_prev;

	pre->_next = newNode;
	newNode->_prev = pre;

	newNode->_next = pos;
	pos->_prev = newNode;

}

PS: ここで操作する場合、前のノードを直接保存できるので、接続の順序を気にする必要はありません。保存しない場合は、接続順序に注意してください。

        リンクされたリストが NULL でセンチネル ビットが 1 つしかない場合、それはそれ自体を指しているため、接続時にこの関数を当然使用できます。

        センチネルの次のノードと前のノードは newNode を指し、newNode の次のノードと前のノードは両方ともセンチネルを指します。

 ヘッド挿入ノード:

        連結リストの挿入は, ヘッダが連結リストの挿入時に関数を直接再利用できるため, 前に書きました.ここで, 連結リストの先頭ノードは番兵位置の次のノードです. このノードを渡すことは, でノードを挿入することと同じです.頭

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListInsert(pHead->_next,x);
}

 

最後にノードを挿入します。

         ここで、リンクされたリストの末尾ノードはセンチネル位置の直前にあるため、センチネル位置の前にノードを挿入することは最後にノードを挿入することと同じであり、挿入関数を直接再利用することもできます。

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

リンクされたリストが空かどうかを確認します。

        セントリー ビットはデータを格納しないため、セントリー ビットの次のビットが自分の場合、リンク リストは空になります。

bool ListEmtpy(ListNode* head)
{
	//如果下一个节点是自己那么其就是空链表。
	assert(head);
	return head->_next==head;
}

ノードを削除:

        削除するノードの前のノードと次のノードを接続してから、ノードを解放します。

void ListErase(ListNode* pos)
{
	assert(pos);
	ListNode* pre = pos->_prev;
	ListNode* next = pos->_next;
	pre->_next = next;
	next->_prev = pre;
	free(pos);
}

ヘッドの取り外し:

        前の挿入と同様に、ノードを削除する機能は直接再利用でき、センチネル位置の次のノードを削除すると、リンクされたリストの先頭ノードが削除されます。

void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmtpy(pHead));
	ListErase(pHead->_next);
}

PS: リンクされたリストが NULL であるかどうかを判断するように注意してください。

尾の除去:

        末尾の挿入と同じで、センチネルの前のノードを削除すると、リンクされたリストの末尾が削除されます。

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmtpy(pHead));//如果链表为NULL,返回
	ListErase(pHead->_prev);
}

ノードの場所を見つける:

        先ほど連結リストを印刷したときと同様に、連結リストをたどると末尾ノードを見つけるのが難しいため、センチネル位置をたどったかどうかを判断し、センチネル位置に到達した場合はデータが見つからないことを意味し、その後、ループを終了して NULL を返します。

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;
}

リンクされたリストを削除:

        リンク リストを削除すると、ノードの削除機能を再利用することもできますが、リンク リスト内のすべてのノードが削除されるだけです。

void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->_next;
	while (cur!=pHead) {
		ListNode* temp = cur;
		cur = cur->_next;
		free(temp);
	}
	free(pHead);
}

 


拡大:

シーケンス テーブルの利点:添字ランダム アクセス、高いカップ キャッシュ ヒット率 (物理空間連続)

シーケンステーブルのデメリット:データの先頭挿入や途中挿入は非効率、容量拡張は性能を消費、スペースの無駄。

連結リストの利点: 任意の位置での挿入と削除は O(1) であり、オンデマンドで解放されます。

リンクされたリストの短所: ランダム アクセスをサポートしていません。

カップ キャッシュのヒット率が高い:

        データはメモリに格納されます.CPUとメモリ間でデータを交換する際にキャッシュ領域があります.キャッシュ領域は一度に一定バイト数のデータを読み込みます.シーケンステーブルの物理空間は連続しているため,先頭のアドレスを読み取ると、次のデータが結合されるので、これを取り込むと、トラバーサル時間が短縮されます。ただし、連結リストの物理空間は連続していないため、走査に必要な時間が長くなります。

おすすめ

転載: blog.csdn.net/weixin_45423515/article/details/124785755