在线性表中,使用最多的莫过于链表了,链表有多个种类,而且使用方便,接下来我们来慢慢了解链表的创建以及基本操作;
链表的类型
1:单向链表
2:双向链表
3:带头链表
4:不带头链表
5:循环链表
6:非循环链表
以上是一些链表的基本图示,而我们最常用的是单向无头非循环链表和双向带头循环链表;
单向无头非循环链表
双向带头循环链表
当然,从结构的难易程度上来说,我们先来体会下单向无头非循环链表的创建以及基本操作;
单向无头非循环链表的创建
typedef int DataType;
typedef struct STD {
DataType data;
struct STD* next;
}STD;
所谓链表,顾名思义,就是通过某些东西链接起来的表,在c语言中,链接两个变量最好的东西就是指针,而链表就是通过一个自引用的指针变量next来链接每一个节点;
在明白了链表的基本变量后我们来完成增删查改的基本操作;
创建节点
STD* BuyNewNode(DataType x)
{
STD* ret = (STD*)malloc(sizeof(STD));
if (ret == NULL)
{
perror("malloc fail");
return NULL;
}
ret->data = x;
ret->next = NULL;
return ret;
}
头插
void STDPushFront(STD** head, DataType x)
{
assert(head);
STD* tmp = BuyNewNode(x);
tmp->next = *head;
*head = tmp;
}
尾插
void STDPushBack(STD** head, DataType x)
{
assert(head);
STD* tmp = BuyNewNode(x);
if (*head == NULL)//第一次插入数据
{
(*(head)) = tmp;
}
else
{
STD* tail = *head;
while (tail->next != NULL)//找到尾部节点
{
tail = tail->next;
}
tail->next = tmp;
}
}
主函数中的测试函数
void test1()
{
STD* head = NULL;
STDPushBack(&head, 1);
STDPushBack(&head, 2);
STDPushBack(&head, 3);
STDPushBack(&head, 4);
STDprint(head);
STDPushFront(&head, 5);
STDPushFront(&head, 6);
STDPushFront(&head, 7);
STDPushFront(&head, 8);
STDprint(head);
}
看到这里大家应该会有一些疑惑,比如为什么头节点是指针类型,为什么传参需要传指针的地址之类的,这里一一为大家解答;
为什么链表的头节点是指针类型
链表的每一个节点都是由malloc之类的动态内存开辟函数开辟出来的,而这种函数开辟出来的地址都必须用指针类型接受,因此我们的头节点也必须是一样的指针类型;
为什么传参需要传指针地址
我们首先看头插函数,该函数每次都会开辟一个新的节点,然后将节点的next指向head,再改变head的内容,这里我用几张图来方便理解;
首先如图,main函数中创建了一个*head变量,指向NULL,而在头插函数内部,创建了一个*tmp并且为它开辟了一个空间,而头插就是需要将*tmp插在*head的之前,而我们要在*head之前插入一个变量很简单,就只需要将*tmp的*next指向*head就可以了;
但是这样仅仅只是将*tmp插入在*head之前, 但我们的头指针还没有改变,若是不改变*head那么就相当于没有头插,而我们若是想改变*head,根据函数传址传值的规则,我们必须传*head的地址,才能改变*head本身,这就是为什么我们需要传二级指针才行;
再看尾插,我们发现,尾插函数中只有当*head为NULL时,也就是第一次尾插才需要解引用变量,而不是第一次尾插则是需要找到尾节点然后改变其*next指针所指向的内容就行了;
现在我们就明白了,当我们要改变头节点本身时就需要传二级指针或者进行解引用操作,而当我们只要改变节点的next指针域的时候,就不要传二级指针或者解引用操作;
那么有没有一种办法让我们什么时候都不要传二级指针呢?当然有,那就是创建带头的链表;当一个节点有了首元节点时,我们不管是尾插还是头插,不管是第一次插入还是多次插入,都不用传二级指针,因为每次都是改变的*head内部的*next所指向的内容,而非*head本身;
当我们明白为什么传二级指针之后,接下来的操作都十分简单了;
头删
void STDDealFront(STD** head)
{
assert(head);
if (*head == NULL)
{
return;
}
STD* Del = *head;
*head = (*head)->next;
free(Del);
}
当我们进行头删的时候,我们需要让*head指向下一个节点,再free原本的节点,这就改变了*head本身,于是我们要传二级指针;
就像这样,del指向原本head的位置,head往后走,最后free(del);
void STDDealBack(STD** head)
{
assert(head);
if (*head == NULL)
{
return;
}
STD* tail = *head;
if ((*head)->next == NULL)
{
free(*head);
*head = NULL;
}
else
{
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
而尾删也是差不多的,只不过需要分两种情况,一种是只有一个节点了,一种是还有一个以上的节点,这里也画图来简单理解下;
首先是有一个以上节点的链表进行尾删
我们需要找到该链表的尾节点才能进行尾删操作,于是我们用*tail指向*head,然后循环知道找到最后一个节点 ,但我们还是需要将链表的最后一个节点的next指向NULL,否则链表会出错,因此我们这样寻找到倒数第二个节点;
找到倒数第二个节点后再free(tail->next);
这样我们就成功的尾删了,并且我们能够成功的将tail的next置为NULL;
然后是只有一个节点的时候,我们只用直接free(*head),然后将*head置空就ok了;
之后就是查找,修改等函数了;
查找某个节点,有则返回地址,无则返回NULL
STD* STDFind(STD** head, DataType x)
{
assert(head);
STD* pos = *head;
while (pos)
{
if (pos->data == x)
{
return pos;
}
pos = pos->next;
}
return NULL;
}
某位后插入节点
void STDInsertAfter(STD*pos, DataType x)
{
assert(pos);
STD* tmp = BuyNewNode(x);
tmp->next = pos->next;
pos->next = tmp;
}
某位前插入节点
void STDInsertFront(STD** head, STD* pos, DataType x)
{
assert(head);
assert(pos);
STD* tmp = BuyNewNode(x);
STD* prev = *head;
if (pos == *head)//就在头节点
{
STDPushFront(head, x);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
assert(prev);//pos为空的可能
}
prev->next = tmp;
tmp->next = pos;
}
}
大家发现某位前插入节点和某位后插入节点不一样,这是因为单向无头非循环链表的性质所决定的,因为若是要在某位前插入节点,不仅需要改变该位置的节点,还需要改变该位置之前的节点;
这里也简单的画几个图方便各位理解;
这里,我们成功找到了pos以及pos前一个节点pre,然后我们需要将tmp插入在pos之前,并且改变pre的next指针,就变成了这样;