【データ構造】C--単一連結リスト(初心者向け基礎知識)

少し前にシーケンス テーブルに関するブログを書きました ( http://t.csdn.cn/0gCRp)

シーケンス テーブルには、避けられない欠点がある場合があります。

質問:
1. 途中 / 先頭の挿入と削除、時間計算量は O(N)
2. 容量を増やすには、新しいスペースを申請し、データをコピーし、古いスペースを解放する必要があります。消費量も多くなるでしょう。
3. 容量の増加は通常 2 倍であり、ある程度のスペースが無駄になるはずです。たとえば、現在の容量は 100 で、いっぱいになると 200 に増やされます。引き続き 5 つの データを挿入しますが、その後データは挿入されないため、 95 個の データ スペースが無駄になります。
他の解決策を探しています:
1. 容量拡張なし
2. オンデマンドでリリースを申請する
3. 先頭・途中の挿入・削除時にデータの移動が必要(連続した物理空間)の問題を解決

                       1. 単結合リストの概念、構造、メリット、デメリット

1.1 コンセプト

概念: リンク リストは、 物理ストレージ構造における 非連続かつ非順次のストレージ構造です。データ要素の 論理順序 は、リンク リストによって決まります。
のポインタリンク 順序 が実装されています。

1.2 単結合リストの構造

単結合リストは一連のノードで構成される線形構造であり、各ノードにはデータ フィールドポインター フィールドの2 つのフィールドが含まれます。

データフィールドはデータを格納するために使用され、ポインタフィールドは次のノードのポインタを格納するために使用されます。単一リンクリストのヘッドノードは最初のノードを指し、最後のノードのポインタフィールドは空です。

ノードの構造:

論理構造:イメージを理解しやすくするために、イメージ化しています。

 物理的構造: 実際の記憶、実際の外観。

1.3 単一リンクリストの長所と短所

単一リンク リストは一般的なデータ構造ですが、次のような利点と欠点があります。

アドバンテージ:

  1.         挿入および削除操作の高効率: 単一リンクリストのノードには次のノードへのポインタが含まれているため、ノードの挿入および削除の際、ポインタの位置のみを変更する必要があり、大量のノードを移動する必要はありません。データ要素が少ないため、効率が高くなります。
  2.         高いスペース使用率: 単一リンク リストのノードには、データとポインターの 2 つの部分のみが含まれており、メモリ スペースを事前に割り当てる必要がないため、スペース使用率は比較的高くなります。
  3.         可変長: 単一リンクリストの長さは、必要に応じて動的に増減でき、配列のサイズを事前に定義する必要がないため、柔軟性に優れています。

欠点:

  1.         ランダムアクセス効率が低い: 単結合リストのノードには次のノードへのポインタしか含まれていないため、あるノードより前のノードに直接アクセスすることは不可能であり、ヘッドノードからノードまでを横断する必要があるため、ランダムアクセスは効率が低いです。
  2.         記憶領域の無駄:単結合リストの各ノードは次のノードのポインタを記憶する必要があるため、同じ量のデータを記憶する場合、単結合リストの方がより多くの記憶領域を必要とする。
  3.         リンク情報の損失: 単一リンクリストには次のノードへのポインタのみが存在し、前のノードに直接アクセスできないため、リンクリストを逆にたどったり、ノードを削除したりする必要がある場合、前のノードのポインタを保存しないと、操作は完了しません。

2. 単一リンクリストの実装

単一リンクリストの各インターフェース機能

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;//这样做的目的是为了增加代码的可读性和可维护性,以及提高代码的可移植性,
//因为如果将来需要更改 SLTDataType 的类型,只需要在 typedef 语句中修改一处即可,
// 如果我们在程序的其他地方需要修改 SLTDataType 的类型,
//只需在 typedef 语句中修改 int 为其他类型即可,不需要修改其他代码。
//typedef int SLTADataType;

typedef struct SListNode //--single Linked List
{
	SLTDataType data;//成员变量
	struct SListNode* next;
}SLTNode;

void SLTPrint(SLTNode* phead);

//void SLPushFront(SLTNode* pphead,SLTDataType x);
void SLPushFront(SLTNode** pphead, SLTDataType x);//头部插入

//void SLPushBack(SLTNode* phead, SLTDataType x);
void SLPushBack(SLTNode** pphead, SLTDataType x);//尾部插入

void SLPopFront(SLTNode** pphead);//头部删除

void SLPopBack(SLTNode** pphead);//尾部删除

//单链表查找
SLTNode* STFind(SLTNode* phead, SLTDataType x);

//单链表pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//单链表pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDataType x);
//单链表pos位置删除
void SLErase(SLTNode** pphead, SLTNode* pos);
//单链表pos之后删除
void SLEraseAfter(SLTNode* phead);

2.1 ノードの定義

英語の略語:

単一リンクリストの英語は次のとおりです: Single linked list -- 略称 SL

シーケンス テーブルの英語は次のとおりです。 Sequence table -- 略して Seq

ノードの英語は次のとおりです。

typedef の主な機能は次のとおりです。これは主にコードの可読性と保守性を向上させるために使用され、SLTDataType という名前が変数 x の型の意味を説明するため、コードの可読性が向上します。より明確なエイリアスにより、コードがより読みやすく、保守しやすくなります。

typedef int SLTDataType;
typedef struct SListNode //--single Linked List
{
	SLTDataType data;//成员变量
	struct SListNode* next;
}SLTNode;

単一リンク リスト ノード の構造を定義します 。これには、 という名前の int 変数と、  という名前の次のノードへのポインタというSLTNode2 つのメンバー変数が含まれます 。data SLTDataTypenext

2.2 リンクリストの印刷

知っておくべきこと:

                               「ポインタ代入の本質は、あるポインタ変数のアドレスを別のポインタ変数にコピーすることです。」

int *p1, *p2; 

p1 = (int*)malloc(sizeof(int)); // 为p1分配内存
*p1 = 10; // 设置p1指向的内存的值为10

p2 = p1; // 将p1的地址赋值给p2

上記のコードでは、p1 と p2 は両方ともポインター変数です。p1 は割り当てられたメモリであり、そのメモリを指します。コード行 p2 = p1 は、p1 のアドレスを p2 にコピーするため、p2 も p1 が指すメモリを指します。

したがって、ポインタ割り当て後、2 つのポインタ変数はメモリの同じブロックを指し、どのポインタ変数を介してメモリにアクセスしても値は同じになります。

ポインタの割り当てでは、ポインタが指すメモリの内容はコピーされずポインタ変数のアドレス値のみがコピーされます

描画:

 コード:

//函数的作用是遍历单链表,并将每个节点的数据元素打印到屏幕上。
void SLTPrint(SLTNode* phead)//参数是一个指向 SLTNode 类型的指针 phead,表示单链表的头节点。
{
	SLTNode* cur = phead;//头结点存储的地址给cur指针。
	while (cur != NULL)//使用一个while循环对单链表进行遍历,循环条件为 cur 不为 NULL。
	{    //cur 可以表示当前正在处理的节点的地址,
		//通过访问 cur->data 和 cur->next 成员变量,可以获取当前节点的数据元素和下一个节点的地址
		
		printf("%d->", cur->data);
		cur = cur->next;//cur是存着当前结点的地址,cur->next是下一结点地址
	}
	printf("NULL\n");
}


2.3 新しいノードの作成

SLTNode* BuyLTNode(SLTDataType x)//表示要创建的节点的数据元素。
//函数的作用是创建一个新的单链表节点,并将其初始化为包含数据元素 x 的节点。
{
	//SLTNode node;//这样是不行,处于不同的空间,出了作用域是会销毁的。

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//使用malloc函数动态分配了一个大小为SLTNode的内存块,
	//并将其强制转换为 SLTNode 指针类型,即创建了一个新的节点。
	
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	//需要注意的是,在使用 malloc 函数动态分配内存时,需要手动释放内存,否则会导致内存泄漏
	//因此,在创建单链表节点后,我们应该在适当的时候使用 free 函数释放节点所占用的内存
	
	newnode->data = x;
	newnode->next = NULL;

	return newnode;//返回的是一个结点的地址
}

この関数が行うことは、新しい単一リンク リスト ノードを作成し、データ要素 x を含むように初期化することです。具体的な実装プロセスは、malloc 関数を使用してメモリ ブロックを動的に割り当て、それを SLTNode ポインタ型にキャストすることです。つまり、新しいノードが作成されます。次に、ノードのデータ メンバーを x に設定し、次のメンバーを NULL に設定し、最後に新しいノードのポインター アドレスを返します。

malloc 関数を使用して動的にメモリを割り当てる場合、メモリを手動で解放する必要があることに注意してください。解放しないとメモリ リークが発生します。したがって、単一リンクリストノードを作成した後、適切なタイミングで free 関数を使用してノードが占有しているメモリを解放する必要があります。

2.4 シングルリンクリストテールプラグ

エラーコード1:

図:

void SLLPushBack(SLTNode* phead, SLTDataType x)
{     SLTNode* tail = phead;     while (tail != NULL)     {           tail = tail->next;     }




    SLTNode* newnode = BuyLTNode(x);
    tail = newnode;//この場所はリンクされていません
}

理由:
1. リンク リストがリンクされていない
2. ポインタ変数がスコープ外で破棄され、メモリ リークが発生する

 メモリ リークに関する知識を追加します。

malloc 関数によって割り当てられたメモリ空間を解放するために free 関数が使用されない場合、これらのメモリ空間はプログラムの終了またはオペレーティング システムの再起動まで常にシステム リソースを占有します。

プログラムが終了すると、オペレーティング システムは、free 関数によって解放されなかった領域を含む、プログラムによって使用されていたすべてのメモリ領域を再利用しますただし、プログラムの実行中、これらの未解放のメモリ空間は常にシステム リソースを占有するため、システムメモリ リソースが枯渇し、システムの安定性とパフォーマンスに影響を与える可能性があります。

したがって、メモリ リークを回避するには、開発者はプログラム内で明示的に free 関数を使用して、使用されなくなったメモリ領域を解放し、システム リソースを有効に使用する必要があります。

エラーコード2:

void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode* phead, SLTDataType x);
void TestSList1() 
{     SLTNode* plist = NULL;     SLPushFront(&plist,1);     SLPushFront(&plist,2);     SLPushFront(&plist,3);     SLPushFront(&plist,4);




    SLTPrint(plist);
    SL​​PushBack(plist, 5);//これは第 1 レベルのポインタです
    SLTPrint(plist);

}
void SLPushBack(SLTNode* phead, SLTDataType x)
{     SLTNode* tail = phead;     while (tail->next!= NULL)     {         tail = tail->next;     }




    SLTNode* newnode = BuyLTNode(x);
    末尾->次 = 新しいノード;
}

この場合、最初にヘッドが差し込まれ、次にテールが差し込まれます。最初のプラグは毎回第 2 レベルのポインタを必要としますが、テール プラグは最初に第 2 レベルのポインタを通過する必要があり、2 回目はレベル ポインタは後から渡すことができますが、パラメータは均一なので書き込むことはできません。2 つの同一の関数の場合、第 1 レベル ポインタが渡され、第 2 レベル ポインタが後で渡されます。

したがって、セカンダリ ポインタを一貫して渡します。

この状況の前提条件は、リンク リストが空ではなく、構造の次の部分を直接変更して新しいノードのアドレスを保存できることです。

埋め込む:

エラーコード 3:

描画:

void SLPushFront(SLTNode** pphead, SLTDataType x);
void SLPushBack(SLTNode* phead, SLTDataType x);

void TestSList2()
{     SLTNode* plist = NULL;     SLPushBack(plist, 1);     SLPushBack(plist, 2);     SLTPrint(plist); }




void SLLPushBack(SLTNode* phead, SLTDataType x)
{     SLTNode* newnode = BuyLTNode(x);


    if (phead == NULL)
    {         phead = 新しいノード;     }     else     {         SLTNode* テール = phead;         while (tail->next != NULL)         {             tail = tail->next;         }








        末尾->次 = 新しいノード;
    }
}

出力:

 非常に多くの間違ったケースを列挙してから、正しいケースに進みます。

void SLPushBack(SLTNode** pphead, SLTDataType x)
void TestSList2()
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);
}
//要让新节点和tail链接起来,一定要去改tail的next
void SLPushBack(SLTNode** pphead, SLTDataType x)//尾插的本质是让上一个结点链接下一个结点
{
	SLTNode* newnode = BuyLTNode(x);
	// 1、空链表
	// 2、非空链表
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}

}

 コードの実行:

2.5シングルリンクテーブルプラグ

ヘッドプラグのアイデア:

2 番目のノードを挿入するアイデア:

ヘッダーにプラグインするコードは次のように記述できます

void SLPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//使用malloc函数动态分配了一个大小为SLTNode的内存块,
	//并将其强制转换为 SLTNode 指针类型,即创建了一个新的节点。

	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	//需要注意的是,在使用 malloc 函数动态分配内存时,需要手动释放内存,否则会导致内存泄漏
	//因此,在创建单链表节点后,我们应该在适当的时候使用 free 函数释放节点所占用的内存

	newnode->data = x;
	newnode->next = NULL;

	//return newnode;//返回的是一个结点的地址
	newnode->next = *pphead;
	*pphead = newnode;//将头节点*pphead 更新为新节点的地址,以使新节点成为新的头节点。
}

ただし、新しいノードを作成するためのプログラムを繰り返し記述することを避けるために、上記の BuyLTNode(x) 関数を使用して新しいノードを定義し、省略の目的を達成するか、プラグインできる関数を次のように記述することもできます。 :

void SLPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyLTNode(x);

	newnode->next = *pphead;
	*pphead = newnode;
}

SLPushFront この関数 では 、まずBuyLTNode(x) 関数を呼び出して新しいノードを作成し、次に next 新しいノードのポインタを現在のリンク リストの先頭ノードにポイントし、次にリンク リストの先頭のポインタを新しいノードにポイントします。これにより、リンクされたリストの先頭に新しいノードを挿入する操作が完了します。

コードの実行:

2.6 単一リンクリスト末尾の削除 

エラーの場合:

 方法 1:

方法 2:

 コード例:

void SLPopBack(SLTNode** pphead)
{
	//没有节点(空链表)
	//暴力检查
	assert(*pphead);

	//温柔检查
	if (*pphead == NULL)
	{
		return;
	}
	//一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}//多个节点
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		//找尾
		//方法一
		/*while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;*/

		//方法二
		SLTNode* tail = *pphead;
		while (tail->next->next)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}

埋め込む: 

 

 2.7 単一リンクヘッダーの削除

アイデア:

void SPopFront(SLTNode** pphead)
{
	//没有节点
	//暴力检查
	assert(*pphead);
	//温柔检查
	if (*pphead == NULL)
		return;// 一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点
	else
	{
		SLTNode* del = *pphead;//相当于一个标记,删掉的标记
		//写法一
		//*pphead = del->next;
		//写法二 
		*pphead = (*pphead)->next;
		free(del);
	}



}

埋め込む:

2.8 値を検索/変更するための単一リンクリスト

末尾挿入を使用して、リンク リストに 1、2、3、4 を順番に挿入し、リンク リストで値 3 の要素を見つけて、それを 30 に変更します (データが 3 のノードにアクセスするには、構造体型 point のポインタを定義し、pos->next=30 を通じて直接変更します)

注: main 関数内でテスト関数を直接定義して値を変更できます。新しい関数を定義する必要はありません。

SLTNode* STFind(SLTNode* phead, SLTDataType x)
{
	//assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

埋め込む:

2.9 単一リンクリストは pos の前に挿入されます

アイデア:

void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);//&plist
	assert(pos);
	//assert(*pphead);
	//一个节点
	if (*pphead == NULL)
	{
		SLPushFront(pphead, x);
	}
	else//多个节点
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

 埋め込む:

2.10 単一リンクリストは pos の後に挿入されます

アイデア:

void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	SLTNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

2.11 単一リンクリストは pos の値を削除します

アイデア: 

void SLErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SLPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next!=pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

2.12 単一リンクリストは pos の後の値を削除します

アイデア:

void SLEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* next= pos->next;
	pos->next = next->next;
	free(next);
}

 

3. Case関数の実現

3.1 テスト

void TestSList1() 
{
	SLTNode* plist = NULL;
	SLPushFront(&plist,1);
	SLPushFront(&plist,2);
	SLPushFront(&plist,3);
	SLPushFront(&plist,4);

	SLTPrint(plist);
	SLPushBack(plist, 5);
	SLTPrint(plist);

}

3.2ヘッドプラグ

void TestSList2()
{
	SLTNode* plist = NULL;
	SLPushFront(&plist, 1);
	SLPushFront(&plist, 2);
	SLPushFront(&plist, 3);
	SLPushFront(&plist, 4);

	SLTPrint(plist);
	SLPushBack(&plist, 5);

	SLTPrint(plist);
}

3.3テールプラグ

void TestSList3()
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLTPrint(plist);
	SLPushBack(&plist, 2);
	SLTPrint(plist);
	SLPushBack(&plist, 3);
	SLTPrint(plist);
	SLPushBack(&plist, 4);
	SLTPrint(plist);
}

3.4 ヘッダーの削除

void TestSList4()
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);
	//头删

	SLPopFront(&plist);
	SLTPrint(plist);
	
	SLPopFront(&plist);
	SLTPrint(plist);
	
	SLPopFront(&plist);
	SLTPrint(plist);

	SLPopFront(&plist);
	SLTPrint(plist);
}

3.5 テールの削除

void TestSList5()
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);

	SLPopBack(&plist);
	SLTPrint(plist);

	SLPopBack(&plist);
	SLTPrint(plist);

	SLPopBack(&plist);
	SLTPrint(plist);
}

3.6 値の検索/変更

void TestSList6()
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);

	SLTNode* pos = STFind(plist,3);
	if (pos)
		pos->data = 30;

	SLTPrint(plist);
}

3.7 pos の前に挿入 

void TestSList7()//pos之前插入 
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);

	SLTNode* pos = STFind(plist,3);
	if (pos)
	{
		SLInsert(&plist, pos, 30);
	}
	SLTPrint(plist);
}

3.8 pos の後に挿入 

void TestSList8()//pos之后插入 
{
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);

	SLTNode* pos = STFind(plist, 3);
	if (pos)
	{
		SLInsertAfter(pos, 50);
	}
	SLTPrint(plist);
}

3.9 pos 位置の値を削除する

void TestSList9() {
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLTPrint(plist);

	SLTNode* pos = STFind(plist, 3);
	if (pos)
	{
		SLErase(&plist,pos);
	}
	SLTPrint(plist);


}

3.10 pos以降の値を削除

void TestSList10() {
	SLTNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	SLPushBack(&plist, 5);
	SLTPrint(plist);

	SLTNode* pos = STFind(plist, 2);
	if (pos)
	{
		SLEraseAfter(pos);
	}
	SLTPrint(plist);
}

この章の最後に、間違いがある場合は、ご指摘ください。ご訪問いただきありがとうございます。

おすすめ

転載: blog.csdn.net/weixin_65186652/article/details/131777464