(
1
)时刻谨记:链表就是用来解决数组的大小不能动态扩展的问题,所以链表其实就是当数组用的。直白点:链表能完成的任务用数组也能完成,数组能完成的任务用链表也能完成。但是灵活性不一样。
( 2 )简单说:链表就是用来存储数据的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。数组的优势是使用简单(简单粗暴)。
// 构建一个链表的节点
struct node
{
int data; // 有效数据
struct node * pNext; // 指向下一个节点的指针
};
( 1 )链表的内存要求比较灵活,不能用栈,也不能用data数据段。只能用堆内存。
( 2 )使用堆内存来创建一个链表节点的步骤: 1 、申请堆内存,大小为一个节点的大小(检查申请结果是否正确); 2 、清理申请到的堆内存; 3 、把申请到的堆内存当作一个新节点; 4 、填充你哦个新节点的有效数据和指针区域。
// 定义头指针
struct node * pHeader = NULL;
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p, sizeof ( struct node)); // 也可以用memset:memset(p,0,sizeof(struct node));
// 填充节点
p - > data = 1 ;
p - > pNext = NULL; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
pHeader = p; // 将本节点和它前面的头指针关联起来
/********************************************************************/
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p1 = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p1)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p1, sizeof ( struct node));
// 填充节点
p1 - > data = 2 ;
p1 - > pNext = NULL; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
p - > pNext = p1; // 将本节点和它前面的头指针关联起来
/********************************************************************/
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p2 = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p2)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p2, sizeof ( struct node));
// 填充节点
p2 - > data = 3 ;
p1 - > pNext = p2; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
// 访问链表第1个节点的有效数据
printf( "node1 data: %d.\n" , pHeader - > data);
printf( "p->data: %d.\n" , p - > data); // pHeader->data等同于p->data
// 访问链表第2个节点的有效数据
printf( "node2 data: %d.\n" , pHeader - > pNext - > data);
printf( "p1->data: %d.\n" , p1 - > data);
// pHeader->pNext->data等同于p1->data
// 访问链表第3个节点的有效数据
printf( "node3 data: %d.\n" , pHeader - > pNext - > pNext - > data);
printf( "p2->data: %d.\n" , p2 - > data);
// pHeader->pNext->pNext->data等同于p2->data
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;
return p;
}
pHeader = create_node( 1 );
// 将本节点和它前面的头指针关联起来
pHeader - > pNext = create_node( 432 );
// 将本节点和它前面的头指针关联起来
pHeader - > pNext - > pNext = create_node( 123 );
// 将来要指向下一个节点的首地址
从链表尾部插入新节点
// 思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。
void insert_tail( struct node * pH, struct node * new )
{
// 分两步来完成插入
// 第一步,先找到链表中最后一个节点
struct node * p = pH;
while (NULL != p - > pNext)
{
p = p - > pNext; // 往后走一个节点
}
// 第二步,将新节点插入到最后一个节点尾部
p - > pNext = new ;
}
// 计算添加了新的节点后总共有多少个节点,然后把这个数写进头节点中。
//由上一个函数修改所得
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 *pHeader = NULL; // 这样直接insert_tail会段错误。
struct node * pHeader = create_node( 0 );
insert_tail(pHeader, create_node( 1 ));
insert_tail(pHeader, create_node( 2 ));
insert_tail(pHeader, create_node( 3 ));
return 0 ;
}
4 . 9 . 5 .从链表头部插入新节点
// 思路:
void insert_head( struct node * pH, struct node * new )
{
// 第1步: 新节点的next指向原来的第一个节点
new - > pNext = pH - > pNext;
// 第2步: 头节点的next指向新节点的地址
pH - > pNext = new ;
// 第3步: 头节点中的计数要加1
pH - > data += 1 ;
}
int main( void )
{
// 定义头指针
//struct node *pHeader = NULL; // 这样直接insert_tail会段错误。
struct node * pHeader = create_node( 0 );
insert_head(pHeader, create_node( 1 ));
insert_tail(pHeader, create_node( 2 ));
insert_head(pHeader, create_node( 3 ));
printf( "beader node data: %d.\n" , pHeader - > data);
// 访问链表第1个节点的有效数据
printf( "node1 data: %d.\n" , pHeader - > pNext - > data);
// 访问链表第2个节点的有效数据
printf( "node2 data: %d.\n" , pHeader - > pNext - > pNext - > data);
// 访问链表第3个节点的有效数据
printf( "node3 data: %d.\n" , pHeader - > pNext - > pNext - > pNext - > data);
return 0 ;
}
4 . 9 . 6 .单链表的算法之遍历节点
// 遍历单链表,pH为指向单链表的头指针,遍历的节点数据打印出来
void bianli( struct node * pH)
{
//pH->data // 头节点数据,不是链表的常规数据,不要算进去了
//struct node *p = pH; // 错误,因为头指针后面是头节点
struct node * p = pH - > pNext; // p直接走到第一个节点
printf( "-----------开始遍历-----------\n" );
while (NULL != p - > pNext) // 是不是最后一个节点
{
printf( "node data: %d.\n" , p - > data);
p = p - > pNext; // 走到下一个节点,也就是循环增量
}
printf( "node data: %d.\n" , p - > data);
printf( "-------------完了-------------\n" );
}
// 1、思考下为什么这样能解决问题;2、思考下设计链表时为什么要设计头节点
void bianli2( struct node * pH)
{
//pH->data // 头节点数据,不是链表的常规数据,不要算进去了
struct node * p = pH; // 头指针后面是头节点
printf( "-----------开始遍历-----------\n" );
while (NULL != p - > pNext) // 是不是最后一个节点
{
p = p - > pNext; // 走到下一个节点,也就是循环增量
printf( "node data: %d.\n" , p - > data);
}
printf( "-------------完了-------------\n" );
}
单链表的算法之删除节点
// 从链表pH中删除节点,待删除的节点的特征是数据区等于data
// 返回值:当找到并且成功删除了节点则返回0,当未找到节点时返回-1
int delete_node( struct node * pH, int data)
{
// 找到这个待删除的节点,通过遍历链表来查找
struct node * p = pH; // 用来指向当前节点
struct node * pPrev = NULL; // 用来指向当前节点的前一个节点
while (NULL != p - > pNext) // 是不是最后一个节点
{
pPrev = p; // 在p走向下一个节点前先将其保存
p = p - > pNext; // 走到下一个节点,也就是循环增量
// 判断这个节点是不是我们要找的那个节点
if (p - > data == data)
{
// 找到了节点,处理这个节点
// 分为2种情况,一个是找到的是普通节点,另一个是找到的是尾节点
// 删除节点的困难点在于:通过链表的遍历依次访问各个节点,找到这个节点
// 后p指向了这个节点,但是要删除这个节点关键要操作前一个节点,但是这
// 时候已经没有指针指向前一个节点了,所以没法操作。解决方案就是增加
// 一个指针指向当前节点的前一个节点
if (NULL == p - > pNext)
{
// 尾节点
pPrev - > pNext = NULL; // 原来尾节点的前一个节点变成新尾节点
free(p); // 释放原来的尾节点的内存
}
else
{
// 普通节点
pPrev - > pNext = p - > pNext; // 要删除的节点的前一个节点和它的后一个节点相连,这样就把要删除的节点给摘出来了
free(p);
}
// 处理完成之后退出程序
return 0 ;
}
}
// 到这里还没找到,说明链表中没有我们想要的节点
printf( "没找到这个节点.\n" );
return - 1 ;
}
delete_node(pHeader, 12 );
4 . 9 . 8 . 2 、单链表逆序算法分析
//思路:首先遍历原链表,然后将原链表中的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个依次取出来,采用头插入的方法插入新链表中即可。
// 将pH指向的链表逆序
void reverse_linkedlist( struct node * pH)
{
struct node * p = pH - > pNext; // pH指向头节点,p指向第1个有效节点
struct node * pBack; // 保存当前节点的后一个节点地址
// 当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
if ((NULL == p) || (NULL == p - > pNext))
return ;
// 当链表有2个及2个以上节点时才需要真正进行逆序操作
while (NULL != p - > pNext) // 是不是最后一个节点
{
// 原链表中第一个有效节点将是逆序后新链表的尾节点,尾节点的pNext指向NULL
pBack = p - > pNext; // 保存p节点后面一个节点地址
if (p == pH - > pNext)
{
// 原链表第一个有效节点
p - > pNext = NULL;
}
else
{
// 原链表的非第1个有效节点
p - > pNext = pH - > pNext;
}
pH - > pNext = p;
//p = p->pNext; // 这样已经不行了,因为p->pNext已经被改过了
p = pBack; // 走到下一个节点
}
// 循环结束后,最后一个节点仍然缺失
insert_head(pH, p);
}
( 2 )简单说:链表就是用来存储数据的。链表用来存数据相对于数组来说优点就是灵活性,需要多少个动态分配多少个,不占用额外的内存。数组的优势是使用简单(简单粗暴)。
// 构建一个链表的节点
struct node
{
int data; // 有效数据
struct node * pNext; // 指向下一个节点的指针
};
( 1 )链表的内存要求比较灵活,不能用栈,也不能用data数据段。只能用堆内存。
( 2 )使用堆内存来创建一个链表节点的步骤: 1 、申请堆内存,大小为一个节点的大小(检查申请结果是否正确); 2 、清理申请到的堆内存; 3 、把申请到的堆内存当作一个新节点; 4 、填充你哦个新节点的有效数据和指针区域。
// 定义头指针
struct node * pHeader = NULL;
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p, sizeof ( struct node)); // 也可以用memset:memset(p,0,sizeof(struct node));
// 填充节点
p - > data = 1 ;
p - > pNext = NULL; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
pHeader = p; // 将本节点和它前面的头指针关联起来
/********************************************************************/
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p1 = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p1)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p1, sizeof ( struct node));
// 填充节点
p1 - > data = 2 ;
p1 - > pNext = NULL; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
p - > pNext = p1; // 将本节点和它前面的头指针关联起来
/********************************************************************/
/********************************************************************/
// 每创建一个新的节点,把这个新的节点和它前一个节点关联起来
// 创建一个链表节点
struct node * p2 = ( struct node * )malloc( sizeof ( struct node));
if (NULL == p2)
{
printf( "malloc error.\n" );
return - 1 ;
}
// 清理申请到的堆内存
bzero(p2, sizeof ( struct node));
// 填充节点
p2 - > data = 3 ;
p1 - > pNext = p2; // 将来要指向下一个节点的首地址
// 实际操作时将下一个节点malloc返回的指针赋值给这个
// 访问链表第1个节点的有效数据
printf( "node1 data: %d.\n" , pHeader - > data);
printf( "p->data: %d.\n" , p - > data); // pHeader->data等同于p->data
// 访问链表第2个节点的有效数据
printf( "node2 data: %d.\n" , pHeader - > pNext - > data);
printf( "p1->data: %d.\n" , p1 - > data);
// pHeader->pNext->data等同于p1->data
// 访问链表第3个节点的有效数据
printf( "node3 data: %d.\n" , pHeader - > pNext - > pNext - > data);
printf( "p2->data: %d.\n" , p2 - > data);
// pHeader->pNext->pNext->data等同于p2->data
将创建节点的代码封装成一个函数
( 1 )封装时的关键点就是函数的接口(函数参数和返回值)的设计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;
return p;
}
pHeader = create_node( 1 );
// 将本节点和它前面的头指针关联起来
pHeader - > pNext = create_node( 432 );
// 将本节点和它前面的头指针关联起来
pHeader - > pNext - > pNext = create_node( 123 );
// 将来要指向下一个节点的首地址
从链表尾部插入新节点
// 思路:由头指针向后遍历,直到走到原来的最后一个节点。原来最后一个节点里面的pNext是NULL,现在我们只要将它改成new就可以了。添加了之后新节点就变成了最后一个。
void insert_tail( struct node * pH, struct node * new )
{
// 分两步来完成插入
// 第一步,先找到链表中最后一个节点
struct node * p = pH;
while (NULL != p - > pNext)
{
p = p - > pNext; // 往后走一个节点
}
// 第二步,将新节点插入到最后一个节点尾部
p - > pNext = new ;
}
// 计算添加了新的节点后总共有多少个节点,然后把这个数写进头节点中。
//由上一个函数修改所得
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 *pHeader = NULL; // 这样直接insert_tail会段错误。
struct node * pHeader = create_node( 0 );
insert_tail(pHeader, create_node( 1 ));
insert_tail(pHeader, create_node( 2 ));
insert_tail(pHeader, create_node( 3 ));
return 0 ;
}
4 . 9 . 5 .从链表头部插入新节点
// 思路:
void insert_head( struct node * pH, struct node * new )
{
// 第1步: 新节点的next指向原来的第一个节点
new - > pNext = pH - > pNext;
// 第2步: 头节点的next指向新节点的地址
pH - > pNext = new ;
// 第3步: 头节点中的计数要加1
pH - > data += 1 ;
}
int main( void )
{
// 定义头指针
//struct node *pHeader = NULL; // 这样直接insert_tail会段错误。
struct node * pHeader = create_node( 0 );
insert_head(pHeader, create_node( 1 ));
insert_tail(pHeader, create_node( 2 ));
insert_head(pHeader, create_node( 3 ));
printf( "beader node data: %d.\n" , pHeader - > data);
// 访问链表第1个节点的有效数据
printf( "node1 data: %d.\n" , pHeader - > pNext - > data);
// 访问链表第2个节点的有效数据
printf( "node2 data: %d.\n" , pHeader - > pNext - > pNext - > data);
// 访问链表第3个节点的有效数据
printf( "node3 data: %d.\n" , pHeader - > pNext - > pNext - > pNext - > data);
return 0 ;
}
4 . 9 . 6 .单链表的算法之遍历节点
// 遍历单链表,pH为指向单链表的头指针,遍历的节点数据打印出来
void bianli( struct node * pH)
{
//pH->data // 头节点数据,不是链表的常规数据,不要算进去了
//struct node *p = pH; // 错误,因为头指针后面是头节点
struct node * p = pH - > pNext; // p直接走到第一个节点
printf( "-----------开始遍历-----------\n" );
while (NULL != p - > pNext) // 是不是最后一个节点
{
printf( "node data: %d.\n" , p - > data);
p = p - > pNext; // 走到下一个节点,也就是循环增量
}
printf( "node data: %d.\n" , p - > data);
printf( "-------------完了-------------\n" );
}
// 1、思考下为什么这样能解决问题;2、思考下设计链表时为什么要设计头节点
void bianli2( struct node * pH)
{
//pH->data // 头节点数据,不是链表的常规数据,不要算进去了
struct node * p = pH; // 头指针后面是头节点
printf( "-----------开始遍历-----------\n" );
while (NULL != p - > pNext) // 是不是最后一个节点
{
p = p - > pNext; // 走到下一个节点,也就是循环增量
printf( "node data: %d.\n" , p - > data);
}
printf( "-------------完了-------------\n" );
}
单链表的算法之删除节点
// 从链表pH中删除节点,待删除的节点的特征是数据区等于data
// 返回值:当找到并且成功删除了节点则返回0,当未找到节点时返回-1
int delete_node( struct node * pH, int data)
{
// 找到这个待删除的节点,通过遍历链表来查找
struct node * p = pH; // 用来指向当前节点
struct node * pPrev = NULL; // 用来指向当前节点的前一个节点
while (NULL != p - > pNext) // 是不是最后一个节点
{
pPrev = p; // 在p走向下一个节点前先将其保存
p = p - > pNext; // 走到下一个节点,也就是循环增量
// 判断这个节点是不是我们要找的那个节点
if (p - > data == data)
{
// 找到了节点,处理这个节点
// 分为2种情况,一个是找到的是普通节点,另一个是找到的是尾节点
// 删除节点的困难点在于:通过链表的遍历依次访问各个节点,找到这个节点
// 后p指向了这个节点,但是要删除这个节点关键要操作前一个节点,但是这
// 时候已经没有指针指向前一个节点了,所以没法操作。解决方案就是增加
// 一个指针指向当前节点的前一个节点
if (NULL == p - > pNext)
{
// 尾节点
pPrev - > pNext = NULL; // 原来尾节点的前一个节点变成新尾节点
free(p); // 释放原来的尾节点的内存
}
else
{
// 普通节点
pPrev - > pNext = p - > pNext; // 要删除的节点的前一个节点和它的后一个节点相连,这样就把要删除的节点给摘出来了
free(p);
}
// 处理完成之后退出程序
return 0 ;
}
}
// 到这里还没找到,说明链表中没有我们想要的节点
printf( "没找到这个节点.\n" );
return - 1 ;
}
delete_node(pHeader, 12 );
4 . 9 . 8 . 2 、单链表逆序算法分析
//思路:首先遍历原链表,然后将原链表中的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个依次取出来,采用头插入的方法插入新链表中即可。
// 将pH指向的链表逆序
void reverse_linkedlist( struct node * pH)
{
struct node * p = pH - > pNext; // pH指向头节点,p指向第1个有效节点
struct node * pBack; // 保存当前节点的后一个节点地址
// 当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
if ((NULL == p) || (NULL == p - > pNext))
return ;
// 当链表有2个及2个以上节点时才需要真正进行逆序操作
while (NULL != p - > pNext) // 是不是最后一个节点
{
// 原链表中第一个有效节点将是逆序后新链表的尾节点,尾节点的pNext指向NULL
pBack = p - > pNext; // 保存p节点后面一个节点地址
if (p == pH - > pNext)
{
// 原链表第一个有效节点
p - > pNext = NULL;
}
else
{
// 原链表的非第1个有效节点
p - > pNext = pH - > pNext;
}
pH - > pNext = p;
//p = p->pNext; // 这样已经不行了,因为p->pNext已经被改过了
p = pBack; // 走到下一个节点
}
// 循环结束后,最后一个节点仍然缺失
insert_head(pH, p);
}