【基本データ構造】 3. 線形リストの連結リスト(ヘッドレス+一方向+非巡回連結リスト)

================================================= =======================

関連するコードは gitee から取得できます

C言語学習日記:がんばりましょう(gitee.com)

 ================================================= =======================

前号からの続き:

[要素データ構造] 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;
}

おすすめ

転載: blog.csdn.net/weixin_63176266/article/details/132781490
おすすめ