链表的概念及其结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链表接次序实现的。
链表不能将元素直接存储在不连续的内存空间,因为此结构中下一个元素未知,因此除了要存储元素的信息外,还必须要存储元素下一个节点的位置(地址)
链表的结构
链表可以分为下面三类。组合起来共有八种结构
1.单向、双向
2.带头节点、不带头
3.循环、不循环
带头节点和不带头节点的区分:检测第一个节点是否放置有效数据,放置有效数据的是不带头节点
在我们的实际运用中,使用较多的有两种结构:
1.无头单向非循环链表:常用于其他数据结构的子结构
2.带头双向循环链表:一般用于单独存储数据,结构虽复杂,但实现起来很简单
无头单向非循环链表的实现
(1)动态申请节点
顺序表结构与数组结构类似,使用时进行开辟空间,而链表在进行使用时,则需要进行申请节点
SListNode* BuySListNode(SLDataType data)//动态申请一个节点
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));//使用malloc申请空间
if (NULL == newNode)
{
assert(0);
return NULL;//申请空间失败
}
newNode->next = NULL;//申请空间成功
newNode->data = data;
return newNode;
}
(2)尾插
单链表在进行尾插操作的时候,我们分为两种情况进行讨论
i 单链表中没有节点时,则head->NULL,那么在进行插入操作,只需要将head指针指向插入的节点就可以完成尾插操作
ii 当链表中有节点时:
①找到链表中的最后一个节点(最后一个节点特征为:next 域为空)借助head 指针进行遍历进行检测,即可找到最后一个节点
②让最后一个节点的next 域指向新节点 tail->next=newNode;
即可将新节点连接在最后一个节点后
由于链表不是连续的,所以在进行遍历操作的时候,不能直接将指针tail++,让指针指向它的next 域即可完成我们想要的遍历操作 tail=tail->next;
在尾插方法实现的过程中,函数传参需要使用二级指针进行传参,(关于二者的区别,详细的请看笔者的下篇博客噢)
a.若实参是变量,则传递变量的地址。—>形参为一级指针
b.若实参是指针,则传递指针的地址。—>形参为二级指针
在最开始进行函数的编写过程中,没有考虑到使用二级指针测试代码和用例如下:
void SListPushBack(SListNode* head, SLDataType data)//尾插
{
SListNode* newNode = BuySListNode(data);
//1.空链表
if (NULL == head)
{
head = newNode;
}
else
{
//2.链表中有节点时
SListNode* tail = head;//借助辅助指针
while (tail->next)//找到最后一个节点
{
tail = tail->next;//进行链表的遍历
}
//最后一个节点指向要插入的元素
tail->next = newNode;
}
}
在插入第一个元素1时,可以发现已被写入空间中
但在执行结束操作后,进行插入第二个元素时发现值为空了
这个时候才意识到自己的错误,想要在函数中通过形参修改外部实参head指针的指向,一级指针无法实现这一操作
经过调整后的代码如下:
void SListPushBack(SListNode** head, SLDataType data)//尾插
{
assert(head);
SListNode* newNode = BuySListNode(data);
//1.空链表
if (NULL == *head)
{
*head = newNode;
}
else
{
//2.链表中有节点时
SListNode* tail = *head;//借助辅助指针
while (tail->next)//找到最后一个节点
{
tail = tail->next;//进行链表的遍历
}
//最后一个节点指向要插入的元素
tail->next = newNode;
}
}
经过调整后,我们发现值可以成功插入
小结:
通过我们的验证可以得出其本质就是在函数体内是否需要通过形参指针改变外部实参指针的指向。
如果想在函数中改变头指针的指向,形参必须为二级指针;
如果不需要在函数中改变头指针的指向,传递一级指针就可以实现。
(3)尾删
在进行尾删操作时每次需要删除最后一个节点,所以思路如下:首先需要找到最后一个节点(next域为空的节点),然后对其进行删除操作
进行删除链表时分为以下三种情况:
①空链表
②链表中只有一个节点
③链表中有多个节点
在进行删除时我们常常会犯下一个错误,链表的地址是不连续的,如果直接删除节点,那么删除节点前的节点的指向便会随机,成为野指针。那么在完成删除操作时,我们需要将删除节点的前一个结点的next域指向NULL
那么此时遇到的问题就是如何让前一个节点的next域指向NULL
在这里可以采取前后指针的方式进行实现,借助tail找到最后一个节点的位置,并保存tail前一个节点的位置
prev->next=tail->next;
free(tail);
这样简单的两个步骤即可完成
因为在动态申请节点的时候我们采取的是malloc方法,所以在删除节点时,我们直接使用free进行释放空间,在释放时我们需要考虑到最后一个节点是否为空即可。
void SListPopBack(SListNode** head)//尾删
{
assert(head);
SListNode* tail = head;
SListNode* prev = NULL;//prev用来标记tail的前一个节点
if (NULL == *head)
{
//空链表
return;
}
else if (NULL == (*head)->next)
{
//链表中只有一个节点
free(*head);
*head=NULL;
}
else
{
//链表非空,链表中至少有一个节点
while (tail->next)
{
prev = tail;
//找到最后一个节点
tail = tail->next;
}
prev->next = tail->next;
//删除节点
free(tail);
}
}
(4)头插
在进行头插操作时,分为以下两种情况:
①空链表
②非空
在进行分情况讨论后,发现非空链表的插入操作对于空链表也适合
void SListPushFront(SListNode** head, SLDataType data)//头插
{
assert(*head);
SListNode* tail = head;
SListNode* newNode = BuySListNode(data);
//插入节点的next域指向原来的第一个节点
newNode->next = *head;
*head = newNode;//头指针连接新节点
/*1.空链表
if (NULL == *head)
{
*head= newNode;
}
else
2.有多个节点
{
newNode->next = *head;
*head = newNode;
}*/
}
(5)头删
在进行头删操作的时候,先标记需要删除的节点,在进行移动头指针,最后对其进行删除操作。
在对单链表进行删除操作时,要先连接再删除
void SListPopFront(SListNode** head)//头删
{
asser(*head);
if (NULL == *head)
{
return;
}
else
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}
/*else if (NULL == (*head)->next )
{
free(*head);
*head=NULL;
}
else
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}*/
}
(6)单链表的查找
SListNode* SListFind(SListNode* head, SLDataType data)
{
SListNode* tail = head;
while (tail)
{
if (tail->data = data)
return tail;
tail = tail->next;
}
return NULL;
}
(7)在pos位置之后插入元素data
由于单链表的不连续性,所以数据只能插在pos位置之后,若在之前新插入的元素将无法与链表进行连接,在插入的过程中,先将新元素连接到链表之中再断开
void SListInsertAfter(SListNode* pos, SLDataType data)//任意位置的插入
{
if (NULL == pos)
return;
SListNode* newNode = BuySListNode(data);
newNode->next = pos->next;
pos->next = newNode;
}
(8)删除pos之后的元素
先连接再删除
void SListEraseAfter(SListNode* pos)
{
if (NULL == pos|| pos->next)
return;
SListNode* delNode = pos->next;
pos->next = delNode->next;
free(delNode);
}
(9)计算链表的长度
int SListSize(SListNode* head)
{
//不需要进行断言,空链表是合法的情况
/*if (NULL == head)
{
return 0;
}*/
SListNode* tail = head;
int count=0;
while (tail)
{
count++;
tail = tail->next;
}
return count;
}
(10)链表的销毁
void SListDestroy(SListNode** head)//采用头删法
{
assert(head);
while (*head)
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}
}
将主要的代码分成了三个源文件,SList.c,test.c,SList.h,其中SList.c实现代码中的各个函数,test.c实现顺序表的相关测试,SList.h实现用到的各种头文件与函数声明。
SList.h
#pragma once
//不带头节点的单链表
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;//存储该节点的数据
struct SListNode* next;//访问下一个节点的地址
}SListNode;
SListNode* BuySListNode(SLDataType data);//动态申请一个节点
void SListPrint(SListNode* head);//打印
void SListPushBack(SListNode** head, SLDataType data);//尾插
void SListPopBack(SListNode** head);//尾删
void SListPushFront(SListNode** head, SLDataType data);//头插
void SListPopFront(SListNode** head);//头删
void SListInsertAfter(SListNode* pos, SLDataType data);//任意位置的插入
void SListEraseAfter(SListNode* pos);
SListNode* SListFind(SListNode* head, SLDataType data);
int SListSize(SListNode* head);
void SListDestroy(SListNode** head);
SList.c
#include "SList.h"
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
SListNode* BuySListNode(SLDataType data)//动态申请一个节点
{
SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));//使用malloc申请空间
if (NULL == newNode)
{
assert(0);
return NULL;//申请空间失败
}
newNode->next = NULL;//申请空间成功
newNode->data = data;
return newNode;
}
void SListPrint(SListNode* head)
{
SListNode* tail = head;
while (tail)
{
printf("%d--->", tail->data);
tail = tail->next;
}
printf("NULL\n");
}
//head保存的是链表中头指针的地址
void SListPushBack(SListNode** head, SLDataType data)//尾插
{
assert(head);
SListNode* newNode = BuySListNode(data);
//1.空链表
if (NULL == *head)
{
*head = newNode;
}
else
{
//2.链表中有节点时
SListNode* tail = *head;//借助辅助指针
while (tail->next)//找到最后一个节点
{
tail = tail->next;//进行链表的遍历
}
//最后一个节点指向要插入的元素
tail->next = newNode;
}
}
void SListPopBack(SListNode** head)//尾删
{
assert(head);
SListNode* tail = head;
SListNode* prev = NULL;//prev用来标记tail的前一个节点
if (NULL == *head)
{
//空链表
return;
}
else if (NULL == (*head)->next)
{
//链表中只有一个节点
free(*head);
*head=NULL;
}
else
{
//链表非空,链表中至少有一个节点
while (tail->next)
{
prev = tail;
//找到最后一个节点
tail = tail->next;
}
prev->next = tail->next;
//删除节点
free(tail);
}
}
void SListPushFront(SListNode** head, SLDataType data)//头插
{
assert(*head);
SListNode* tail = head;
SListNode* newNode = BuySListNode(data);
newNode->next = *head;
*head = newNode;
/*1.空链表
if (NULL == *head)
{
*head= newNode;
}
else
2.有多个节点
{
newNode->next = *head;
*head = newNode;
}*/
}
void SListPopFront(SListNode** head)//头删
{
assert(*head);
if (NULL == *head)
{
return;
}
else
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}
/*else if (NULL == (*head)->next )
{
free(*head);
*head=NULL;
}
else
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}*/
}
void SListInsertAfter(SListNode* pos, SLDataType data)//任意位置的插入
{
if (NULL == pos)
return;
SListNode* newNode = BuySListNode(data);
newNode->next = pos->next;
pos->next = newNode;
}
void SListEraseAfter(SListNode* pos)
{
if (NULL == pos|| pos->next)
return;
SListNode* delNode = pos->next;
pos->next = delNode->next;
free(delNode);
}
//需要进行链表的遍历操作,此时head指向第一个节点
int SListSize(SListNode* head)
{
//不需要进行断言,空链表是合法的情况
/*if (NULL == head)
{
return 0;
}*/
SListNode* tail = head;
int count=0;
while (tail)
{
count++;
tail = tail->next;
}
return count;
}
SListNode* SListFind(SListNode* head, SLDataType data)
{
SListNode* tail = head;
while (tail)
{
if (tail->data = data)
return tail;
tail = tail->next;
}
return NULL;
}
void SListDestroy(SListNode** head)//采用头删法
{
assert(head);
while (*head)
{
SListNode* delNode = *head;
*head = delNode->next;
free(delNode);
}
}
test.c
#include "SList.h"
#include <stdio.h>
int main()
{
SListNode* listhead = NULL;
SListPushBack(&listhead, 1);
SListPushBack(&listhead, 2);
SListPushBack(&listhead, 3);
SListPushBack(&listhead, 4);
SListPushBack(&listhead, 5);
SListPrint(listhead);
SListPopBack(&listhead);
SListPopBack(&listhead);
SListPopBack(&listhead);
SListPopBack(&listhead);
SListPopBack(&listhead);
SListPrint(listhead);
SListPushFront(&listhead, 1);
SListPushFront(&listhead, 2);
SListPushFront(&listhead, 3);
SListPushFront(&listhead, 4);
SListPushFront(&listhead, 5);
SListPrint(listhead);
SListPopFront(&listhead);
SListPopFront(&listhead);
SListPopFront(&listhead);
SListPopFront(&listhead);
SListPopFront(&listhead);
SListPrint(listhead);
SListInsertAfter(SListFind(listhead, 2), 100);
SListPrint(listhead);
SListEraseAfter(SListFind(listhead, 4));
SListPrint(listhead);
SListDestroy(&listhead);
return 0;
}