链表基础知识点
1 引入链表的原因–数组的缺点
- 数组中所有元素必须相同-----------结构体解决
- 数组定义时必须明确数组的个数-----变长数组或者链表解决
- 某个元素移动可能会造成其他元素大面积移动----------链表解决
2 链表的感性认识
- 链表是由若干个节点(C语言中为结构体)组成;
- 链表是锁链连接起来的表,锁链是指指针,表是指存放数据的节点;
- 数组和链表之间的关系:互补。
3 链表实现前的准备
一个链表的创建步骤:
- 创建空的链表;
- 操作链表,创建节点;
- 链表节点删除。
接下来我们一步一步操作:
3.1 创建空的链表
//创建一个节点结构体
struct node
{
int data; //节点的有效数据
struct node *pNext; //struct node*类型的指针
};
注:上述代码只是定义了一个结构体类型,并没有变量产生,不占用内存,相当于给链表节点定义了一个模板。
3.2 使用堆创建节点create_node()
使用堆内存创建一个节点,因为链表的内存是需要多少要多少,随时删除释放。
struct node * create_node(int data //节点中要存的数据)
{
//1. 申请一个节点大小的堆内存
//1.1 使用malloc来分配内存
struct node *p = (struct node *)malloc(sizeof(struct node));
//2. 判断堆内存是否申请成功
if(NULL == p){
printf("malloc error!\n");
return NULL;
}
//3. 清理申请到的空内存
bzero(p, sizeof(struct node));
//4. 填充节点中的数据
p->data = data;
//5. 节点中的指针初始化为NULL
p->pNext = NULL;
//6. 返回一个struct node* 类型的指针
//指向刚创建出来的节点的首地址的指针
return p;
}
注:
- malloc是什么意思,怎么使用
- bzero函数是什么意思,怎么使用
4 链表的头指针与头节点
头指针:普通的指针(可以理解,最开始并没有节点,但是需要一个指向第一个节点的首地址的指针),占4个字节;
类型:struct node *
一个典型的链表:头指针—指向—>第一个节点,第一个节点指针-----指向----->下一个节点
头节点:紧跟在头指针的后面,数据部分为空,指针部分指向第一个有效节点。
注:头节点有无让对链表的操作有所不同,头节点的作用
5 总结:构建一个简单的链表
- 定义一个头指针:pHeader
- 创建第一个节点,让头指针指向第一个节点
- 第一个节点指向下一个节点
int main(void){
struct node *pHeader = NULL; //定义一个头指针:pHeader
struct node *pHeader = create_node(1); //创建第一个节点,并让头指针指向第一个节点
return 0;
}
5.1 尾部插入节点(尾插法)
//输入参数:头指针和新节点地址
void insert_tail(struct node *pH, struct node *new){
//用p来代替pH操作,或者可以不用p直接用pH?
struct node *p = pH;
//要是没有指向空,说明后面还有节点连着,继续向后循环
while(NULL != p->pNext){
p = p->pNext;
}
//将当前的最后一个节点的地址指向新节点的首地址
p->pNext = new;
}
5.2头部插入节点(头插法)
有头节点的情况:
//输入参数:头指针和新节点地址
void insert_head(struct node *pH, struct node *new){
//注:pH指向头节点的地址,也就是头指针
//代码简单,逻辑要理一理
//1.首先将要新加入的节点*new中的指针new->pNext指向原来第一个节点 //(非头节点)的地址pH->pNext
new->pNext = pH->pNext;
//2.这时头节点指针的指向还是为原来的第一个节点首地址,应该让头节点中 //的指针pH->pNext指向新加入的节点首地址,即new
pH->pNext = new;
//此时完成了新节点的头部插入
}
没有头节点的情况:
下面代码有错误,无法加入节点,不知道问题出在哪里
//输入参数:头指针和新节点地址
void insert_head(struct node *pH, struct node *new){
//假设没有头节点的情况
//注:pH指向第一个节点的地址,也就是头指针
//代码简单,逻辑要理一理
//1.首先将要新加入的节点*new中的指针new->pNext指向原来第一个节点 //的地址pH
new->pNext = pH;
//2.这时头指针的指向还是为原来的第一个节点首地址,应该让头指针pH指向新加入的节点首地址,即new
pH = new;
//此时完成了新节点的头部插入
}
5.2 全部代码—链表的实现看这里
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//创建一个节点结构体
struct node
{
int data; //节点的有效数据
struct node *pNext; //struct node*类型的指针
};
//创建节点的函数,返回值为指向创建的节点的首地址指针
struct node * create_node(int data)//节点中要存的数据
{
//1. 申请一个节点大小的堆内存
//1.1 使用malloc来分配内存
struct node *p = (struct node *)malloc(sizeof(struct node));
//2. 判断堆内存是否申请成功
if(NULL == p){
printf("malloc error!\n");
return NULL;
}
//3. 清理申请到的空内存
bzero(p, sizeof(struct node));
//4. 填充节点中的数据
p->data = data;
//5. 节点中的指针初始化为NULL
p->pNext = NULL;
//6. 返回一个struct node* 类型的指针
//指向刚创建出来的节点的首地址的指针
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 = create_node(1);
insert_tail(pHeader, create_node(2));
insert_tail(pHeader, create_node(3));
insert_tail(pHeader, create_node(4));
printf("node1 data: %d.\n", pHeader->data);
printf("node2 data: %d.\n", pHeader->pNext->data);
printf("node3 data: %d.\n", pHeader->pNext->pNext->data);
return 0;
}
6 读取数据:遍历链表
思考一下:
遍历就是要依次访问链表中的数据,而链表中的数据由节点中的数据提供,而节点之间通过每个节点的指针相连,所以当操作了当前节点后,再由当前节点的指针就可以操作下一个节点,直到后面没有节点。
void list_for_each(struct node *pH){
struct node *p = pH;
printf("-------------begin---------------");
//非空就不是最后一个节点,继续循环
while(NULL != p->pNext){
p = p->pNext;//退出条件,假设这句话执行后,p节点为最后一个节点,下面语句也会打印出信息,不存在漏掉的风险
printf("node data: %d.\n", p->data);
}
printf("---------------end---------------");
}
7 链表节点的删除
思考一下:
链表就是一个一个节点组成,每个节点中有数据(头节点没有数据)和指向下一个节点的指针,当我们要删除一个节点的时候,需要将上一个节点的指针指向待删除节点的下一个节点的首地址,然后对删除后的节点进行内存释放free。
删除分为两种情况:
- 删除的节点为尾节点(因为尾节点的指针指向NULL)
- 删除的节点不为尾节点
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(p->data == data){
//尾节点的情况
if(NULL == p->pNext){
pPrev->pNext = NULL;
free(p);
}
//普通节点的情况
else{
pPrev->pNext = p->pNext;
free(p);
}
return 0; //删除成功
}
printf("没有要删除的节点.\n");
return -1;
}
}
最后测试代码
工程src下创建main.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//创建一个节点结构体
struct node
{
int data; //节点的有效数据
struct node *pNext; //struct node*类型的指针
};
//创建节点的函数,返回值为指向创建的节点的首地址指针
struct node * create_node(int data)//节点中要存的数据
{
//1. 申请一个节点大小的堆内存
//1.1 使用malloc来分配内存
struct node *p = (struct node *)malloc(sizeof(struct node));
//2. 判断堆内存是否申请成功
if(NULL == p){
printf("malloc error!\n");
return NULL;
}
//3. 清理申请到的空内存
bzero(p, sizeof(struct node));
//4. 填充节点中的数据
p->data = data;
//5. 节点中的指针初始化为NULL
p->pNext = NULL;
//6. 返回一个struct node* 类型的指针
//指向刚创建出来的节点的首地址的指针
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;
}
//1头插法插入在头节点之后
void insert_head1(struct node *pH, struct node *new)
{
new->pNext = pH->pNext;
pH->pNext = new;
}
//没有头节点的处理,代码有bug,先注释了
/*
void insert_head2(struct node *pH, struct node *new)
{
new->pNext = pH;
pH = new;
}
*/
//遍历循环链表,打印信息
void list_for_each(struct node *pH){
struct node *p = pH;
int n = 1; //记录打印了几次,链表中节点的个数
printf("-------------begin---------------\n");
//非空就不是最后一个节点,继续循环
while(NULL != p->pNext){
p = p->pNext;//退出条件,假设这句话执行后,p节点为最后一个节点,下面语句也会打印出信息,不存在漏掉的风险
printf("node%d data: %d.\n", n, p->data);
n++;
}
printf("---------------end---------------\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(p->data == data){
//尾节点的情况
if(NULL == p->pNext){
pPrev->pNext = NULL;
printf("delete %d.\n", p->data);
free(p);
}
//普通节点的情况
else{
pPrev->pNext = p->pNext;
printf("delete data == %d node.\n", p->data);
free(p);
}
return 0; //删除成功
}
printf("没有要删除的节点.\n");
return -1;
}
}
int main(void){
struct node *pHeader = create_node(1); //头节点
insert_tail(pHeader, create_node(2)); //尾插法,插在头节点之后
insert_tail(pHeader, create_node(3)); //尾插法,插在2后面
insert_head1(pHeader, create_node(95)); //头插法,插在头节点之后,2之前
insert_tail(pHeader, create_node(4)); //尾插法,插在3后面
insert_head1(pHeader, create_node(97)); //头插法,插在头节点之后,95之前
insert_head1(pHeader, create_node(90)); //头插法,插在头节点之后,97之前
list_for_each(pHeader); //应该输出 90 97 95 2 3 4
delete_node(pHeader, 90);
list_for_each(pHeader); //应该输出 97 95 2 3 4
return 0;
}
工程目录下创建CMakeLists.txt
CMAKE_MINIMUM_REQUIRED( VERSION 2.8)
ADD_EXECUTABLE(main src/main.c)
操作
mkdir build;
cd build;
cmake .. && make
./main
参考:《C语言内核深度解析》–朱有鹏 张先凤著