文章目录
提示:这只是笔记
一、链表的引入
1·1 先说说数组的缺陷:数组元素必须一致;数组元素个数一旦指定就不可更改。
1·2 结构体解决了数组的第一个缺陷,链表就是解决了数组的第二个缺陷。
1·3 时刻记住链表是用来解决数组的大小不能扩展问题。
二、单链表的实现
2·1 单链表的节点构成 : 有效数据 + 指针
2·2 定义的struct node 只是一个结构体,本身并没有变量生成,也不占内存。结构体的定义相当于为链表的节点定义了一个模板,但是还没有一个节点,将来在实际创建链表时需要一个节点时用这个模板来复制一个即可。
2·3 堆内存的申请 与 使用
(1)链表的内存要求比较灵活,不能用栈,也不能用数据段(存储全局变量的)。只能用堆内存。
(2)使用堆内存来创建一个链表节点的步骤:第一,申请堆内存,大小为一个节点(包括检查申请结果是否正确);第二,清理申请到的堆内存;第三,把申请到的堆内存当作一个新节点;第四,填充新节点的有效数据和指针区域。
2·4 链表的头指针
(1)头指针并不是节点,而是一个普通的指针,只占4个字节。头指针的类型是struct node * 类型的,所以他才能指向链表的节点。
(2)一个链表的典型实现是:头指针指向链表的第一个节点,第一个节点中的指针指向第二个节点,第二个节点的指针指向第三个节点,这样一直推。形成了链表。
单链表实现代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
/* 创建一个链表节点的函数
返回值:结构体类型的指针,
指向本函数新创建的节点的首地址。
*/
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,
// p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,
//实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
int main(void)
{
// 定义头指针
struct node *pHeader = NULL;
// 创建第一个节点,并且和头节点绑定
pHeader = cread_node(1);
// cread_node函数的返回值(指针p)就是(第一个)一个节点的首地址
// 创建第二个节点
pHeader -> pNext = cread_node(2);
// 创建第3个节点
pHeader ->pNext -> pNext = cread_node(3);
// 访问节点,拿出节点数据域的数据
printf("node 1 data = %d\n",pHeader -> data );
printf("node 2 data = %d\n",pHeader ->pNext -> data );
printf("node 3 data = %d\n",pHeader -> pNext ->pNext -> data );
return 0;
}
结果:
node 1 data = 1
node 2 data = 2
node 3 data = 3
注意点:
(1)因为是链表,所以在填充数据以及访问节点的时候 应该从头指针(pHeader)处入手。
(2)封装 创建新节点函数的关键点在于:函数的接口设计(函数传参和返回值的设计)。
三、 单链表的插入
3·1 单链表的尾部插入
// 头指针 -> 第一个节点 -> 第二个节点 -> 第三个节点......的方式。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
// 尾部插入 函数
void insert_tail(struct node *pH,struct node *new)
{
// 两步完成尾部插入
// 第一步,先找到链表最后一个节点
struct node *p = pH;
while (NULL != p->pNext)
{
p = p->pNext; // 往后走一个节点
}
// 第二步,将新节点插入到最后一个节点尾部
p->pNext = new;
}
int main(void)
{
// 定义头指针
struct node *pHeader = cread_node(1);// pHeader 与第一个节点绑定
insert_tail(pHeader,cread_node(23)); // 将第二个节点插入第一个节点尾部
insert_tail(pHeader,cread_node(356));// 将第3个节点插入第2个节点尾部
insert_tail(pHeader,cread_node(467));// 将第4个节点插入第3个节点尾部
// 访问节点,拿出节点数据域的数据
printf("node 1 data = %d\n",pHeader -> data );
printf("node 2 data = %d\n",pHeader ->pNext -> data );
printf("node 3 data = %d\n",pHeader -> pNext ->pNext -> data );
printf("node 4 data = %d\n",pHeader->pNext->pNext->pNext->data );
return 0;
}
结果:
node 1 data = 1
node 2 data = 23
node 3 data = 356
node 4 data = 467
链表还有一种用法,就是把头指针指向第一个节点作为头节点使用。
头节点的特点是:第一,紧跟在头指针后面;第二,头节点的数据是空的(有时是存储的整个链表的节点数),指针部分指向下一节点,也就是第一节点。
如此看来,头节点与其他节点确实不同。所以我们在创建一个链表是添加节点的方法也不同。头节点在创建头指针的时候一并创建并且和头指针关联起来;后面指针存储数据的节点用节点添加函数(intsert_tail)进行添加。
总结来说,没有头结点对第一个结点的操作大多和中间结点不太一样,在第一个元素之前插入或删除第一个元素,每个操作都要考虑特殊情况,有头结点的话就不必考虑那么多了,还不容易出现代码错误。空表和非空表的处理也统一了,有头节点空表head->next==NULL,没有头节点空表,head->NULL,可以很明显的看出来,带头节点的与后面其他节点的格式类似 -----------------------这是借鉴的某一位大神的总结
// 头指针 -> 头节点 -> 第一个节点 -> 第二个节点.....的方式。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
// 尾部插入 函数
// 并且计算添加了新的节点后节点总数,把节点总数写进头节点中。
/* 思路:头指针遍历,直到走到原来的最后一个节点。
原来最后一个节点里面的pNext是NULL,
现在我们只要将它改成new就行了。添加了之后新节点就变成了最后一个。
*/
void insert_tail(struct node *pH,struct node *new)
{
int node_sum_num = 0;
// 两步完成尾部插入
// 第一步,先找到链表最后一个节点
struct node *p = pH;
while (NULL != p->pNext)
{
p = p->pNext; // 往后走一个节点
node_sum_num ++;
}
// 第二步,将新节点插入到最后一个节点尾部
p->pNext = new;
pH -> data = node_sum_num + 1; // 这个1表示头节点
}
int main(void)
{
// 定义头指针 he 绑定头节点
struct node *pHeader = cread_node(-1);
insert_tail(pHeader,cread_node(1)); // 将第1个节点插入头节点尾部
insert_tail(pHeader,cread_node(356));// 将第2个节点插入第1个节点尾部
insert_tail(pHeader,cread_node(467));// 将第3个节点插入第2个节点尾部
// 访问节点,拿出节点数据域的数据
printf("head node data is = %d\n",pHeader -> data );
printf("node 1 data is = %d\n",pHeader ->pNext -> data );
printf("node 2 data is = %d\n",pHeader -> pNext ->pNext -> data );
printf("node 3 data is = %d\n",pHeader->pNext->pNext->pNext->data );
return 0;
}
注意点:链表有没有头节点是不同的,体现在链表的插入节点,删除节点,遍历节点,解析链表的各个算法都不同。如果有头节点,那后面的处理都按照有头节点的情况处理。现实中两种情况都有,所以看别人链表代码时需要注意是否有头节点。
3·2 从链表头部插入新节点(有头节点)
【有头节点的写法】
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
/*
// 尾部插入 函数
// 并且计算添加了新的节点后节点总数,把节点总数写进头节点中。
void insert_tail(struct node *pH,struct node *new)
{
int node_sum_num = 0;
// 两步完成尾部插入
// 第一步,先找到链表最后一个节点
struct node *p = pH;
while (NULL != p->pNext)
{
p = p->pNext; // 往后走一个节点
node_sum_num ++;
}
// 第二步,将新节点插入到最后一个节点尾部
p->pNext = new;
pH -> data = node_sum_num + 1; // 这个1表示头节点
}
*/
// 从链表头部插入新节点
/*
思路:
如果按照:
将头节点的pNext指向新节点的首地址,将新节点的pNext指向原来第一个节点的首地址。
这样想,会有一个后果:头节点指向新节点的地址后,就把原来第一个节点的地址丢失了。
所以:应该先把原来第一个节点的地址 先与 将要添加的新节点的pNext先绑定了来,
以防原来第一个节点地址丢失。
*/
void insert_head(struct node *pH,struct node *new)
{
// 第一: 新节点的pNext指向原来第一个节点
new -> pNext = pH -> pNext;
// 第二: 头节点的pNext指向新节点的地址
pH -> pNext = new;
// 头节点中的数据 要加一。
pH -> data += 1;
}
int main(void)
{
// 定义头指针 he 绑定头节点
struct node *pHeader = cread_node(0); //头指针 与 头节点绑定
/*
insert_tail(pHeader,cread_node(1)); // 将第1个节点插入头节点尾部
insert_tail(pHeader,cread_node(356));// 将第2个节点插入第1个节点尾部
insert_tail(pHeader,cread_node(467));// 将第3个节点插入第2个节点尾部
*/
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(22));
insert_head(pHeader,cread_node(333));
// 访问节点,拿出节点数据域的数据
printf("head node data is = %d\n",pHeader -> data );
printf("node 1 data is = %d\n",pHeader ->pNext -> data );
printf("node 2 data is = %d\n",pHeader -> pNext ->pNext -> data );
printf("node 3 data is = %d\n",pHeader->pNext->pNext->pNext->data );
return 0;
}
结果:
head node data is = 3
node 1 data is = 333
node 2 data is = 22
node 3 data is = 1
如果向既要头部插入,也要尾部插入,只需要将尾部插入的注释放开,并且在主函数中调用。
3·3 单链表的算法之遍历节点
(1)什么是遍历
遍历就是把单链表中的各个节点挨个拿出来。
链表遍历函数 traverse(struct node *pH);
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
// 从链表头部插入新节点
void insert_head(struct node *pH,struct node *new)
{
// 第一: 新节点的pNext指向原来第一个节点
new -> pNext = pH -> pNext;
// 第二: 头节点的pNext指向新节点的地址
pH -> pNext = new;
// 头节点中的数据 要加一。
pH -> data += 1;
}
// 遍历单链表的函数,pH为头指针
void traverse(struct node *pH)
{
// pH -> data :头节点的数据,不是链表的常规数据,不要算进去了
struct node *p = pH;// 头指针后面就是头节点,
printf("--------------------kaishi--------------------------------\n");
while(NULL != p->pNext) // 是不是最后一个节点
{
p = p->pNext;// p开始指向的是头节点,在这个语句后,p就跳过头节点 到第一个节点了
printf("node dara is %d\n",p -> data);
}
printf("--------------------kaishi--------------------------------\n");
}
int main(void)
{
// 定义头指针 he 绑定头节点
struct node *pHeader = cread_node(0); //头指针 与 头节点绑定
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(22));
insert_head(pHeader,cread_node(333));
traverse(pHeader);
return 0;
}
结果:
--------------------kaishi--------------------------------
node dara is 333
node dara is 22
node dara is 1
--------------------kaishi--------------------------------
3·4 单链表的算法之删除节点
删除节点的2个步骤:
第一步,找到需要删除的节点;第二步,删除这个节点。
找到待删除的节点:
通过遍历来查找节点。从头指针+头节点开始,顺着链表依次将各个节点拿出来,按照一定的方法比对,找到我们要删除的那个节点。
情况一:待删除节点不是尾节点
首先把待删除节点的 前一个节点的pNext指针指向 待删除节点的后一个节点的首地址。然后再free掉 所删除的节点的内存空间。
情况二:待删除节点是尾节点:
首先把待删除尾节点的前一个节点的pNext指针指向NULL。相当于原来尾节点的前一个节点变成新的尾节点。最后再free 删除的节点的内存空间。
提示:注意堆内存的释放
(1)上面代码都没有释放堆内存。当程序都结束了的情况下那些没有free的堆内存也被释放了。
(2)有时候程序运行时间很久,这时候malloc的内存没有free会一直被占用,直到free它或者整个程序终止。所以对于以上代码,都应该在main函数的return 0;之前free掉链表,但是由于上面的例子代码十分简单,所以即使没有free也不会有大影响。考虑到良好的编程习惯,需要free。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,
//实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
// 从链表头部插入新节点
void insert_head(struct node *pH,struct node *new)
{
// 第一: 新节点的pNext指向原来第一个节点
new -> pNext = pH -> pNext;
// 第二: 头节点的pNext指向新节点的地址
pH -> pNext = new;
// 头节点中的数据 要加一。
pH -> data += 1;
}
// 遍历单链表的函数,pH为头指针
void traverse(struct node *pH)
{
// pH -> data :头节点的数据,不是链表的常规数据,不要算进去了
struct node *p = pH;// 头指针后面就是头节点,
while(NULL != p->pNext) // 是不是最后一个节点
{
p = p->pNext;
// p开始指向的是头节点,在这个语句后,p就跳到第一个节点了
printf("node dara is %d\n",p -> data);
}
}
// 删除节点函数:删除链表中数据域为Data的节点
int delete_node(struct node *pH,int Data)
{
// 第一,遍历链表,找到待删除节点
struct node *p = pH; // 这个p用来指向当前节点
struct node *pBefore = pH;
// pBefore用来莱指向当前节点的前一个节点
while(NULL != p->pNext) // 是不是最后一个节点
{
pBefore = p;
//把p走向下一个节点前先把p存到pBefore中,这样pBefore就比p慢了一个节点。
p = p->pNext;
// p开始指向的是头节点,在这个语句后,p就跳到第一个节点了,
// 这句语句就是让p走向下一个节点
// 判断这个节点是不是待删除节点
if(p->data == Data)
{
// 找到了待删除节点,删除这个节点.
// 这个节点分两种情况:普通节点与尾节点
/* 删除节点的困难在于:通过链表的遍历依次访问各个节点,找到这个节点后p就指向了这个节点
但是要删除这个节点就必须操作前一个节点,但是这时候已经没有指针指向前一个节点了,没办法操作。
解决方法就是再定义一个指针来指向前一个节点。
*/
if(NULL == p->pNext)//表示是尾节点
{
pBefore -> pNext = NULL;
free(p); // 释放原来尾节点
}
else // 表示是普通节点
{
pBefore ->pNext = p -> pNext;
free(p);
}
// 删除节点后推出循环
printf("delete success!\n");
return 0;
}
}
printf("do not fond this node !\n");
return -1;
}
int main(void)
{
// 定义头指针 he 绑定头节点
struct node *pHeader = cread_node(0); //头指针 与 头节点绑定
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(22));
insert_head(pHeader,cread_node(333));
insert_head(pHeader,cread_node(4444));
insert_head(pHeader,cread_node(1233));
printf("delete before :\n");
traverse(pHeader);
delete_node(pHeader,22);
printf("delete after :\n");
traverse(pHeader);
return 0;
}
结果:
delete before :
node dara is 1233
node dara is 4444
node dara is 333
node dara is 22
node dara is 1
delete success!
delete after :
node dara is 1233
node dara is 4444
node dara is 333
node dara is 1
3·5 单链表的算法之逆序
(1)逆序:那链表中的有效节点(不包括头节点)在链表中的顺序反过来。
(2)思路:先遍历原链表,把原链表的头指针与头节点作为新链表的头指针与头节点,然后将原链表中的有效节点依次取出,头插的方法插入新链表。
(3)链表逆序:遍历 + 头部插入
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 构建一个链表节点,结构体打包
struct node
{
int data; //有效数据
struct node *pNext; // 指向下一个节点的指针
};
// 创建一个链表节点的函数
// 返回值:结构体类型的指针,指向本函数新创建的节点的首地址。
struct node *cread_node(int Data)
{
// 创建一个结构体指针p,p指向的就是malloc申请的新节点首地址
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;
//pNext 指向的应该是下一个节点的首地址,实际操作时将下一个节点malloc返回的指针赋值给pNext。
return p;
}
// 从链表头部插入新节点
void insert_head(struct node *pH,struct node *new)
{
// 第一: 新节点的pNext指向原来第一个节点
new -> pNext = pH -> pNext;
// 第二: 头节点的pNext指向新节点的地址
pH -> pNext = new;
// 头节点中的数据 要加一。
pH -> data += 1;
}
// 遍历单链表的函数,pH为头指针
void traverse(struct node *pH)
{
// pH -> data :头节点的数据,不是链表的常规数据,不要算进去了
struct node *p = pH;// 头指针后面就是头节点,
while(NULL != p->pNext) // 是不是最后一个节点
{
p = p->pNext;// p开始指向的是头节点,在这个语句后,p就跳到第一个节点了
printf("node dara is %d\n",p -> data);
}
}
// 删除节点函数:删除链表中数据域为Data的节点
int delete_node(struct node *pH,int Data)
{
// 第一,遍历链表,找到待删除节点
struct node *p = pH; // 这个p用来指向当前节点
struct node *pBefore = pH; // pBefore用来莱指向当前节点的前一个节点
while(NULL != p->pNext) // 是不是最后一个节点
{
pBefore = p;
//把p走向下一个节点前先把p存到pBefore中,这样pBefore就比p慢了一个节点。
p = p->pNext;
// p开始指向的是头节点,在这个语句后,p就跳到第一个节点了,
// 这句语句就是让p走向下一个节点
// 判断这个节点是不是待删除节点
if(p->data == Data)
{
// 找到了待删除节点,删除这个节点.
// 这个节点分两种情况:普通节点与尾节点
/* 删除节点的困难在于:通过链表的遍历依次访问各个节点,找到这个节点后p就指向了这个节点
但是要删除这个节点就必须操作前一个节点,但是这时候已经没有指针指向前一个节点了,没办法操作。
解决方法就是再定义一个指针来指向前一个节点。
*/
if(NULL == p->pNext)//表示是尾节点
{
pBefore -> pNext = NULL;
free(p); // 释放原来尾节点
}
else // 表示是普通节点
{
pBefore ->pNext = p -> pNext;
free(p);
}
// 删除节点后推出循环
printf("delete success!\n");
return 0;
}
}
printf("do not fond this node !\n");
return -1;
}
// 链表逆序函数
void reverse_linklist(struct node *pH)
{
struct node *p =pH -> pNext;
// pH指向头节点,p指向第一个有效节点
struct node *pBack = NULL;//保存p指向节点的后面一个节点
if((NULL == pH->pNext) || (NULL == p->pNext))
// 没有有效节点 只有一个有效节点 的情况的链表逆序没有意义
{
return ;
}
// 当链表有2个以及以上有效节点时需要逆序
//第一步,先遍历。
while(NULL != p->pNext)
{
pBack = p->pNext; //处理第一个有效节点之前先将下一个有效节点地址保存,免得丢失。
//满足条件说明有第二个(或者第n个)有效节点,
// 原链表第一个有效节点会是逆序后的新链表的尾节点,
// 尾节点的pNext指向NULL
if(p == pH->pNext)
// 判断如果p指向的是第一个有效节点,就把第一个有效节点的pNext指向NULL
{
p->pNext = NULL; // 确立好逆序后链表的尾节点
}
else
{
p->pNext = pH->pNext;
pH->pNext = p; // 这两句需要结合上面画的图来理解
}
p = pBack; // 这样就走到了下一个节点
}
// 循环结束,最后一个节点任然消失,可以用头插入来插入最后一个节点。
insert_head(pH,p);
}
int main(void)
{
// 定义头指针 he 绑定头节点
struct node *pHeader = cread_node(0); //头指针 与 头节点绑定
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(22));
insert_head(pHeader,cread_node(333));
insert_head(pHeader,cread_node(4444));
insert_head(pHeader,cread_node(1233));
printf("-------------reverse before :-------------\n");
traverse(pHeader);
printf(":-------------reverse after :-------------\n");
reverse_linklist(pHeader);
traverse(pHeader);
return 0;
}
结果:
-------------reverse before :-------------
node dara is 1233
node dara is 4444
node dara is 333
node dara is 22
node dara is 1
:-------------reverse after :-------------
node dara is 1
node dara is 22
node dara is 333
node dara is 4444
node dara is 1233
四、 双链表
4·1 双链表的引入和基本实现
(1)单链表的局限性:
单链表是堆数组的扩展,单链表各个节点之间只由一个指针单向连接,存在一些局限性。局限性主要体现在单链表只能经由指针单向移动(一旦指针移动过某个节点就无法再回来,如果需要再次操作这个节点除非从头开始再次遍历一次),因此单链表的某些操作就比较麻烦。比如说之前单链表的插入,删除,遍历等等的操作因为单链表的单向移动导致不少麻烦。
(2)有效数据 + 2个指针的节点(双链表):一个指针指向后一个节点,一个指针指向前一个节点。
4·1 双链表的插入节点
头部插入 与 尾部插入
(1)尾部插入
第一步,先走到链表的尾节点;第二步,将新节点插入到原来尾节点的后面。
(2)头部插入
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 创建一个双链表节点
struct node
{
struct node *pPrev; // 前向指针,指向前一个节点
int data ;
struct node *pNext; // 后向指针,指向后一个节点
};
struct node * cread_node(int Data)
{
struct node *p = (struct node *)malloc(sizeof(struct node));
if(NULL == p)
{
printf("malloc struct node error!\n");
return NULL;
}
memset(p,0,sizeof(struct node));
p-> data = Data;
p-> pPrev = NULL;
p-> pNext = NULL;
return p;
}
// 尾部插入节点 函数
void insert_tail(struct node *pH,struct node *new)
{
// 第一步先走到链表尾节点
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
}
// 循环结束后p就指向了原来尾节点
// 第二步,将新节点插入到原来尾节点的后面
p->pNext = new; // p的后向指针与new节点首地址关联
new->pPrev = p;// new的前向指针与p(原来尾节点)的地址关联
// 前节点的pPrev 与新节点的pNext指针未动
}
// 头部插入
void insert_head(struct node *pH,struct node *new)
{
// 新节点的pNext指向原来第一个节点的pPrev,防止后面节点丢失
new->pNext = pH->pNext;
// 原来第一个节点的pPrev指向new节点的pPrev。因为保持了p指针现在还是指向的原来第一个节点的位置
// 如果没有 有效节点,pH->pNext 就是指向的NULL,继续pH->pNext->pPrev就会段错误。
// 所以,加一个if语句。
if(NULL != pH->pNext)
pH->pNext->pPrev = new;// 结合图面理解
// 头节点的pNext指针新节点的首地址
pH->pNext = new;
// 新节点的pPrev指向头节点的pPrev
new ->pPrev = pH;
}
int main(void)
{
struct node *pHeader = cread_node(0);// 头指针指向了头节点
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(12));
insert_head(pHeader,cread_node(123));
insert_head(pHeader,cread_node(1234));
struct node *p = pHeader->pNext->pNext->pNext->pNext;
printf("node 4 is %d\n",p->data );
printf("node 3 is %d\n",p->pPrev->data );
printf("node 2 is %d\n",p->pPrev->pPrev->data );
printf("node 1 is %d\n",p->pPrev->pPrev->pPrev->data );
return 0;
}
4·2 双链表的遍历节点
双链表可以向后遍历,也可以向前遍历。(向前遍历意义不算很大)根据情况选择单链表还是双链表,以及向后遍历与向前遍历。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 创建一个双链表节点
struct node
{
struct node *pPrev; // 前向指针,指向前一个节点
int data ;
struct node *pNext; // 后向指针,指向后一个节点
};
struct node * cread_node(int Data)
{
struct node *p = (struct node *)malloc(sizeof(struct node));
if(NULL == p)
{
printf("malloc struct node error!\n");
return NULL;
}
memset(p,0,sizeof(struct node));
p-> data = Data;
p-> pPrev = NULL;
p-> pNext = NULL;
return p;
}
// 尾部插入节点 函数
void insert_tail(struct node *pH,struct node *new)
{
// 第一步先走到链表尾节点
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
}
// 循环结束后p就指向了原来尾节点
// 第二步,将新节点插入到原来尾节点的后面
p->pNext = new; // p的后向指针与new节点首地址关联
new->pPrev = p;// new的前向指针与p(原来尾节点)的地址关联
// 前节点的pPrev 与新节点的pNext指针未动
}
// 头部插入
void insert_head(struct node *pH,struct node *new)
{
// 新节点的pNext指向原来第一个节点的pPrev,防止后面节点丢失
new->pNext = pH->pNext;
// 原来第一个节点的pPrev指向new节点的pPrev。因为保持了p指针现在还是指向的原来第一个节点的位置
// 如果没有 有效节点,pH->pNext 就是指向的NULL,继续pH->pNext->pPrev就会段错误。
// 所以,加一个if语句。
if(NULL != pH->pNext)
pH->pNext->pPrev = new;// 结合图面理解
// 头节点的pNext指针新节点的首地址
pH->pNext = new;
// 新节点的pPrev指向头节点的pPrev
new ->pPrev = pH;
}
// 遍历链表,向后遍历
void traverse(struct node *pH)
{
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
printf("data = %d\n",p->data );
}
}
// 向前遍历一个双链表,参数pTail要指向链表末尾
void forward_traverse(struct node *pTail)
{
struct node *p = pTail;
while(NULL != p->pPrev)
{
printf("data is %d\n",p->data );
p = p->pPrev; // 注意 这两句的顺序。如果p指针先前移后打印,就会造成尾节点漏了。
}
}
int main(void)
{
struct node *pHeader = cread_node(0);// 头指针指向了头节点
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(12));
insert_head(pHeader,cread_node(123));
insert_head(pHeader,cread_node(1234));
traverse(pHeader);
struct node *p = pHeader->pNext->pNext->pNext->pNext;
forward_traverse(p);
/*
struct node *p = pHeader->pNext->pNext->pNext->pNext;
printf("node 4 is %d\n",p->data );
printf("node 3 is %d\n",p->pPrev->data );
printf("node 2 is %d\n",p->pPrev->pPrev->data );
printf("node 1 is %d\n",p->pPrev->pPrev->pPrev->data );
*/
return 0;
}
结果:
data = 1234
data = 123
data = 12
data = 1
data is 1
data is 12
data is 123
data is 1234
4·3 双链表的删除节点
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 创建一个双链表节点
struct node
{
struct node *pPrev; // 前向指针,指向前一个节点
int data ;
struct node *pNext; // 后向指针,指向后一个节点
};
struct node * cread_node(int Data)
{
struct node *p = (struct node *)malloc(sizeof(struct node));
if(NULL == p)
{
printf("malloc struct node error!\n");
return NULL;
}
memset(p,0,sizeof(struct node));
p-> data = Data;
p-> pPrev = NULL;
p-> pNext = NULL;
return p;
}
// 尾部插入节点 函数
void insert_tail(struct node *pH,struct node *new)
{
// 第一步先走到链表尾节点
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
}
// 循环结束后p就指向了原来尾节点
// 第二步,将新节点插入到原来尾节点的后面
p->pNext = new; // p的后向指针与new节点首地址关联
new->pPrev = p;// new的前向指针与p(原来尾节点)的地址关联
// 前节点的pPrev 与新节点的pNext指针未动
}
// 头部插入
void insert_head(struct node *pH,struct node *new)
{
// 新节点的pNext指向原来第一个节点的pPrev,防止后面节点丢失
new->pNext = pH->pNext;
// 原来第一个节点的pPrev指向new节点的pPrev。因为保持了p指针现在还是指向的原来第一个节点的位置
// 如果没有 有效节点,pH->pNext 就是指向的NULL,继续pH->pNext->pPrev就会段错误。
// 所以,加一个if语句。
if(NULL != pH->pNext)
pH->pNext->pPrev = new;// 结合图面理解
// 头节点的pNext指针新节点的首地址
pH->pNext = new;
// 新节点的pPrev指向头节点的pPrev
new ->pPrev = pH;
}
// 遍历链表,向后遍历
void traverse(struct node *pH)
{
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
printf("data = %d\n",p->data );
}
}
// 向前遍历一个双链表,参数pTail要指向链表末尾
void forward_traverse(struct node *pTail)
{
struct node *p = pTail;
while(NULL != p->pPrev)
{
printf("data is %d\n",p->data );
p = p->pPrev;
// 注意 这两句的顺序。如果p指针先前移后打印,
// 就会造成尾节点漏了。
}
}
// 删除节点函数
int delete_node(struct node *pH,int Data )
{
struct node *p = pH;
while(NULL != p->pNext)
{
p = p->pNext;
if(p->data == Data)
{
if(NULL == p->pNext)
{
p->pPrev->pNext = NULL;
//p表示当前节点的地址,p->pPrev表示p前一个节点的地址
}
else // 普通节点的删除
{
p->pPrev->pNext = p->pNext;
p->pNext->pPrev = p->pPrev;
}
free(p);
return 0;
}
}
return -1;
}
int main(void)
{
struct node *pHeader = cread_node(0);// 头指针指向了头节点
insert_head(pHeader,cread_node(1));
insert_head(pHeader,cread_node(12));
insert_head(pHeader,cread_node(123));
insert_head(pHeader,cread_node(1234));
delete_node(pHeader,12);
traverse(pHeader);
return 0;
}
结果:
data = 1234
data = 123
data = 1
五、 Linux内核链表
5·1 前述链表数据区域的局限性
(1)之前定义的数据区域时直接int data;但是实际上现实编程中链表的节点不可能如此简单,而是多种多样的。
(2)一般项目中的链表,节点中存储的数据其实是一个结构体,这个结构体中包含若干的成员,这些成员加起来就构成了节点数据区域。
5·2 内核链的设计思路
内核链表中直接实现了一个纯链表(没有数据域,只有前后向指针)的封装,以及纯链表的各种操作函数(节点创建,插入,删除,遍历等等),这个纯链表本身没有任何用处,它的用法就给我们具体链来调用。
5·3 list.h文件
内核中纯链表的实现在include / linux / list.h 文件中
六、状态机
6·1 常说的状态机是有限状态机FSM。FSM是指有限个状态(一般是一个状态变量的值),这个机器同时能够从外部接收信号和信息输入,机器在接收到外部输入信号后会综合考虑当前自己的状态和用户信息,然后机器做出动作:跳转到另一个状态。
6·2 两种状态机:Moore型和Mealy型
(1)Moore型状态机:输出只与当前状态机有关(输入信号无关)。相对简单,考虑状态机的下一个状态时只需要考虑它的当前状态就行了。
(2)Mealy型:输出不仅和状态有关而且和输入有关系。状态机接收到一个输入信号需要跳转到下一个状态时,状态机综合考虑到2个条件(当前状态,输入值)后才决定跳转到哪个状态。
6·3 状态机的主要用途:电路设计,FPGA程序设计,软件设计。
7· 多线程简介
7·1 操作系统下的并行执行机制
宏观上的并行,微观上的串行。
理论上,单核CPU本身只有一个核心,同时只能执行一条指令,这种CPU只能实现宏观上的并行,微观上一定是串行的。微观上的并行要求多核CPU,多核CPU中的多个核心可以同时微观上执行多个指令,因此可以达到微观上的并行,从而提升宏观上的并行度。
7·2 进程和线程的区别和联系
(1)进程和线程是操作系统的两种不同软件技术,目的是实现宏观上的并行(通俗一点就是让多个进程同时在一个机器上运行,达到宏观上看起来并行)。
(2)进程和线程在实现并行效果的原理上不同。而且这个差异和操作系统有关。比如windows中进程和线程差异比较大,在Linux中进程与线程差异不大(Linux中线程就是轻量级的进程)。
(3)不管是多进程还是多线程,最终目标都是实现并行 执行。
7·3 多线程的优势
之前多进程多一些,近些年多线程开始多一些。现代操作系统设计时,考虑到多核CPU的优化问题,保证了:多线程程序在运行的时候,操作系统会优先将多个线程放在多个核心中分别单独运行。所以说多核心CPU给多线程程序提供了完美的运行环境。所以在多核心CPU上使用多线程程序由极大好处。
7·4 线程同步 和 锁
具体详细内容看 系统编程那一块。