文章目录
注:本文记录以结构体实现结点的链表。
结构体是一种数据结构,而链表是一种基础的数据结构,采用多个结点记录数据,并用指针作为索引。
创建链表
结构体回顾
由于链表的结点以结构体形式实现,故欲学链表先学结构体,对于结构体的只是我们进行简单回顾:
结构体是一种可以由我们自行规定内容的数据类型,定义完毕后可以和int
,double
等数据类型一样使用,其内容可以包含多个和多种数据类型,
结构体的定义方式:
//声明内容:
struct booksMember{
char writer[50];
char name[100];
int price;
};
//声明后实例化,并正常赋值,使用
struct bookMember book;
book.price = 50;
book.price ++;
其中用.
来表示对结构体内部成员的查询。对于实例化,与int
,double
一样,只是这里的数据类型是struct+你的结构体名称
。int
实例化即int+变量名
对应例子中的struct bookMember book;
在结构体结束的分号前加上自定义变量名称可以等同于后续的struct+你的结构体名称+变量名称
,即
struct booksMember{
char writer[50];
char name[100];
int price;
}book;
book.price = 50;
book.price ++;
typedef关键字
typedef
是配合结构体使用的关键字,可以通过为结构体数据类型加一个别名,简化结构体部分的代码。
回归到之前的例子:
如果只使用struct
,需要定义多个结构体实例。
//声明内容:
struct booksMember{
char writer[50];
char name[100];
int price;
};
//声明后实例化,并正常赋值,使用
struct bookMember book1;
struct bookMember book2;
struct bookMember book3;
若使用typedef
struct booksMember{
char writer[50];
char name[100];
int price;
};
// 为数据类型添加别名,此刻booklist和“struct booksMember”是等价的。
typedef struct bookMember bookList;
bookList book1;
bookList book2;
bookList book3;
同理,
typedef struct booksMember{
char writer[50];
char name[100];
int price;
}bookList;
与上面效果一样。
使用结构体作为链表的结点
复习完结构体后,将其应用到链表当中。
链表与结构体差在,链表是使用结构体作为结点,以指针作为索引的数据结构。简单来说,在结构体的基础上加上索引指针。
数组同样是储存数据的一种数据结构,采用固定长度的块状内存,固角标相对位置保持不变,故可使用角标作为索引。
而链表的索引原理,即每个结点携带下一个结点的地址。由于结点地址在内存随机分布,且可以按照需求任意选择长度(设置几个结点),无法使用角标索引,故只能靠记录其他结点的地址索引。
typedef struct booksMember{
char writer[50];
char name[100];
int price;
struct booksMember* next;
}bookList;
增
以上规定了的内容,接下来我们学习如何使用使用它。
动态申请一个结点
上面提到链表结点无固定长度,是根据程序员设置几个结点决定的,相比于数组类似int a[100]
的粗暴分配内存方式,链表的结点需要动态分布,即新增一个结点则分配一个结点的空间。配套的函数有malloc
和new
,本文使用的方法是malloc。
malloc
返回所分配的内存的首地址。格式如下:
(*所需分配的内存数据类型)malloc(所需要分配的内存大小)
// 前面的括号加*是因为malloc要返回地址。
尾插法
每个结点的数据域部分内容按照根据自己的需求决定,而指针域指向下一个结点的地址,最后一个结点的指针为空(NULL
)。这里我们发现链表各个结点之间关联性弱,无法直接找到某个特定结点,只能从头遍历,所以需要将第一个结点的位置(头指针)特殊保存下来,以便查询。
而我们欲在链表尾部新增结点,只需找到指针为NULL的结点,更新指针为新增结点的地址即可。
代码示例:
//假设head为头指针
p = *head;
while(p) p = p->next;
// 此时p为尾结点的指针域
// 按照上一个版块的内容,动态分配结点内存,并赋值。
// ……
p = 新增结点的地址
头插法
上面提到由于链表各个结点之间关联性弱,需要将头指针保存下来,以便查询。而对于链表的头指针,有两种不同的处理。
带头指针
第一种是头结点不存放数据,由一个指针存放头指针的地址。
如果需要在首部插入新节点,则需要将新增结点的指针域指向头节点当前的指针域,并将头指针更新为新节点的地址。
//设新增结点为p
p->next = head->next;
head->next = *p;
不带头指针
第二种是头节点存放数据,如果要读取第一个数据,直接读取head
即可。
总而言之,头节点的两种处理方式对于尾插法蚌埠影响,区别在于,一个头指针的数据域是空的,要访问第一个数据需要查看head->next,而另一种头指针数据域存放了数据,可以直接访问。
中途插入
若要在非头非尾处进行插入操作,由于链表各结点之间关联较小,无需对整体进行操作,只需要对需要插入位置的相邻两个指针的指针域进行操作即可。
图示:
若查到需要在r结点之后插入新节点,设新节点为p,则:
p->next = r->next;
r->next = *p;
删
尾删
链表的空间动态在于可以随时增加一个新结点也可以随时将一个结点的内存释放。而讲完动态分配内存,我们看如何释放内存,与mallloc
和new
对应的,需要一个free
函数,给定参数为需要释放的结点的地址。
故需要尾删的话只需找到最后一个结点,释放该结点的空间即可。
p = *head;
while(p->next) p = p->next;
free(p);
原理:链表更新结点只需更新相邻元素的指针,而尾结点只有一个相邻结点,释放尾结点后,尾结点的前一个结点的指针指向则变成了NULL
,自动更新为了尾指针。
头删
带头指针
将头指针指向的结点更新为第二个结点即可。
不带头指针
//将head结点更新为第二个结点。
p->next = head->next;
head = p;
中途删除
将需要删除结点的前一个结点的指针域更新为需要删除结点的下一个结点,与带头指针的头删法类似。
改/查
如果需要查找包含指定数据的结点,可以单独写一个函数,找到指定元素的位置,返回该结点的地址,一般返回指定地址的前一个结点的地址,因为可以方便进行增删等操作。
SListNode* SListFind(SListNode* head, SLTDateType x)
{
SListNode* cur = head;
while (cur->next)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
}
如果需要对查到的结点进行更改,一般都是更改数据,重新赋值即可。
释放空间
如果需要销毁单链表:
cur = head;
while(cur) {
head->next = cur->next;
free(cur);
}
代码汇总
#include <iostream>
using namespace std;
typedef struct linkList {
int data;
struct linkList* next;
} linkList;
linkList* create() {
return (linkList*)malloc(sizeof(linkList));
}
// 头插
void headAssert(linkList* head, int d) {
linkList* p = create();
p->next = head->next;
head->next = p;
p->data = d;
}
// 尾插
void tailAssert(linkList* head, int d) {
linkList* p = create();
p->next = NULL;
linkList* cur = head;
while (cur->next) {
cur = cur->next;
}
cur->next = p;
p->data = d;
}
// 打印链表
void printList(linkList* head) {
linkList* cur = head->next;
while (cur) {
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
// 释放链表内存
void freeList(linkList* head) {
linkList* cur = head->next;
while (cur) {
head->next = cur->next;
free(cur);
cur = head->next;
}
free(head);
}
// 中途插入
void insertAfter(linkList* prev, int d) {
linkList* p = create();
p->next = prev->next;
prev->next = p;
p->data = d;
}
// 尾删
void deleteTail(linkList* head) {
linkList* cur = head;
while (cur->next && cur->next->next) {
cur = cur->next;
}
if (cur->next) {
linkList* temp = cur->next;
cur->next = NULL;
free(temp);
}
}
// 头删
void deleteHead(linkList* head) {
if (head->next) {
linkList* temp = head->next;
head->next = temp->next;
free(temp);
}
}
// 查找节点
linkList* searchNode(linkList* head, int target) {
linkList* cur = head->next;
while (cur) {
if (cur->data == target) {
return cur;
}
cur = cur->next;
}
return NULL; // 如果找不到目标节点,则返回NULL
}
// 更改节点数据
void modifyNode(linkList* node, int newData) {
if (node) {
node->data = newData;
}
}
int main() {
linkList* head = create();
head->next = NULL;
//在链表首部插入1
headAssert(head, 1);
//在链表尾部插入2
tailAssert(head, 2);
//打印链表
printf("头插尾插:\n");
printList(head);
//查找需要更改的结点地址
linkList* nodeToModify = searchNode(head, 2);
//如果找到了,进行更改
if (nodeToModify) {
modifyNode(nodeToModify, 3);
}
//更改
printf("更改内容:\n");
printList(head);
//中途插入
printf("在指定地址后插入结点:\n");
insertAfter(head, 4);
printList(head);
// 尾删
printf("去尾:\n");
deleteTail(head);
printList(head);
//头删
printf("去头:\n");
deleteHead(head);
printList(head);
//释放链表
freeList(head);
return 0;
}
运行结果:
总结
单链表麻烦之处在于查找,而胜在空间和数据插入和删除方便,数组反之。可根据题目要求选择数据结构。