双向链表-(增删减改)+双链表实现通讯录项目

声明

单链表(增删减改)+单链表实现通讯录项目+链表的专用题型-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137722729

双链表和单链表就是异曲同工

链表的分类

这里我们主要讲解的是不带头的单向不循环链表,在题型解析里面,我们会讲解带头单向循环链表。

什么是双链表

双链表是线性表的链式存储结构的一种,它除了包含线性表所具有的数据元素外,每个数据元素还包含两个指针,分别指向前一个元素和后一个元素。这样,通过这两个指针,就可以很方便地访问双链表中的任何一个元素的前驱和后继。

在双链表中,每个节点通常包含三个部分:

  1. 数据域:用于存储节点的数据。

  2. 左指针域:指向节点的前一个节点。

  3. 右指针域:指向节点的后一个节点。

双链表的主要特点和优势如下:

  • 删除和插入操作更加高效,因为可以直接通过指针跳转到前一个或后一个节点,不需要像数组那样进行搬移操作。

  • 可以很方便地实现循环链表。

  • 由于每个节点都有两个指针,因此需要更多的空间来存储数据。

双链表常用于实现一些高级数据结构,如栈、队列、双向队列等。

双链表的概念

双链表是一种线性数据结构,它由一系列节点组成,每个节点都包含数据域和两个指针域,分别称为左指针和右指针。这种结构使得双链表中的每个节点都能够快速地访问其前一个节点和后一个节点。

双链表的节点结构通常如下所示:

struct Node {
    elementType data;  // 数据域
    Node* next;        // 右指针,指向下一个节点
    Node* prev;        // 左指针,指向前一个节点
};

在这个结构中,`elementType` 是节点的数据类型,它可以是任何用户自定义的类型或者是基本数据类型。`next` 指针指向双链表中紧邻当前节点的下一个节点,而 `prev` 指针则指向前一个节点。

双链表的特点和用途包括:

  1. **快速访问**:在双链表中,每个节点都可以通过 `next` 和 `prev` 指针快速访问其前驱和后继节点,而不需要像数组那样需要通过索引逐个访问。

  2. **高效的插入和删除**:在双链表中插入或删除节点时,由于不需要移动其他节点,只需要改变指针的指向,因此这些操作的时间复杂度为 O(1)。

  3. **可以实现循环链表**:通过适当设置头节点的 `next` 和 `prev` 指针,可以实现循环链表,这在某些算法中非常有用。

  4. **空间开销**:由于每个节点都需要存储两个指针,因此双链表比单链表或数组结构占用更多的空间。

双链表常用于实现各种数据结构,如双向队列、双向栈、双向链表等。这些数据结构在需要频繁的插入和删除操作,并且需要维护元素的前后关系时特别有用。

双向链表的结构

顺序表

单链表

双链表

双向链表的语法

  1. 定义节点结构体:在C语言中,我们可以定义一个节点结构体来表示双向链表的节点,包括数据域、指向下一个节点的指针(next)和指向前一个节点的指针(prev)。

    typedef struct Node {
    int data; // 数据域
    struct Node* next; // 指向下一个节点的指针
    struct Node* prev; // 指向前一个节点的指针
    } Node;

顺序表和双向链表的优缺点分析

顺序表和双向链表都是线性数据结构,它们在内存中存储数据的方式不同,因此各自具有不同的优缺点。

**顺序表**:

优点:

  1. **随机访问**:顺序表中的元素按照一定的顺序存储在连续的内存空间中,因此可以通过索引直接访问任何一个元素,访问速度快。

  2. **空间利用率高**:顺序表在分配内存时可以一次性分配足够大的空间,可以减少内存碎片。

  3. **节省存储空间**:顺序表不需要存储额外的指针信息,因此每个元素只需要存储数据本身,节省存储空间。

缺点:

  1. **插入和删除操作效率低**:在顺序表中插入或删除一个元素时,可能需要移动大量元素,时间复杂度为 O(n)。

  2. **大小固定**:顺序表在创建时需要指定大小,如果空间用尽,需要扩容,扩容通常涉及到申请新的更大的内存空间,并将原有数据复制过去,效率较低。

**双向链表**:

优点:

  1. **插入和删除操作效率高**:双向链表在插入或删除节点时只需要改变指针的指向,不需要移动其他元素,时间复杂度为 O(1)。

  2. **无空间浪费**:双向链表根据实际需要动态分配节点,不会像顺序表那样存在空间浪费。

  3. **可以轻松实现循环结构**:双向链表可以通过设置头节点的指针来实现循环结构,这在某些算法中非常有用。

缺点:

  1. **随机访问效率低**:双向链表不能像顺序表那样通过索引直接访问元素,需要从头节点开始逐个遍历,访问速度慢。

  2. **存储额外信息**:每个节点需要存储两个指针,增加了存储空间的消耗。

总的来说,顺序表适合于频繁的随机访问操作,而双向链表适合于频繁的插入和删除操作。在实际应用中,应根据具体需求选择合适的数据结构。

头结点

头结点(Sentinel Node)是双链表中的一种特殊节点,它在逻辑上不属于双链表中的有效数据节点,但其存在对双链表的操作至关重要。头结点具有以下特点:

  1. **始终存在**:在创建一个双链表时,头结点被首先创建,并且在其生命周期内始终存在。

  2. **不存储实际数据**:头结点不包含任何有效数据,它的作用主要是为了简化双链表的操作,而不是存储链表的数据元素。

  3. **next指针和prev指针**:头结点的next指针指向双链表的第一个有效节点,而prev指针指向双链表的最后一个有效节点。这样,无论链表是否为空,头结点总是有下一个节点(next指针所指节点)和前一个节点(prev指针所指节点)。

  4. **简化操作**:头结点使得双链表的操作更加简便。例如,在插入或删除节点时,我们总是从头结点开始操作,不需要检查链表是否为空。此外,头结点可以避免在空链表的情况下访问非法的内存地址。

  5. **实现循环链表**:头结点可以用来实现循环链表。只需将头结点的next指针指向头结点本身,就可以形成一个环状结构,这在某些算法中非常有用。

总结来说,头结点是双链表中的一个特殊节点,它的存在提高了双链表操作的效率和可靠性,同时简化了链表的处理逻辑。

头节点存在的目的:

**在单链表的使用中,头结点(Header Node)是一个常用的概念,特别是在进行链表操作时。头结点不是数据域中实际存储的数据节点,而是作为链表操作的辅助节点,它包含对第一个实际数据节点的引用。以下是一些在使用单链表时可能需要头结点的情况:

  1. **简化插入操作**:头结点可以简化插入操作,特别是在插入节点到链表的头部时。不需要修改已有节点的指针,只需要改变头结点的指针即可。
  2. **统一插入和删除操作**:头结点使得对链表的插入和删除操作更加统一。无论是插入还是删除,都可以通过头结点来定位到操作位置的前一个节点,而不需要关心链表的具体内容。
  3. **处理空链表**:在处理空链表时,头结点非常有用。例如,当检查链表是否为空时,只需要检查头结点的指针是否为`NULL`,而不需要遍历整个链表。
  4. **保护头结点**:头结点可以作为链表的防护措施,当链表为空时,头结点可以防止访问非法的内存地址。
  5. **方便遍历链表**:头结点可以作为遍历链表的起点,从头结点开始,可以逐一访问链表中的每个节点。
  6. **实现双向链表**:在实现双向链表时,头结点可以同时存储向前和向后的指针,这样可以更方便地实现双向遍历和操作。
    总结来说,头结点在单链表的使用中提供了许多便利,它使得链表的操作更加简洁、统一,并且更加安全和高效。因此,在实现和操作单链表时,头结点是一个非常有用的工具。**

当然很多时候你也是可以不进行初始化的。但是初始化之后对于代码是书写可以更方便。

举例哨兵位的申请和头结点的申请的区别:

在C语言中,哨兵位节点和申请空间的实现代码主要区别在于它们各自的应用场景和目的。

  1. 哨兵位节点:

哨兵位节点通常用于解决链表中的循环链表问题。在循环链表中,我们需要一个特殊的节点来标记链表的末尾,这个特殊的节点就是哨兵位节点。哨兵位节点的实现代码通常包括创建一个哨兵位节点,并将其指向链表的头节点。

以下是一个简单的哨兵位节点的实现代码:

#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;
    struct Node* next;
} Node;
Node* createSentryNode(Node* head) {
    Node* sentry = (Node*)malloc(sizeof(Node));
    sentry->data = -1; // 哨兵位节点的数据域通常设置为一个特殊值,如-1
    sentry->next = head;
    return sentry;
}
int main() {
    Node* head = (Node*)malloc(sizeof(Node));
    head->data = 1;
    head->next = (Node*)malloc(sizeof(Node));
    head->next->data = 2;
    head->next->next = (Node*)malloc(sizeof(Node));
    head->next->next->data = 3;
    head->next->next->next = head; // 创建循环链表
    Node* sentry = createSentryNode(head);
    // 接下来可以使用sentry节点进行循环链表的操作,如查找、删除等
    return 0;
}
  1. 申请空间的申请代码:

申请空间的申请代码通常用于动态分配内存空间,例如在程序运行过程中创建动态数据结构。在C语言中,我们通常使用`malloc()`函数来申请内存空间。

以下是一个简单的申请空间的实现代码:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int* ptr = (int*)malloc(sizeof(int)); // 申请一个整数大小的内存空间
    if (ptr == NULL) {
        printf("内存申请失败\n");
        return 1;
    }
    *ptr = 42; // 在申请的内存空间中存储一个整数
    printf("存储的整数: %d\n", *ptr);
    free(ptr); // 释放申请的内存空间
    return 0;
}

双链表通常需要一个头结点

双链表通常需要一个头结点(sentinel node),也称为哨兵节点。头结点是双链表的特殊节点,它不存储实际的数据,而是用于简化双链表的操作。头结点对于双链表的效率和正确性至关重要,它的存在有以下几个作用:

  1. **简化插入和删除操作**:在双链表的插入和删除操作中,我们经常需要处理空表的情况,头结点提供了一个安全的空节点,可以避免在空表的情况下访问非法的内存地址。同时,头结点使得插入和删除操作在逻辑上更加清晰,不需要单独处理边界条件。

  2. **方便遍历**:头结点可以作为遍历双链表的起点,无论链表是否为空,我们都可以从头结点开始遍历,而不需要检查链表是否为空。

  3. **避免死循环**:在单链表中,删除最后一个节点时,如果直接释放内存而不修改其后继节点的指针,会导致内存泄露和可能的死循环。双链表中的头结点可以解决这个问题,因为即使最后一个节点的后继指针指向头结点,也不会导致无限循环,因为头结点的 prev 指针指向的是最后一个有效节点。

  4. **实现循环链表**:头结点使得双链表很容易实现循环结构。只需要将头结点的 next 指针指向头结点本身,就可以形成一个循环链表。这在某些算法中非常有用,例如在实现某些算法数据结构时,需要一个循环的边界条件。

头结点的存在并不影响双链表的基本功能,但它可以提高双链表操作的效率和可靠性。在实现双链表时,头结点通常是一个静态的只读节点,它的 next 指针指向第一个实际节点,而 prev 指针指向最后一个实际节点。

双链表的实现

创建文件

和顺序表一样,也是创建三个文件,分别是头文件,实现文件,测试文件

创建链表

链表的实现

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdbool.h>
#include<stdlib.h>
#pragma once
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;//数值
	struct ListNode* next;//下一个节点
	struct ListNode* prev;//上一个节点
}LTNode;

申请节点

链表本质还是需要占用空间的,所以我们创建好链表之后不能直接进行使用,因为没有空间,所以我们需要申请空间

//申请节点
LTNode* SLBuyNode(LTDataType x)
{
	//开辟空间
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("SLBuyNode:newnode:error:");
		exit(1);
	}
	//赋值
	newnode->data = x;
	//形成环形链表,头结点尾结点都指向自己
	newnode->next = newnode->prev = newnode;
	return newnode;
}

**这段代码是用来创建一个单链表节点的函数实现,我们逐行分析:

  1. `LTNode* SLBuyNode(LTDataType x)`: 这是函数的声明,`SLBuyNode` 是一个函数名,用来表示"申请节点"。`LTNode*` 表示这个函数会返回一个 `LTNode` 类型的指针,`LTDataType` 是一个类型别名,代表链表中数据元素的类型。参数 `x` 是要存储在链表节点中的数据。**

2. `LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));`: 这行代码使用 `malloc` 函数动态分配了一块内存,大小正好能够存放一个 `LTNode` 类型的结构体。`malloc` 返回的是一个 `void*` 类型的指针,所以这里进行了类型转换,将它转换为 `LTNode*` 类型。

3. `if (newnode == NULL)`: 这里检查 `malloc` 分配的内存是否为 `NULL`,如果为 `NULL`,说明内存分配失败。

4. `perror("SLBuyNode:newnode:error:");`: 如果内存分配失败,使用 `perror` 函数打印一个错误信息,提示内存分配失败。

5. `exit(1);`: 调用 `exit` 函数,结束程序的运行,返回码为 1,通常表示程序异常终止。

6. `newnode->data = x;`: 如果内存分配成功,这行代码将传入的参数 `x` 赋值给新节点的 `data` 字段,即新节点的数据域设置为 `x`。

7. `newnode->next = newnode->prev = newnode;`: 这行代码将新节点的 `next` 和 `prev` 指针都设置为自己,这样做是为了形成一个环形链表。在环形链表中,每个节点的 `next` 指针都指向自己,这样就可以通过节点的 `next` 或 `prev` 指针循环访问链表中的所有节点。

8. `return newnode;`: 函数返回新创建的节点指针。

初始化

初始化是最关键的一步

//初始化空间
void LTInit(LTNode** pphead)
{
	//初始化空间就是创建头结点,双向链表是需要头结点的
	*pphead = SLBuyNode(-1);
}

初始化就是创建一个头节点,这里什么都没有,就是作为指向指向后面的节点

**这段代码定义了一个函数 `LTInit`,用于初始化一个双向链表的空间,即创建并返回头结点。下面是函数的详细解释:

  1. `void LTInit(LTNode** pphead)`: 这是函数的声明,`LTInit` 是函数名,`void` 表示这个函数不返回任何值。`LTNode**` 表示这个函数的参数是一个指向 `LTNode` 指针的指针,`pphead` 用来指向双向链表的头结点。**

2. `*pphead = SLBuyNode(-1);`: 这行代码通过指针 `pphead` 修改它所指向的内存地址,即初始化双向链表的头结点。这里调用了之前定义的 `SLBuyNode` 函数,传入特殊值 `-1` 作为新节点的数据域。这个头结点是一个特殊的节点,它不存储链表的实际数据,而是作为链表的入口点。

**函数 `LTInit` 的作用是:

  • 创建一个头结点:头结点是双向链表的一个特殊节点,它不存储链表的有效数据,而是作为链表操作的起点。
  • 将头结点的地址存储在 `pphead` 指向的内存地址中:这样,以后就可以通过 `pphead` 访问到双向链表的头结点,从而进行链表的操作。
    在双向链表中,头结点的前驱指针(`prev`)通常指向链表的最后一个节点,而它的后继指针(`next`)则指向链表的第一个节点。这样,无论链表是否为空,都可以通过头结点访问到链表中的元素。**

初始化的目的:

初始化基本上是每一个代码使用都需要进行初始化,养成良好代码风格

**单链表初始化的目的是为了创建一个空的链表数据结构,以便后续可以在这个基础上进行各种操作,如插入、删除、查找等。初始化通常包括以下几个步骤:

  1. 分配内存空间:为链表的头部创建一个节点,这个节点将包含指向链表第一个元素的指针。如果链表为空,这个指针将为`NULL`。
  2. 设置初始状态:将链表的各个属性和指针初始化为默认状态,比如长度为0,头节点指向`NULL`等。
  3. 确保可使用性:初始化之后,链表应该处于一个可以进行操作的状态,这意味着它可以安全地接收新的元素,同时用户可以检查链表是否为空等。
    初始化单链表的目的是为了让程序有一个干净的起点,并能够按照预定的方式进行扩展。这样做可以避免在使用链表之前,对链表结构进行不必要的猜测或错误操作,确保数据结构的一致性和稳定性。**

初始化的代码解释:

简单是说就是初始化的目的往往是创建一个头结点,头结点和第一个节点是不一样的,头结点是null的节点,图解里面1是第一个节点是有实际数值的,但是头结点是没有实际数值的,也就是哨兵位的意思,哨兵位顾名思义也就是放哨的地方,而你进行尾插的时候,第一个数值的插入往往都是从头结点往后进行插入,

打印单链表

打印是很简单的

头文件

//打印双链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	//头结点之后的第一个节点是第一节点,也就是实际有效节点
	LTNode* prev = phead->next;
	while (prev != phead)
	{
		printf("%d->", prev->data);
		prev = prev->next;
	}
	printf("NULL\n");
}

**这段代码定义了一个函数 `LTPrint`,用于打印双向链表中的所有元素。下面是函数的详细解释:

  1. `void LTPrint(LTNode* phead)`: 这是函数的声明,`LTPrint` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,****`phead` 用来指向双向链表的头结点。**

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* prev = phead->next;`: 这行代码初始化了一个指针 `prev`,并将其设置为头结点的后继指针,即实际有效节点的指针。因为在双向链表中,头结点本身不算作有效节点,它只是作为链表操作的起点。

4. `while (prev != phead)`: 这个 `while` 循环用来遍历链表中的所有有效节点。循环条件是 `prev` 指针不等于头结点 `phead`,即只要 `prev` 没有指向头结点,就继续循环。

5. `printf("%d->", prev->data);`: 在每次循环中,这行代码打印当前节点 `prev` 的数据域,后面跟一个 `->` 符号,表示这是链表中的一个节点。

6. `prev = prev->next;`: 这行代码将 `prev` 指针移动到下一个节点。

7. `printf("NULL\n");`: 当 `prev` 指针再次指向头结点 `phead` 时,说明已经遍历完整个链表。这时,打印 `NULL` 表示链表的末尾,并跟着一个换行符 `\n`,以便在输出中区分不同的链表。

然后我们在test.c里面进行测试申请空间是否成功(发现没有问题)

链表的销毁

这里先进行链表的销毁代码的实现,因为这个代码的书写比较方便。

//销毁空间
void LTDestroy(LTNode* phead)
{
	assert(phead);
	//销毁空间是从头结点之后进行销毁的
	LTNode* prev = phead->next;
	// 双链表尾结点的下一个节点就是第一个节点(不是头结点,头结点指的是哨兵位)
	// 所以当需要循环的时候,遇到第一个节点说明循环结束
	while (prev != phead)
	{
		LTNode* next = prev->next;
		free(prev);
		prev = next;
	}
	free(phead);
	phead = NULL;
}

1. `void LTDestroy(LTNode* phead)`: 这是函数的声明,`LTDestroy` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* prev = phead->next;`: 这行代码初始化了一个指针 `prev`,并将其设置为头结点的后继指针,即实际有效节点的指针。因为在双向链表中,头结点本身不算作有效节点,它只是作为链表操作的起点。

4. `while (prev != phead)`: 这个 `while` 循环用来遍历链表中的所有有效节点。循环条件是 `prev` 指针不等于头结点 `phead`,即只要 `prev` 没有指向头结点,就继续循环。

5. `LTNode* next = prev->next;`: 这行代码保存了 `prev` 节点的后继节点的地址,以便于接下来释放 `prev` 节点的内存。

6. `free(prev);`: 这行代码释放了 `prev` 指向的内存,即删除了当前的节点。

7. `prev = next;`: 这行代码将 `prev` 指针移动到下一个节点,以便于下一次循环。

8. `free(phead);`: 当 `prev` 指针再次指向头结点 `phead` 时,说明已经遍历完整个链表。这时,释放头结点的内存。

9. `phead = NULL;`: 最后,将头结点的指针设置为 `NULL`,以防止链表指针悬挂,即防止后续代码通过 `phead` 访问已经释放的内存。

总结来说,`LTDestroy` 函数的作用是遍历双向链表中的所有节点,并释放每个节点的内存。这个函数用于在不再需要链表时,清理占用的资源,防止内存泄漏。

链表的尾插

哨兵位的节点是不发生改变的,就算删除,头结点也是不能被删除的

你不能不拿保险柜的钱,你非得拿着保险柜的钥匙(传递一级指针,因为我们不改变哨兵位)(需要修改哨兵位,那就需要传递二级)

哪些位置受到影响

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	// 申请节点
	LTNode* newnode = SLBuyNode(x);
	// newnode作为第一步的原因就是newnode的操作不影响其他的数值
	newnode->next = phead;
	// 如果有phead->d1->d2->d3=phead->prev
	// 这里我们先把newnode不会影响数值变化的newnode的下一节点指向phead
	// 这里我们注意,我们不能先改变d3也就是phead->prev
	// 所以我们可以先把d3的下一节点指向新节点,要是改变d3也就是phead->prev的节点,我们就找不到参数
	// 此时我们还是可以找到头结点的上一个节点的d3
	newnode->prev = phead->prev;
	
	// 这里是很关键的一步,我们还是不能先改变头结点的上一个节点
	// 所以我们先改变d3的下一个节点
	phead->prev->next = newnode;
	// 最后改变头节点的上一个节点,指向尾结点
	phead->prev = newnode;
}

1. `void LTPushBack(LTNode* phead, LTDataType x)`: 这是函数的声明,`LTPushBack` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的第一个参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。`LTDataType` 是链表中数据元素的类型。`x` 是将要插入链表尾部的数据。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* newnode = SLBuyNode(x);`: 这行代码使用 `SLBuyNode` 函数申请一个新节点,并传入数据 `x` 作为新节点的数据域。

4. `newnode->next = phead;`: 新节点的 `next` 指针指向当前的头结点 `phead`,这是因为新节点将成为链表的最后一个节点,它的下一个节点应该是链表的头结点。

5. `newnode->prev = phead->prev;`: 新节点的 `prev` 指针指向头结点的上一个节点,这是因为在双向链表中,每个节点都有一个指向其前一个节点的指针。

6. `phead->prev->next = newnode;`: 这行代码将头结点的上一个节点的 `next` 指针指向新节点,这样就完成了新节点插入链表的过程。

7. `phead->prev = newnode;`: 最后,将头结点的 `prev` 指针指向新节点,这样新节点就成为了链表的尾结点。

总结来说,`LTPushBack` 函数的作用是在双向链表的尾部插入一个新节点。新节点的 `next` 指针指向头结点,`prev` 指针指向头结点的上一个节点。通过更新头结点的 `prev` 指针和头结点上一个节点的 `next` 指针,完成新节点的插入。

为什么不能交换位置

在双向链表中,每个节点都有一个指向其前一个节点的 `prev` 指针和一个指向其下一个节点的 `next` 指针。在插入新节点时,需要确保新节点的 `prev` 指针正确地指向当前链表的尾结点,同时新节点的 `next` 指针指向链表的头结点。这样,新节点就成为了链表的尾结点,而原来的尾结点成为了新节点的下一个节点。
原始的代码顺序是:

phead->prev->next = newnode;
phead->prev = newnode;

这个顺序是正确的,因为它首先确保了新节点的 `next` 指针指向头结点,然后才将新节点加入到链表的尾部。这样做的好处是,即使在 `newnode` 节点插入链表之前,我们也可以通过 `phead->prev` 访问到链表的尾结点,因为 `phead` 总是指向链表的头结点,而头结点的 `prev` 指针指向尾结点。

如果交换这两行的顺序,即先设置 `phead->prev = newnode;` 然后再设置 `phead->prev->next = newnode;`,会导致链表的结构不正确。

**因为:

  • 设置 `phead->prev = newnode;` 后,`newnode` 成为了头结点的上一个节点。但是,此时 `newnode` 的 `next` 指针还没有指向头结点,也就是说,链表的结构还没有完全建立起来。
  • 接着设置 `phead->prev->next = newnode;`,这时候 `newnode` 的 `next` 指针应该指向头结点,但是由于上一步的操作,`newnode` 已经是头结点的上一个节点了,所以这一步实际上是在尝试将头结点指向自己,这会导致链表进入一个无限循环的状态。
    因此,这两行的顺序不能交换,否则会导致链表结构错误。正确的顺序是先确保新节点的 `next` 指针指向头结点,然后再将新节点加入到链表的尾部。**

链表的头插

头插
//void LTPushFront(LTNode* phead, LTDataType x)
//{
//	assert(phead);
//	// 申请节点
//	LTNode* newnode = SLBuyNode(x);
//	//还是需要先移动新节点,因为新节点的指向不会影响链表后面的数值
//	newnode->next = phead->next;
//	//链接newnode节点,此时链接节点不会导致节点的丢失
//	phead->next = newnode;
//
//	// 这里已经把newnode和头尾进行了链接所以不会导致节点的丢失
//	// 这里是newnode的下一节点的上一节点链接
//	newnode->next->prev = newnode;
//	//newnode的上一节点链接头结点,从而形成双链表
//	newnode->prev = phead;
//}

//头插优化
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = SLBuyNode(x);
	
	//这里的逻辑是先把新节点的整体的逻辑指向数值中间,保证不影响头结点下一个节点的参数
	newnode->next = phead->next;
	newnode->prev = phead;


	//此时newnode的指向是不影响其他数值的,所以我们为了不丢失节点,我们就不能先改变phead->next,这个节点的位置
	//phead->next这个节点只能是在进行改变,因为我们可以把这个节点当做参数,对照物
	phead->next->prev = newnode;
	//最后改变phead->next节点的位置,变成newnode
	phead->next = newnode;

}

1. `void LTPushFront(LTNode* phead, LTDataType x)`: 这是函数的声明,`LTPushFront` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的第一个参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。`LTDataType` 是链表中数据元素的类型。`x` 是将要插入链表头部的数据。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* newnode = SLBuyNode(x);`: 这行代码使用 `SLBuyNode` 函数申请一个新节点,并传入数据 `x` 作为新节点的数据域。

4. `newnode->next = phead->next;`: 新节点的 `next` 指针指向当前头结点的下一个节点,这是因为新节点将成为链表的新头节点,它的下一个节点应该是链表原来的头结点的下一个节点。

5. `newnode->prev = phead;`: 新节点的 `prev` 指针指向头结点,这是因为在双向链表中,每个节点都有一个指向其前一个节点的指针。

6. `phead->next->prev = newnode;`: 这行代码将头结点的下一个节点的 `prev` 指针指向新节点,这样新节点就成为了链表的头节点。

7. `phead->next = newnode;`: 最后,将头结点的 `next` 指针指向新节点,这样新节点就成为了链表的头节点。

优化后的代码顺序进行了调整,先设置新节点的 `next` 和 `prev` 指针,确保新节点正确地插入到链表中,然后再更新头结点的 `next` 指针。这样做的好处是,即使在 `newnode` 节点插入链表之前,我们也可以通过 `phead` 访问到链表的头结点,因为 `phead` 总是指向链表的头结点。

总结来说,`LTPushFront` 函数的作用是在双向链表的头部插入一个新节点。新节点的 `next` 指针指向原链表的头结点的下一个节点,`prev` 指针指向头结点。通过更新头结点的 `next` 指针和新节点的 `next` 指针,完成新节点的插入。

**在优化后的代码中,后两个语句 `newnode->next->prev = newnode;` 和 `phead->next = newnode;` 交换位置会导致链表的结构不正确。下面是为什么这两个语句不能交换位置的解释:

  1. `newnode->next->prev = newnode;`: 这行代码将新节点 `newnode` 的下一个节点的 `prev` 指针指向 `newnode`。这意味着新节点的下一个节点将认为 `newnode` 是它的前一个节点。这是正确的,因为在新节点被插入到链表中之后,新节点应该成为头节点,而原来的头节点的下一个节点将成为新节点的下一个节点。**

2. `phead->next = newnode;`: 这行代码将头结点 `phead` 的 `next` 指针指向新节点 `newnode`。这意味着新节点将成为链表的头节点。这是正确的,因为在新节点被插入到链表中之后,新节点应该成为头节点。

如果交换这两个语句的位置,那么会发生以下情况:

- 首先执行 `phead->next = newnode;`,将新节点 `newnode` 设置为头节点。

- 然后执行 `newnode->next->prev = newnode;`,尝试将新节点的下一个节点的 `prev` 指针指向新节点。但是,因为新节点刚刚被设置为头节点,它的 `next` 指针还没有被更新,仍然指向原来的头节点的下一个节点。这样,新节点的下一个节点的 `prev` 指针就会被错误地设置为新节点,而不是应该设置为原来的头节点。

这样的结果是新节点的下一个节点的 `prev` 指针指向新节点,而新节点的 `next` 指针指向原来的头节点的下一个节点。这样就会导致链表的结构不正确,因为每个节点都应该有一个指向其前一个节点的 `prev` 指针和一个指向其下一个节点的 `next` 指针。

因此,这两个语句的位置不能交换,以确保链表的结构正确。正确的顺序是先将新节点的 `next` 指针指向原来的头节点的下一个节点,然后将原来的头节点的 `next` 指针指向新节点,这样新节点就正确地成为了链表的头节点。

链表的头删

/头删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	//为了方便创建删除的节点
	LTNode* del = phead->next;
	//头结点的下一个节点,是第二节点。
	phead->next = del->next;
	//第三节点的上一节点是phead,也就是头结点
	del->next->prev = phead;
	
	//释放节点
	free(del);
	del = NULL;
}

1. `void LTPopBack(LTNode* phead)`: 这是函数的声明,`LTPopBack` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* del = phead->next;`: 这行代码创建了一个指针 `del`,并将其初始化为头结点的下一个节点。这是为了方便后续操作中引用即将被删除的节点。

4. `phead->next = del->next;`: 这行代码将头结点的 `next` 指针指向 `del` 节点的下一个节点。这样做是为了将头结点与即将被删除的节点断开连接,因为头结点的下一个节点将成为新的尾结点。

5. `del->next->prev = phead;`: 这行代码将新尾结点(即原尾结点的下一个节点)的 `prev` 指针指向头结点。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其前一个节点的指针。

6. `free(del);`: 这行代码释放了 `del` 指向的节点内存。这是因为在删除节点时,我们需要释放该节点的内存以避免内存泄漏。

7. `del = NULL;`: 这行代码将 `del` 指针设置为 `NULL`,以防止后续代码通过 `del` 访问到已经释放的内存。

总结来说,`LTPopBack` 函数的作用是删除双向链表中的尾节点。通过更新头结点的 `next` 指针和新尾结点的 `prev` 指针,完成节点的删除。然后,释放被删除节点的内存,并将其指针设置为 `NULL`。

链表的尾删

我们需要保存删除的节点,不然删除的时候会导致找不到删除的节点

//尾删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//为了删除的节点
	LTNode* del = phead->prev;
	//头结点的上一节点作为参照不能优先删除
	phead->prev = del->prev;
	del->prev->next = phead;
}

1. `void LTPopFront(LTNode* phead)`: 这是函数的声明,`LTPopFront` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* del = phead->prev;`: 这行代码创建了一个指针 `del`,并将其初始化为头结点的上一个节点。这是为了方便后续操作中引用即将被删除的节点。

4. `phead->prev = del->prev;`: 这行代码将头结点的 `prev` 指针指向 `del` 节点的前一个节点。这样做是为了将头结点与即将被删除的节点断开连接,因为头结点的上一个节点将成为新的头结点。

5. `del->prev->next = phead;`: 这行代码将新头结点(即原头结点的前一个节点)的 `next` 指针指向头结点。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其下一个节点的指针。

6. 函数没有释放 `del` 指向的节点内存,因为 `del` 是头结点的前一个节点,而头结点不能被释放。如果需要释放节点内存,通常在创建节点时分配内存,并在适当的时候释放,例如在链表销毁时。

总结来说,`LTPopFront` 函数的作用是删除双向链表中的头节点。通过更新头结点的 `prev` 指针和新头结点的 `next` 指针,完成节点的删除。注意,头结点本身不会被释放,因为它是链表的入口点。

链表的查找

链表的查找,这里的目的不仅仅是进行链表的查找,还有就是我们进行查找的时候,我们需要先找得到才能进行删除。所以我们才需要进行查找

这里查找到之后返回的是节点的地址,之后我们进行数值的操作的时候,可以直接查找到之后进行删除

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* prev = phead->next;
	while (prev != phead)
	{
		if (prev->data == x)
		{
			return prev;
		}
		prev = prev->next;
	}
	return NULL;
}

1. `LTNode* LTFind(LTNode* phead, LTDataType x)`: 这是函数的声明,`LTFind` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的第一个参数是一个指向 `LTNode` 类型的指针,`phead` 用来指向双向链表的头结点。`LTDataType` 是链表中数据元素的类型。`x` 是需要查找的数据。

2. `assert(phead);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `phead` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `phead` 是一个有效的头结点。

3. `LTNode* prev = phead->next;`: 这行代码创建了一个指针 `prev`,并将其初始化为头结点的下一个节点。这个节点将作为起始点,用于遍历链表。

4. `while (prev != phead)`: 这个循环会一直执行,直到 `prev` 指针指向头结点本身为止。这是因为在双向链表中,头结点的 `next` 指针指向第一个有效节点,而最后一个节点的 `next` 指针指向头结点。

5. `if (prev->data == x)`: 在循环中,这行代码检查当前 `prev` 指向的节点的数据是否等于要查找的数据 `x`。如果相等,说明找到了目标节点,函数返回 `prev`。

6. `prev = prev->next;`: 如果当前节点的数据不等于 `x`,则 `prev` 指针移动到下一个节点,继续遍历链表。

7. `return NULL;`: 如果循环结束后,没有找到具有数据 `x` 的节点,函数返回 `NULL`,表示查找失败。

总结来说,`LTFind` 函数的作用是在双向链表中查找具有特定数据 `x` 的节点。函数从头结点的下一个节点开始遍历链表,直到找到具有目标数据的节点或者遍历完整个链表。如果找到目标节点,函数返回该节点;如果没有找到,返回 `NULL`。

在pos位置之后删除数据

//在pos位置之后删除数据
void LTErase(LTNode* pos)
{
	assert(pos);
	//创建需要删除的节点
	LTNode* del = pos->next;
	//双链表的关键是不循环,那么不循环的情况,
	//我们需要有一个数值是不变化的,或者是最后才变化的
	//所以。最后变化的数值往往是需要动的节点的下一个节点
	del->next->prev = pos;
	pos->next = del->next;

}

1. `void LTErase(LTNode* pos)`: 这是函数的声明,`LTErase` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,`pos` 用来指向双向链表中要删除节点的前一个节点。

2. `assert(pos);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `pos` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `pos` 是一个有效的节点。

3. `LTNode* del = pos->next;`: 这行代码创建了一个指针 `del`,并将其初始化为 `pos` 节点的下一个节点。这个节点是需要删除的节点。

4. `del->next->prev = pos;`: 这行代码将 `del` 节点的下一个节点的 `prev` 指针指向 `pos`。这意味着 `del` 节点的下一个节点将认为 `pos` 是它的前一个节点。这是因为在双向链表中,每个节点都有一个指向其前一个节点的指针。

5. `pos->next = del->next;`: 这行代码将 `pos` 节点的 `next` 指针指向 `del` 的下一个节点。这样做是为了断开 `pos` 和 `del` 的连接,使得 `pos` 的下一个节点成为 `pos` 的新下一个节点。

6. 函数没有释放 `del` 指向的节点内存,因为 `del` 节点已经被从链表中移除,它的 `next` 指针已经指向了 `pos` 的下一个节点,而它的 `prev` 指针已经指向了 `pos`。如果需要释放节点内存,通常在创建节点时分配内存,并在适当的时候释放,例如在链表销毁时。

总结来说,`LTErase` 函数的作用是在双向链表中删除指定位置 `pos` 之后的节点。通过更新 `pos` 的 `next` 指针和删除节点的 `next` 指针的 `prev` 指针,完成节点的删除。注意,删除节点本身不会被释放,因为它的引用计数还没有归零。通常,节点内存的释放会在链表销毁时进行。

在pos位置之后插入数据

先让newnode指向d2 d3两个节点

然后再让d2 d3 指向newnode节点

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//申请节点
	LTNode* newnode = SLBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;

}

1. `void LTInsert(LTNode* pos, LTDataType x)`: 这是函数的声明,`LTInsert` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的第一个参数是一个指向 `LTNode` 类型的指针,`pos` 用来指向双向链表中要插入新节点的位置节点。`LTDataType` 是链表中数据元素的类型。`x` 是需要插入的新节点的数据。

2. `assert(pos);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `pos` 是否为 `NULL`,如果是 `NULL`,则程序将抛出错误并终止执行。这用来确保传入的 `pos` 是一个有效的节点。

3. `LTNode* newnode = SLBuyNode(x);`: 这行代码调用了另一个函数 `SLBuyNode`,该函数用于创建一个新节点,并返回指向该新节点的指针。`newnode` 指向新创建的节点,该节点的数据被设置为 `x`。

4. `newnode->next = pos->next;`: 这行代码将新节点的 `next` 指针指向 `pos` 节点的下一个节点。这样做是为了将新节点连接到链表中,新节点将成为 `pos` 节点的下一个节点。

5. `newnode->prev = pos;`: 这行代码将新节点的 `prev` 指针指向 `pos`。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其前一个节点的指针。

6. `pos->next->prev = newnode;`: 这行代码将 `pos` 节点的下一个节点的 `prev` 指针指向新节点。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其前一个节点的指针。

7. `pos->next = newnode;`: 这行代码将 `pos` 节点的 `n****ext` 指针指向新节点。这样做是为了完成新节点的插入,使得新节点成为链表的一部分。

总结来说,`LTInsert` 函数的作用是在双向链表中在指定位置 `pos` 之后插入一个新节点,该节点的数据为 `x`。通过更新新节点的 `next` 和 `prev` 指针,以及 `pos` 节点的 `next` 指针,完成节点的插入。

删除数据pos节点

//删除数据pos节点
void LTdel(LTNode* pos)
{
	assert(pos);
	//头节点
	if (pos == pos->next) 
	{
		free(pos);
		pos = NULL;
		return;
	}
	LTNode* del = pos;
	LTNode* next = pos->next;
	LTNode* pure = pos->prev;
	pure->next = next;
	next->prev = pure;

	free(pos);
	pos = NULL;
}

1. `void LTdel(LTNode* pos)`: 这是函数的声明,`LTdel` 是函数名,`void` 表示这个函数不返回任何值。`LTNode*` 表示这个函数的参数是一个指向 `LTNode` 类型的指针,`pos` 用来指向双向链表中要删除的节点。

2. `assert(pos);`: 这行代码使用了 `assert` 函数,它是 C 语言中的一个宏,用于进行条件判断。这里判断 `pos` 是否为 `NULL`,如果是 `NU****LL`,则程序将抛出错误并终止执行。这用来确保传入的 `pos` 是一个有效的节点。

3. `if (pos == pos->next)`: 这个条件判断用于处理链表中只有一个节点的情况。如果 `pos` 就是链表的头节点,并且没有其他节点,那么直接释放 `pos` 的内存并将其指针设置为 `NULL`。

4. `LTNode* del = pos;`: 这行代码创建了一个指针 `del`,并将其初始化为要删除的节点 `pos`。

5. `LTNode* next = pos->next;`: 这行代码创建了一个指针 `next`,并将其初始化为 `pos` 的下一个节点。

6. `LTNode* pure = po****s->prev;`: 这行代码创建了一个指针 `pure`,并将其初始化为 `pos` 的前一个节点。

7. `pure->next = next;`: 这行代码将 `pure` 的 `next` 指针指向 `next`。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其下一个节点的指针。

8. `next->prev = pure;`: 这行代码将 `next` 的 `prev` 指针指向 `pure`。这样做是为了保持双向链表的完整性,确保每个节点都有一个指向其前一个节点的指针。

9. `free(pos);`: 这行代码释放了 `pos` 指向的节点内存。这是因为在删除节点时,我们需要释放该节点的内存以避免内存泄漏。

10. `pos = NULL;`: 这行代码将 `pos` 指针设置为 `NULL`,以防止后续代码通过 `pos` 访问到已经释放的内存。

总结来说,`LTdel` 函数的作用是在双向链表中删除指定位置 `pos` 的节点。通过更新前一个节点的 `next` 指针和后一个节点的 `prev` 指针,完成节点的删除。然后,释放被删除节点的内存,并将其指针设置为 `NULL`。

单链表代码的总结

List.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdbool.h>
#include<stdlib.h>
#pragma once
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;//数值
	struct ListNode* next;//下一个节点
	struct ListNode* prev;//上一个节点
}LTNode;

//申请节点
LTNode* SLBuyNode(LTDataType x);
//初始化空间
void LTInit(LTNode** pphead);
//销毁空间
void LTDestroy(LTNode* phead);
//打印双链表
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopBack(LTNode* phead);
//尾删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//在pos位置之后删除数据
void LTErase(LTNode* pos);

//删除数据pos节点
void LTdel(LTNode* pos);

List.c

#include"List.h"
//申请节点
LTNode* SLBuyNode(LTDataType x)
{
	//开辟空间
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("SLBuyNode:newnode:error:");
		exit(1);
	}
	//赋值
	newnode->data = x;
	//形成环形链表,头结点尾结点都指向自己
	newnode->next = newnode->prev = newnode;
	return newnode;
}
//初始化空间
void LTInit(LTNode** pphead)
{
	//初始化空间就是创建头结点,双向链表是需要头结点的
	*pphead = SLBuyNode(-1);
}
//销毁空间
void LTDestroy(LTNode* phead)
{
	assert(phead);
	//销毁空间是从头结点之后进行销毁的
	LTNode* prev = phead->next;
	// 双链表尾结点的下一个节点就是第一个节点(不是头结点,头结点指的是哨兵位)
	// 所以当需要循环的时候,遇到第一个节点说明循环结束
	while (prev != phead)
	{
		LTNode* next = prev->next;
		free(prev);
		prev = next;
	}
	free(phead);
	phead = NULL;
}
//打印双链表
void LTPrint(LTNode* phead)
{
	assert(phead);
	//头结点之后的第一个节点是第一节点,也就是实际有效节点
	LTNode* prev = phead->next;
	while (prev != phead)
	{
		printf("%d->", prev->data);
		prev = prev->next;
	}
	printf("NULL\n");
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	// 申请节点
	LTNode* newnode = SLBuyNode(x);
	// newnode作为第一步的原因就是newnode的操作不影响其他的数值
	newnode->next = phead;
	// 如果有phead->d1->d2->d3=phead->prev
	// 这里我们先把newnode不会影响数值变化的newnode的下一节点指向phead
	// 这里我们注意,我们不能先改变d3也就是phead->prev
	// 所以我们可以先把d3的下一节点指向新节点,要是改变d3也就是phead->prev的节点,我们就找不到参数
	// 此时我们还是可以找到头结点的上一个节点的d3
	newnode->prev = phead->prev;
	
	// 这里是很关键的一步,我们还是不能先改变头结点的上一个节点
	// 所以我们先改变d3的下一个节点
	phead->prev->next = newnode;
	// 最后改变头节点的上一个节点,指向尾结点
	phead->prev = newnode;
}

头插
//void LTPushFront(LTNode* phead, LTDataType x)
//{
//	assert(phead);
//	// 申请节点
//	LTNode* newnode = SLBuyNode(x);
//	//还是需要先移动新节点,因为新节点的指向不会影响链表后面的数值
//	newnode->next = phead->next;
//	//链接newnode节点,此时链接节点不会导致节点的丢失
//	phead->next = newnode;
//
//	// 这里已经把newnode和头尾进行了链接所以不会导致节点的丢失
//	// 这里是newnode的下一节点的上一节点链接
//	newnode->next->prev = newnode;
//	//newnode的上一节点链接头结点,从而形成双链表
//	newnode->prev = phead;
//}

//头插优化
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = SLBuyNode(x);
	
	//这里的逻辑是先把新节点的整体的逻辑指向数值中间,保证不影响头结点下一个节点的参数
	newnode->next = phead->next;
	newnode->prev = phead;


	//此时newnode的指向是不影响其他数值的,所以我们为了不丢失节点,我们就不能先改变phead->next,这个节点的位置
	//phead->next这个节点只能是在进行改变,因为我们可以把这个节点当做参数,对照物
	phead->next->prev = newnode;
	//最后改变phead->next节点的位置,变成newnode
	phead->next = newnode;

}
//头删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	//为了方便创建删除的节点
	LTNode* del = phead->next;
	//头结点的下一个节点,是第二节点。
	phead->next = del->next;
	//第三节点的上一节点是phead,也就是头结点
	del->next->prev = phead;
	
	//释放节点
	free(del);
	del = NULL;
}

//尾删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//为了删除的节点
	LTNode* del = phead->prev;
	//头结点的上一节点作为参照不能优先删除
	phead->prev = del->prev;
	del->prev->next = phead;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* prev = phead->next;
	while (prev != phead)
	{
		if (prev->data == x)
		{
			return prev;
		}
		prev = prev->next;
	}
	return NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//申请节点
	LTNode* newnode = SLBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;

}

//在pos位置之后删除数据
void LTErase(LTNode* pos)
{
	assert(pos);
	//创建需要删除的节点
	LTNode* del = pos->next;
	//双链表的关键是不循环,那么不循环的情况,
	//我们需要有一个数值是不变化的,或者是最后才变化的
	//所以。最后变化的数值往往是需要动的节点的下一个节点
	del->next->prev = pos;
	pos->next = del->next;

}
//删除数据pos节点
void LTdel(LTNode* pos)
{
	assert(pos);
	//头节点
	if (pos == pos->next) 
	{
		free(pos);
		pos = NULL;
		return;
	}
	LTNode* del = pos;
	LTNode* next = pos->next;
	LTNode* pure = pos->prev;
	pure->next = next;
	next->prev = pure;

	free(pos);
	pos = NULL;
}

test.c

#include"List.h"
void SLIST01()
{
	LTNode* s1 = NULL;
	//创建一个头节点
	LTInit(&s1);
	//尾插
	LTPushBack(s1, 1);
	LTPrint(s1);
	//尾插
	LTPushBack(s1, 2);
	LTPrint(s1);
	//尾插
	LTPushBack(s1, 3);
	LTPrint(s1);
	//头插
	LTPushFront(s1, 0);
	LTPrint(s1);
	//尾删
	LTPopBack(s1);
	LTPrint(s1);
	//头删
	LTPopFront(s1);
	LTPrint(s1);
	//查找
	LTNode* find = LTFind(s1, 1);
	if (find == NULL)
	{
		printf("没有找到\n");
	}
	else
	{
		printf("找到了地址是:%d\n", find);
	}

	//指定位置之后插入
	LTInsert(find, 99);
	LTPrint(s1);

	//指定位置之后删除
	LTErase(find);
	LTPrint(s1);

	//销毁空间
	LTDestroy(s1);
	s1 = NULL;
}
int main()
{
	SLIST01();
	return 0;
}

通讯录

声明

这里首先进行声明,这里的通讯录的实现是基于单链表进行实现的

这里我们再开一个项目篇章,不和上面的链表重复

自定义数据

自定义数据之后我们可以把之前定义的int类型进行替换

//前置声明
typedef struct SListNode Address_Book;

//用户数据
typedef struct PersonInfo
{
    char name[NAME_MAX];//姓名
    char sex[SEX_MAX];//性别
    int age;//年龄
    char tel[TEL_MAX];//电码
    char addr[ADDR_MAX];//地址
}PeoInfo;

结构体-前置声明-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137497379前置声明,不理解的可以去看看

通讯录的初始化

目的就是创建头节点

#include"SList.h"
//通讯录的初始化
void InitContact(contact** pphead)
{
	//初始化空间就是创建头结点,双向链表是需要头结点的
	LTNode* newpphead = (LTNode*)malloc(sizeof(LTNode));
	if (newpphead == NULL)
	{
		perror("error:newpphead:ltinit");
		exit(1);
	}
	*pphead = newpphead;
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;

}

1. `void InitContact(contact** pphead)`: 这是函数的声明,`InitContact` 是函数名,`void` 表示这个函数不返回任何值。`contact**` 表示这个函数的参数是一个指向 `contact` 类型指针的指针,`pphead` 用来指向将要被初始化的通讯录链表的头结点。

2. `LTNode* newpphead = (LTNode*)malloc(sizeof(LTNode));`: 这行代码使用 `malloc` 函数动态分配了一块内存,大小正好能够存放一个 `LTNode` 类型的结构体。`malloc` 返回的是一个 `void*` 类型的指针,所以需要将它强制转换为 `LTNode*` 类型。这个新分配的内存块将用来作为双向链表的头结点。

3. `if (newpphead == NULL)`: 这段代码检查 `malloc` 是否成功分配了内存。如果 `newpphead` 为 `NULL`,说明内存分配失败。

4. `perror("error:newpphead:ltinit");`: 如果内存分配失败,这个函数会打印出错误信息,内容为 "error:newpphead:ltinit"。

5. `exit(1);`: 这行代码调用 `exit` 函数,以状态码 `1` 退出程序。通常 `1` 表示错误退出。

6. `*pphead = newpphead;`: 这行代码将新分配的头结点地址赋值给 `pphead` 指向的指针,也就是将新头结点设置为通讯录链表的头结点。

7. `(*pphead)->next = *pphead;`: 这行代码将头结点的 `next` 指针指向自己,因为这是一个双向链表,所以每个节点都需要指向前一个节点和后一个节点。这里设置为头结点自环。
8. `(*pphead)->prev = *pphead;`: 这行代码将头结点的 `prev` 指针也指向自己,同样是为了实现头结点自环。

总结来说,`InitContact` 函数的作用是初始化一个通讯录双向链表的头结点。通过使用 `malloc` 函数动态分配内存并创建一个头结点,然后将这个头结点设置为链表的起始点。头结点的 `next` 和 `prev` 指针都指向自己,表示这是一个空的链表。

添加通讯录数据

这里我们调用链表的尾插函数,进行添加通讯录项目,当然我们需要输入,最后进行插入就可以

//添加通讯录数据
void AddContact(contact* con)
{
	PeoInfo info;
	printf("请输入姓名:\n");
	scanf("%s", info.name);

	printf("请输入性别:\n");
	scanf("%s", info.sex);

	printf("请输入年龄:\n");
	scanf("%d", &info.age);

	printf("请输入电话:\n");
	scanf("%s", info.tel);


	printf("请输入地址:\n");
	scanf("%s", info.addr);

	LTPushBack(con, info);
	printf("添加成功\n\n");
}

1. `void AddContact(contact* con)`: 这是函数的声明,`AddContact` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构。

2. `PeoInfo info;`: 这行代码定义了一个 `PeoInfo` 类型的变量 `info`,用于存储用户输入的联系人信息。

3. `printf("请输入姓名:\n");`: 这行代码打印出提示信息,要求用户输入姓名。

4. `scanf("%s", info.name);`: 这行代码使用 `scanf` 函数读取用户输入的姓名,并将其存储在 `info.name` 字段中。

5. `printf("请输入性别:\n");`: 这行代码打印出提示信息,要求用户输入性别。

6. `scanf("%s", info.sex);`: 这行代码使用 `scanf` 函数读取用户输入的性别,并将其存储在 `info.sex` 字段中。

7. `printf("请输入年龄:\n");`: 这行代码打印出提示信息,要求用户输入年龄。

8. `scanf("%d", &info.age);`: 这行代码使用 `scanf` 函数读取用户输入的年龄,并将其存储在 `info.age` 字段中。注意 `&` 符号,它用于取 `info.age` 的地址。

9. `printf("请输入电话:\n");`: 这行代码打印出提示信息,要求用户输入电话号码。

10. `scanf("%s", info.tel);`: 这行代码使用 `scanf` 函数读取用户输入的电话号码,并将其存储在 `info.tel` 字段中。

11. `printf("请输入地址:\n");`: 这行代码打印出提示信息,要求用户输入地址。

12. `scanf("%s", info.addr);`: 这行代码使用 `scanf` 函数读取用户输入的地址,并将其存储在 `info.addr` 字段中。

13. `LTPushBack(con, info);`: 这行代码调用了 `LTPushBack` 函数,将 `info` 中的数据添加到 `con` 指向的通讯录结构中的末尾。这个函数可能是链表中的一个函数,用于在链表的末尾添加新的节点。

14. `printf("添加成功\n\n");`: 这行代码打印出提示信息,表示联系人添加成功。

总结来说,`AddContact` 函数的作用是向一个通讯录结构中添加新的联系人信息。通过提示用户输入各种信息,并使用 `scanf` 函数读取这些信息,然后将其存储在一个 `PeoInfo` 类型的结构中。最后,调用 `LTPushBack` 函数将这些信息添加到通讯录结构中。

展示通讯录数据

这里打印一个表头,创建一个变量,变量进行移动

//展示通讯录数据
void ShowContact(contact* con)
{
	printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "地址");
	contact* pure = con->next;
	while (pure != con)
	{
		printf("%s %s %d %s %s\n",
			pure->data.name,
			pure->data.sex,
			pure->data.age,
			pure->data.tel,
			pure->data.addr);
		pure = pure->next;
	}
}

1. `void ShowContact(contact* con)`: 这是函数的声明,`ShowContact` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构。

2. `printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "地址");`: 这行代码使用 `printf` 函数打印出列标题,即联系人信息的五个字段:姓名、性别、年龄、电话和地址。
3. `contact* pure = con->next;`: 这行代码定义了一个指针 `pure`,并将其初始化为 `con` 的 `next` 指针,也就是通讯录链表的第一个联系人节点。

4. `while (pure != con)`: 这个 `while` 循环会一直执行,直到 `pure` 指针指向 `con` 本身,即遍历完整个链表。

5. `printf("%s %s %d %s %s\n",`: 这行代码是循环体内打印每个联系人信息的格式化字符串。

6. `pure->data.name`, `pure->data.sex`, `pure->data.age`, `pure->data.tel`, `pure->data.addr);`: 这些代码片段从链表中的当前节点 `pure` 中分别获取姓名、性别、年龄、电话和地址,并打印出来。

7. `pure = pure->next;`: 这行代码将 `pure` 指针移动到链表中的下一个节点。
8. `}`: 这个大括号标记了 `while` 循环的结束。

总结来说,`ShowContact` 函数的作用是遍历通讯录链表,并打印出每个联系人的详细信息。通过 `con->next` 开始的循环,函数遍历整个链表,使用 `printf` 函数格式化并打印出每个联系人的姓名、性别、年龄、电话和地址。

删除通讯录之前的准备

//查找通讯录数据(查找名字)
contact* FindByName(contact* con, char name[])
{
	assert(con);
	LTNode* pure = con->next;
	while (pure != con)
	{
		if (0 == strcmp(pure->data.name, name))
		{
			return pure;//返回当前的节点
		}
		pure = pure->next;
	}
	return NULL;
}

1. `contact* FindByName(contact* con, char name[])`: 这是函数的声明,`FindByName` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构。第二个参数是一个字符数组 `name`,用来接收用户输入的联系人姓名。

2. `assert(con);`: 这行代码使用 `assert` 函数来检查 `con` 指针是否为 `NULL`。如果 `con` 为 `NULL`,则函数将终止并打印出一条错误信息。`assert` 是 C 语言中的一个用于调试的函数,通常用于检查程序中的条件是否为真。

3. `LTNode* pure = con->next;`: 这行代码定义了一个指针 `pure`,并将其初始化为 `con` 的 `next` 指针,也就是通讯录链表的第一个联系人节点。

4. `while (pure != con)`: 这个 `while` 循环会一直执行,直到 `pure` 指针指向 `con` 本身,即遍历完整个链表。

5. `if (0 == strcmp(pure->data.name, name))`: 这行代码使用 `strcmp` 函数比较当前节点 `pure` 的 `name` 字段和用户提供的 `name` 参数是否相等。`strcmp` 函数在字符串相等时返回 `0`。

6. `return pure;//返回当前的节点`: 如果找到了匹配的姓名,函数通过 `return` 语句返回当前的节点 `pure`。

7. `pure = pure->next;`: 这行代码将 `pure` 指针移动到链表中的下一个节点。

8. `}`: 这个大括号标记了 `while` 循环的结束。

9. `return NULL;`: 如果遍历完整个链表都没有找到匹配的姓名,函数通过 `return` 语句返回 `NULL`,表示没有找到指定的联系人。

总结来说,`FindByName` 函数的作用是在通讯录链表中查找具有特定姓名的联系人。函数通过遍历链表,并与用户提供的姓名进行比较,如果找到匹配的姓名,则返回该节点;如果遍历完链表都没有找到,则返回 `NULL`。

删除通讯录数据

只要是涉及到删除,我们肯定需要进行查找,需要找到是否有这个名字,才能进行删除。所以我们需要调用查找函数。查找函数的实现下面我们会进行实现。

//删除通讯录数据
void DelContact(contact* con)//con的参数是头结点
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要删除的姓名:\n");
	scanf("%s", name);
	contact* find = FindByName(con, name);//这里需要传递
	if (find == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);

	}
	else
	{
		LTdel(find);
		printf("删除成功\n\n");
	}
}

1. `void DelContact(contact* con)`: 这是函数的声明,`DelContact` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构,即头结点。

2. `assert(con);`: 这行代码使用 `assert` 函数来检查 `con` 指针是否为 `NULL`。如果 `con` 为 `NULL`,则函数将终止并打印出一条错误信息。`assert` 是 C 语言中的一个用于调试的函数,通常用于检查程序中的条件是否为真。

3. `char name[NAME_MAX];`: 这行代码定义了一个字符数组 `name`,用于存储用户输入的联系人姓名。`NAME_MAX` 是一个宏,表示姓名缓冲区的最大长度。

4. `printf("请输入你需要删除的姓名:\n");`: 这行代码打印出提示信息,要求用户输入需要删除的联系人姓名。

5. `scanf("%s", name);`: 这行代码使用 `scanf` 函数读取用户输入的姓名,并将其存储在 `name` 数组中。

6. `contact* find = FindByName(con, name);`: 这行代码调用 `FindByName` 函数,查找头结点 `con` 中是否有姓名与 `name` 数组中相同的联系人。如果找到,`find` 指针将指向该联系人节点;如果没有找到,`find` 将为 `NULL`。

7. `if (find == NULL)`: 这段代码检查 `find` 是否为 `NULL`。如果 `find` 为 `NULL`,说明没有找到匹配的联系人。

8. `printf("没有找到这个人\n\n");`: 如果 `find` 为 `NULL`,打印出提示信息,表示没有找到这个人。

9. `exit(1);`: 这行代码调用 `exit` 函数,以状态码 `1` 退出程序。通常 `1` 表示错误退出。

10. `else`: 这个 `else` 语句对应于 `if` 语句的 else 分支,即如果找到了匹配的联系人。

11. `LTdel(find);`: 这行代码调用一个名为 `LTdel` 的函数,该函数可能是链表中的一个函数,用于删除链表中的节点。`find` 指针指向的是需要删除的联系人节点。

12. `printf("删除成功\n\n");`: 如果联系人被成功删除,打印出提示信息,表示删除成功。
总结来说,`DelContact` 函数的作用是删除通讯录中指定姓名的联系人。函数通过提示用户输入需要删除的姓名,然后查找该姓名对应的联系人节点。如果找到,则调用链表删除函数 `LTdel` 来删除该节点,并打印出删除成功的提示信息;如果没有找到,则打印出没有找到这个人的提示信息,并退出程序。

查找通讯录数据

//查找通讯录数据(查找名字)
void FindContact(contact* con)
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要查找的姓名:\n");
	scanf("%s", name);
	contact* ret = FindByName(con, name);//这里需要传递
	if (ret == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);
	}
	else
	{
		printf("%s %s %d %s %s\n",
			ret->data.name,
			ret->data.sex,
			ret->data.age,
			ret->data.tel,
			ret->data.addr
		);
		printf("查找成功\n\n");
	}
}

1. `void FindContact(contact* con)`: 这是函数的声明,`FindContact` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构,即头结点。

2. `assert(con);`: 这行代码使用 `assert` 函数来检查 `con` 指针是否为 `NULL`。如果 `con` 为 `NULL`,则函数将终止并打印出一条错误信息。`assert` 是 C 语言中的一个用于调试的函数,通常用于检查程序中的条件是否为真。

3. `char name[NAME_MAX];`: 这行代码定义了一个字符数组 `name`,用于存储用户输入的联系人姓名。`NAME_MAX` 是一个宏,表示姓名缓冲区的最大长度。

4. `printf("请输入你需要查找的姓名:\n");`: 这行代码打印出提示信息,要求用户输入需要查找的联系人姓名。

5. `scanf("%s", name);`: 这行代码使用 `scanf` 函数读取用户输入的姓名,并将其存储在 `name` 数组中。

6. `contact* ret = FindByName(con, name);`: 这行代码调用 `FindByName` 函数,查找头结点 `con` 中是否有姓名与 `name` 数组中相同的联系人。如果找到,`ret` 指针将指向该联系人节点;如果没有找到,`ret` 将为 `NULL`。

7. `if (ret == NULL)`: 这段代码检查 `ret` 是否为 `NULL`。如果 `ret` 为 `NULL`,说明没有找到匹配的联系人。

8. `printf("没有找到这个人\n\n");`: 如果 `ret` 为 `NULL`,打印出提示信息,表示没有找到这个人。

9. `exit(1);`: 这行代码调用 `exit` 函数,以状态码 `1` 退出程序。通常 `1` 表示错误退出。

10. `else`: 这个 `else` 语句对应于 `if` 语句的 else 分支,即如果找到了匹配的联系人。

11. `printf("%s %s %d %s %s\n",`: 这行代码是 `else` 分支中打印联系人类型和详细信息的格式化字符串。

12. `ret->data.name,`: 这行代码从 `ret` 指针指向的联系人节点中获取姓名。

13. `ret->data.sex,`: 这行代码从 `ret` 指针指向的联系人节点中获取性别。

14. `ret->data.age,`: 这行代码从 `ret` 指针指向的联系人节点中获取年龄。

15. `ret->data.tel,`: 这行代码从 `ret` 指针指向的联系人节点中获取电话。

16. `ret->data.addr`: 这行代码从 `ret` 指针指向的联系人节点中获取地址。

17. `);`: 这个圆括号标记了 `printf` 函数的参数列表结束。

18. `printf("查找成功\n\n");`: 如果联系人被成功找到,打印出提示信息,表示查找成功。

总结来说,`FindContact` 函数的作用是在通讯录中查找指定姓名的联系人,并打印出该联系人的详细信息。函数通过提示用户输入需要查找的姓名,然后查找该姓名对应的联系人节点。如果找到,则打印出联系人的详细信息并提示查找成功;如果没有找到,则打印出没有找到这个人的提示信息,并退出程序。

修改通讯录数据

修改通讯录其实就是直接在原来的函数基础上进行覆盖,当然还是进行查找,找到才能修改。找到后会直接返回节点,我们根据节点直接对其进行数值的覆盖。

//修改通讯录数据
void ModifyContact(contact* con)
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要修改的姓名:\n");
	scanf("%s", &name);
	// 这里需要传递指针,因为接受的是二级指针,我们需要形参的改变影响实参,传递来的是指向链表的指针的地址
	// 这个指针指向链表的空间,所以我们要修改通讯录的内容,需要把指向这个链表的指针传递过去
	// 指针找到这个名字,返回值不是空,说明找到了,返回的是节点的地址
	// 最后我们直接对节点内存空间进行修改,因为我们这里申请空间是节点指向的下一个的内存空间
	// 所以我们需要每次进入到节点的内存块里面进行内存的修改
	contact* ret = FindByName(con, name);
	if (ret == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);

	}
	printf("请输入姓名:\n");
	scanf("%s", ret->data.name);

	printf("请输入性别:\n");
	scanf("%s", ret->data.sex);

	printf("请输入年龄:\n");
	scanf("%d", &ret->data.age);

	printf("请输入电话:\n");
	scanf("%s", ret->data.tel);


	printf("请输入地址:\n");
	scanf("%s", ret->data.addr);

	printf("修改成功\n\n");
}

1. `void ModifyContact(contact* con)`: 这是函数的声明,`ModifyContact` 是函数名,`void` 表示这个函数不返回任何值。`contact*` 表示这个函数的参数是一个指向 `contact` 类型的指针,`con` 用来指向整个通讯录的结构,即头结点。

2. `assert(con);`: 这行代码使用 `assert` 函数来检查 `con` 指针是否为 `NULL`。如果 `con` 为 `NULL`,则函数将终止并打印出一条错误信息。`assert` 是 C 语言中的一个用于调试的函数,通常用于检查程序中的条件是否为真。

3. `char name[NAME_MAX];`: 这行代码定义了一个字符数组 `name`,用于存储用户输入的联系人姓名。`NAME_MAX` 是一个宏,表示姓名缓冲区的最大长度。

4. `printf("请输入你需要修改的姓名:\n");`: 这行代码打印出提示信息,要求用户输入需要修改的联系人姓名。

5. `scanf("%s", &name);`: 这行代码使用 `scanf` 函数读取用户输入的姓名,并将其存储在 `name` 数组中。

6. `contact* ret = FindByName(con, name);`: 这行代码调用 `FindByName` 函数,查找头结点 `con` 中是否有姓名与 `name` 数组中相同的联系人。如果找到,`ret` 指针将指向该联系人节点;如果没有找到,`ret` 将为 `NULL`。

7. `if (ret == NULL)`: 这段代码检查 `ret` 是否为 `NULL`。如果 `ret` 为 `NULL`,说明没有找到匹配的联系人。

8. `printf("没有找到这个人\n\n");`: 如果 `ret` 为 `NULL`,打印出提示信息,表示没有找到这个人。

9. `exit(1);`: 这行代码调用 `exit` 函数,以状态码 `1` 退出程序。通常 `1` 表示错误退出。

10. `else`: 这个 `else` 语句对应于 `if` 语句的 else 分支,即如果找到了匹配的联系人。

11. `printf("请输入姓名:\n");`: 这行代码打印出提示信息,要求用户输入新的联系人姓名。

12. `scanf("%s", ret->data.name);`: 这行代码使用 `scanf` 函数读取用户输入的新姓名,并将其存储在 `ret` 指针指向的联系人节点的 `name` 字段中。

13. `printf("请输入性别:\n");`: 这行代码打印出提示信息,要求用户输入新的联系人性别。

14. `scanf("%s", ret->data.sex);`: 这行代码使用 `scanf` 函数读取用户输入的新性别,并将其存储在 `ret` 指针指向的联系人节点的 `sex` 字段中。

15. `printf("请输入年龄:\n");`: 这行代码打印出提示信息,要求用户输入新的联系人年龄。

16. `scanf("%d", &ret->data.age);`: 这行代码使用 `scanf` 函数读取用户输入的新年龄,并将其存储在 `ret` 指针指向的联系人节点的 `age` 字段中。注意这里需要使用 `&` 符号来获取变量的地址。

17. `printf("请输入电话:\n");`: 这行代码打印出提示信息,要求用户输入新的联系人电话。

18. `scanf("%s", ret->data.tel);`: 这行代码使用 `scanf` 函数读取用户输入的新电话,并将其存储在 `ret` 指针指向的联系人节点的 `tel` 字段中。

19. `printf("请输入地址:\n");`: 这行代码打印出提示信息,要求用户输入新的联系人地址。

20. `scanf("%s", ret->data.addr);`: 这行代码使用 `scanf` 函数读取用户输入的新地址,并将其存储在 `ret` 指针指向的联系人节点的 `addr` 字段中。

写入到文件里面

C语言-文件操作函数基础fgetc(读字符),fputc(写字符),fgets(读文本),fputs(写文本),fclose(关闭文件),fopen(打开文件)-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137128099文件其实就是函数的理解,这里附带两个链接。讲解的还是很透彻的

//写入到文件里面
void LoadContact(contact* con)
{
	assert(con);
	FILE* ps = fopen("Address_Book.txt", "w");
	if (ps == NULL)
	{
		perror("fopen:book:");
		exit(1);
	}
	contact* newnode = con->next;
	while (newnode != con)
	{
		fgets(con, 1, ps);
		fprintf(ps, "%s %s %d %s %s\n",
			newnode->data.name,
			newnode->data.sex,
			newnode->data.age,
			newnode->data.tel,
			newnode->data.addr);
		newnode = newnode->next;
	}
	fclose(ps);
	ps = NULL;
	printf("成功写到文件里面\n");
}

C语言-文件操作函数进阶+printf+scanf+sscanf+sprintf+fprintf+fscanf+fwrite+fread+fseek+ftell+rewind+feof-CSDN博客https://blog.csdn.net/Jason_from_China/article/details/137155626https://blog.csdn.net/Jason_from_China/article/details/137155626

首先,代码打开一个名为"Address_Book.txt"的文件,以写入模式("w")。如果文件打开失败,代码将输出错误信息并退出程序。
然后,代码遍历循环链表,从第一个节点开始,直到回到起始节点。在每次迭代中,代码将当前节点的联系人信息写入文件,格式为:姓名 性别 年龄 电话 地址,每个信息之间用空格分隔,每行结束后添加一个换行符。
最后,代码关闭文件,并将文件指针设置为NULL,表示文件已经关闭。然后输出"成功写到文件里面"的信息,表示联系人信息已经成功写入文件。

通讯录的销毁

也就是调用链表的销毁

//销毁通讯录数据
void DestroyContact(contact* con)
{
	assert(con);
	LTDestroy(con);
	printf("销毁节点\n");
}

通讯录代码的总结

contact.h文件

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#define NAME_MAX 20//姓名
#define SEX_MAX 20//性别
#define TEL_MAX 20//电话
#define ADDR_MAX 100//地址

//前置声明
typedef struct ListNode contact;

//用户数据
typedef struct PersonInfo
{
    char name[NAME_MAX];//姓名
    char sex[SEX_MAX];//性别
    int age;//年龄
    char tel[TEL_MAX];//电码
    char addr[ADDR_MAX];//地址
}PeoInfo;
//通讯录的初始化
void InitContact(contact** con);

//添加通讯录数据
void AddContact(contact* con);
//删除通讯录数据
void DelContact(contact* con);
//展示通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);

//修改通讯录数据
void ModifyContact(contact* con);
//销毁通讯录数据
void DestroyContact(contact* con);
//写入到文件里面
void  LoadContact(contact* con);

contact.c文件

#include"SList.h"
//通讯录的初始化
void InitContact(contact** pphead)
{
	//初始化空间就是创建头结点,双向链表是需要头结点的
	LTNode* newpphead = (LTNode*)malloc(sizeof(LTNode));
	if (newpphead == NULL)
	{
		perror("error:newpphead:ltinit");
		exit(1);
	}
	*pphead = newpphead;
	(*pphead)->next = *pphead;
	(*pphead)->prev = *pphead;

}
//添加通讯录数据
void AddContact(contact* con)
{
	PeoInfo info;
	printf("请输入姓名:\n");
	scanf("%s", info.name);

	printf("请输入性别:\n");
	scanf("%s", info.sex);

	printf("请输入年龄:\n");
	scanf("%d", &info.age);

	printf("请输入电话:\n");
	scanf("%s", info.tel);


	printf("请输入地址:\n");
	scanf("%s", info.addr);

	LTPushBack(con, info);
	printf("添加成功\n\n");
}
//展示通讯录数据
void ShowContact(contact* con)
{
	printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "地址");
	contact* pure = con->next;
	while (pure != con)
	{
		printf("%s %s %d %s %s\n",
			pure->data.name,
			pure->data.sex,
			pure->data.age,
			pure->data.tel,
			pure->data.addr);
		pure = pure->next;
	}
}
//查找通讯录数据(查找名字)
contact* FindByName(contact* con, char name[])
{
	assert(con);
	LTNode* pure = con->next;
	while (pure != con)
	{
		if (0 == strcmp(pure->data.name, name))
		{
			return pure;//返回当前的节点
		}
		pure = pure->next;
	}
	return NULL;
}
//删除通讯录数据
void DelContact(contact* con)//con的参数是头结点
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要删除的姓名:\n");
	scanf("%s", name);
	contact* find = FindByName(con, name);//这里需要传递
	if (find == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);

	}
	else
	{
		LTdel(find);
		printf("删除成功\n\n");
	}
}

//查找通讯录数据(查找名字)
void FindContact(contact* con)
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要查找的姓名:\n");
	scanf("%s", name);
	contact* ret = FindByName(con, name);//这里需要传递
	if (ret == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);
	}
	else
	{
		printf("%s %s %d %s %s\n",
			ret->data.name,
			ret->data.sex,
			ret->data.age,
			ret->data.tel,
			ret->data.addr
		);
		printf("查找成功\n\n");
	}
}
//修改通讯录数据
void ModifyContact(contact* con)
{
	assert(con);
	char name[NAME_MAX];//姓名
	printf("请输入你需要修改的姓名:\n");
	scanf("%s", &name);
	// 这里需要传递指针,因为接受的是二级指针,我们需要形参的改变影响实参,传递来的是指向链表的指针的地址
	// 这个指针指向链表的空间,所以我们要修改通讯录的内容,需要把指向这个链表的指针传递过去
	// 指针找到这个名字,返回值不是空,说明找到了,返回的是节点的地址
	// 最后我们直接对节点内存空间进行修改,因为我们这里申请空间是节点指向的下一个的内存空间
	// 所以我们需要每次进入到节点的内存块里面进行内存的修改
	contact* ret = FindByName(con, name);
	if (ret == NULL)
	{
		printf("没有找到这个人\n\n");
		exit(1);

	}
	printf("请输入姓名:\n");
	scanf("%s", ret->data.name);

	printf("请输入性别:\n");
	scanf("%s", ret->data.sex);

	printf("请输入年龄:\n");
	scanf("%d", &ret->data.age);

	printf("请输入电话:\n");
	scanf("%s", ret->data.tel);


	printf("请输入地址:\n");
	scanf("%s", ret->data.addr);

	printf("修改成功\n\n");
}
//写入到文件里面
void LoadContact(contact* con)
{
	assert(con);
	FILE* ps = fopen("Address_Book.txt", "w");
	if (ps == NULL)
	{
		perror("fopen:book:");
		exit(1);
	}
	contact* newnode = con;
	while (newnode != NULL)
	{
		fgets(con, 1, ps);
		fprintf(ps, "%s %s %d %s %s\n",
			newnode->data.name,
			newnode->data.sex,
			newnode->data.age,
			newnode->data.tel,
			newnode->data.addr);
		newnode = newnode->next;
	}
	fclose(ps);
	ps = NULL;
	printf("成功写到文件里面\n");
}
//销毁通讯录数据
void DestroyContact(contact* con)
{
	assert(con);
	LTDestroy(con);
	printf("销毁节点\n");
}

test.c文件

#include"SList.h"

void menu()
{
	printf("********************通讯录********************\n");
	printf("*     1, 增加联系人     2,删除联系人        *\n");
	printf("*     3,修改联系人     4,查找联系人        *\n");
	printf("*     5,展示联系人     6,存储到文件        *\n");
	printf("********************0退出*********************\n");
}
int main()
{
	int input = 1;
	PeoInfo* info = NULL;
	InitContact(&info);
	do
	{
		menu();
		printf("输入数值进行通讯录的使用操作:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			AddContact(info);
			break;
		case 2:
			DelContact(info);
			break;
		case 3:
			ModifyContact(info);
			break;
		case 4:
			FindContact(info);
			break;
		case 5:
			ShowContact(info);
			break;
		case 6:
			LoadContact(info);
			break;
		case 0:
			DestroyContact(info);
			printf("退出成功\n");
			break;
		default:
			printf("请选择正确的数值\n");
			break;
		}
	} while (input != 0);

	return 0;
}

SList.h文件

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdbool.h>
#include<stdlib.h>
#include"contact.h"
#pragma once

//自定义类型
typedef PeoInfo LTDataType;

typedef struct ListNode
{
	LTDataType data;//数值
	struct ListNode* next;//下一个节点
	struct ListNode* prev;//上一个节点
}LTNode;

//申请节点
LTNode* SLBuyNode(LTDataType x);
//销毁空间
void LTDestroy(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopBack(LTNode* phead);
//尾删
void LTPopFront(LTNode* phead);

//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//在pos位置之后删除数据
void LTErase(LTNode* pos);

//删除数据pos节点
void LTdel(LTNode* pos);

SList.c文件

#include"SList.h"
//申请节点
LTNode* SLBuyNode(LTDataType x)
{
	//开辟空间
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("SLBuyNode:newnode:error:");
		exit(1);
	}
	//赋值
	newnode->data = x;
	//形成环形链表,头结点尾结点都指向自己
	newnode->next = newnode->prev = newnode;
	return newnode;
}
//销毁空间
void LTDestroy(LTNode* phead)
{
	assert(phead);
	//销毁空间是从头结点之后进行销毁的
	LTNode* prev = phead->next;
	// 双链表尾结点的下一个节点就是第一个节点(不是头结点,头结点指的是哨兵位)
	// 所以当需要循环的时候,遇到第一个节点说明循环结束
	while (prev != phead)
	{
		LTNode* next = prev->next;
		free(prev);
		prev = next;
	}
	free(phead);
	phead = NULL;
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	// 申请节点
	LTNode* newnode = SLBuyNode(x);
	// newnode作为第一步的原因就是newnode的操作不影响其他的数值
	newnode->next = phead;
	// 如果有phead->d1->d2->d3=phead->prev
	// 这里我们先把newnode不会影响数值变化的newnode的下一节点指向phead
	// 这里我们注意,我们不能先改变d3也就是phead->prev
	// 所以我们可以先把d3的下一节点指向新节点,要是改变d3也就是phead->prev的节点,我们就找不到参数
	// 此时我们还是可以找到头结点的上一个节点的d3
	newnode->prev = phead->prev;

	// 这里是很关键的一步,我们还是不能先改变头结点的上一个节点
	// 所以我们先改变d3的下一个节点
	phead->prev->next = newnode;
	// 最后改变头节点的上一个节点,指向尾结点
	phead->prev = newnode;
}

//头插优化
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = SLBuyNode(x);

	//这里的逻辑是先把新节点的整体的逻辑指向数值中间,保证不影响头结点下一个节点的参数
	newnode->next = phead->next;
	newnode->prev = phead;


	//此时newnode的指向是不影响其他数值的,所以我们为了不丢失节点,我们就不能先改变phead->next,这个节点的位置
	//phead->next这个节点只能是在进行改变,因为我们可以把这个节点当做参数,对照物
	phead->next->prev = newnode;
	//最后改变phead->next节点的位置,变成newnode
	phead->next = newnode;

}
//头删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	//为了方便创建删除的节点
	LTNode* del = phead->next;
	//头结点的下一个节点,是第二节点。
	phead->next = del->next;
	//第三节点的上一节点是phead,也就是头结点
	del->next->prev = phead;

	//释放节点
	free(del);
	del = NULL;
}

//尾删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//为了删除的节点
	LTNode* del = phead->prev;
	//头结点的上一节点作为参照不能优先删除
	phead->prev = del->prev;
	del->prev->next = phead;
}


//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	//申请节点
	LTNode* newnode = SLBuyNode(x);
	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;

}

//在pos位置之后删除数据
void LTErase(LTNode* pos)
{
	assert(pos);
	//创建需要删除的节点
	LTNode* del = pos->next;
	//双链表的关键是不循环,那么不循环的情况,
	//我们需要有一个数值是不变化的,或者是最后才变化的
	//所以。最后变化的数值往往是需要动的节点的下一个节点
	del->next->prev = pos;
	pos->next = del->next;

}
//删除数据pos节点
void LTdel(LTNode* pos)
{
	assert(pos);
	//头节点
	if (pos == pos->next) 
	{
		free(pos);
		pos = NULL;
		return;
	}
	LTNode* del = pos;
	LTNode* next = pos->next;
	LTNode* pure = pos->prev;
	pure->next = next;
	next->prev = pure;

	free(pos);
	pos = NULL;
}

测试

没有问题

相关推荐
五味香几秒前
Linux学习,ip 命令
linux·服务器·c语言·开发语言·git·学习·tcp/ip
欧阳枫落6 分钟前
python 2小时学会八股文-数据结构
开发语言·数据结构·python
何曾参静谧13 分钟前
「QT」文件类 之 QTextStream 文本流类
开发语言·qt
monkey_meng17 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
手握风云-19 分钟前
零基础Java第十六期:抽象类接口(二)
数据结构·算法
落落落sss25 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
2401_853275731 小时前
ArrayList 源码分析
java·开发语言
zyx没烦恼1 小时前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++
lb36363636361 小时前
整数储存形式(c基础)
c语言·开发语言
feifeikon1 小时前
Python Day5 进阶语法(列表表达式/三元/断言/with-as/异常捕获/字符串方法/lambda函数
开发语言·python