借助力扣上设计链表的这道题,我为大家详细解析下C语言的单链表应该怎么写
1.设置链表结点
结点的内容分为两部分,一部分是数据域,一部分是指针域,数据域存储了你想存储的数据,而指针域指向下一个结点,从结点的写法我们就可以看出来,链表它在逻辑上是连续的,但是他不像数组在空间上是连续的,因为我们后面创建结点时候使用的动态内存开辟函数,malloc或是calloc都是系统随机分配一块空间的,如下图
因为题目指定了结点的内容分为val和next,代码如下
typedef struct MylinkedList{
int val;
struct MyLinkedList* next;
} MyLinkedList;
我在struct的后面补全了 MylinkedList,更利于理解,这样子MyLinkList就可以直接代表struct MyLinkedList,简化后面代码的书写
2.创建链表
创建链表的头结点,单链表的头结点分为储值头结点和不储值的头结点,我在这里使用是不储值的头结点,这与我们后面要实现的操作有关系,使用不储值的头结点方便我们只用一种逻辑就可以处理所有的情况,而不用对于特定情况再拿出来单独写代码分析,代码如下
MyLinkedList* myLinkedListCreate() {
MyLinkedList* ret = (MyLinkedList*)malloc(sizeof(struct MyLinkedList));
ret -> next = NULL;
return ret;
}
先为头结点申请一块空间,数据域不用赋值,因为我们不用头结点存储,它只是开始标志,然后将指针域置为NULL,最后返回,
3.根据索引求值
画图解释下我写的代码怎么看索引
所以我们先设置一个指针指向结点0,也就是obj -> next,然后通过遍历链表找到目标索引结点,并返回其值,如果找不到返回-1,代码如下
int myLinkedListGet(MyLinkedList* obj, int index) {
int i = 0;
MyLinkedList* cur = obj -> next;
for(i = 0; cur != NULL; i++)
{
if(i == index)
{
return cur -> val;
}
else
{
cur = cur -> next;
}
}
return -1;
}
4.头插法插入结点
这乍一看就像是头结点的next指向新节点,然后新结点的next指向结点0,但是我们可不能这么写,如果一开始就将头结点的next指向新节点,那么我们再也找不到结点0了,所以我们要先将新节点的next指向结点0,再将头结点的next指向新节点,代码如下
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newNode -> val = val;
newNode -> next = obj -> next;
obj -> next = newNode;
}
5.尾插法插入结点
逻辑上与头插法一模一样,我们只需要找到尾部再重复一下头插法的操作就好了,代码如下
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList* ntail = obj;
while(ntail -> next != NULL)
{
ntail = ntail -> next;
}
MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newNode -> val = val;
newNode -> next = ntail -> next;
ntail -> next = newNode;
}
在这里我想讨论下为什么要使用不赋值的头结点,我们通过找到前一个结点来进行尾插,如果链表中只有一个结点的话,而且又使用赋值头结点的写法的话,第一个节点就是头结点,它是没有前一个节点的,就要单独拿出来分类讨论,但是使用不赋值头结点的写法的话,第一个节点是有前一个结点的,前一个结点也就是头结点,这样我们用同一种逻辑就可以处理所有的情况
6.根据索引插入结点
根据索引插入结点,我们只要找到目标索引的前一个结点就可以利用同样的逻辑插入了,在这里我们也可以看到使用不赋值头结点的好处,代码如下
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
MyLinkedList* cur = obj;
int i = 0;
for(i = 0; cur != NULL; i++)
{
if(index == i)
{
MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newNode -> val = val;
newNode -> next = cur -> next;
cur -> next = newNode;
}
else
{
cur = cur -> next;
}
}
}
在这里我们的cur指针指向头结点,然后i从0开始遍历,当i等于index时候,cur指向了目标索引结点的前一个结点
7.根据索引删除结点
删除结点的操作之前还没出现过,这里画图解释一下,假设删除结点2
简而言之就是将结点1的next指向结点3,但我们需要保存结点2的位置,用于后面free结点2的空间,而且通过观察我们可以发现这根插入操作逻辑相近,关键也在于找到目标索引的前一个结点,再次看到了不赋值头结点的好处,代码如下
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
MyLinkedList* cur = obj;
int i = 0;
for(i = 0; cur != NULL; i++)
{
if(index == i)
{
if(cur -> next != NULL)
{
MyLinkedList* targetNode = cur -> next;
cur -> next = targetNode -> next;
free(targetNode);
}
}
else
{
cur = cur -> next;
}
}
}
在这中间有个细节需要注意下,因为我们的cur指向的前一个结点,我们需要判断目标索引结点存不存在,你可能想问怎么之前插入时候不判断,因为之前是插入操作,不存在就插入,也就不用判断,而现在是删除操作,不存在就不用删除
8.释放列表
将包括头结点的所有结点一个个释放,代码如下
void myLinkedListFree(MyLinkedList* obj) {
while(obj)
{
MyLinkedList* cur = obj;
obj = obj -> next;
free(cur);
}
}