文章目录
本文涉及实现单向链表完整代码已提交至码云Gitee中,供读者参考。
因本人为编程初学者,文中及代码中难免出现错误,恳请同志们批评指正!
本章内容:
单链表的概念;
单链表结点模型;
单链表的各运算实现。
🎃链表
我们之前实现了数据结构中第一个结构–顺序表。虽然顺序表结构清晰、物理存储和逻辑存储结构明确,但是我们也提到它有不足之处:
- 顺序表在存储数据的时候需要一整片连续的内存。
- 如果需要插入或者删除数据,对于顺序表来说很可能需要挪动大量的数据。
而与之对应的链表则:
- 链表不需要连续的内存。
- 插入删除数据时,链表只需要改一下指针的指向即可。
由此可见,链表在实现大数据量的线性存储时是非常有效的一种结构。
链表是由多个结点组成的,什么是链表的结点呢?
将线性表L=(a0,a1,……,an-1)中各元素分布在存储器的不同存储块,称为结点,通过地址或指针建立元素之间的联系。
结点的data域存放数据元素ai,而next域是一个指针,指向ai的直接后继ai+1所在的结点。
多个结点一个连着一个,像一条链子一样存储着多个数据,这就是链表。
而单向链表,顾名思义,就是只能从前往后链接起来的链表,其中的某个节点只能访问后继结点,而不能指向前趋结点。
🎃链表的实现(C语言)
既然我们已经知道了链表是什么东西,那么如何用C语言去描述呢?
📌结点类型描述
我们已经直到链表由多个结点组成,每个结点分为数据域和指针域。既然有两个部分,我们考虑用一个结构体来实现一个结点,结构体中应该有两个变量成员:1.数据;2.指针。
其中数据的类型应该是自由的,随着使用者的需求而改变,这里直接重定义一个类型,方便以后的修改。
指针因为要指向下一个结点,所以应该和结点的类型相同,也就是结构体指针类型。
typedef int data_t;
//定义一个结构体类型,来表示我们一个结点的模型
//其中应该包括 数据域:data 指针域:next
typedef struct node
{
data_t data;//数据域
struct node* next;//指针域---指向另一个结点,所以类型是struct node*
//切不可丢掉*号,那样会使得这个结构体大小变成无穷
}listnode, * linklist;
定义了一个结点之后,就迈出了实现链表的第一步。那么对于链表的运算有哪些呢?和顺序表相似,我们也应该实现这些功能:
- 创建并初始化一个结点作为链表的头结点;
- 从链表尾部插入新数据;
- 链表遍历打印;
- 查找链表:按结点序号查找;
- 查找链表:按数据内容查找;
- 在链表任意位置插入一个新数据;
- 删除链表:按结点序号删除;
- 删除链表:按数据内容删除;
- 释放链表动态内存;
- 求链表数据个数;
- 反转单向链表;
- 求链表中两相邻节点数据和最大的第一个结点
- 有序链表的合并;
- 单向链表排序—冒泡排序 ;
- 单向链表实现按数据内容修改数据;
📌1.创建初始化一个链表结点
链表的结构是动态形成的,即算法运行前,表结构是不存在的。
为了能建立一个链表,我们第一步必须先创建初始化一个结点出来,将这个结点作为头结点,以便后续的其他操作。如何创建一个结点呢?我们应该想到这几步:
- 申请动态内存;
- 给结点的各元素赋值:头结点的data域赋值为0、头结点的指针域赋值为NULL;
- 将申请好的结点的地址返回。
//1.创建初始化一个结点:头结点
linklist list_init(void)
{
//1.申请动态内存
linklist p = (linklist)malloc(sizeof(listnode));
//2.赋值
p->data = HEAD_DATA;
p->next = NULL;
//3.将结点地址返回
return p;
}
在main()函数中:
//定义一个头结点并初始化:
linklist H = link_init();
📌2.从链表尾部插入数据
我们已经创建好一个头结点了,但是整个链表中也只有这一个头结点,我们如何往里面放入数据呢?这里先考虑直接从尾部插入,就是说从头结点之后再新建一个结点存放数据,这个存放数据的结点我们可以认为是第0个结点,之后的尾部插入就从第0个结点开始插入,依次往后。注意,最后一个尾结点的指针域应该是空指针!
实现这个函数应该有如下步骤:
- 将要放入的这个数据封装成一个结点p:数据域+指针域。这个过程其实就是创建一个新结点来存放我们要放入的数据.
- 寻找尾结点。因为我们可能插入时已经有多个结点,我们要在尾结点后追加上一个结点存放我们的数据,那么就要先找到尾结点。我们知道尾结点的指针域是空指针,因此只需要判断p->next是不是NULL即可。因为要一个一个往后找尾结点,所以这里应该是一个while循环。
- 存放数据。把上一步找到的尾结点的指针域指向我们要插入的数据封装成的结点p,再将p的指针域置为空指针,作为新的尾结点。
代码:
//2. 从链表尾部插入新数据;
/****
*****@return -1:failed 0:success
*****@para H:链表头结点 val:要插入的值
****/
int list_tail_insert(linklist H, data_t val)
{
//判断H是不是空指针
if (H == NULL)
{
printf("链表头有错\n");
return -1;
}
//1.封装新结点
linklist p = (linklist)malloc(sizeof(listnode));
if (p == NULL)
{
printf("数据插入失败\n");
return -1;
}
p->data = val;//把创建的结点的数据域赋值我们要插入的数据val
p->next = NULL;//指针域置空指针,作为新的尾结点
//2.寻找尾结点
linklist q = H;//为了寻找尾结点,不要把传进来的H的位置改变
while (q->next != NULL)
{
//不是尾结点就往后走
q = q->next;
}
//跳出循环后,q->next已经是尾结点指针域
//3.尾部插入:
q->next = p;
return 0;
}
📌3.链表遍历打印
为了能知道链表里存了什么,我们需要打印出来直观的观察。这和我们在上一个函数中寻找尾结点的思想一样,同样是从头开始找尾结点,只不过找一个打印一个数据:p->data。
//3. 链表遍历打印;
/****
*****@return -1:failed 0:success
*****@para H:链表头结点
****/
int list_show(linklist H)
{
//照例判断是不是空指针
if (H == NULL)
{
printf("链表头有错\n");
return -1;
}
//开始遍历
linklist p = H;
while (p->next != NULL)
{
//注意要从第0个结点开始打:
//如果是p->data就是从头结点开始
printf("%d ", p->next->data);
//这里一定要加上,以达到循环:
p = p->next;
}
return 0;
}
📌4.查找链表:按结点序号查找
我们要按照序号来找结点中的内容,就需要从头结点开始往后循环着找。我们假定结点的计数从0开始,那么从头结点到第0个结点需要让指针走1次,也就是循环一次。以此类推,查找第i个结点,需要循环i+1次。
值得注意的是,如果用户查找的结点序号大于我们真实存在的结点数的时候,需要对用户提出警告并阻止这种行为,防止引入野指针,出现段错误。
//4. 查找链表:按结点序号查找;
/****
*****@return -1:failed 0:success
*****@para H:链表头结点 pos:需要查找的序号
****/
int list_search_num(linklist H, int pos)
{
//照例判断H是不是空指针:
if (H == NULL)
{
printf("链表头有错\n");
return -1;
}
//如果参数太小:
if (pos <= -1)
{
printf("\n位置参数有问题\n");
return -