[Grundlegende Datenstruktur] 3. Verknüpfte Liste in linearer Liste (kopflose + unidirektionale + azyklische verknüpfte Liste)

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

Den entsprechenden Code erhalten Sie bei gitee :

C-Sprachlerntagebuch: Arbeiten Sie weiter hart (gitee.com)

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

Fortsetzung der vorherigen Ausgabe :

[Elementare Datenstruktur] 2. Sequenztabelle in linearer Tabelle_Gaogao Fattys Blog-CSDN-Blog

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

                   

Einführung

Durch die Einführung und Verwendung von Sequenztabellen in der vorherigen Ausgabe

Wir können wissen, dass die Sequenztabelle die folgenden Vor- und Nachteile hat:

            

Vorteile der Sequenztabelle

                  

  • Schwanzeinfügungs- und Schwanzlöschvorgänge sind schnell genug
                   
  • Die Möglichkeit, wahlfreien Zugriff und die Änderung von Indizes zu nutzen , ist sehr praktisch

            

-------------------------------------------------- ---------------------------------------------

            

Nachteile der Sequenztabelle

                  

  • Einfüge- und Löschvorgänge im Kopf/in der Mitte sind langsamer und die Zeitkomplexität beträgt O(N).
                              
  • Für die Kapazitätserweiterung ist die Beantragung von neuem Speicherplatz , das Kopieren von Daten und die Freigabe von altem Speicherplatz erforderlich . Es wird viel Verbrauch geben .
                         
  • Der Kapazitätsausbau erhöht sich in der Regel um den Faktor 2 , was unweigerlich zu einer gewissen Verschwendung von Platz führt .
    Beispiel: Die
    aktuelle Kapazität beträgt 100 , und wenn sie voll ist , wird die Kapazität auf 200 erhöht. Wir fügen weiterhin 5 Daten ein , und
    später werden keine Daten eingefügt , sodass 95 Datenplätze verschwendet werden .

            

            

Die unten eingeführte und implementierte verknüpfte Liste weist diese Probleme nicht auf.

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

             

1. Verknüpfte Liste

(1) Konzept und Struktur der verknüpften Liste:

           

Das Konzept der verknüpften Liste

Eine verknüpfte Liste ist eine physische Speicherstruktur , die nicht kontinuierlich und nicht sequentiell ist .

Die logische Reihenfolge der Datenelemente wird durch die Zeigerverknüpfungsreihenfolge in der verknüpften Liste realisiert .
Einfach verknüpfte Listen werden im Allgemeinen nicht alleine verwendet .
Nur das Einfügen und Löschen von Köpfen ist einfach und effizient zu implementieren.

              

Verknüpfte Listenstruktur

  • Verknüpfte Listen sind auch lineare Listen . Wenn lineare Listen physisch gespeichert werden , werden sie normalerweise in Form von Arrays (sequentiellen Listen) und verknüpften Strukturen ( verknüpften Listen) gespeichert . Verknüpfte Strukturen sind logisch kontinuierlich , aber nicht unbedingt physisch kontinuierlich .


                     
  • In der Realität werden Knoten im Allgemeinen vom Heap aus angewendet .
                  
  • Der vom Heap beantragte Speicherplatz wird nach einer bestimmten Strategie zugewiesen .
    Der doppelt beantragte Speicherplatz kann kontinuierlich oder diskontinuierlich sein.

Illustration:

                     


                    

(2) Klassifizierung verknüpfter Listen:

            

In der Praxis sind die Strukturen verknüpfter Listen sehr unterschiedlich . In Kombination mit
den folgenden Situationen gibt es 8 Arten verknüpfter Listenstrukturen :

           

Einseitig oder zweiseitig verknüpfte Liste

Diagramm einer einseitig verknüpften Liste

                

Doppelt verknüpftes Listendiagramm

            

            

-------------------------------------------------- -------------------------------------------

            

Verkettete Liste mit oder ohne Kopf

Symbol für verknüpfte Liste mit Überschrift

               

Verknüpfte Liste ohne Kopfzeilensymbol

            

            

-------------------------------------------------- -------------------------------------------

            

Zirkuläre oder azyklische verknüpfte Liste

Symbol für kreisförmige verknüpfte Liste

                  

Azyklisches verknüpftes Listendiagramm

            

            

Obwohl es so viele verknüpfte Listenstrukturen gibt, verwenden wir in der Praxis am häufigsten
zwei Strukturen :

Kopflose, nicht kreisförmige, einseitig verknüpfte Liste und  zweiseitig, kreisförmig verknüpfte Liste mit Kopf

                     


                    

(3) Zwei häufig verwendete verknüpfte Listenstrukturen:

            

Kopflose, einseitig azyklische verknüpfte Liste

Einführung:

Die Struktur ist einfach und wird im Allgemeinen nicht allein zum Speichern von Daten verwendet .

In der Praxis handelt es sich eher um eine Unterstruktur anderer Datenstrukturen .

Wie Hash-Buckets , Adjazenzlisten von Diagrammen usw.

Darüber hinaus kommt diese Struktur häufig in schriftlichen Interviews vor .

          

Illustration:

            

            

-------------------------------------------------- -------------------------------------------

            

Zweiseitig zirkulär verknüpfte Liste mit Kopf

Einführung:

Die Struktur ist am komplexesten und wird im Allgemeinen zum separaten Speichern von Daten verwendet .

Die in der Praxis verwendeten verknüpften Listendatenstrukturen sind allesamt bidirektionale, zirkulär verknüpfte Listen mit Kopf .

Obwohl diese Struktur komplex ist ,

Nachdem Sie jedoch Code zur Implementierung verwendet haben, werden Sie feststellen, dass die Struktur viele Vorteile mit sich bringt und die Implementierung einfacher wird .

          

Illustration:

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

             

2. Implementierung einer verknüpften Liste
(Headless + Einweg + azyklische verknüpfte Liste)

(Eine ausführliche Erklärung finden Sie in den Kommentaren zum Bild. Die Codedatei befindet sich am Ende.)

                  

Vorbereitende Arbeiten vor der Implementierung spezifischer Funktionen

  • Erstellen Sie einen einfach verknüpften Listendatentypden Datentyp, der im Datenfeld des verknüpften Listenknotens gespeichert ist
                    
  • Erstellen Sie eine einfach verknüpfte Listenknotenstruktur ( Typ ) – der Knoten sollte ein Datenfeld und ein Zeigerfeld haben

Illustration

            

            

-------------------------------------------------- -------------------------------------------

            

PrintSList-Funktion – Durchlaufen der Druckliste

  • Der Kopfzeiger der verknüpften Liste phead muss verwendet werden . Für die spätere Verwendung kann der Kopfzeiger nicht , daher muss der Kopfzeiger ersetzt werden.
                     
  • Da die verknüpfte Liste erst bei NULL (0x00000000) im Zeigerfeld im letzten Knoten endet , können Sie zum Durchlaufen und Drucken eine While-Schleife verwenden

                  
  • Die letzte Eingabeaufforderung zeigt auf den NULL-Zeiger der verknüpften Liste und der Durchlauf endet.

Illustration

            

            

-------------------------------------------------- -------------------------------------------

            

SLTNode-Funktion – generiert die Knoten der verknüpften Liste

  • Um den für Knoten erforderlichen dynamischen Raum zu erschließen , ist zu beachten , dass die Größe eines Knotens von der Größe des Datenfelds und des Zeigerfelds abhängt .
               
  • Prüfen Sie, ob der Space erfolgreich geöffnet wurde
               
  • Geben Sie die Daten ein, die in das Knotendatenfeld eingefügt werden sollen
               
  • Legen Sie das Zeigerfeld des neu erstellten Knotens fest
               
  • Gibt den Zeiger (Adresse) des neu erstellten Knotens zurück

Illustration

Test – PrintSList, SLTNode-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTPushBack-Funktion (schwierig) – Fügen Sie einen Knoten am Ende der verknüpften Liste ein (Endeinfügung).

  • Da Sie einen Knoten einfügen möchten , verwenden Sie zunächst die Funktion BuySListNode, um einen Knoten zu erstellen.
               
  • Beim Einfügen am Ende muss festgestellt werden, ob die verknüpfte Liste bereits einen Knoten hat .
    Wenn kein Knoten vorhanden ist , wird die Adresse des neu erstellten Knotens dem Kopfzeiger
    zugewiesen . Da der Zeiger selbst bedient wird , handelt es sich um einen sekundären Zeiger dient zur Bedienung des Kopfzeigers.

               
  • Wenn bereits ein Knoten vorhanden ist , erstellen Sie
    zunächst einen Zeiger, um den Kopfzeiger zu ersetzen .
    (Hier arbeiten wir mit der Knotenstruktur der geraden Linie des Kopfzeigers , sodass ein Zeiger ausreicht und kein sekundärer Zeiger erforderlich ist .) Verwenden Sie
    dann eine While-Schleife um den letzten Knoten zu erhalten. Die Adresse des Punktes und
    schließlich die am Ende eingefügten Knoten verbinden.

Illustration

(Kann in Verbindung mit dem physikalischen Diagramm einer einfach verknüpften Liste verstanden werden )

↓↓↓↓

Test – SLTPushBack-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTPushFront-Funktion – Fügen Sie einen Knoten in den Kopf der verknüpften Liste ein (Kopfeinfügung).

  • Da Sie einen Knoten einfügen möchten , verwenden Sie zunächst die Funktion BuySListNode, um einen Knoten zu erstellen.
               
  • Verwenden Sie plist, um die ursprüngliche Kopfknotenadresse dem neu eingefügten Kopfknoten-Zeigerfeld zuzuweisen.
               
  • Ändern Sie dann den Kopfzeiger so, dass er auf den neu eingefügten Kopfknoten zeigt

Illustration

Test – SLTPushFront-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTPopBack-Funktion – Löschen Sie den Endknoten der verknüpften Liste (Endlöschung).

  • Beim Löschen des Endes einer verknüpften Liste müssen drei Situationen berücksichtigt werden :
    Die erste Situation : Die verknüpfte Liste ist leer und hat keine Knoten.
    In diesem Fall bestätigen Sie einfach
               
  • Der zweite Fall : Die verknüpfte Liste hat nur einen Knoten.
    In diesem Fall muss der einzige Knoten freigegeben werden.
               
  • Der dritte Fall : Die verknüpfte Liste hat mehr als einen Knoten.
    In diesem Fall sind zwei Zeiger für den Betrieb erforderlich.

Illustration

Test – SLTPopBack-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTPopFront-Funktion – Löschen Sie den Kopfknoten der verknüpften Liste (Kopflöschung).

  • Es reicht aus, die Kopflöschung in zwei Situationen zu unterteilen : leere verknüpfte Liste und nicht leere verknüpfte Liste.
               
  • Löschen Sie den leeren Kopf der verknüpften Liste : Bestätigen Sie ihn
    direkt und übergeben Sie ihn
    (dies ist ein lustiger Punkt ohne Grund, hahahahahahahaha)
               
  • Löschen des Kopfes einer nicht leeren verknüpften Liste : Erhalten Sie
    zuerst die Adresse des zweiten Knotens über den Plist-Kopfzeiger , sodass der ursprüngliche zweite Knoten nach dem Löschen des Kopfes als neuer Kopfknoten verwendet werden kann,
    und geben Sie dann den U-Turn-Zeiger frei .
    Lassen Sie schließlich den Plist-Kopfzeiger auf den Originalknoten zeigen. Der zweite Knoten wird zum neuen Kopfknoten

Illustration

Test – SLTPopFront-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTFind-Funktion – Finden Sie die Adresse des Knotens, auf dem sich der angegebene Wert (x) befindet

  • Erstellen Sie eine Variable anstelle des Kopfzeigers
               
  • Verwenden Sie eine While-Schleife , um die verknüpfte Liste zu durchlaufen und den angegebenen Wert (x) zu finden.

Illustration

Test – SLTFind-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTInsert-Funktion – Fügt einen Wert (x) vor der angegebenen Position (pos) in der verknüpften Liste ein

  • Besprechen Sie drei Situationen :
    Die verknüpfte Liste ist leer (leere verknüpfte Liste) , wird vor dem ersten Knoten eingefügt (Kopfeinfügung) und wird ohne Kopf eingefügt
               
  • Die verknüpfte Liste ist leer :
    direkt bestätigen und übergeben.
               
  • Einfügen vor dem ersten Knoten (Kopfeinfügung) :
    Verwenden Sie die SLTPushFront-Funktion zum Einfügen des Kopfes erneut zum Einfügen
               
  • Nicht-Kopf-Einfügung :
    Verwenden Sie den Kopfzeiger der verknüpften Liste, um die vorherige Knotenadresse prev des POS-Knotens zu finden
  • Erstellen Sie einen neuen Knoten newnode
    und fügen Sie den neuen Knoten newnode vor dem pos-Knoten und nach dem prev-Knoten ein.

Illustration

Test – SLTInsert-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTInsertAfter-Funktion – Fügt einen Wert (x) nach der angegebenen Position (pos) in der verknüpften Liste ein

  • Wenn die empfangene POS-Knotenadresse leer ist , kann der Vorgang nicht fortgesetzt werden.
    Sie müssen Assert verwenden , um die Verarbeitung fortzusetzen.
               
  • Da Sie einen Wert in die verknüpfte Liste einfügen möchten , müssen Sie die Funktion BuySListNode verwenden
    , um einen neuen Knoten hinzuzufügen.
               
  • Lassen Sie zunächst das Zeigerfeld neben dem neuen Knoten auf den nächsten Knoten zeigen.
               
  • Lassen Sie dann den POS-Knoten auf den Newnode-Knoten zeigen , um
    die verknüpften Listen zu verbinden.

Illustration

Test – SLTInsertAfter-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTErase-Funktion – löscht den Knoten an der angegebenen Position (pos) in der verknüpften Liste

  • Um zu verhindern, dass der zu löschende POS-Knotenzeiger ein Nullzeiger ist ,
    verwenden Sie Assert.
               
  • Es gibt zwei Situationen :
    Löschen des Kopfes und Löschen des Nicht-Kopfes.
               
  • Header-Löschung : Verwenden Sie die SLTPopFront-Funktion zum Löschen von Headern
    direkt wieder
               
  • Nicht-Kopf-Löschung :
    Suchen Sie den vorherigen Knoten vor dem POS-Knoten und verbinden Sie den
    vorherigen Knoten mit dem Knoten nach dem POS-Knoten .
               
  • Geben Sie abschließend den POS-Knoten frei (löschen), um den Löschvorgang abzuschließen

Illustration

Test – SLTErase-Funktion

            

            

-------------------------------------------------- -------------------------------------------

            

SLTEraseAfter-Funktion – löscht einen Knoten nach der angegebenen Position (pos) in der verknüpften Liste

  • Verwenden Sie zunächst die Behauptungszusicherung, um die Situation auszuschließen , dass der pos-Knotenzeiger leer ist und der pos-Knoten der Endknoten ist
    . Wir löschen den Knoten nach dem pos-Knoten .
    Wenn pos der Endknoten ist , ist die Operation nicht möglich .
               
  • Speichern Sie zuerst die posNext-Adresse des nächsten Knotens des zu löschenden POS-Knotens.
               
  • Verbinden Sie dann den POS-Knoten mit dem nächsten Knoten
               
  • Geben Sie abschließend den posNext-Knoten frei (löschen).

Illustration

Test – SLTEraseAfter-Funktion

         

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

             

3. Entsprechender Code

SList.h – einfach verknüpfte Listenheaderdatei

//链表头文件:
#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 – Implementierungsdatei für eine einzelne verknüpfte Listenfunktion

//链表实现文件:
#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 – Testdatei für einfach verknüpfte Listen

//链表测试文件:
#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;
}

Ich denke du magst

Origin blog.csdn.net/weixin_63176266/article/details/132781490
Empfohlen
Rangfolge