今日は引き続き、データ構造をリードする二重リンクリストについてお話します。
目次
通常の二重リンクリストと比較して、ヘッド付き二重リンクリストはヘッドノードを追加します。ヘッドノードには実際のデータは格納されず、リンクリストの開始位置を示すためにのみ使用されます。リーディング二重リンクリストの利点をいくつか示します。
リンク リストは操作が簡単です。先頭の双方向リンク リストにより、リンク リストの先頭と末尾に直接アクセスできるため、リンク リストの挿入や削除などの操作がより効率的になります。先頭ノードから最初の要素をすばやく挿入したり、末尾ノードから新しい要素をすばやく挿入したりできます。同時に、リンク リストの双方向性により、リンク リスト内の任意の位置に簡単に挿入および削除できます。
トラバーサルの柔軟性: 見出し付き二重リンクリストは、最初から最後まで、または最後から先頭までトラバースできます。これは、リンクされたリストを前から後ろに走査する場合でも、後ろから前に走査する場合でも、ニーズに応じて適切な走査方法を選択できることを意味し、要素を簡単に取得できます。
逆引き参照: 先頭二重リンク リストの重要な利点は、バック リンクを介して任意のノードからノードの前のノードに迅速にアクセスできることです。これにより、リンクされたリストの逆引き検索が効率的に行われます。先頭二重リンク リストの逆引き時間の複雑さは、単一リンク リストと比較して O(n) から O(1) に減少します。これは、一部のアプリケーションでは非常に重要になる可能性があります。
便利なノード削除: 先頭の二重リンク リストでは、ノードの削除に前のノードにアクセスする必要はなく、現在のノードの前方リンクと後方リンクを変更するだけで済みます。このようにして、ノードの削除操作がより便利かつ効率的になります。
主要な双方向循環リンク リスト: 最も複雑な構造で、一般にデータを個別に保存するために使用されます。実際に使用されるリンク リストのデータ構造は、リード付きの双方向循環リンク リストです。また、構造は複雑ですが、コードを使用して実装すると、この構造が多くの利点をもたらし、実装が簡単であることがわかります。
リード二重リンクリストの実装
構造の作成
typedef int LTDataType;
typedef struct ListNode
{
int data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
構造体を作成し、typedef を使用して名前を使いやすいように LTNode に変更し、次のノードと前のノードを作成して次のノードと前のノードを保存します。
兵士を初期化してセンチネルノードを作成する
番兵の位置を作成するときは、データの途中に -1 を追加し、作成後に次と前の位置を自分自身にポイントすることができます。ただし、この関数はノード関数の追加方法とよく似ているため、最初にノード関数を作成し、冗長性を避けるためにセンチネルの初期化時にそれを呼び出すことができます。
LTNode* BuyLTNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->prev = NULL;
return node;
}
LTNode* LTInit()
{
LTNode* phead = BuyLTNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
さらに 2 つの関数を作成するときは、LTInit 関数で BuyLTNode 関数を呼び出してセンチネル ビットを初期化します。
リンクされたリストを解放すると、コンテンツが
realloc を使用してリンク リストによって解放された領域はすべてヒープ領域にあるため、メモリ リークを防ぐために、リンク リストによって解放されたすべての領域を解放する必要があります。
void LTDestory(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
phead = NULL;
}
リンクされたリストの印刷機能
リンク リストを印刷するときは、2 番目のリンク リストを単一のリンク リストから区別する必要があります。単一リンク リストは末尾の NULL を見つけて停止するだけでよく、二重リンク リストの末尾はセンチネル ビットを指しているため、別の検出方法を使用できます。
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("phead<->");
while (cur != phead)
{
printf("%d<->", cur->data);
cur = cur->next;
}
}
トラバーサル ポインター cur を作成し、cur = phead->next がセンチネル位置の次のノードを指すようにします。次にトラバースし、トラバースがセンチネル位置に戻ったら停止します。
テールプラグ
末尾の挿入も単一リンク リストと比較して非常に便利で、末尾ノードを見つけるためにトラバースする必要はなく、末尾ノードを見つけるために phead の prev にアクセスするだけで済みます。
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = BuyLTNode(x);
newnode->prev = tail;
newnode->next = phead;
tail->next = newnode;
phead->prev = newnode;
}
末尾削除
まず、phead センチネルビットが空かどうかを確認する必要がありますが、リンクリストが空の場合は正常に削除できないため、phead->next != phead を判定し続ける必要があります。自分自身の次のノードは自分自身を指すことはできません。
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
phead->prev = tail->prev;
tail->prev->next = phead;
free(tail);
}
削除は非常に簡単で、tail として最後のノードを訪問し、tail ノードに接続されている phead のアドレスを tail ノードの前のノードに切り替え、tail ノードの次のノードをセンチネルのアドレスに置き換えて、tail を解放します。ノード。
プラグ
ヘッドプラグとテールプラグの原理は基本的に同じで、センチネル位置の内容を変更するだけです。
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
/*newnode->next = phead->next;
phead->next->prev = newhead;
phead->next = newhead;
newhead->prev = phead;*/
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
first->prev = newnode;
newnode->next = first;
}
ヘッダー挿入については上記のコードを参照できますが、2 つの方法があります。1 つ目 (注釈付き) は交換置換用のポインター変数を作成しないこと、2 つ目 (注釈なし) は交換用の変数を作成することです。最初の方法は、交換の順序が厳密である必要があります。最初に末尾ノードと対話し、次に最初のノードと対話します。そうしないと、交換が失敗し、エラー データが表示されます。
頭の削除
削除するときは、リンク リストが空かどうか、リンク リストがセンチネル ビットでのみ削除できるかどうかを必ず確認してください。
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
phead->next = second;
second->prev = phead;
}
カウント関数の実装
大量のデータを入力したときに、カウントするのに便利な関数です。'
int LTsize(LTNode* phead)
{
assert(phead);
int size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
++size;
cur = cur->next;
}
return size;
}
ここでは、リンクされたリストの各ノードをセンチネルの位置に戻るまでたどることだけが必要です。
データの対応する位置関数を見つけます
オペレータがリンクリスト内の特定のデータの位置を知り、それを操作したい場合、この関数を呼び出して、対応するデータが配置されているノードのアドレスを返すことができます。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while(cur != phead)
{
if (cur->data == x)
return cur;
}
return NULL;
}
ここでは、総当たり検索アルゴリズムを使用してリンクされたリストのセット全体を検索し、見つかった後に対応するデータを返します。
pos 位置の前に挿入
ここでは、find location 関数と組み合わせて呼び出す必要があります。
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* newnode = BuyLTNode(x);
posprev->next = newnode;
newnode->next = pos;
newnode->prev = posprev;
pos->prev = newnode;
}
これは実際にはヘッド プラッギングとテール プラッギングの原理と同じであるため、この関数を使用してヘッド プラッギングとテール プラッギングを実行することもできます。phead->next (ヘッド プラッギング)、phead である pos パラメーターを正しく渡すだけです。 ->prev (テールプラグ)。
pos 位置で削除
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
free(pos);
posprev->next = posnext;
posnext->prev = posprev;
}
これも先頭削除と末尾削除の原理と同じで、パラメータが正しく渡されていれば先頭削除と末尾削除になります。
シーケンスリストとリンクリストの違い
したがって、状況に応じて異なるデータ構造を選択することが非常に重要であり、さまざまな問題の異なるニーズを満たすために、違いと利点を十分に理解する必要があります。