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