一、数组和链表
1、什么是链表
链表是一种常见的重要的数据结构,是一种存放数据的思想,它是动态地进行存储分配的一种结构,
当我们用数组存放数据时必须事先定义固定的数组长度(元素个数),并且容易浪费空间,比如在不知道元素个数时,就必须把数组定义得足够大,以便能存下数据,显然这样非常浪费内存。链表就没有这种缺点,它根据需要来开辟空间。
2、链表和数组的区别
首先定义一个数组 int array[10],array[0]存放1、array[1]存放2,依次存放到array[9] = 10
可以看到数组存放数据都是连续存放的,如果想增加一项或者删除一项是非常麻烦的,比如想在array[1]和array[2]之间增加一项元素,或者删除array[7]这一项元素数组都显得非常尴尬,如果想实现这样的功能我们应该如何去完成呢?
先看下面的图片
开始在内存中有一串数据1 ~10,如果想在3和4之间插入数据100,由图片可以大概看出,原本的线路是3——>4,插入100后线路是3——>100——>4,那么怎样才能插入100呢?
首先我们应该想到指针,在3里面存放100的地址,在100里面存放4的地址,这样数据就串起来了。
同理如果想删除一个数据也是同样的思想
由图片可以知道是想删除数据4,我们可以在3里面直接存放5的地址,3访问完了就直接访问5,这样就完成了一个数据的删除。
其实上面两种改变数据地址的方法就是和链表的思想大同小异了。
二、链表的定义
链表有一个“头指针”变量,一般用head来表示,它用来存放一个地址,该地址指向一个元素。链表中每一个元素称为“结点”,每一个节点都应该包含两部分:(1)用户需要的实际数据;(2)下一个结点的地址。
由链表的定义可以看出上图中1代表数据1,只要在1中定义一个指针变量用来存放数据2的地址,在2中定义一个指针变量用来存放数据3的地址,以此类推直到最后一个元素,可知10中定义的指针变量没有指向其他变量,该元素被称为“表尾”,它的指针变量一般指向NULL(表示空地址),链表到此结束。
可以看到链表的各个元素是不连续的,要找一个元素必须要找到上一个元素,根据它提供的下一元素地址才能找到下一个元素,如果不提供“头指针”(head),整个链表都是无法访问的,链表就像链子一样,一环扣一环,中间是不能断开的。
很显然,链表这种数据结构,必须要利用指针变量用来存放下一个节点的地址和一个存放数据的变量,比如一个整型和一个指针两个不同类型的变量,这时我们应该想到结构体,用它去创建链表是最适合的。
一个结构体包含若干成员,这些成员可以是数值类型、字符类型、数值类型、指针类型,可以利用指针类型成员和整型类型成员来创建一个链表。
首先看下面一段代码
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
int main()
{
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
printf("%d %d %d %d\n",t1.data,t1.next->data,t1.next->next->data,t1.next->next->next->data);
}
运行结果:
代码中首先定义了一个结构体,结构体成员有一个整型变量,一个结构体指针,main函数中先定义了4个结构体变量t1 t2 t3 t4,并且分别赋值1 2 3 4和NULL,可以看到t1 t2 t3 t4是完全不相关的变量,那为什么在打印结果的时候只用到了t1呢?由t1.next = &t2就可以知道,t1的next里面存放的是t2的地址,以此类推,t2的next里面存放的是t3的地址,t3的next里面存放的是t4的地址,这样就可以通过t1来访问其他变量了,即t1.data = 1 t1.next->data = 2 t1.next->next->data = 3 t1.next->next->next->data = 4
三、链表的遍历
由上面代码可以看出这个代码有点啰嗦,我们可以利用t4 = NULL来封装一个函数进行优化代码,代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
void printLink(struct datas *hade)
{
struct datas *point = hade;
while(1){
if(point != NULL){
printf("%d",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
int main()
{
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
printLink(&t1);
return 0;
}
运行结果:
printLink函数是利用了t4->next = NULL来实现链表的遍历的,在main函数中调用printLink函数,实际参数为t1的地址,函数 中形式参数为一个结构体指针变量hade,进入while死循环中,如果point(hade)不等于NULL,就打印point(hade)->data,然后让结构体指针指向下一个地址,(point原本是指向t1, point->next = &t2,即point就指向t2了),否则就跳出循环,链表遍历结束。
上面的printLink函数还可以进行改进,代码如下
void printLink(struct datas *head)
{
while(head != NULL){
printf("%d ",head->data);
head = head->next;
}
putchar('\n');
}
四、链表的增删改查
1、链表节点的查找
首先我们来做一个链表的个数统计,代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
int num(struct datas *p)
{
int cnt = 0;
while(p != NULL) {
cnt += 1;
p = p->next;
}
return cnt;
}
int main()
{
int number = 0;
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
number = num(&t1);
printf("有%d个\n",number);
return 0;
}
运行结果:
现在来实现链表的查找,代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
int seek(struct datas *p,int data)
{
while(p != NULL){
if(data == p->data){
return 0;
}
p = p->next;
}
return 1;
}
void printFind(int data,int number)
{
if(data == 0){
printf("find %d\n",number);
}else{
printf("No %d\n",number);
}
}
int main()
{
int data = 0;
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
data = seek(&t1,2);
printFind(data,2);
data = seek(&t1,10);
printFind(data,10);
return 0;
}
运行结果:
程序中,首先在main函数中调用seek函数,seek函数中用t1的地址和要查找的数作为实际参数,即形式参数是一个结构体指针和一个int型的变量,进入while循环,循环结束也是利用t4 = NULL,如果要查找的数和地址中的data相等就返回0,如果把链表都遍历完了也没有要找的数就返回1,main函数这边调用printFind函数,将返回来的值和要查找的值进行比较来进行打印结果。
2、链表节点的插入(节点的增加)
链表节点的插入分为节点前插入和节点后插入。
首先看一下从链表节点后面插入新节点
从上图知,如果在3的后面插入新节点100,可以先把数据3找到,然后让new->next = 3->next , 在3->next = new,这样就实现了在3的后面插入新节点100。
代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
void printLink(struct datas *hade)
{
struct datas *point = hade;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
void hind(struct datas *head,struct datas *new,int data)
{
struct datas *p = head;
while(p != NULL){
if(p->data == data){
new->next = p->next;
p->next = new;
}
p = p->next;
}
}
int main()
{
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
struct datas new ={9,NULL};
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
printf("插入前:");
printLink(&t1);
printf("插入后:");
hind(&t1,&new,3);
printLink(&t1);
return 0;
}
运行结果:
在main函数中定义了一个结构体变量new作为新节点,为new赋值为9和NULL,在hind函数中实现了节点的插入,hind函数的实际参数为t1的地址、new的地址、和数值3(在3后面插入节点),在函数中如果结构体节点等于3,那么就在后面插入新节点,new->next = p->next和p->next = new这两句就实现了在3后面插入9。
从链表节点前面插入
从节点前面插入要分两种情况,第一种情况就是在头节点前面插入新节点,这种需要改头节点,第二种就是在非头节点前面插入一个新的节点,先看一下原理。
即如果在头节点前面插入一个节点,那就需要改头节点,让新插入的节点成为头节点,如果在其他节点前面插入新节点就需要用当前节点的下一个节点数据去和需要在某节点前面插入新节点的数做比较,如果相等则新节点的下一个地址等于当前节点的下一个地址,当前节点等于新节点。
程序如下:
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
void printLink(struct datas *hade)
{
struct datas *point = hade;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
struct datas* front(struct datas *head,struct datas *new,int data)
{
struct datas *p = head;
if(p->data == data){
new->next = p;
return new;
}
while(p->next != NULL){
if(p->next->data == data){
new->next = p->next;
p->next = new;
return p;
}
p = p->next;
}
return p;
}
int main()
{
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
struct datas new ={6,NULL};
struct datas *head = NULL;
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
head = &t1;
printf("插入前:");
printLink(head);
printf("插入后:");
head = front(head,&new,1);
printLink(head);
return 0;
}
运行结果:
从代码中可以看到,在main函数中定义了一个结构体指针head用来存放头节点,front函数的实际参数分别是t1的地址、新节点的地址、和用于比较的数据(表示在此数据前面插入),在head函数中第一个if语句是解决在第一个节点前面插入的情况,如果数据相等,那么新节点的下一个就等于原头节点,在将新节点作为新的头节点返回,while循环中就是要插入节点不在头结点前面的情况,如果当前节点的下一个的数据等于函数中传递过来的数据,那么新节点的下一个就等于当前节点的下一个,当前节点的下一个就等于new,然后返回头节点。
3、链表结点的删除
链表节点的删除分为删除第一个节点和删除非第一个节点
先看下面图片
从图片中大致可以看出链表节点删除的原理,即如果想要删除第一个节点,就需要将头节点改为head->next,让当前头节点等于原头节点的下一个节点,如果想要删除非头节点,就需要将当前节点的下一个节点改为当前节点下一个的下一个,即p->next = p->next->next ,如图。
程序如下:
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
void printLink(struct datas *hade)
{
struct datas *point = hade;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
struct datas* Delete(struct datas* head,int data)
{
struct datas *p = head;
if(p->data == data){
head = head->next;
return head;
}
while(p->next != NULL){
if(p->next->data == data){
p->next = p->next->next;
return head;
}
p = p->next;
}
return head;
}
int Print()
{
int data = 0;
printf("请输入想要删除的节点\n");
scanf("%d",&data);
return data;
}
int Texts(struct datas *t,int Text)
{
struct datas *head = NULL;
printf("删除前:");
printLink(t);
printf("删除后:");
head = Delete(t,Text);
printLink(head);
}
int main()
{
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
struct datas *head = NULL;
int Text = 0;
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
head = &t1;
Text = Print();
Texts(head,Text);
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
head = &t1;
Text = Print();
Texts(head,Text);
return 0;
}
运行结果:
在main函数中先调用print函数,用来获取需要删除的节点,然后返回给Text,再调用Texts函数,将头节点和要删除的节点作为实际参数传递给Texts函数,在Texts函数中先打印删除前的节点,再打印删除后的节点,调用Delete函数,将头节点和要删除节点传递过去,在Delete函数中第一个if语句是用来删除头节点的,如果第一个节点的数据等于传递过去的数据,就让头节点等于当前节点的下一个,即head = head->next,然后将头节点返回给Texts函数,再遍历链表,while循环就是用来删除其它节点的(p->next != NULL是利用t4->next = NULL),if语句中如果下一个节点的data等于传递过去的data,就让下一个节点等于下一个的下一个节点,即 p->next = p->next->next,这样就跳过了p->next这个节点,即实现了节点的删除,然后在返回头节点进行遍历链表。
4、链表节点的修改
链表节点的修改可以利用链表节点的查询来进行操作,即当查询到一个节点时,就将当前节点修改为想要的节点,代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
int revise(struct datas *p,int data,int newdata)
{
struct datas* head = p;
while(p != NULL){
if(data == p->data){
p->data = newdata;
return 0;
}
p = p->next;
}
return 1;
}
void printFind(int data,int number)
{
if(data == 0){
printf("find %d\n",number);
}else{
printf("No %d\n",number);
}
}
void printLink(struct datas *hade)
{
struct datas *point = hade;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
int main()
{
int data = 0;
struct datas t1 = {1,NULL};
struct datas t2 = {2,NULL};
struct datas t3 = {3,NULL};
struct datas t4 = {4,NULL};
struct datas *head = NULL;
t1.next = &t2;
t2.next = &t3;
t3.next = &t4;
head = &t1;
data = revise(head,2,6);
printFind(data,2);
printf("将2修改为%d\n",head->next->data);
printLink(head);
return 0;
}
运行结果:
代码的其他部分和链表节点的查询一样,修改的只是revise函数,即当找到一个节点时,就将当前节点的data修改为想要的数据(p->data = newdata),然后将链表遍历一遍就可以看到节点已经修改了。
五、链表的动态创建
1、头插法
头插法就是输入一个新节点它就成为头节点,如图
图中先插入了一个新节点5,5就是头节点,然后在5后面插入了一个新节点4,4就变成了头节点,4后面插入了一个新节点3,3就变成了头节点,这就是链表头插法。
代码如下
#include<stdio.h>
struct datas{
int data;
struct datas *next;
};
struct datas* movement(struct datas *head)
{
struct datas* new;
while(1){
new = (struct datas *)malloc(sizeof(struct datas));
printf("please input your new data\n");
scanf("%d",&(new->data));
new->next = NULL;
if(new->data == 0){
printf("0 quit\n");
return head;
}
if(head == NULL){
head = new;
}else{
new->next = head;
head = new;
}
}
return head;
}
void printLink(struct datas *head)
{
struct datas *point = head;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
int main()
{
struct datas *head = NULL;
head = movement(head);
printLink(head);
return 0;
}
运行结果:
代码中是在 movement函数中实现的链表头插创建,进入movement函数中首先定义了一个结构体指针变量new,while死循环中,先给new开辟空间,提示用户输入,这里要注意:malloc只是给new开辟了空间,但是new->next还是一个野指针,这里new->next要指向NULL,第一个if是用来判断输入是否为0,如果为0就返回头结点,下一个if是用来处理头结点为NULL的情况,如果head = NULL就让头结点等于new,否则进行头插,即如果hade不等于NULL,就让新结点的下一个等于当前头结点,让头结点等于新结点new。
头插法的代码优化
#include<stdio.h>
#include<stdlib.h>
struct datas{
int data;
struct datas *next;
};
struct datas* movement2(struct datas *head,struct datas* new)
{
if(head == NULL){
head = new;
}else{
new->next = head;
head = new;
}
return head;
}
struct datas* movement1(struct datas *head)
{
struct datas* new;
while(1){
new = (struct datas *)malloc(sizeof(struct datas));
new->next = NULL;
printf("please input your new data\n");
scanf("%d",&(new->data));
if(new->data == 0){
printf("0 quit\n");
free(new);
return head;
}
head = movement2(head,new);
}
}
void printLink(struct datas *head)
{
struct datas *point = head;
while(1){
if(point != NULL){
printf("%d ",point->data);
point = point->next;
}else{
putchar('\n');
break;
}
}
}
int main()
{
struct datas *head = NULL;
head = movement1(head);
printLink(head);
return 0;
}
运行结果和上面没有任何区别,只是在代码中多封装了一个函数movement2,这样写的好处是可以单独调用movement函数进行操作,free§是释放为new开辟的空间,因为new->data = 0,这个空间的意义不大,其次是因为用malloc申请的空间是不能自动释放的,要通过free进行释放,如果这样的空间开辟多了会出现内存泄漏。
2、尾插法
尾插法就是在最后面插入新结点,先看下图
尾插法就是利用最后一个结点为NULL的特点进行插入的,当最后一个结点等于NULL时,直接让最后一个结点的下一个等于new,就实现了从尾插入结点。
代码如下
#include<stdio.h>
#include<stdlib.h>
struct datas{
int data;
struct datas *next;
};
void printLink(struct datas *p)
{
while(p != NULL){
printf("%d",p->data);
p = p->next;
}
putchar('\n');
}
struct datas* rear2(struct datas* head,struct datas* new)
{
struct datas* p = head;
if(p == NULL){
head = new;
return head;
}
while(p->next != NULL){
p = p->next;
}
p->next = new;
return head;
}
struct datas* rear1(struct datas *head)
{
struct datas* new;
while(1){
new = (struct datas *)malloc(sizeof(struct datas));
printf("please input your new data\n");
scanf("%d",&(new->data));
new->next = NULL;
if(new->data == 0){
printf("0 \n");
free(new);
return head;
}
head = weicha2(head,new);
}
}
int main()
{
struct strp* head = NULL;
head = rear1(head);
printLink(head);
system("pause");
return 0;
}
运行结果:
代码其他部分和头插法都差不多,直接进入rear2函数在while循环中如果当前结点的下一个不等于NULL时,表示链表还没有遍历到最后一个结点,一直执行p = p->next,当遍历到最后一个结点时,直接让最后一个结点的下一个等于new,即p->next = new,然后返回头结点。