双方向循環連結リストは最適な連結リストであり、単一連結リストの短所を補うことができ、構造が複雑で操作が簡単な連結リストです。ノードの挿入と削除の時間計算量は 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とメモリ間でデータを交換する際にキャッシュ領域があります.キャッシュ領域は一度に一定バイト数のデータを読み込みます.シーケンステーブルの物理空間は連続しているため,先頭のアドレスを読み取ると、次のデータが結合されるので、これを取り込むと、トラバーサル時間が短縮されます。ただし、連結リストの物理空間は連続していないため、走査に必要な時間が長くなります。