記事ディレクトリ
序文
前回の学習で、単連結リストの欠点や使い方については皆さんもう理解できたと思いますが、今日は二重連結リストについて学習します。今までと違い、二重連結リストの実装の難易度は上がっていますが、こうした困難は誰にとっても問題ではありません。シングル リンク リストの以前の実装と比較して、データ型は固定されています。main 関数で渡されるものが何であっても、シングル リンク リスト構造には対応するデータ型が必要です。今日、ダブル リンク リストの実装では、 main 関数 (ユーザー) は、すべてのデータ型を受信して実装できるものは何でも渡します。
この章では、関数ポインタ、コールバック関数、およびフレキシブル配列の知識ポイントを説明します。忘れた人は、この章で復習できます。
1. 二重リンクリストの考え方
実装する前に、まず二重リンク リストについて理解しましょう: 二
重リンク リストとは:二重リンク リストのノードには 2 つのポインター prev と next があり、それぞれ先行ノードと後続ノードを指します。
ダブル リンク リストの利点: 単一のリンク リストがノードの先行ノードにアクセスする場合、先頭からしかたどることができないという問題が解決されます。後続ノードへのアクセスの複雑さは O(1) です。先行ノードへのアクセスの複雑さは O( n) 問題です。
二重リンクリストの構造:
非巡回二重リンクリストでは、二重リンクリストの先頭ノードまたは最初のノードには先行ノードがなく、最後のノードには後続ノードがありません。ヘッド ノードに有効なデータが保存されていません。!!
循環二重リンクリストでは、ヘッドノードの先行ノードまたは二重リンクリストの最初のノードが最後のノードであり、最後のノードの後続ノードがヘッドノードまたは二重リンクリストの最初のノードです。ヘッド ノードに有効なデータが保存されていません。!!
この構造で大まかな実装アイデアはできていると思いますが、今日実装したいのは、二重リンクリストの中で最も完璧な構造です。ループの先頭に立つ二重リンクリストは、私たちが一般的に使用している構造でもあります。将来的に使用します。
2. 主要な循環二重リンクリストの実装分析
ユーザー (メイン関数) によって渡された任意のデータ型が二重リンク リストを形成できることを認識したいと考えています。そのため、ユーザーの二重リンク リスト データ フィールドの型は不明です。では、どのような種類のデータを構築したいのでしょうか? char 型のポインタを選択すると、整数型または構造体 char 型のポインタが渡された場合、ユーザーはどのように応答すべきかという新たな問題が発生します。上記の問題に対して、次の 2 つの構造を設計します:
これが私たちが設計した構造です。ヘッド ノードはデータを保存しておらず、他のノードと異なるため、それをどのように指すべきか?
ポインタは位置を指し、末尾ノードと最初の有効なノードのポインタは、先頭ノードのリンク リスト ノード メンバーの位置を指します。このようにして、リンクされたリストは循環型になります。
2. 循環型二重連結リスト1の先導実装
1. 循環型ダブルリンクリスト実装を先取りするためのヘッダファイルの概要
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<time.h>
#define HEADINSERTION 1 //头插选项
#define BACKINSERTION 2 //尾插选项
typedef struct ListNode//链表的节点
{
char* data;//传入的数据
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
}ListNode;
typedef struct ListHead //不一样的头节点
{
int size;//用来存储传入数据的大小
ListNode list;//用来存储头节点的数据
}ListHead;
typedef void Printf(const void* );//对用户传递的打印函数进行重命名
typedef int Cmp(const void*, const void*);//对用户传递的打印函数进行重命名
ListHead* ListCreate(int datasize);//用来创建特殊的头节点
void Destory(ListHead* pHead);// 双向链表销毁
void ListPrint(ListHead* pHead, Printf* print);// 双向链表打印
int ListInsert(ListHead* pHead, const void* pos, int Optional);//双向链表的插入
void* ListFind(ListHead* pHead, const void* key,Cmp* cmp);//双向链表查找
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp);//双向链表删除
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp,void* retu);//双向链表删除,并把删除节点返回
まず、ヘッダー ファイル内の関数と、その関数がこのように設計されている理由を見てみましょう。以下で一つずつ答えていきます。
2. 循環二重リンクリストを率先して初期化する
ListHead* ListCreate(int datasize)//用来创建特殊的头节点
{
ListHead* pHead = (ListHead*)malloc(sizeof(ListHead));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->size = datasize;//用来接收用户要构建链表的数据区的大小
pHead->list.data = NULL;//头节点不存储有效数据,所以置为NULL
pHead->list.next = &pHead->list;//后继节点指向自己
pHead->list.prev = &pHead->list;//前驱节点指向自己
return pHead;
}
初期化中に特別なヘッド ノードを構築する必要があるため、ユーザーが構築したいデータ領域のサイズがわかっている場合にのみ、後続の挿入用のスペースを作成できます。
3. 循環二重リンクリストの挿入を主導
//pos:用来接收用户所传数据,由于用户所传数据未知,所以我们用void指针进行接收
//Optional:插入选择,接收用户是头插还是尾插
//#define HEADINSERTION 1 //头插选项,在我们的头文件中定义的
//#define BACKINSERTION 2 //尾插选项,在我们的头文件中定义的
int ListInsert(ListHead* pHead, const void* pos, int Optional)//双向链表的插入
{
assert(pHead);
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)//开辟空间失败就报错结束
{
perror("node malloc");//节点开辟失败
return 1;//返回值为1代表节点开辟失败
}
node->data = (char*)malloc(sizeof(char) * pHead->size);//数据区的开辟,开辟的大小为用户所传的大小
if (node->data == NULL)
{
free(node);//释放开辟好的节点,防止内存泄露
perror("node->data malloc");//数据域开辟失败,进行报错
return 2;//返回值为2代表节点开辟成功,但数据区开辟失败
}
memcpy(node->data, (char*)pos,pHead->size);//把数据拷贝到我们开的节点中
if (HEADINSERTION == Optional)//判断是否为头插
{
node->next = (pHead->list).next;
node->prev = &(pHead->list);//取头节点中的链表节点地址
}
else if (BACKINSERTION == Optional)//判断是否为尾插
{
node->next = &pHead->list;
node->prev = pHead->list.prev;
}
else
{
free(node->data);//释放开辟好的数据区
free(node);//释放开辟好的节点,防止内存泄露
return 3;//返回值为3代表插入位置不符合要求
}
node->prev->next = node;
node->next->prev = node;
return 0;//代表此函数正常结束
}
このプロジェクトでは、リンクされたリスト要素の内容が不明であるため、ランダムに挿入位置を指定することは禁止されており、ランダムに挿入すると構造が崩れやすいため、ここでは末尾挿入と先頭挿入を実装します。サイズから始まる位置ではなく、ヘッド ノード内の二重リンク リスト ノードの位置を指すように、ヘッド ノードへのポインティングを挿入します。!!
ユーザーの選択に応じてヘッドプラグとテールプラグを実装しますが、ここで実現するために 2 つの機能に分割する必要はありません。
プロジェクトでは、関数内の印刷関数の使用を減らす必要があります。ここでは戻り値によって戻ります。ユーザーは戻り値を使用してエラーの原因を判断できます。これにより、関数の特異性が保証されます。
ユーザーが渡したデータ領域は不明なので、ライブラリ関数 memcpy または strcpy を使用してコピーする必要があります。
ここでは、頭栓と尾栓は同じ考え方なので、絵を描くことができます。ダブルリンクリストノードの開発は成功したが、データ領域のオープンに失敗した場合、オープンしたノードを解放する必要があり、解放しないとメモリリークが発生することに注意する必要があります。
4. 率先して循環二重リンクリストの印刷と破棄を行う
void Destory(ListHead* pHead)// 双向链表销毁
{
assert(pHead);
ListNode* pos = (&pHead->list)->next;//从头节点的下一个节点开始
while (pos != &pHead->list)
{
ListNode* del = pos;//要释放的节点
pos = pos->next;//保存下一个节点
free(del->data);//data数据也是动态开辟的
free(del);//释放节点
}
}
void ListPrint(ListHead* pHead, Printf * print)// 双向链表打印,使用回调函数
{
assert(pHead);
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
print(pos->data);//调用用户提供的打印函数
pos = pos->next;
}
}
ここで、ヘッドノードであるか否かは、二重リンクリストノードのアドレスとヘッドノード内の二重リンクリストノードとを比較することにより判定される。
ユーザーのタイプがわからない場合、どうやって印刷すればよいでしょうか?
これはコールバック関数を通じて行われます。ユーザーのデータ型はわかりませんが、ユーザーは知っているため、関数の型を設定してユーザーが自分で実装できるようにし、コールバック関数を通じてそれを呼び出します。
ヘッダー ファイルの名前変更機能はここで機能します
typedef void Printf(const void* );//对用户传递的打印函数进行重命名
//void Printf(const void* );//用户需要实现的函数
//typedef对该函数进行重命名
テスト関数は次のとおりです。
#define NAME_SIZE 32
typedef struct Stu
{
int id;
char name[NAME_SIZE];
int math;
int chinese;
}Stu;
void Printf_s(const void* print)//用户写的打印函数
{
Stu* prin = (Stu*)print;//把void类型转换为用户的类型
printf("id:%2d name:%s math:%2d chinese:%2d\n",prin->id, prin->name, prin->math, prin->chinese);
}
void test1()
{
ListHead* pHead = ListCreate(sizeof(Stu));
Stu stu;
int i = 0;
for (i = 0; i < 5; i++)
{
stu.id = i;
snprintf(stu.name, NAME_SIZE, "stu%d:", i);
stu.math = rand()%100;
stu.chinese = rand()%100;
ListInsert(pHead, &stu, 1);//传入1,进行头插
//ListInsert(pHead, &stu, 2);//传入2,进行尾插
}
ListPrint(pHead, Printf_s);// 双向链表打印
Destory(pHead);// 双向链表销毁
}
int main()
{
srand((unsigned)time(NULL));
test1();
return 0;
}
先頭挿入:
末尾挿入:
スコアの値をランダムに選択するため、2 つの結果は異なります。ここで文字列配列に値を割り当てる方法は、単一リンク リストの方法と同じです。ここではあまり紹介しません。
5. 率先して循環二重リンクリストの検索と削除を行う
ListNode* find(ListHead* pHead, const void* key, Cmp* cmp)//双向链表查找
{
assert(pHead);
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
if (cmp(key, pos->data) == 0)//调用用户提供的比较函数
{
break;
}
pos = pos->next;
}
return pos;//如果找到时,循环终止,返回值为找到的节点,如果找不到,则返回pos为头节点。
}
void* ListFind(ListHead* pHead, const void* key, Cmp* cmp)//双向链表查找
{
assert(pHead);
return (void*)find(pHead, key, cmp)->data;//返会我们的数据区。如果返回的头节点,头节点的值为NULL。不影响我们正常判断
}
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp)//双向链表删除
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del->data);//我们的数据区也是动态开辟的,所以也要内存释放
free(del);
return 0;
}
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp, void* retu)//双向链表删除,并把删除节点返回
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
if (del->data != NULL)
{
memcpy(retu, del->data, pHead->size);//通过函数参数返回
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del->data);
free(del);
return 0;
}
検索と削除を実装するには、最初にノードを見つける必要があるため、検索されたノードを返す別の関数を作成します。比較対象の型がわからないため、検索を実装するには、ユーザーが比較したい型とユーザーが提供する比較関数を受け取る必要があります。
ヘッド ノードに有効なデータが含まれていません。ヘッド ノードのデータを空に設定しているため、検索で見つからなかった場合、返されるデータはヘッド ノードのデータ (NULL) です。
2 つの削除関数を設定しました。1 つは直接削除するもの、もう 1 つは削除して削除された値をユーザーに返すものです。2 つの実装アイデアは同じですが、1 つは追加のパラメーターを追加しています。
typedef int Cmp(const void*, const void*);//对用户传递的比较函数进行重命名
テスト機能:
#define NAME_SIZE 32
typedef struct Stu
{
int id;
char name[NAME_SIZE];
int math;
int chinese;
}Stu;
void Printf_s(const void* print)//用户写的打印函数
{
Stu* prin = (Stu*)print;
printf("id:%2d name:%s math:%2d chinese:%2d\n",prin->id, prin->name, prin->math, prin->chinese);
}
int cmp_id(const void* s1, const void*s2)//用户写的比较函数
{
int *key = (int*)s1;
Stu *stu = (Stu*)s2;
return (*key - stu->id);
}
int cmp_name(const void* s1, const void* s2)//用户写的name比较函数
{
char* key = (char*)s1;
Stu* stu = (Stu*)s2;
return strcmp(key, stu->name);
}
void test1()
{
ListHead* pHead = ListCreate(sizeof(Stu));
Stu stu;
int i = 0;
for (i = 0; i < 5; i++)
{
stu.id = i;
snprintf(stu.name, NAME_SIZE, "stu%d:", i);
stu.math = rand()%100;
stu.chinese = rand()%100;
ListInsert(pHead, &stu, 1);//传入1,进行头插
//ListInsert(pHead, &stu, 2);//传入2,进行尾插
}
ListPrint(pHead, Printf_s);// 双向链表打印
printf("\n\n");
//链表元素的查找,通过id查找
int id = 3;
Stu *st = ListFind(pHead, &id, cmp_id);
if (st == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(st);
}
//链表元素的删除,通过id删除
printf("\n\n");
Listdestroy(pHead, &id, cmp_id);
ListPrint(pHead, Printf_s);
//链表元素的删除并且返回,通过姓名删除
printf("\n\n");
char* p = "stu2:";//不要忘了加分号
Stu *s = &stu;
Listtravel(pHead, p, cmp_name, s);
if (s == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(s);
}
printf("\n\n");
ListPrint(pHead, Printf_s);
Destory(pHead);// 双向链表销毁
}
int main()
{
srand((unsigned)time(NULL));
test1();
return 0;
}
任意のタイプを選択できることを確認するために、ここではそれぞれ id の削除と名前の削除を使用します (実装の考え方は qsort ライブラリ関数と同じです)。
第三に、誘導循環二重連結リスト2の実現
1 つ目の方法は、動的メモリを通じてスペースを開くことです。データ領域のスペースを動的にオープンできないでしょうか? 答えは「はい」です。2 番目のアイデアは、データ領域を動的にオープンしないことで二重リンク リストを実装することです。
2 番目のアイデアは、最初の関数実装に基づいて変更を加えるというもので、テスト関数と変更された関数は変更しないでください。!!
1.巡回二重リンクリスト実装2の仕組みを先導する
typedef struct ListNode//链表的节点
{
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
char data[];//传入的数据
//char data[1];//传入的数据
}ListNode;
typedef struct ListHead //不一样的头节点
{
int size;//用来存储传入数据的大小
ListNode list;//用来存储头节点的数据
}ListHead;
ここでは、フレキシブル配列を使用して実装します。コンパイラがフレキシブル配列をサポートしていない場合は、配列内の要素の数を 1 に割り当てることができます。
ここでのデータの目的は、データ領域のアドレスを見つけやすくすることです。
2. 循環型二重リンクリストを率先して機能変更を実現
ListHead* ListCreate(int datasize)//用来创建特殊的头节点
{
ListHead* pHead = (ListHead*)malloc(sizeof(ListHead));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->size = datasize;
pHead->list.next = &pHead->list;//后继节点指向自己
pHead->list.prev = &pHead->list;//前驱节点指向自己
return pHead;
}
ここで、データ領域は動的にスペースを開く必要がなくなったので、ここで値を割り当てる必要はありません。
void Destory(ListHead* pHead)// 双向链表销毁
{
assert(pHead);
ListNode* pos = (&pHead->list)->next;//从头节点的下一个节点开始
while (pos != &pHead->list)
{
ListNode* del = pos;//要释放的节点
pos = pos->next;//保存下一个节点
free(del);
}
}
int ListInsert(ListHead* pHead, const void* pos, int Optional)//双向链表的插入
{
assert(pHead);
ListNode* node = (ListNode*)malloc(sizeof(ListNode)+pHead->size);//申请一个节点结构体的大小加上用户所传数据大小
if (node == NULL)//开辟空间失败就报错结束
{
perror("node malloc");//节点开辟失败
return 1;//返回值为1代表节点开辟失败
}
memcpy(node->data, (char*)pos,pHead->size);//把数据拷贝到我们开的节点中
if (HEADINSERTION == Optional)//判断是否为头插
{
node->next = (pHead->list).next;
node->prev = &(pHead->list);
}
else if (BACKINSERTION == Optional)//判断是否为尾插
{
node->next = &pHead->list;
node->prev = pHead->list.prev;
}
else
{
return 2;//返回值为3代表插入位置不符合要求
}
node->prev->next = node;
node->next->prev = node;
return 0;//代表此函数正常结束
}
ここでは、領域を解放して開く関数を削除する必要がありますが、2 つのポインタ用の領域と、ダブル リンク リスト構造に対してユーザーが渡したデータの領域サイズだけを解放する必要があります。
コピーしたデータはデータ領域に配置されます。
void* ListFind(ListHead* pHead, const void* key, Cmp* cmp)//双向链表查找
{
assert(pHead);
ListNode* pos = find(pHead, key, cmp);
if (pos == &pHead->list)//如果是头节点,则证明没找到
{
return NULL;
}
return pos->data;//返会我们的数据区。
}
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp)//双向链表删除
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp, void* retu)//双向链表删除,并把删除节点返回
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
if (del->data != NULL)
{
memcpy(retu, del->data, pHead->size);//通过函数参数返回
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
ここでは、ヘッドノードにはデータ領域のメモリがないため、データ領域のメモリを解放する関数とメモリを解放する関数を削除し、ヘッドノードであるかどうかの判定を追加します。
テストを実行します。
4. 先頭循環二重リンクリストの隠蔽カプセル化
非表示のカプセル化は実装 2 に基づいて変更されています。!!
1. ヘッダー ループのダブル リンク リストの隠しカプセル化ヘッダー ファイルのプレビュー
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<time.h>
#define HEADINSERTION 1 //头插选项
#define BACKINSERTION 2 //尾插选项
typedef void Printf(const void*);//对用户传递的打印函数进行重命名
typedef int Cmp(const void*, const void*);//对用户传递的打印函数进行重命名
typedef struct ListNode//链表的节点
{
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
char data[];//传入的数据
}ListNode;
typedef struct ListHead //不一样的头节点
{
int size;//用来存储传入数据的大小
void (*destory)(struct ListHead* pHead);//双向链表销毁
void (*listprint)(struct ListHead* pHead, Printf* print);//双向链表打印
int (*listinsert)(struct ListHead* pHead, const void* pos, int Optional);//双向链表的插入
void* (*listfind)(struct ListHead* pHead, const void* key, Cmp* cmp);//双向链表查找
int (*listdestroy)(struct ListHead* pHead, const void* key, Cmp* cmp);//双向链表删除
int (*listtravel)(struct ListHead* pHead, const void* key, Cmp* cmp, void* retu);//双向链表删除,并把删除节点返回
ListNode list;//用来存储头节点的数据,柔性数组,这个必须放在最下方
}ListHead;
ListHead* ListCreate(int datasize);//用来创建特殊的头节点
ここでは、実装したいすべての関数を関数ポインターとして設定します。これにより、関数ユーザーは、関数がどのように実装されているかを知らなくても、構造体を通じて直接呼び出すことができます。
2. 最初のループ二重リンクリスト関数の変更
2 番目のコードに基づいて変更を加えているため、ここでは変更されたコードのみを示します。
//提前声明
void Destory(struct ListHead* pHead);// 双向链表销毁
void ListPrint(struct ListHead* pHead, Printf* print);// 双向链表打印
int ListInsert(struct ListHead* pHead, const void* pos, int Optional);//双向链表的插入
void* ListFind(struct ListHead* pHead, const void* key,Cmp* cmp);//双向链表查找
int Listdestroy(struct ListHead* pHead, const void* key, Cmp* cmp);//双向链表删除
int Listtravel(struct ListHead* pHead, const void* key, Cmp* cmp,void* retu);//双向链表删除,并把删除节点返回
ListHead* ListCreate(int datasize)//用来创建特殊的头节点
{
ListHead* pHead = (ListHead*)malloc(sizeof(ListHead));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->size = datasize;
pHead->list.next = &pHead->list;//后继节点指向自己
pHead->list.prev = &pHead->list;//前驱节点指向自己
pHead->destory = Destory;//把我们封装函数指针指向相应的函数
pHead->listprint = ListPrint;
pHead->listinsert = ListInsert;
pHead->listfind = ListFind;
pHead->listdestroy = Listdestroy;
pHead->listtravel = Listtravel;
return pHead;
}
関数ポインターを割り当てるときは、エラーが報告されないように、まず指す先の関数を宣言する必要があります。
テスト関数の変更:
#define NAME_SIZE 32
typedef struct Stu
{
int id;
char name[NAME_SIZE];
int math;
int chinese;
}Stu;
void Printf_s(const void* print)//用户写的打印函数
{
Stu* prin = (Stu*)print;
printf("id:%2d name:%s math:%2d chinese:%2d\n",prin->id, prin->name, prin->math, prin->chinese);
}
int cmp_id(const void* s1, const void*s2)//用户写的id比较函数
{
int *key = (int*)s1;
Stu *stu = (Stu*)s2;
return (*key - stu->id);
}
int cmp_name(const void* s1, const void* s2)//用户写的name比较函数
{
char* key = (char*)s1;
Stu* stu = (Stu*)s2;
return strcmp(key,stu->name);
}
void test1()
{
ListHead* pHead = ListCreate(sizeof(Stu));
Stu stu;
int i = 0;
for (i = 0; i < 5; i++)
{
stu.id = i;
snprintf(stu.name, NAME_SIZE, "stu%d:", i);
stu.math = rand()%100;
stu.chinese = rand()%100;
pHead->listinsert(pHead, &stu, 2);
}
pHead->listprint(pHead, Printf_s);// 双向链表打印
//链表元素的查找,通过id查找
printf("\n\n");
int id = 3;
Stu *st = pHead->listfind(pHead, &id, cmp_id);
if (st == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(st);
}
//链表元素的删除,通过id删除
printf("\n\n");
pHead->listdestroy(pHead, &id, cmp_id);
pHead->listprint(pHead, Printf_s);
//链表元素的删除,通过姓名删除
printf("\n\n");
char* p = "stu2:";//不要忘了加分号
Stu *s = &stu;
pHead->listtravel(pHead, p, cmp_name, s);
if (s == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(s);
}
printf("\n\n");
pHead->listprint(pHead, Printf_s);
pHead->destory(pHead);// 双向链表销毁
}
int main()
{
srand((unsigned)time(NULL));
test1();
return 0;
}
関数を通じて実装した転置構造の一部を呼び出す必要があります。
5. 循環二重リンクリストをライブラリとしてカプセル化することに率先して取り組む
ライブラリとしてのカプセル化も実装 2 に基づいて変更を加えています。!!
1. ヘッダー循環二重リンクリストはライブラリーのヘッダーファイルプレビューとしてカプセル化されます。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<time.h>
#define HEADINSERTION 1 //头插选项
#define BACKINSERTION 2 //尾插选项
//可以做出成一个动态库或静态库,不需要知道我们的结构体类型就可以操作构建一个结构体
typedef void ListHead;// 把void类型改为ListHead,我们函数进行传参都是用的void
typedef void Printf(const void* );//对用户传递的打印函数进行重命名
typedef int Cmp(const void*, const void*);//对用户传递的打印函数进行重命名
ListHead* ListCreate(int datasize);//用来创建特殊的头节点
void Destory(ListHead* pHead);// 双向链表销毁
void ListPrint(ListHead* pHead, Printf* print);// 双向链表打印
int ListInsert(ListHead* pHead, const void* pos, int Optional);//双向链表的插入
void* ListFind(ListHead* pHead, const void* key,Cmp* cmp);//双向链表查找
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp);//双向链表删除
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp,void* retu);//双向链表删除,并把删除节点返回
ここでは、構造体の隠蔽を実装しました。ユーザーは構造体の型を知りませんが、関数がどのような機能を達成できるかを知っているだけであり、渡されるパラメーターはすべて void ポインター型です。これにより、ユーザーは構造タイプを知り、変更を加えることができなくなります。関数を実装するソース ファイルに構造体のタイプを入れます。
2. 主要な循環二重リンクリスト関数を実装するためのソースファイル
typedef struct ListNode//链表的节点
{
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
char data[];//传入的数据
}ListNode;
struct ListHead //不一样的头节点
{
int size;//用来存储传入数据的大小
ListNode list;//用来存储头节点的数据
};
ListHead* ListCreate(int datasize)//用来创建特殊的头节点
{
struct ListHead* pHead = (struct ListHead*)malloc(sizeof(struct ListHead));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->size = datasize;
pHead->list.next = &pHead->list;//后继节点指向自己
pHead->list.prev = &pHead->list;//前驱节点指向自己
return pHead;
}
void Destory(ListHead* p)// 双向链表销毁
{
assert(p);
struct ListHead *pHead = p;
ListNode* pos = (&pHead->list)->next;//从头节点的下一个节点开始
while (pos != &pHead->list)
{
ListNode* del = pos;//要释放的节点
pos = pos->next;//保存下一个节点
free(del);
}
}
void ListPrint(ListHead* p, Printf * print)// 双向链表打印,使用回调函数
{
assert(p);
struct ListHead* pHead = p;
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
print(pos->data);//调用用户提供的打印函数
pos = pos->next;
}
}
int ListInsert(ListHead* p, const void* pos, int Optional)//双向链表的插入
{
assert(p);
struct ListHead* pHead = p;
ListNode* node = (ListNode*)malloc(sizeof(ListNode)+pHead->size);//申请一个节点结构体的大小加上用户所传数据大小
if (node == NULL)//开辟空间失败就报错结束
{
perror("node malloc");//节点开辟失败
return 1;//返回值为1代表节点开辟失败
}
memcpy(node->data, (char*)pos,pHead->size);//把数据拷贝到我们开的节点中
if (HEADINSERTION == Optional)//判断是否为头插
{
node->next = (pHead->list).next;
node->prev = &(pHead->list);
}
else if (BACKINSERTION == Optional)//判断是否为尾插
{
node->next = &pHead->list;
node->prev = pHead->list.prev;
}
else
{
return 2;//返回值为3代表插入位置不符合要求
}
node->prev->next = node;
node->next->prev = node;
return 0;//代表此函数正常结束
}
ListNode* find(ListHead* p, const void* key, Cmp* cmp)//双向链表查找
{
assert(p);
struct ListHead* pHead = p;
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
if (cmp(key, pos->data) == 0)//调用用户提供的比较函数
{
break;
}
pos = pos->next;
}
return pos;//如果找到时,循环终止,返回值为找到的节点,如果找不到,则返回pos为头节点。
}
void* ListFind(ListHead* p, const void* key, Cmp* cmp)//双向链表查找
{
assert(p);
struct ListHead* pHead = p;
ListNode* pos = find(pHead, key, cmp);
if (pos == &pHead->list)//如果是头节点,则证明没找到
{
return NULL;
}
return pos->data;//返会我们的数据区。
}
int Listdestroy(ListHead* p, const void* key, Cmp* cmp)//双向链表删除
{
assert(p);
struct ListHead* pHead = p;
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
int Listtravel(ListHead* p, const void* key, Cmp* cmp, void* retu)//双向链表删除,并把删除节点返回
{
assert(p);
struct ListHead* pHead = p;
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
if (del->data != NULL)
{
memcpy(retu, del->data, pHead->size);//通过函数参数返回
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
変更したのは、実装した関数内の void 型ポインターを独自の構造体の型に変換することです。
テスト関数は実装 2 と同じです。
3. 率先して循環二重リンクリストをライブラリにパッケージ化する
1. プロジェクトを見つけて [プロパティ] を選択し、構成プロパティで構成タイプを静的ライブラリ (.lib) に変更します。
2. 生成します。
3. プロジェクト ファイル内で .lib ファイルを見つけます。これは、生成された静的ライブラリです。
6. カーネル二重リンクリストの一部を分析して、二重リンクリストを迅速に実装します。
1. カーネル二重リンクリストの部分解析
リンク リストで最も重要なノード ポインタは、カーネル内のリンク リスト構造です。その中にはデータ領域がありません。二重リンク リストを作成したい場合は、その構造を構造に含める必要があります。構造内のどこにでも現れる可能性があります。
ここで実装されている先頭挿入と末尾挿入は両方とも定義された関数であり、この関数は挿入と削除を実装するために呼び出されます。
2. シンプルな先頭循環二重リンクリストを素早く実装する
typedef struct ListNode//链表的节点
{
int data;
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
}ListNode;
ListNode* ListCreate()//用来创建头节点
{
ListNode* pHead = (ListNode*)malloc(sizeof(ListNode));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->data = 0;
pHead->next = pHead;//后继节点指向自己
pHead->prev = pHead;//前驱节点指向自己
return pHead;
}
void Destory(ListNode* pHead)// 双向链表销毁
{
assert(pHead);
ListNode* pos = pHead->next;//从头节点的下一个节点开始
while (pos != pHead)
{
ListNode* del = pos;//要释放的节点
pos = pos->next;//保存下一个节点
free(del);//释放节点
}
}
void ListPrint(ListNode* pHead)// 双向链表打印
{
assert(pHead);
ListNode* pos = pHead->next;//从头节点的下一个节点开始
while (pos != pHead)
{
printf("%d->", pos->data);
pos = pos->next;
}
printf("\n");
}
void ListInsert(ListNode* pos, int key)//双向链表的插入
{
assert(pos);
ListNode* init = (ListNode*)malloc(sizeof(ListNode));
if (init == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
init->data = key;
init->next = pos->next;
init->prev = pos;
pos->next->prev = init;
pos->next = init;
}
void Listdestroy(ListNode* pHead,ListNode* pos)//双向链表删除
{
assert(pos);
if (pos == pHead)
{
return;
}
ListNode* del = pos;
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(del);
}
void SListPushBack(ListNode* pHead, int x)//单链表尾插
{
assert(pHead);
ListInsert(pHead->prev, x);
}
void SListPushFront(ListNode* pHead, int x)//单链表的头插
{
assert(pHead);
ListInsert(pHead, x);
}
void SListPopBack(ListNode* pHead)// 单链表的尾删
{
assert(pHead);
Listdestroy(pHead,pHead->prev);
}
void SListPopFront(ListNode* pHead)// 单链表头删
{
assert(pHead);
Listdestroy(pHead,pHead->next);
}
ListNode* ListFind(ListNode* pHead, int key)//双向链表查找
{
assert(pos);
ListNode* pos = pHead->next;
while (pos != pHead)
{
if (pos->data == key)
{
return pos;
}
pos = pos->next;
}
return NULL;
}
上記のコードによると、二重リンク リストのほとんどの機能を実現するには、二重リンク リストの挿入と削除を実装するだけでよいことがわかります。
多くの事例と演習を通じて、ダブルリンクリストの実装と使い方については皆さんも理解できたと思いますので、実際に実装してみましょう。
7. ダブルリンクリスト実装コード2
以降のカプセル化ライブラリと合成ライブラリは 2 によって変更されるため、2 を実装するコードを詳しく見てみましょう。
1. ダブルリンクリスト実装コードのヘッダファイル 2
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<time.h>
#define HEADINSERTION 1 //头插选项
#define BACKINSERTION 2 //尾插选项
typedef struct ListNode//链表的节点
{
struct ListNode* next;//指向下一个节点的指针
struct ListNode* prev;//指向上一个节点的指针
char data[];//传入的数据
//char data[1];//传入的数据
}ListNode;
typedef struct ListHead //不一样的头节点
{
int size;//用来存储传入数据的大小
ListNode list;//用来存储头节点的数据
}ListHead;
typedef void Printf(const void* );//对用户传递的打印函数进行重命名
typedef int Cmp(const void*, const void*);//对用户传递的打印函数进行重命名
ListHead* ListCreate(int datasize);//用来创建特殊的头节点
void Destory(ListHead* pHead);// 双向链表销毁
void ListPrint(ListHead* pHead, Printf* print);// 双向链表打印
int ListInsert(ListHead* pHead, const void* pos, int Optional);//双向链表的插入
void* ListFind(ListHead* pHead, const void* key,Cmp* cmp);//双向链表查找
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp);//双向链表删除
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp,void* retu);//双向链表删除,并把删除节点返回
2. ダブルリンクリスト実装の関数実装ソースファイル2
#include"main2.h"
ListHead* ListCreate(int datasize)//用来创建特殊的头节点
{
ListHead* pHead = (ListHead*)malloc(sizeof(ListHead));
if (pHead == NULL)//开辟空间失败就报错结束
{
perror("pHead malloc");
exit(-1);
}
pHead->size = datasize;
pHead->list.next = &pHead->list;//后继节点指向自己
pHead->list.prev = &pHead->list;//前驱节点指向自己
return pHead;
}
void Destory(ListHead* pHead)// 双向链表销毁
{
assert(pHead);
ListNode* pos = (&pHead->list)->next;//从头节点的下一个节点开始
while (pos != &pHead->list)
{
ListNode* del = pos;//要释放的节点
pos = pos->next;//保存下一个节点
free(del);
}
}
void ListPrint(ListHead* pHead, Printf * print)// 双向链表打印,使用回调函数
{
assert(pHead);
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
print(pos->data);//调用用户提供的打印函数
pos = pos->next;
}
}
int ListInsert(ListHead* pHead, const void* pos, int Optional)//双向链表的插入
{
assert(pHead);
ListNode* node = (ListNode*)malloc(sizeof(ListNode)+pHead->size);//申请一个节点结构体的大小加上用户所传数据大小
if (node == NULL)//开辟空间失败就报错结束
{
perror("node malloc");//节点开辟失败
return 1;//返回值为1代表节点开辟失败
}
memcpy(node->data, (char*)pos,pHead->size);//把数据拷贝到我们开的节点中
if (HEADINSERTION == Optional)//判断是否为头插
{
node->next = (pHead->list).next;
node->prev = &(pHead->list);
}
else if (BACKINSERTION == Optional)//判断是否为尾插
{
node->next = &pHead->list;
node->prev = pHead->list.prev;
}
else
{
return 2;//返回值为3代表插入位置不符合要求
}
node->prev->next = node;
node->next->prev = node;
return 0;//代表此函数正常结束
}
ListNode* find(ListHead* pHead, const void* key, Cmp* cmp)//双向链表查找
{
assert(pHead);
ListNode* pos = pHead->list.next;
while (pos != &pHead->list)//判断是否走到头节点
{
if (cmp(key, pos->data) == 0)//调用用户提供的比较函数
{
break;
}
pos = pos->next;
}
return pos;//如果找到时,循环终止,返回值为找到的节点,如果找不到,则返回pos为头节点。
}
void* ListFind(ListHead* pHead, const void* key, Cmp* cmp)//双向链表查找
{
assert(pHead);
ListNode* pos = find(pHead, key, cmp);
if (pos == &pHead->list)//如果是头节点,则证明没找到
{
return NULL;
}
return pos->data;//返会我们的数据区。
}
int Listdestroy(ListHead* pHead, const void* key, Cmp* cmp)//双向链表删除
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
int Listtravel(ListHead* pHead, const void* key, Cmp* cmp, void* retu)//双向链表删除,并把删除节点返回
{
assert(pHead);
ListNode* del = find(pHead, key, cmp);
if (del == &pHead->list)
{
return 1;//代表未找到我们要删除的节点
}
if (del->data != NULL)
{
memcpy(retu, del->data, pHead->size);//通过函数参数返回
}
//删除节点
del->prev->next = del->next;
del->next->prev = del->prev;
free(del);
return 0;
}
3. ダブルリンクリスト実装のメイン関数ソースファイル2
#include"main2.h"
#define NAME_SIZE 32
typedef struct Stu
{
int id;
char name[NAME_SIZE];
int math;
int chinese;
}Stu;
void Printf_s(const void* print)//用户写的打印函数
{
Stu* prin = (Stu*)print;
printf("id:%2d name:%s math:%2d chinese:%2d\n",prin->id, prin->name, prin->math, prin->chinese);
}
int cmp_id(const void* s1, const void*s2)//用户写的比较函数
{
int *key = (int*)s1;
Stu *stu = (Stu*)s2;
return (*key - stu->id);
}
int cmp_name(const void* s1, const void* s2)//用户写的name比较函数
{
char* key = (char*)s1;
Stu* stu = (Stu*)s2;
return strcmp(key, stu->name);
}
void test1()
{
ListHead* pHead = ListCreate(sizeof(Stu));
Stu stu;
int i = 0;
for (i = 0; i < 5; i++)
{
stu.id = i;
snprintf(stu.name, NAME_SIZE, "stu%d:", i);
stu.math = rand()%100;
stu.chinese = rand()%100;
ListInsert(pHead, &stu, 1);//传入1,进行头插
//ListInsert(pHead, &stu, 2);//传入2,进行尾插
}
ListPrint(pHead, Printf_s);// 双向链表打印
//链表元素的查找,通过id查找
printf("\n\n");
int id = 3;
Stu *st = ListFind(pHead, &id, cmp_id);
if (st == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(st);
}
//链表元素的删除,通过id删除
printf("\n\n");
Listdestroy(pHead, &id, cmp_id);
ListPrint(pHead, Printf_s);
//链表元素的删除并且返回,通过姓名删除
printf("\n\n");
char* p = "stu2:";//不要忘了加分号
Stu *s = &stu;
Listtravel(pHead, p, cmp_name, s);
if (s == NULL)
{
printf("can not find\n");
}
else
{
Printf_s(s);
}
printf("\n\n");
ListPrint(pHead, Printf_s);
Destory(pHead);// 双向链表销毁
}
int main()
{
srand((unsigned)time(NULL));
test1();
return 0;
}
要約する
ダブル リンク リストについては皆さんすでに深く理解されていると思いますが、この主要な循環リンク リストは私たちが最も使用する構造になるため、これには深い理解と習熟が必要です。皆さんもぜひメッセージを残してください。ご協力ありがとうございます。