一、为什么要引入链表?
1、数组的特点:
①数据类型一样的,为了解决数组的数据类型是一样的缺点,引入了结构体!②分布的空间是连续的;
③数组的大小一但确定就很难改变,重新定义一个char a[20],然后将char a[10]里面的数据复制到a[20];(C++,Java里面是有可变数组的)2、链表的概念:
可以这么说:链表一个可以变化的数组,可以由一个结点连接另一个结点,然后再由这个结点连接到另外的结点,形成一个锁链,锁链连接的是一个一个结点,也就是我们保存
的数据,锁链就是地址,第一个结点里面保存第二个结点的地址,第二个结点保存第三个结点的地址,以此类推……
3、结点的构建:结构体来作为一个结点:关键的一步:构造一个结构体类型:
struct node{
int data; //保存的本身的数据;
struct node *pNext; //结构体类型的指针,保存下一个结构体的地址; 指针域;
};
现在没有给它分配内存空间,因为只是定义了一个结构体的类型,并没有定义具体的结构体类型的变量;
struct node = int; //地位是等价的
struct node Node2; //定义一个结构体类型的变量;
struct node Node3; //定义一个结构体类型的变量;
因为可以通过Node1找到Node2,由Node2找到Node3,最关键的一步找到Node1,引入了“头指针”概念!
头指针就是第一个结点的位置,或者地址!只有创建若干个结点之后,才能够把这些结点连接在一起----》创建结点;
创建一个结点:
void create_node(int data)
{1、分配空间:因为链表要随时能够添加和删除,采用的是“堆空间”;
2、检查堆空间是否分配成功;
3、清空申请到的堆空间;
4、填充堆空间;给data赋值!
5、让pNext执行NULL;}
4、具体的操作(带头节点):
①插入:
尾部插入:1、首先找到链表的最后一个结点,也就是尾节点;
2、将原来的尾节点指向新结点;3、将新结点的pNext指向NULL;
void insert_tail(struct node *pHeader,struct node *new){
struct node *p = pHeader; //定义一个结构体类型的执行指向链表的头指针;
//1、首先找到链表的最后一个结点,也就是尾节点;
while( p->pNext != NULL) //保证p不是尾节点{
p = p->pNext; //往后移动一个结点;
}
//2、将原来的尾节点指向新结点;
p->pNext = new;//3、将新结点的pNext指向NULL;
new->pNext = NULL;}
头插:从结点的前面插入
1、将新结点的pNext指向原来的第一个有效结点;2、将头结点的pNext指向待插入的新结点;
void insert_head(struct node *pHeader,struct node *new){
//1、将新结点的pNext指向原来的第一个有效结点;
new->pNext = pHeader->pNext;
//2、将头结点的pNext指向待插入的新结点;pHeader->pNext = new;
}注意点:上面的两步操作的顺序不能颠倒!
②遍历:
大家牢记一点,链表是用来存储数据的。既然是存储数据,有存肯定有取。遍历的要求:不能重复、不能遗漏、尽可能追求效率的最大化。
伪代码:
1、找到链表的第一个有效结点,取出数据!查看他的pNext是不是指向NULL;如果不是,移到下一个结点;
2、取出数据,再判断pNext是不是NULL,如果不是,移到下一个结点;
3、找到最后一个结点的时候,把数据取出来!
代码实现:
void display_link(struct node *pHeader){
struct node *p = pHeader;
while(p->pNext != NULL)
{
p = p->pNext; //跳过头结点;
printf("%d\n",p->data);
}
}
③中间插入:
要求:在链表里面,找到一个数据,然后呢,将待插入的结点插入到这个数据结点的后面。
伪代码:
1、遍历链表,找到相应的数据所在结点;
2、将待插入的结点插入到第一步寻找到的结点的后面;
(1)将待插入的结点的pNext指向第一步结点的后面的那个结点;
(2)将第一步寻找到的结点指向待插入的结点;
代码:
void insert_mid(struct node *pHeader,int num,struct node *new)
{
int flag = 0;
struct node *p = pHeader;
//1、遍历链表,找到相应的数据所在结点;
while(p->pNext != NULL)
{
p = p->pNext; //跳过头结点以及移到下一个结点;
if(p->data == num) //判断当前结点的数据域是否是查找的数据;
{
//1、将待插入的结点的pNext指向第一步结点的后面的那个结点;
new->pNext = p->pNext;
p->pNext = new;
flag = 1;
break;
}
}
//防止在链表里面没有找到相应的数据;
if(flag == 0)
{
printf("insert_mid error!\n");
}
}
④删除:
链表是用来存储数据的!既然是存储数据,有存肯定有取!有取肯定有删除。
伪代码:
1、遍历链表,找到要删除的结点:Node2;
2、将Node2前面的一个结点Node1指向Node2后面的那个结点Node3;
3、将Node2所分配的空间释放;
代码:
int delete_node(struct node *pHeader,int num)
{
struct node *p = pHeader;
struct node *prev = NULL;
//1、遍历链表,找到要删除的结点:Node2;
while(p->pNext != NULL)
{
//prev保存的是p前面一个结点的地址;
prev = p;
p = p->pNext; //跳过头结点或者移到下一个结点;
if(p->data == num)
{
if(NULL == p->pNext)
{
//2、将Node2前面的一个结点Node1指向Node2后面的那个结点Node3;
prev->pNext = NULL;
//3、将Node2所分配的空间释放;
free(p);
p = NULL;
return 0;
}
else
{
prev->pNext = p->pNext; //p结点的前一个结点指向p的下一个结点;
free(p);
p = NULL;
return 0;
}
}
}
printf("delete_node error!\n");
}
⑤逆序:
原来的顺序: 头结点-->Node1-->Node2-->Node3-->NULL;
逆序之后的顺序: NULL <-- Node1<--Node2<--Node3<--头结点;
伪代码:【目前只有三个结点】
1、判断该链表是否只有头结点或者只有一个有效结点;【参数入口检查】
2、将Node2指向Node1,将Node3指向Node2;
3、将Node1的pNext指向NULL;
4、将头结点的pNext指向Node3;
代码:
int reverse_link(struct node *pHeader)
{
//1、判断该链表是否只有头结点或者只有一个有效结点;【参数入口检查】
if(pHeader->pNext == NULL || pHeader->pNext->pNext == NULL)
{
printf("reverse_link error!\n");
return -1;
}
//说明至少有两个有效结点;
struct node *ptr1 = pHeader->pNext; //ptr1指向Node1;
struct node *ptr2 = ptr1->pNext; //ptr2指向Node2;
struct node *ptr3 = ptr2->pNext; //ptr3指向Node3;
//因为至少有两个有效结点,所以要判断Node2的下一个结点是否为NULL
//根据上面的定义知道Node2的pNext是用ptr3来保存的,所以要判断ptr3是否为NULL;
while( ptr3 != NULL)
{
ptr2->pNext = ptr1; //Node2指向Node1;
ptr1 = ptr2;
ptr2 = ptr3;
ptr3 = ptr3->pNext;
}
ptr2->pNext = ptr1; //Node3指向Node2
//3、将Node1的pNext指向NULL;
pHeader->pNext->pNext = NULL; //目前pHeader->pNext指向的是Node1;
//4、将头结点的pNext指向Node3;
pHeader->pNext = ptr2; //因为此时ptr2指向的是Node3;
}
★头结点和头指针:
链表分为两类:带头结点的和不带头结点的;不带头结点的:头指针指向的是第一个有效结点的,保存的是第一个有效节点的地址;
带头结点的: 头指针指向的是头结点,保存的是头结点的地址;头结点里面可以不存储数据只存储第一个有效结点的地址,当然也可以存储比如链表的结点个数。
链表里面:不一定要有头结点,但是一定要有头指针。