(一)链表的引入
1.从数组的缺陷说起
- 数组有2个缺陷,一个是数组中所有元素的类型必须一致;第二个是数组的元素个数必须事先制定并且一旦指定之后不能更改。
- 如何解决数组的2个缺陷:数组的第一个缺陷靠结构体去解决。结构体允许其中的元素的类型不相同,因此解决了数组的第一个缺陷。所以说结构体是因为数组不能解决某些问题所以才发明的。
- 如何解决数组的第二个缺陷?我们希望数组的大小能够实时扩展。譬如我刚开始定了一个元素个数是10,后来程序运行时觉得不够因此动态扩展为20.普通的数组显然不行,我们可以对数组进行封装以达到这种目的;我们还可以使用一个新的数据结构来解决,这个新的数据结构就是链表。
- 总结:几乎可以这样理解:链表就是一个元素个数可以实时变大/变小的数组。
2.链表是什么样的?
- 顾名思义,链表就是用锁链连接起来的表。这里的表指的是一个一个的节点,节点中有一些内存可以用来存储数据(所以叫表,表就是数据表);这里的锁链指的是链接各个表的方法,C语言中用来连接2个表(其实就是2块内存)的方法就是指针。
- 链表是由若干个节点组成的(链表的各个节点结构是完全类似的),节点是由有效数据和指针组成的。有效数据区域用来存储信息完成任务的,指针区域用于指向链表的下一个节点从而构成链表。
3.时刻别忘了链表是用来干嘛的
- 时刻谨记:链表就是用来解决数组的大小不能动态扩展的问题,所以链表其实就是当数组用的。直白点:链表能完成的任务用数组也能完成,数组能完成的任务用链表也能完成。但是灵活性不一样。
- 简单说:链表就是用来存储数据的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。数组的优势是使用简单(简单粗暴)。
(二)单链表的实现
1.单链表的节点构成
- 链表是由节点组成的,节点中包含:有效数据和指针。
- 定义的struct node只是一个结构体,本身并没有变量生成,也不占用内存。结构体定义相当于为链表节点定义了一个模板,但是还没有一个节点,将来在实际创建链表时需要一个节点时用这个模板来复制一个即可。
2.堆内存的申请和使用
- 链表的内存要求比较灵活,不能用栈,也不能用data数据段。只能用堆内存。
- 使用堆内存来创建一个链表节点的步骤:1、申请堆内存,大小为一个节点的大小(检查申请结果是否正确);2、清理申请到的堆内存;3、把申请到的堆内存当作一个新节点;4、填充你哦个新节点的有效数据和指针区域。
3.链表的头指针
- 头指针并不是节点,而是一个普通指针,只占4字节。头指针的类型是struct node *类型的,所以它才能指向链表的节点。
- 一个典型的链表的实现就是:头指针指向链表的第1个节点,然后第1个节点中的指针指向下一个节点,然后依次类推一直到最后一个节点。这样就构成了一个链。
4.访问链表中各个节点的数据
- 只能用头指针,不能用各个节点自己的指针。因为在实际当中我们保存链表的时候是不会保存各个节点的指针的,只能通过头指针来访问链表节点。
- 前一个节点内部的pNext指针能帮助我们找到下一个节点。
5.实战:构建一个简单的单链表
- 目标:构建一个链表,然后将一些数据(譬如1,2,3三个数字)存储在链表中
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node
{
int data;
struct node *pNext;
};
int main(void)
{
struct node *pHeader = NULL;
struct node *p = (struct node *)malloc(sizeof(struct node));
if(NULL == p)
{
printf("malloc error.\n");
return -1;
}
memset(p, 0 , sizeof(struct node));
p->data = 1;
p->pNext = NULL;
pHeader = p;
struct node *p1 = (struct node *)malloc(sizeof(struct node));
if(NULL == p1)
{
printf("malloc error.\n");
return -1;
}
memset(p1, 0 , sizeof(struct node));
p1->data = 2;
p1->pNext = NULL;
p->pNext = p1;
struct node *p2 = (struct node *)malloc(sizeof(struct node));
if(NULL == p2)
{
printf("malloc error.\n");
return -1;
}
memset(p2, 0 , sizeof(struct node));
p2->data = 3;
p2->pNext = NULL;
p1->pNext = p2;
printf("p->data = %d.\n", pHeader->data);
printf("p1->data = %d.\n", pHeader->pNext->data);
printf("p2->data = %d.\n", pHeader->pNext->pNext->data);
return 0;
}
将创建节点的代码封装成一个函数
struct node *create_node(int data)
{
struct node *p = (struct node *)malloc(sizeof(struct node));
if(NULL == p)
{
printf("malloc error.\n");
return NULL;
}
memset(p, 0 , sizeof(struct node));
p->data = data;
p->pNext = NULL;
return p;
}
- 函数调用
(三)单链表的算法-插入节点
1.从链表尾部插入新的节点
void insert_tail(struct node *pH, struct node *new)
{
struct node * p = pH;
while(NULL != p->pNext)
{
p = p->Next;
}
p->Next = new;
}
- 函数调用
1.头节点引入
1.什么是头节点
- 问题:因为我们在insert_tail中直接默认了头指针指向的有一个节点,因此如果程序中直接定义了头指针后就直接insert_tail就会报段错误。我们不得不在定义头指针之后先create_node创建一个新节点给头指针初始化,否则不能避免这个错误;但是这样解决让程序看起来逻辑有点不太顺,因为看起来第一个节点和后面的节点的创建、添加方式有点不同。
- 链表还有另外一种用法,就是把头指针指向的第一个节点作为头节点使用。头节点的特点是:第一,它紧跟在头指针后面。第二,头节点的数据部分是空的(有时候不是空的,而是存储整个链表的节点数),指针部分指向下一个节点,也就是第一个节点。
- 这样看来,头节点确实和其他节点不同。我们在创建一个链表时添加节点的方法也不同。头节点在创建头指针时一并创建并且和头指针关联起来;后面的真正的存储数据的节点用节点添加的函数来完成,譬如insert_tail.
- 链表有没有头节点是不同的。体现在链表的插入节点、删除节点、遍历节点、解析链表的各个算法函数都不同。所以如果一个链表设计的时候就有头节点那么后面的所有算法都应该这样来处理;如果设计时就没有头节点,那么后面的所有算法都应该按照没有头节点来做。实际编程中两种链表都有人用,所以大家在看别人写的代码时一定要注意看它有没有头节点。
2.从链表头部插入新的节点
void insert_head(struct node *pH, struct node *new)
{
new->pNext = pH->pNext;
pH->pNext = new;
pH->data += 1;
}
(四)单链表的算法之遍历节点
1.什么是遍历
- 遍历就是把单链表中的各个节点挨个拿出来,就叫遍历。
- 遍历的要点:一是不能遗漏、二是不能重复、追求效率。
2.如何遍历单链表
- 分析一个数据结构如何遍历,关键是分析这个数据结构本身的特点。然后根据本身特点来制定它的遍历算法。
- 单链表的特点就是由很多个节点组成,头指针+头节点为整个链表的起始,最后一个节点的特征是它内部的pNext指针值为NULL。从起始到结尾中间由各个节点内部的pNext指针来挂接。由起始到结尾的路径有且只有一条。单链表的这些特点就决定了它的遍历算法。
- 遍历方法:从头指针+头节点开始,顺着链表挂接指针依次访问链表的各个节点,取出这个节点的数据,然后再往下一个节点,直到最后一个节点,结束返回。
3.遍历函数实例
void bianli(struct node *pH)
{
struct node *p = pH->pNext;
int count = 0;
printf("-------------------链表遍历开始-------------------------\n");
while(NULL != p->pNext)
{
count++;
printf("node%d = %d.\n", count, p->data);
p = p->pNext;
}
printf("node%d = %d.\n", count + 1, p->data);
printf("-------------------链表遍历结束-------------------------\n");
}
void bianli2(struct node *pH)
{
struct node *p = pH;
int count = 0;
printf("-------------------链表遍历开始-------------------------\n");
while(NULL != p->pNext)
{
count++;
p = p->pNext;
printf("node%d = %d.\n", count, p->data);
}
printf("-------------------链表遍历结束-------------------------\n");
}
- 函数调用及运行结果
(五)单链表的算法之删除节点
- 实例
int delete_node(struct node *pH, int data)
{
struct node *p = pH;
struct node *pPrev = NULL;
while (NULL != p->pNext)
{
pPrev = p;
p = p->pNext;
if(data == p->data)
{
if(NULL == p->pNext)
{
pPrev->pNext = NULL;
free(p);
}
else
{
pPrev->pNext = p->pNext;
free(p);
}
pH->data -= 1;
return 0;
}
}
printf("没有找到%d这个节点.\n", data);
return -1;
}
- 如何找到待删除的节点
- 通过遍历来查找节点。从头指针+头节点开始,顺着链表依次将各个节点拿出来,按照一定的方法比对,找到我们要删除的那个节点。
- 如何删除一个节点
- 待删除的节点不是尾节点的情况:首先把待删除的节点的前一个节点的pNext指针指向待删除的节点的后一个节点的首地址(这样就把这个节点从链表中摘出来了),然后再将这个摘出来的节点free掉接口。
- 待删除的节点是尾节点的情况:首先把待删除的尾节点的前一个节点的pNext指针指向null(这时候就相当于原来尾节点前面的一个节点变成了新的尾节点),然后将摘出来的节点free掉。
(六)单链表的算法之逆序链表
- 实例
void reverse_node(struct node *pH)
{
struct node *p = pH->pNext;
struct node *pBack = NULL;
if((NULL == p) || (NULL == p->pNext))
return;
while(NULL != p->pNext)
{
pBack = p->pNext;
if(p == pH->pNext)
{
p->pNext = NULL;
}
else
{
p->pNext = pH->pNext;
pH->pNext = p;
}
p = pBack;
}
insert_head(pH, p);
}
- 什么是链表的逆序
- 链表的逆序又叫反向,意思就是把链表中所有的有效节点在链表中的顺序给反过来。
- 单链表逆序算法分析
- 当我们对一个数据结构进行一个操作时,我们就需要一套算法。这就是数据结构和算法的关系。
- 我总结:算法有2个层次。第一个层次是数学和逻辑上的算法;第二次个层次是用编程语言来实现算法。
- 从逻辑上来讲,链表的逆序有很多种方法。这些方法都能实现最终的需要,但是效率是不一样的。彼此的可扩展性、容错性等不同。
- 思路:首先遍历原链表,然后将原链表中的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个依次取出来,采用头插入的方法插入新链表中即可。
- 链表逆序 = 遍历 + 头插入