在学习数据结构的时候,最开始接触到的一种数据结构就是线性表,对于线性表的定义是:零个或多个数据元素的有限序列,那对于线性表来讲,又分为顺序存储结构和链式存储结构,对于顺序存储结构来说,也就是数组,数组的每个元素之间的地址是连续的;对于链式存储来说,也就是平常所说的链表,链表每个元素之间的地址并不是连续的,而是分散的,他们之间的联系通过结点的 next 指针来建立。本文尽可能地将链表的知识详细地叙述,所涉及的链表类型包括:单链表,双链表,循环链表,每个链表的操作涉及到创建链表,删除链表,插入链表结点,删除链表结点。
何为单链表呢,看定义往往让人一时摸不到头脑,直接通过图的形式来展示:
image-20210725104003036
可以看到结点与结点之间都是通过一个指针来建立联系的,所以对于链表结点的定义往往遵循如下的形式:
typedef struct Node
{
int data;
struct Node *next;
}ListNode,*LinkList;
而对于单链表来说,其还可以进行细分,可以分为带头结点的单链表和不带头结点的单链表,具体是什么意思呢?我们下面分别对这两种形式进行叙述。
说到头结点,就必须要与另外一个概念进行对比阐述,就是头指针,头指针并不是一个结点,它的作用是指向链表的第一个结点,也就是说我们是通过头指针来找到链表的;那头结点的意思是什么呢?头结点是一个结点,但是这个结点的数据域是没有值的,它的存在是方便我们对于链表的操作,比方说如果要往链表中插入一个结点,而这个结点插入的位置就是第一个结点(如果有头结点,那么头结点就是第0个结点),如果没有头结点的存在,那么就需要更改头指针的值,而如果有头结点的存在,头指针的值是一直不用变的。下图是带有头结点的链表的示意图:
image-20210725103816693
在知道了单链表的基本形式之后,那自然也就需要创建一个单链表了,在创建一个单链表时,主要分为两种创建方法,分别是头插法和尾插法,下面分别就这两种方法进行叙述。
其创建链表所遵循的一个基本步骤如下所示:
image-20210725110252272
从上图可以看出来头插法创建单链表的一个基本过程,同时可以看到,因为有头结点的存在,在每次新增结点的时候,头指针的值也是不变的,依据上述原理,写出创建单链表的代码,如下所示:
void AddNodeHead(LinkList *head, int value)
{
ListNode* Node = (ListNode*)malloc(sizeof(ListNode));
if (Node == NULL)
return;
/* 如果是首次插入结点,那么应该创建头结点 */
if (*head == NULL)
{
*head = (ListNode*)malloc(sizeof(ListNode));
if (*head == NULL)
return;
(*head)->next = NULL;
}
Node->data = value;
Node->next = NULL;
(*head)->next = Node;
}
头插法创建链表有一个特点就是,它所形成的链表的顺序是反的,也就是说后插入的链表结点反而在前面,如果从第一个结点开始遍历的话,那遍历得到的元素的顺序是倒过来的;那怎么样才能使得链表的插入的顺序和遍历的顺序一致呢?这个时候就需要引入尾插法创建单链表了。
尾插法也就正如其名字所表征的含义一样,它的意思是从尾部逐渐将结点插入,其所遵循的一个基本过程如下图所示:
image-20210725111844263
代码如下所示:
void AddNodeTail(LinkList *head,int value)
{
LinkList Node = (LinkList)malloc(sizeof(ListNode));
if (Node == NULL)
return;
/* 创建一个临时结点 */
LinkList TempNode = NULL;
/* 先创建头结点 */
if (*head == NULL)
{
*head = (LinkList)malloc(sizeof(ListNode));
(*head)->next = NULL;
}
TempNode = (*head);
Node->data = value;
Node->next = NULL;
while (TempNode->next)
{
TempNode = TempNode->next;
}
TempNode->next = Node;
}
如果按照上述两种方式构建的链表是每个元素都是从前往后依次递减的,现在要将一个数按照顺序插入到链表中,那么其所遵循的基本原理示意图如下所示:
image-20210725203214469
根据上述的结点插入示意图,写出如下所示的代码:
void IncertNode(LinkList head, int value)
{
if (head == NULL)
return;
LinkList temp = head;
LinkList Node = (LinkList)malloc(ListNode);
if (Node == NULL)
return;
while (temp->next && value > temp->next->data)
{
temp = temp->next;
}
Node->data = value;
Node->next = temp->next;
temp-> next = Node;
}
通过上述的代码我们可以看到,我们在进行遍历找到要插入的那个结点的时候,引入了一个临时变量来实现这个功能;实际上可以将这个地方进行简化,通过函数调用传递给形参的是值拷贝,那么对于传进去的 head
变量来说,其变量的地址是不会改变的。
void IncertNode(LinkList head, int value)
{
if (head == NULL)
return;
LinkList Node = (LinkList)malloc(ListNode);
while (head->next && value > head->next->data)
{
head = head->next;
}
Node->data = value;
Node->next = head->next;
head->next = Node;
}
可以看到,代码比最初那个版本要少了一个临时变量,但是其功能是没有变化的。
在叙述了插入结点之后,那如何进行删除结点操作呢?删除一个结点的操作所遵循的基本步骤如下如图所示:
image-20210725204142199
根据上述示意图的原理,删除结点的代码如下所示:
void DeleteNode(LinkList head, int target)
{
LinkList temp = NULL;
if (head == NULL || head->next == NULL)
return;
while (head->next && head->next->data != target)
{
head = head->next;
}
temp = head->next;
head-next = temp->next;
free(temp);
}
删除链表的意思也就是将链表的每个结点都释放掉,变成一个空链表,而对于带头结点的链表来说,空链表是包含头结点在内的,删除链表就需要将其他结点释放掉,具体的代码如下所示:
void DeleteList(LinkList* head)
{
if (*head == NULL || (*head)->next == NULL)
return;
LinkList tempNodeP = (*head)->next;
LinkList tempNodeQ = NULL;
while (tempNodeP)
{
tempNodeQ = tempNodeP->next;
free(tempNodeP);
tempNodeP = tempNodeQ;
}
(*head)->next = NULL;
}
最后,就是打印当前链表的元素了,原理也比较简单,直接给出源代码:
void PrintLinkList(LinkList head)
{
if (head->next == NULL || head == NULL)
{
printf("No Element\r\n");
return;
}
while (head->next)
{
printf("%d ",head->next->data);
head = head->next;
}
printf("\r\n");
}
知道了带头结点的单链表,那么不带头结点的单链表也就显而易见了,示意图如下所示:
image-20210725104153366
通过上图可以知道,如果插入的结点或者是删除的结点是第一个结点的话,那么就需要改变头指针的值。
上述中,叙述了关于带头结点的单链表的创建,本小节叙述的是不带头结点的单链表的创建,不带头结点的单链表创建的原理和上述一致,就不在这里给出具体的步骤图了,直接给出操作的代码。
void AddNodeHeadWithNh(LinkList *head, int value)
{
LinkList NodeTemp = (LinkList)malloc(sizeof(ListNode));
if (NodeTemp == NULL)
return;
NodeTemp->data = value;
NodeTemp->next = *head;
*head = NodeTemp;
}
可以看到,如果不带头结点的单链表,相对于带头结点的单链表,要简单很多,最突出的一点就是不用再创建头结点了。
void AddNodeTailWNh(LinkList *head, int value)
{
LinkList temp = NULL;
LinkList Node = (LinkList)malloc(sizeof(ListNode));
if (Node == NULL)
return;
Node->data = value;
Node->next = NULL;
if (*head == NULL)
*head = Node;
else
{
temp = *head;
while (temp->next)
{
temp = temp->next;
}
temp->next = Node;
}
}
尾插法创建单链表不带头节点,要稍微复杂一点,就是涉及到如果最开始是空链表,那么插入第一个结点的时候,需要更改头指针的值,如果不是第一次插入,那么也就不需要改变了;上述代码中,引入了一个临时结点用于遍历,回顾上述中的一个点,就是说在遍历的时候,可以通过不引入临时结点的方式来简化代码,因为函数调用传入形参的是值传递,改进的代码如下所示:
void AddNodeTailWNH(LinkList *head, int value)
{
LinkList Node = (LinkList)malloc(sizeof(ListNode));
if (Node == NULL)
return;
Node->data = value;
Node->next = NULL;
/* 如果是第一次插入 */
if (*head == NULL)
(*head) = Node;
else
{
while ((*head)->next)
{
head = &(*head)->next;
}
(*head)->next = Node;
}
}
void IncertNodeWNH(LinkList *head, int value)
{
LinkList Node = (LinkList)malloc(sizeof(ListNode));
if (Node == NULL)
return;
Node->data = value;
if (*head == NULL)
{
*head = Node;
Node->next = NULL;
}
LinkList Curr = *head;
LinkList Pre = NULL;
/* 假设链表是从小到大排列的 */
while (Curr && value > Curr->data)
{
Pre = Curr;
Curr = Curr->next;
}
/* 如果要插入的结点就是第一个结点 */
if (Pre == NULL)
{
Node->next = *head;
*head = Node;
}
else
{
Node->next = Curr;
Pre->next = Node;
}
}
在上述代码中,如果链表一开始就是为空的,那么就将头指针指向第一个结点就好了,如果链表有元素,但是要插入的元素位于第一个元素之前,那么也需要将头指针进行更改,这里通过引入一个当前结点和前一个结点的方式来完成这个功能。同样的,依据前面对于程序的改进思路,也可以减少定义两个结点的方式来完成,只不过就是说需要增加一个标志位,来记录是插入的位置是不是第一个结点,代码如下所示:
void IncertNode(LinkList *head, int value)
{
LinkList Node = (LinkList)malloc(sizeof(ListNode));
if (Node == NULL)
return;
LinkList Temp = NULL;
Node->data = value;
int count = 0;
/* 假设链表的结点是按照从小到大 */
while ((*head)->next && value > (*head)->next->data)
{
head = &(*head)->next;
count++;
}
if (!count)
{
Node->next = *head;
*head = Node;
}
else
{
Node->next = (*head)->next;
(*head)->next = Node;
}
}
删除链表结点和插入链表结点两种对链表的操作是差不多的,在删除链表结点的时候,我们采用的方式同样是定义一个当前结点,一个上一个结点,代码如下所示:
void DeleteNode(LinkList *head, int target)
{
LinkList Pre = NULL;
LinkList Cur = *head;
if (*head == NULL)
return;
while (Cur != NULL && Cur->data != target)
{
Pre = Cur;
Cur = Cur->next;
}
/* 如果要删除的结点就是第一个结点 */
if (Pre == NULL)
{
*head = Cur->next;
}
else
{
Pre->next = Cur->next;
}
free(Cur);
}
跟上面一样的思路,我们同样可以依据上面的想法来简化我们的代码,简化之后的代码如下所示:
void DeleteNode(LinkList *head, int target)
{
LinkList Temp = NULL;
for (; *head != NULL ; head = &(*head)->next)
{
if ((*head)->data == target)
{
Temp = (*head);
(*head) = (*head)->next;
free(Temp);
break;
}
}
}
在删除链表的操作上和带头结点的链表基本一致,差别就在于说是带头结点的不删除头结点,下面是删除链表的代码:
void ClearLinkList(LinkList *head)
{
if (*head == NULL)
return;
LinkList tempNode = NULL;
LinkList tempNodeQ = *head;
while (tempNodeQ)
{
tempNode = tempNodeQ->next;
free(tempNodeQ);
tempNodeQ = tempNode;
}
*head = NULL;
}
打印链表的操作也较为简单,具体代码如下所示:
void PrintLinkListWithNh(LinkList head)
{
if (head == NULL)
return;
while (head)
{
printf("%d ",head->data);
head = head->next;
}
printf("\r\n");
return;
}
上述就是关于单链表的一个简单的叙述,当然,链表的知识不仅仅是当链表,还有双向链表,循环链表,双向循环链表等等,剩余的 内容在后期的博客中将进行叙述,这次的分享就到这里啦。