一、链表的概念
单链表:线性表的链接存储结构。
存储思想:用一组任意的存储单元存放线性表的元素。
存储特点:1.逻辑次序和物理次序不一定相同;2.元素之间的逻辑关系用指针表示。
![![[Pasted image 20231215190148.png]]](https://i-blog.csdnimg.cn/blog_migrate/d1f0b1c8a6c11a93f9f7601416e8baff.png)
存储包含两部分内容,如a1,保存有a1的元素和下一个元素的地址,而a4是最后一个元素,所以它的下一个元素的地址是空NULL。 从上图也可以注意到,链表存储数据是分散的、不连续的。
![![[1702638042992 1.jpg]]](https://i-blog.csdnimg.cn/blog_migrate/14398c81298c766604ba705881db39c3.png)
单链表由若干个结点构成,每个结点只有一个指针域。
单链表的结点结构:数据域(data) + 指针域(next)。数据域用于存储数据元素,指针域用于存储指向后继结点的地址。
程序:
typedef struct node
{
数据类型 data;//数据域
struct node *next;//指针域
}Node, *Link;
注:用typedef去声明一个结构体的时候,后面的Node和* Link就被声明为了类型,在后面定义的时候可以像这样去定义:
Node st 等价于 struct node st;
Link p 等价于 struct node * p;
如何申请一个结点?
p=(Link)malloc(sizeof(Node));sizeof(Node)算出Node结点的大小,得到该结构体需要多大的存储空间。
如何引用数据元素和指针域?
p->data; p->next;
存储结构的逻辑形式表示:
![![[Pasted image 20231216150701.png]]](https://i-blog.csdnimg.cn/blog_migrate/01eb46767b151ece97375818952b8f02.png)
设置一个head指针指向链表的第一个结点的地址。
头指针(head):指向第一个结点的地址(指向头结点的地址)。
尾标志:最后一个结点的指针域,且为NULL。
头结点:在单链表的第一个有效元素结点之前设置一个类型相同的结点,以便空表和非空表处理统一。头结点的数据域没有作用。若头结点的指针域为NULL的话,则为空表;若头结点的指针域非空,则为非空表。
![![[Pasted image 20231216151049.png]]](https://i-blog.csdnimg.cn/blog_migrate/e6c302e2a85a4424a38e0b12302d38fa.png)
二、单链表的实现(要注意链表的边界条件:头和尾的特殊情况)
1、单链表的遍历操作
void display(Link head)
{
Link p;
p=head->next;
while(p!=NULL)
{
printf("%d", p->data);
p=p->next;//使p等于下一个元素的地址
}
}
2、求单链表的元素个数
int length(Link head)
{
Link p;
int cnt=0;
p=head->next;
while(p)
{
p=p->next;
cnt++;
}
return cnt;
}
3、单链表的查找
int query(Link head, datatype x)//datatype为x的数据类型
{
Link p;
p=head->next;
while(p)
{
if(p->data==x)
{
return true;
}
p=p->next;
}
return false;
}
4、单链表的插入操作
![![[Pasted image 20231216153832.png]]](https://i-blog.csdnimg.cn/blog_migrate/0ca3e9575c9c94ad8a0e7f62f98bce18.png)
上图中橙色虚线箭头的顺序是从左到右进行的,我们需要先将p->next的地址赋给node->next,再将node的地址赋给p-next。
算法描述:
1.工作指针p初始化;
2.查找第i-1个结点并使工作指针p指向该节点;
3.若查找不成功则返回false;否则:
3.1生成一个数据元素值为x的新结点s;
3.2将新结点s插入到结点p之后;
3.3返回true。
bool insert(Link head, int i, DataType x)//i是要插入的位置,希望插入到第i个位置
{
p=head;//工作指针p先指向头结点
int cnt=0;
while(p!=NULL&&cnt<i-1)//查找第i-1个结点
{
p=p->next;
cnt++;
}
//上面的循环结束后,p是第i-1个元素的地址
if(p==NULL) return false;
else
{
Link node;
node=(Link)malloc(sizeof(Node));//创建需要插入的结点
node->data=x;//给入数据组元素值
node->next=p->next;
p->next=node;
//上两行代码的顺序不能够改变,先给新创建的结点的指针域赋上原来链表中第i个结点的地址;
//再给第i-1个结点的指针域赋上新创建的结点的地址。
return true;
}
}
5、创建一个单链表——头插法
操作函数:Link newList(datatype a[ ], int n);
头插法:将待插入结点插在头结点head的后面。
如:
![![[Pasted image 20231216212657.png]]](https://i-blog.csdnimg.cn/blog_migrate/eea8bec8b32c4fd7b76430c20b390c05.png)
我们现在有一个数组a,想要创建一个单链表。
思路:
1.初始化头结点:head=(Link)malloc(sizeof(Node));//分配一块内存给头结点
head->next=NULL;//初始化头结点指向的下一个地址为空
2.插入第一个元素结点:node=(Link)malloc(sizeof(Node));//分配一块内存给第一个元素结点
node->data=a[0];//在数据域存入数据
node->next=head->next;//将终端地址(即NULL)赋给新创建的结点的指针域
head->next=node;//将新创建的结点的地址传给头结点的指针域
![![[Pasted image 20231216214345.png]]](https://i-blog.csdnimg.cn/blog_migrate/44442452dd41acce876068a438d24b19.png)
3.插入后续的元素结点(以第i个元素为例):nodei=(Link)malloc(sizeof(Node));//分配一块内存给下一个元素结点
nodei->data=a[i];//存入数据
nodei-next=head->next;//将head指针域存储的地址传递给新创建的指针域
head->next=nodei;//将新创建的结点的地址传给head的指针域。
![![[Pasted image 20231216214402.png]]](https://i-blog.csdnimg.cn/blog_migrate/dace19d269f86503701ab73881ea26ab.png)
所以从上面我们插入新结点的方式来看,可以发现,最后我们所创建的单链表中,结点数据的排列顺序和我们原本在数组中的排列顺序是相反的。
![![[Pasted image 20231216215222.png]]](https://i-blog.csdnimg.cn/blog_migrate/ee547eb5783deb31ce5f89b77a7562f7.png)
而且由于插入元素结点的步骤都是相似的,且只需要改变i的大小,所以我们可以使用循环的方式去创建这个单链表。
Link newList(Datatype a[], int n)
{
Link head, node;
head=(Link)malloc(sieof(Node));
head->next=NULL;
//以上步骤为创建头结点并初始化
for(i=0;i<n;i++)
{
node=(Link)malloc(sizeof(Node));
node->data=a[i];
node->next=head->next;
head->next=node;
}
return head;
}
6、创建一个单链表——尾插法
操作函数:Link newList(Datatype a[ ], int n)
尾插法:将待插入的结点插在终端结点的后面。
1.初始化头结点:head=(Link)malloc(sizeof(Node));
head->next=NULL;
rear=head;//这里和头插法不同,我们需要创建一个表达尾部的指针,且rear等于head
2.插入第一个元素结点:node=(Link)malloc(sizeof(Node));
node->data=a[0];
rear->next=node;
rear=node;
3.插入后续元素(以第i个元素为例):node=(Link)malloc(sizeof(Node));
node->data=a[i];
rear->next=node;
rear=node;
4.最后一个结点的插入(与头插法不同之处,重要):
rear->next=NULL;
尾插法完成后,单链表中的数据的顺序与我们原来数组中数据的顺序就是一样的了。
Link nweList(Datatype a[], int n)
{
Link head, node, rear;
head->next=NULL;
rear=head;
for(i=0;i<n;i++)
{
node=(Link)malloc(sizeof(Node));
node->data=a[i];
//也可以在这里加一条:node->next=NULL,这样最后的rear->next=NULL就不用写了
rear->next=node;
rear=node;
}
rear->next=NULL;
return head;
}
7、单链表结点的删除
操作函数:bool deleteNode(Link head, Datatype x);
思路:
1.初始化:p=head->next;
q=head;//初始化后p和q就是一前一后的关系
2.移动指针找到需要删除的位置:q=p;
p=p->next;
3.删除:q->next=p->next;
free ( p );
![![[Pasted image 20231216223350.png]]](https://i-blog.csdnimg.cn/blog_migrate/2cc76ddddf734444936ef37f87624a51.png)
注:删除时需要分析如果要删的表是空表怎么办?在查找过程中,如果发现待删除的表是空表(即下图左边两种情况都是空表),则提前返回next (head=NULL可能是动态内存分配失败)
![![[Pasted image 20231216223843.png]]](https://i-blog.csdnimg.cn/blog_migrate/1c59080d6ca99af7d6112c4fa063d0b1.png)
bool delete(Link head, Datatype x)
{
if(head==NULL||head->next==NULL) return false;
p=head->next;
q=head;
while(p!=NULL)
{
if(p->data==x)
{
q->next=p-next;
free(p);
return true;
}
else
{
q=p;
p=p->next;
}
}
return false;
}
8、单链表的释放
操作函数:void clearLink(Link head);//会将头结点也删掉
将单链表中的所有结点的存储空间释放。
![![[Pasted image 20231216224721.png]]](https://i-blog.csdnimg.cn/blog_migrate/016e5ff76194f270904b4f57c9428030.png)
算法描述:q=head;
head=head->next;
free(q);
循环退出的条件为:head == NULL,然后free(head);
三、循环链表
(单)循环链表:将单链表的首尾相接,将终端结点的指针域由空指针改为指向头结点,构成单循环链表。
![![[Pasted image 20231217195156.png]]](https://i-blog.csdnimg.cn/blog_migrate/50735626bf2dd215cbd9b5cdf04c88e3.png)
空循环链表:
![![[Pasted image 20231217195411.png]]](https://i-blog.csdnimg.cn/blog_migrate/1efef8bc8fd61132a801c0fd6990f123.png)
非空循环链表:
![![[Pasted image 20231217195426.png]]](https://i-blog.csdnimg.cn/blog_migrate/d0a9b3292085e91aab4167eab03a628d.png)
循环链表的特点:循环链表没有明显的尾端。所以要防止死循环的出现,比如可以以p!=head或p->next!=head作为循环条件。
循环链表的操作与单链表的操作都是相似的,除了退出循环的条件不同外。
四、双向链表
如果要找到p的前一个元素的话,使用循环链表的时间复杂度较高,所以可以使用双向链表来完成。
![![[Pasted image 20231217195846.png]]](https://i-blog.csdnimg.cn/blog_migrate/5280029b9cb5f804dade88acc47b8230.png)
在单链表的基础上增加一个prior域来指向前一个结点。
双向链表:在单链表的每个结点中再设置一个指向其前驱结点的指针域。
结点结构:
![![[Pasted image 20231217200104.png]]](https://i-blog.csdnimg.cn/blog_migrate/b1d8c50b556a40e489675ebbf841db65.png)
虽然双向链表对于有关前驱问题的时间处理更快,但是占用的内存也更多,结构也更加复杂。