================================================= =======================
関連するコードは gitee から取得できます。
================================================= =======================
前号からの続き:
[要素データ構造] 2. 線形テーブルの数列テーブル_Gaogao Fattyのブログ - CSDN Blog
================================================= =======================
導入
前回のシーケンステーブルの紹介と使い方を通して
シーケンス テーブルには次のような利点と欠点があることがわかります。
シーケンステーブルのメリット
- 末尾の挿入および末尾の削除操作は十分に高速です
- ランダムアクセスや添え字の変更ができるのでとても便利です
-------------------------------------------------- --------------------------------------
シーケンステーブルのデメリット
- 先頭/中間での挿入および削除操作は遅くなり、時間計算量はO ( N) になります。
- 容量を拡張するには、新しいスペースを申請し、データをコピーし、古いスペースを解放する必要があります。消費量も多くなるでしょう。
- 容量拡張は通常2 倍に増加するため、必然的にある程度の無駄なスペースが生じます。
例:
現在の容量は100で、いっぱいになると容量は 200 に増加します。引き続き 5 個のデータを挿入しますが、
その後データは挿入されないため、95 個のデータ スペースが無駄になります。
以下で導入・実装するリンクリストではこうした問題は発生しません。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
1. リンクリスト
(1). リンクリストの概念と構造:
リンクリストの概念
リンク リストは、非連続的かつ非順次的な物理ストレージ構造です。
データ要素の論理順序は、リンク リスト内のポインタ リンク順序によって実現されます。単一リンク リストは通常、単独で使用されません。実装が簡単で効率的なのは、ヘッドの挿入とヘッドの削除のみです。
リンクリスト構造
- リンク リストも線形リストです。線形リストが物理的に保存される場合、通常、配列 (逐次リスト)およびリンクされた構造 (リンク リスト)の形式で保存されます。リンクされた構造は論理的に連続していますが、必ずしも物理的に連続しているとは限りません。
- 実際には、ノードは通常、ヒープから適用されます。
- ヒープから適用される領域は、特定の戦略に従って割り当てられますが、2
回適用される領域は連続的である場合もあれば、不連続である場合もあります。図:
(2). リンクリストの分類:
実際の連結リストの構造は非常に多様で、
以下の状況と組み合わせると8 種類の連結リスト構造が存在します。
一方向または双方向のリンク リスト
一方向リンクリスト図
二重リンクリスト図
-------------------------------------------------- -------------------------------------------
見出し付きまたは見出しなしのリンク リスト
見出し付きリンクリストアイコン
ヘッダーアイコンのないリンクリスト
-------------------------------------------------- -------------------------------------------
循環または非循環リンクリスト
円形のリンク リスト アイコン
非循環リンクリスト図
リンク リスト構造は非常にたくさんありますが、実際に最も一般的に使用される
構造は 2 つあります。ヘッドレス一方向非循環リンクリストと ヘッドレス双方向循環リンクリスト
(3). 一般的に使用される 2 つのリンク リスト構造:
ヘッドレス一方向非巡回リンクリスト
導入:
構造は単純なので、通常はデータの保存に単独で使用されることはありません。
実際には、これは他のデータ構造の下部構造のようなものです。
ハッシュ バケット、グラフの隣接リストなど。
さらに、この構造は書面によるインタビューでもよく現れます。
図:
-------------------------------------------------- -------------------------------------------
見出し付き双方向循環リンクリスト
導入:
この構造は最も複雑で、通常はデータを個別に保存するために使用されます。
実際に使用されるリンク リスト データ構造は、すべて見出し付きの双方向循環リンク リストです。
また、この構造は複雑ですが、
しかし、コードを使用して実装すると、その構造が多くの利点をもたらし、実装がより簡単になることがわかります。
図:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
2. リンクリストの実装
(ヘッドレス+一方向+非巡回リンクリスト)
(詳しい説明は画像のコメントにあり、コードファイルは最後にあります)
特定の機能を実装する前の準備作業
- 単一リンク リストのデータ タイプを作成します。これは、リンク リスト ノードのデータ フィールドに格納されるデータのタイプです。
- 単一リンク リスト ノード構造( type )を作成します-ノードにはデータ フィールドとポインター フィールドが必要です
図
-------------------------------------------------- -------------------------------------------
PrintSList 関数 -- 印刷リストを走査します。
- リンク リストの先頭ポインタ phead を使用する必要があります。その後使用する場合、先頭ポインタは変更できないため、先頭ポインタを置き換える必要があります。
- リンク リストは最後のノードのポインター フィールドが NULL (0x00000000)になるまで終了しないため、while ループを使用して走査して出力
できます。
- 最後のプロンプトは NULL ポインターのリンクされたリストを指し、トラバーサルは終了します。
図
-------------------------------------------------- -------------------------------------------
SLTNode 関数 -- リンク リストのノードを生成します。
- ノードに必要な動的スペースを広げるには、ノードのサイズがデータ フィールドとポインター フィールドのサイズに依存するに注意してください。
- スペースが正常にオープンしたかどうかを確認する
- ノードデータフィールドに入れるデータを入れます。
- 新しく作成したノードのポインタフィールドを設定します
- 作成された新しいノードのポインタ (アドレス)を返します。
図
テスト -- PrintSList、SLTNode 関数
-------------------------------------------------- -------------------------------------------
SLTPushBack関数(難解) -- リンクリストの末尾にノードを挿入(末尾挿入)
- ノード を挿入したいので、まず BuySListNode 関数を使用してノードを作成します。
- 最後に挿入する場合は、リンクリストにすでにノードがあるかどうかを判定する必要があります。ノードがない
場合は、先頭ポインタに新しく作成したノードのアドレスを代入し。ポインタ自体を操作するため、2次ポインタが割り当てられます。ヘッドポインタの操作に使用します。
- すでにノードがある場合は、
最初にヘッド ポインタを置き換えるポインタを作成します
(ここでは、ヘッド ポインタの直線のノード構造を操作するため、ポインタで十分であり、二次ポインタは必要ありません
) 。最後のノードを取得し、その点のアドレスを取得し、
最後に最後に挿入したノードを接続します。図
(単一リンクリストの物理図と併せて理解できます)
↓↓↓↓
テスト -- SLTPushBack 関数
-------------------------------------------------- -------------------------------------------
SLTPushFront 関数 -- リンク リストの先頭にノードを挿入します (先頭挿入)
- ノード を挿入したいので、まず BuySListNode 関数を使用してノードを作成します。
- plist を使用して、元のヘッド ノード アドレスを、新しく挿入されたヘッド ノード ポインター フィールドに割り当てます。
- 次に、ヘッド ポインタを、新しく挿入されたヘッド ノードを指すように変更します。
図
テスト - SLTPushFront 関数
-------------------------------------------------- -------------------------------------------
SLTPopBack 関数 -- リンクされたリストの末尾ノードを削除します (末尾削除)
- リンク リストの末尾を削除する場合は3 つの状況を考慮する必要があります:
1 つ目の状況:リンク リストが空でノードがありません。
この場合は、単にアサートしてください。
- 2 番目のケース:リンク リストにノードが 1 つだけある
場合、唯一のノードを解放する必要があります。
- 3 番目のケース:リンクされたリストに複数のノードがあり、
このケースでは動作するために 2 つのポインターが必要です。図
テスト - SLTPopBack 関数
-------------------------------------------------- -------------------------------------------
SLTPopFront関数 -- リンクリストの先頭ノードを削除(先頭削除)
- ヘッドの削除は、空のリンク リストと空ではないリンク リストの2 つの状況に分けるだけで十分です。
- 空のリンクされたリストのヘッダーを削除します:
直接アサートして渡します
(これは理由もなく面白い点です、ははははははは)
- 空ではないリンクされたリストの先頭を削除します。
最初にplist 先頭ポインタを介して 2 番目のノード アドレスを取得し、先頭を削除した後に元の 2 番目のノードを新しい先頭ノードとして使用できるようにして
から、 U ターン ポインタを解放します。最後に、plist ヘッド ポインタが元のノードを指すように
します。2 番目のノードが新しいヘッド ノードになります。図
テスト - SLTPopFront 関数
-------------------------------------------------- -------------------------------------------
SLTFind 関数 -- 指定された値 (x) が存在するノードのアドレスを検索します。
- ヘッドポインタの代わりに変数を作成する
- while ループを使用してリンク リストを走査し、指定された値 (x) を見つけます。
図
テスト--SLTFind関数
-------------------------------------------------- -------------------------------------------
SLTInsert 関数 -- リンクされたリストの指定された位置 (pos) の前に値 (x) を挿入します。
- 3 つの状況で説明します:
リンク リストが空である (空のリンク リスト)、最初のノードの前に挿入する (ヘッド挿入)、ヘッド以外の挿入
- リンクされたリストは空です。
直接アサートして渡します。
- 最初のノードの前に挿入 (ヘッド挿入) :挿入には
ヘッド挿入 SLTPushFront 関数を再利用します。
- 非ヘッド挿入:リンク リストのヘッド ポインタ
を使用して、 pos ノードの前のノード アドレス prevを検索します。- 新しいノード newnode を作成し、
その新しいノード newnode をpos ノードの前とprev ノードの後に挿入します。図
テスト -- SLTInsert 関数
-------------------------------------------------- -------------------------------------------
SLTInsertAfter 関数 -- リンクされたリストの指定された位置 (pos) の後に値 (x) を挿入します。
- 受信した pos ノード アドレスが空の場合、操作を続行できません。処理を続行するには、assert
を使用する必要があります
- リンク リストに値を挿入したいため、 BuySListNode 関数を使用して新しいノードを追加する必要があります
。
- まず、newnode ノードの next のポインタ フィールドが次のノードを指すようにします。
- 次に、pos ノードが newnode ノードを指すようにして、
リンクされたリストを接続します。図
テスト--SLTInsertAfter関数
-------------------------------------------------- -------------------------------------------
SLTErase 関数 -- リンク リスト内の指定された位置 (pos) にあるノードを削除します。
- 削除する pos ノード ポインターが null ポインターにならないようにするには、
assert を使用します。
ヘッド削除とヘッド以外の削除の2つの状況があります。
- ヘッダー削除:ヘッダー削除 SLTPopFront 関数を
直接再利用します
- 先頭以外の削除:
pos ノードの前のノード prev を検索し
、prevノードをposノードの後のノード に接続します。
- 最後にposノードを解放(削除)して削除操作は完了です。
図
テスト - SLTErase 関数
-------------------------------------------------- -------------------------------------------
SLTEraseAfter 関数 -- リンク リスト内の指定された位置 (pos) 以降のノードを削除します。
- まず、assert アサーションを使用して、pos ノード ポインタが空でposノードが末尾ノードである状況を除外します。posノード以降のノードを削除します
。posが末尾ノードの場合、操作は不可能です。
- 最初に削除するpos ノードの次のノードの posNext アドレスを保存します。
- 次に、 pos ノードを次のノードに接続します
- 最後にposNext ノードを解放 (削除) します。
図
テスト -- SLTEraseAfter 関数
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
3. 対応コード
SList.h -- 単一リンク リスト ヘッダー ファイル
//链表头文件: #pragma once //先包含之后要用到的头文件: #include <stdio.h> #include <stdlib.h> #include <assert.h> //重命名一个单链表数据类型 typedef int SLTDataType; //SLT -- single link table //创建一个单链表结点结构体 //这里数据域是int类型4字节,指针域指针也是4个字节, //所以一个结点就是8个字节 typedef struct SListNode { //结点数据域: SLTDataType data; //结点指针域: //因为是指向单链表结点结构体的指针, //所以是单链表结点结构体类型的指针 struct SListNode* next; //第一个结点要找到第二个结点,物理空间不连续 //通过上个结点指向下个结点,实现逻辑连续 }SLTNode; //将struct SListNode重命名为SLTNode //创建遍历打印单链表的函数 -- 接收单链表头指针 void PrintSList(SLTNode* phead); //创建生成结点的函数 -- 接收要存入结点数据域的数据;返回该结点的指针 SLTNode* BuySListNode(SLTDataType x); //创建尾插函数 -- //使用二级指针接收单链表头指针的地址 和 接收要插入的值 void SLTPushBack(SLTNode** pphead, SLTDataType x); //创建头插函数 -- //使用二级指针接收单链表头指针的地址 和 接收要插入的值 void SLTPushFront(SLTNode** pphead, SLTDataType x); //创建尾删函数 -- //使用二级指针接收单链表头指针的地址 void SLTPopBack(SLTNode** pphead); //创建头删函数 -- //使用二级指针接收单链表头指针的地址 void SLTPopFront(SLTNode** pphead); //创建查找函数 -- //查找指定值(x)所在结点的地址 //接收 链表头指针phead、查找值x SLTNode* SLTFind(SLTNode* phead, SLTDataType x); //创建指定位置插入函数1 -- //在链表指定位置(pos)之前插入一个值(x) //接收 链表头指针地址pphead、指定结点指针pos、插入值x void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //创建指定位置插入函数2 -- //在链表指定位置(pos)之后插入一个值(x) //接收 指定结点指针pos、插入值x void SLTInsertAfter(SLTNode* pos, SLTDataType x); //创建删除指定结点函数1 -- //在链表删除指定位置(pos)结点 ///接收 链表头指针地址pphead、指定结点指针pos void SLTErase(SLTNode** pphead, SLTNode* pos); //创建删除指定结点函数2 -- //在链表删除指定位置(pos)之后一个结点 //接收 指定结点指针pos void SLTEraseAfter(SLTNode* pos);
-------------------------------------------------- -------------------------------------------
SList.c -- 単一のリンク リスト関数実装ファイル
//链表实现文件: #define _CRT_SECURE_NO_WARNINGS 1 //包含链表头文件: #include "SList.h" //创建遍历单链表的函数:接收单链表结点的头指针 void PrintSList(SLTNode* phead) { //phead头指针指向第一个结点, //之后还要多次使用phead头指针, //所以不要改变phead头指针, //所以可以创建一个和phead头指针一样类型的指针cur代替phead头指针 SLTNode* cur = phead; //因为链表到最后结点的指针域为NULL才结束 //所以只要cur不为NULL就继续遍历结点进行打印: while (cur != NULL) { //通过cur当前指向的结点打印cur里数据域的内容: printf("%d->", cur->data); //通过结点里指针域指向下一个结点,方便打印下个结点的数据域内容 cur = cur->next; } //最后提示已指向NULL指针链表,遍历结束: printf("NULL\n"); } //创建生成结点的函数 -- 接收要存入结点数据域的数据;返回该结点的指针 SLTNode* BuySListNode(SLTDataType x) { //给结点开辟动态空间(注意开辟空间的大小是SLTNode结点的大小--8个字节) SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //返回该结点地址 //检查是否开辟成功: if (newnode == NULL) //返回空指针,说明开辟失败 { //打印错误信息: perror("malloc fail"); //开辟失败直接退出程序: exit(-1); } //将接收的值x赋给该结点的数据域: newnode->data = x; //设置新创建结点的指针域: //因为是最新的结点,即最尾部的结点, //所以指针域的指针应是NULL(链表末尾结束) //之后通过指针域连接各个结点的操作要看情况操作,先都初始化为NULL newnode->next = NULL; //返回该新结点的指针(地址) return newnode; } //创建尾插函数 //使用二级指针接收单链表头指针的地址 和 接收要插入的值 void SLTPushBack(SLTNode** pphead, SLTDataType x) { //改变结构体,要用结构体指针 //改变结构体指针,要用结构体指针的指针(二级指针) //先使用newnode函数为要尾插的值创建一个结点 //并返回该结点地址 SLTNode* newnode = BuySListNode(x); //判断phead是否是NULL,如果是说明链表还没开辟过一个结点 if (*pphead == NULL) { //如果为空,则将上面开辟的newnode结点地址赋给phead *pphead = newnode; //要改变结构体的指针,就要用二级指针 //这里pphead是二级指针,存放链表头指针plist的地址 //因为如果用一级指针SLTNode* phead的话, //phead形参只是plist实参的临时拷贝,两者空间相互独立 //改变phead的话不会改变plist,plist还会是空指针 //所以要用二级指针pphead存放plist指针的地址, //*pphead解引用就能得到一级指针plist, //即 *pphead = plist //所以实际上上面的代码就相当于:plist = newnode //这样就让本来指向空指针的头指针指向了新创建的结点指针 //链表就能够连接起来 } else //不为空则往尾部插入新结点: { //创建另一个指针存放单链表头指针 SLTNode* tail = *pphead; //*pphead == plist //使用while循环获得最后一个结点的地址 while (tail->next != NULL) //如果下个结点的next指针域不为NULL, //则继续往后找,直到tail等于最后结点的地址 { //把当前结点指针域里下个结点的地址给到tail, //进行while循环下次判断: tail = tail->next; } //tail找到最后结点地址后, //把尾插的新结点地址给到tail的next指针域, //连接起链表: tail->next = newnode; //要改变结构体,用结构体的指针即可 //因为newnode、tail、pphead都是临时(局部)变量, //所以函数运行结束后都会销毁, //但malloc函数开辟的空间(结点)都在堆上不会销毁 //通过解引用二级指针pphead改变plist也没有问题 } } //创建头插函数 -- //使用二级指针接收单链表头指针的地址 和 接收要插入的值 void SLTPushFront(SLTNode** pphead, SLTDataType x) { //先使用newnode函数为要头插的值创建一个结点 //并返回该结点地址 SLTNode* newnode = BuySListNode(x); //因为也要用到链表头指针本身,所以也要使用二级指针 //因为plist指向原本头结点地址, //所以可以使用plist把原本头结点地址赋给新插入头结点指针域: newnode->next = *pphead; //再把头指针改为指向新插入头结点: *pphead = newnode; } //创建尾删函数 -- //使用二级指针接收单链表头指针的地址 void SLTPopBack(SLTNode** pphead) { //尾删需要考虑三种情况: //注:*pphead 就是 plist // 第一种情况:链表为空 -- 没有任何结点 //没有结点那就删不了了,使用assert接收到空指针就报警告 assert(*pphead); // 第二种情况:链表只有一个结点 if ((*pphead)->next == NULL) // -> 也是解引用(结构体的),优先级比 * 高 //所以 *pphead 要用() 括起来 { //只有一个结点,又要尾删,那就直接把这唯一的结点删掉: free(*pphead); //直接free释放掉plist指向的结点 //再把释放掉的plist置为空指针,防止成为野指针: *pphead = NULL; } else // 第三种情况:链表有一个以上结点 { //这种情况额外两个指针, //一个tail指针 -- 用来找到最后一个结点地址并将其释放, //还有一个tailPrev指针 -- 指向tail指针的前一个结点地址, //用来改变该结点的指针域, //防止原本指向tail结点的指针域变成野指针 //先替代头指针plist: SLTNode* tail = *pphead; //创建tailPrev指针, //之后操作会指向tail结点的前一个结点, //即倒数第二个结点 SLTNode* tailPrev = NULL; //再使用while循环找到尾结点: //和尾插的操作类似 while (tail->next != NULL) { //tail查找之前先把当前指向结点地址给tailPrev //这样最后tail会指向尾结点, //tailPrev会指向倒数第二个结点 tailPrev = tail; tail = tail->next; } //结束while循环后tail指向尾结点地址, //因为是尾删,所以free释放掉tail就可以“删掉”尾结点 free(tail); //因为tail是局部(临时)变量,出了函数就销毁, //所以不置为指针也可以,不用担心成为野指针 //删除原尾结点后,倒数第二个结点成为尾结点, //要把其指针域next置为空指针,成为链表新结尾 tailPrev->next = NULL; } } //创建头删函数 -- //使用二级指针接收单链表头指针的地址 void SLTPopFront(SLTNode** pphead) { //分两种情况: //第一种情况:链表里没有结点(空链表) //没有结点可以删,直接assert断言pass掉: assert(*pphead); //第二种情况:链表有一个和多个结点(非空链表) //因为是删掉头结点,所以不用考虑找尾结点 //所以不用细分一个或多个结点的情况: //这种情况要先通过plist拿到第二个结点的地址: SLTNode* newhead = (*pphead)->next; //使用newhead存储第二个结点地址 //拿到第二个结点地址后,再释放掉头结点, //实现头删效果: free(*pphead); //这时要让第二个结点成为新的头结点: *pphead = newhead; //头指针指向原本的第二个结点 } //创建查找函数 -- //查找指定值(x)所在结点的地址 //接收 链表头指针phead、查找值x SLTNode* SLTFind(SLTNode* phead, SLTDataType x) { //像SLTFind和PrintSList函数只用头指针遍历 //不改变指针本身就不需要用到二级指针 //创建个变量代替头指针: SLTNode* cur = phead; //使用while循环对链表进行遍历查找: while (cur != NULL) //只要cur指针指向地址不为空就继续循环 //while遍历时,(cur->next != NULL) 和 (cur != NULL) 的区别: //(cur->next != NULL):这个条件最后cur会是最后结点的地址 //(cur != NULL):这个条件最后cur会是NULL //(cur->next != NULL) 会比 (cur != NULL) 少一次循环 { //遍历过程中依次查找各结点数据域数据是否与x相同: if (cur->data == x) { //找到了一个结点数据域数据是x,返回该结点地址 return cur; } //这里如果while循环的条件是(cur->next != NULL) //最后一个结点进不来,不能判断最后结点数据域数据是不是x cur = cur->next; //改变循环条件,指向下个结点 } //如果能指向到这说明没找到,返回NULL: return NULL; } //创建指定位置插入函数1 -- //在链表指定位置(pos)之前插入一个值(x) //接收 链表头指针地址pphead、指定结点指针pos、插入值x void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { //第一种情况:空指针 //先排除空指针的情况: assert(pos); //第二种情况:头插 if (pos == *pphead) // *pphead == plist //如果pos是pphead即第一个指针, //则在第一个指针前插入一个值,相当于头插 { //直接复用头插函数SLTPustFront: SLTPushFront(pphead, x); //直接传pphead二级指针过去 } else //第三种情况:非头插 { //从头开始找pos结点的前一个结点: //先获得头指针 SLTNode* prev = *pphead; //当前结点的指针域不是指向pos结点则继续循环 while (prev->next != pos) { prev = prev->next; } //此时prev已指向pos结点的前一个结点 //为要插入的结点创建一个结点newnode: //插入位置是pos结点之前,prev结点之后 SLTNode* newnode = BuySListNode(x); //让prev结点指针域指向新插入结点地址: prev->next = newnode; //newnode结点指针域指向pos结点: newnode->next = pos; //此时newnode新结点就插入完成了 } } //创建指定位置插入函数2 -- //在链表指定位置(pos)之后插入一个值(x) //接收 指定结点指针pos、插入值x void SLTInsertAfter(SLTNode* pos, SLTDataType x) { //因为是在pos结点后插入一个值(结点) //所以不可能会有头插的情况,那就不需要头指针plist //pos指针存储结点地址,可能会接收到空指针 //使用assert断言pass掉 assert(pos); //先为插入值创建一个新结点newnode: SLTNode* newnode = BuySListNode(x); //先让newnode的指针域next指向后一个结点: //这里后一个结点就是pos结点指针域里的地址 //因为未插入前pos就是指向后一个地址 newnode->next = pos->next; //再让pos的指针域next指向newnode: pos->next = newnode; } //创建删除指定结点函数1 -- //在链表删除指定位置(pos)结点 ///接收 链表头指针地址pphead、指定结点指针pos void SLTErase(SLTNode** pphead, SLTNode* pos) { //防止要删的pos结点指针为空指针 //使用assert断言: assert(pos); //分两种情况: // 1.头删: if (pos == *pphead) //pos结点 == 头结点 --> 头删 { //直接复用SLTPopFront头删函数: SLTPopFront(pphead); } else // 2.非头删: //尾删不用单独分出一种情况,因为还得判断是不是尾结点 //直接用通用逻辑删除也可以处理尾删的情况 { //从头开始找pos结点的前一个结点: //先获得头指针 SLTNode* prev = *pphead; //当前结点的指针域不是指向pos结点则继续循环 while (prev->next != pos) { prev = prev->next; } //此时prev已指向pos结点的前一个结点 //因为要删除pos结点, //所以要让pos前一个和后一个结点连接起来: prev->next = pos->next; //连接成功后再把pos结点释放,实现删除效果: free(pos); } } //创建删除指定结点函数2 -- //在链表删除指定位置(pos)之后一个结点 //接收 指定结点指针pos void SLTEraseAfter(SLTNode* pos) { //删除pos结点后的一个结点, //那么就不可能出现头删的情况, //pos结点是尾结点也没用, //因为尾结点后就没有结点可以删了 //使用assert断言处理: assert(pos); //防止接收“空结点” assert(pos->next); //防止接收尾结点 //将要删的pos结点的下个结点posNext先保存起来: SLTNode* posNext = pos->next; //再把pos结点和下下个结点连接起来: pos->next = posNext->next; //posNext的指针域next就是pos结点的下下个结点地址 //最后释放(删除)posNext结点: free(posNext); }
-------------------------------------------------- -------------------------------------------
test.c -- 単一リンクリストのテストファイル
//链表测试文件: #define _CRT_SECURE_NO_WARNINGS 1 /* 链表学习引入小测试: #include <stdio.h> //重命名一个单链表数据类型 typedef int SLTDataType; //SLT -- single link table //创建一个单链表结点结构体 typedef struct SListNode { //结点数据域: SLTDataType data; //结点指针域: //因为是指向单链表结点结构体的指针, //所以是单链表结点结构体类型的指针 struct SListNode* next; }SLTNode; //将struct SListNode重命名为SLTNode //创建遍历单链表的函数:接收单链表结点的头指针 void PrintSList(SLTNode* phead) { //phead头指针指向第一个结点, //之后还要多次使用phead头指针, //所以不要改变phead头指针, //所以可以创建一个和phead头指针一样类型的指针cur代替phead头指针 SLTNode* cur = phead; //因为链表到最后结点的指针域为NULL才结束 //所以只要cur不为NULL就继续遍历结点进行打印: while (cur != NULL) { //通过cur当前指向的结点打印cur里数据域的内容: printf("%d->", cur->data); //通过结点里指针域指向下一个结点,方便打印下个结点的数据域内容 cur = cur->next; } //最后提示已指向NULL指针链表,遍历结束: printf("NULL\n"); } int main() { //创建单链表结点: SLTNode* n1 = (SLTNode*)malloc(sizeof(SLTNode)); n1->data = 10; //在该结点数据域存放数据 SLTNode* n2 = (SLTNode*)malloc(sizeof(SLTNode)); n2->data = 20; //在该结点数据域存放数据 SLTNode* n3 = (SLTNode*)malloc(sizeof(SLTNode)); n3->data = 30; //在该结点数据域存放数据 n1->next = n2; //n1的指针域指向结点n2 n2->next = n3; //n2的指针域指向结点n3 n3->next = NULL; //n3的指针域指向NULL(结束) PrintSList(n1); //调用函数打印单链表 return 0; } */ //包含链表头文件: #include "SList.h" //测试函数1:测试--PrintSList、BuySListNode函数 void TestList1() { int n; //存放链表长度 //打印提示信息: printf("请输入链表的长度:>"); //接收链表长度 scanf("%d", &n); //打印提示信息: printf("\n请依次输入每个结点的值:>"); SLTNode* plist = NULL; //链表头指针,一开始链表没数据所以为NULL //使用for循环,循环创建结点并赋值,形成链表: for (int i = 0; i < n; i++) { int val; //存放结点数据域数据 scanf("%d", &val); //接收结点数据域数据 //使用BuySListNode函数创建结点并给数据域赋值: SLTNode* newnode = BuySListNode(val); //头插: 使用头插把链表连接起来 //把链表头指针plist指向的头结点地址赋给新创建结点的指针域next //这样新结点的指针域next就能指向原来的头结点地址 newnode->next = plist; //再把新创建结点的地址赋给头指针, //这样头指针就指向了新创建结点地址,让其成为新的头结点 plist = newnode; } //使用PrintSList函数打印链表 PrintSList(plist); //接收头指针后打印 //使用SLTPushBack函数手动向链表尾部插入数据(尾插): SLTPushBack(plist, 10000); //再使用PrintSList函数打印插入后的链表 PrintSList(plist); //plist和phead都是单链表头指针, //但 plist是实参 phead是形参 //phead 是 plist 的一份临时拷贝 } //测试函数2:测试--SLTPushBack、SLTPushFront函数 void TestList2() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); SLTPushBack(&plist, 4); SLTPushBack(&plist, 5); //使用头插随机插入几个值: SLTPushFront(&plist, 10); SLTPushFront(&plist, 20); SLTPushFront(&plist, 30); SLTPushFront(&plist, 40); //使用SLTPrintf函数打印该链表: PrintSList(plist); } //测试函数3:测试--SLTPopBack(尾删)函数 void TestList3() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("尾删前链表:\n"); PrintSList(plist); //使用尾删函数: SLTPopBack(&plist); //使用SLTPrintf函数打印该链表: printf("尾删后链表:\n"); PrintSList(plist); SLTPopBack(&plist); //使用SLTPrintf函数打印该链表: printf("尾删后链表:\n"); PrintSList(plist); SLTPopBack(&plist); //使用SLTPrintf函数打印该链表: printf("尾删后链表:\n"); PrintSList(plist); SLTPopBack(&plist); //使用SLTPrintf函数打印该链表: printf("尾删后链表:\n"); PrintSList(plist); //使用SLTPrintf函数打印该链表: printf("尾删后链表:\n"); PrintSList(plist); } //测试函数4:测试--SLTPopFront(头删)函数 void TestList4() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("头删前链表:\n"); PrintSList(plist); SLTPopFront(&plist); //使用SLTPrintf函数打印该链表: printf("头删后链表:\n"); PrintSList(plist); SLTPopFront(&plist); //使用SLTPrintf函数打印该链表: printf("头删后链表:\n"); PrintSList(plist); SLTPopFront(&plist); //使用SLTPrintf函数打印该链表: printf("头删后链表:\n"); PrintSList(plist); SLTPopFront(&plist); //使用SLTPrintf函数打印该链表: printf("头删后链表:\n"); PrintSList(plist); } //测试函数5:测试 -- SLTFind函数 void TestList5() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("操作前链表:\n"); PrintSList(plist); //用SLTFind函数查找链表中数据域为3的结点的地址 SLTNode* pos = SLTFind(plist, 1); if (pos != NULL) { //找到了可以对该结点进行修改 pos->data *= 10; //所以SLTFind查找函数可以负责查找和修改的操作 } printf("操作后链表:\n"); PrintSList(plist); } //测试函数6:测试 -- SLTInsert函数 void TestList6() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("操作前链表:\n"); PrintSList(plist); //用SLTFind函数查找链表中数据域为3的结点的地址 SLTNode* pos = SLTFind(plist, 2); if (pos != NULL) { int x; //接收要插入的值 scanf("%d", &x); //输入要插入的值 SLTInsert(&plist, pos, x); //在2前面插入x } printf("操作后链表:\n"); PrintSList(plist); } //测试函数7:测试 -- SLTInsertAfter函数 void TestList7() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("操作前链表:\n"); PrintSList(plist); //用SLTFind函数查找链表中数据域为3的结点d的地址 SLTNode* pos = SLTFind(plist, 2); if (pos != NULL) { int x; //接收要插入的值 scanf("%d", &x); //输入要插入的值 SLTInsertAfter(pos, x); //在2后面插入x } printf("操作后链表:\n"); PrintSList(plist); } //测试函数8:测试 -- SLTErase函数 void TestList8() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("操作前链表:\n"); PrintSList(plist); //用SLTFind函数查找链表中数据域为3的结点d的地址 SLTNode* pos = SLTFind(plist, 2); if (pos != NULL) { int x; //接收要插入的值 scanf("%d", &x); //输入要插入的值 SLTErase(&plist, pos); //删除pos所在结点 //pos结点指针删除(释放)后,要将其置为空: pos = NULL; } printf("操作后链表:\n"); PrintSList(plist); } //测试函数9:测试 -- SLTEraseAfter函数 void TestList9() { //创建单链表头指针: SLTNode* plist = NULL; //使用尾插随机插入几个值: //(此时头指针指向NULL还没有开辟空间创建结点) SLTPushBack(&plist, 1); SLTPushBack(&plist, 2); SLTPushBack(&plist, 3); //使用SLTPrintf函数打印该链表: printf("操作前链表:\n"); PrintSList(plist); //用SLTFind函数查找链表中数据域为3的结点d的地址 SLTNode* pos = SLTFind(plist, 2); if (pos != NULL) { int x; //接收要插入的值 scanf("%d", &x); //输入要插入的值 SLTEraseAfter(pos); //删除pos结点的下个结点 //pos结点指针删除(释放)后,要将其置为空: pos = NULL; } printf("操作后链表:\n"); PrintSList(plist); } //主函数: int main() { //TestList1(); //TestList2(); //TestList3(); //TestList4(); //TestList5(); //TestList6(); //TestList7(); //TestList8(); TestList9(); return 0; }