一、单链表中的循环链表:
目前我们的链表最后一个结点的pNext指向的是NULL;循环链表中的最后一个结点的pNext指向的是头结点;
循环链表和不是循环链表的区别:判断条件不一样!p->pNext == NULL;
p->pNext == pHeader;
二、单链表和数组的区别:
单链表的优势:1、可以解决数组大小一旦定义之后很难改变的缺点;
2、链表中的各个结点之间可以不用连续分配,分配的是堆空间!可以充分利用碎片化内存!回忆一下malloc的分配原理!
单链表的缺点:
1、每个结点要分配四字节的指针变量用来保存下一个结点的地址,浪费了内存空间;无论是什么类型的指针变量,所占用的字节大小都是4个字节;
2、因为单链表里面只能指向下一个结点,不能够指向上一个结点,导致的结果就是一旦错过了某个结点,还得重新遍历!三、双链表
1、引入:为了解决单链表的第2个缺点;
模型:
在单链表的基础上每个结点加上了指向前一个节点的前向指针!(对于我们这里的带头结点的情况,第一个有效结点的前向指针指向的是头结点,头结点的前向指针指向的
是NULL);
比较单链表和双链表的结点的差别:①单链表的结点:
struct node{
int data; //保存的本身的数据; 数据域;
struct node *pNext; //结构体类型的指针,保存下一个结构体的地址; 指针域;};
②双链表的结点:
struct node{
int data; //保存的本身的数据; 数据域;
struct node *pNext; //结构体类型的指针,保存下一个结构体的地址; 指针域;
struct node *pPrev; //结构体类型的指针,保存上一个结构体的地址; 指针域;};
③双链表:
不是两个链表,其实就是比单链表多了一个指向前一个结点的指针。
单链表理解为单方向行驶的车道,双链表理解为双向行驶的车道。
其实就是比单链表多了一种遍历方式,前向遍历的方式。
2、具体操作:
①创建结点:
struct node *create_node(int num){
struct node *p = (struct node *)malloc(sizeof(struct node)); //注意malloc的返回值;
if( NULL == p){
printf("malloc error!\n");
return NULL;}
bzero(p,sizeof(struct node));
p->pNext = NULL;
p->pPrev = NULL; //多了这一行代码;return p;
}
②插入:尾插:
第一步:找到链表的最后一个结点;
第二步:将新的结点和原来的最后一个结点连接起来;
(1)原来的尾节点的pNext指针指向新结点的首地址;
(2)新结点的pPrev指针指向原来的尾节点的首地址;
(3)新结点的pNext指针指向NULL; //在Create_node执行过了,可以跳过
void insert_tail(struct node *pHeader,struct node *new){
struct node *p = pHeader;
//找到链表的尾节点;
while(p->pNext != NULL){
p = p->pNext;
}
//将新的结点和原来的最后一个结点连接起来
p->pNext = new;
new->pPrev = p; //多了前向指针的指向问题!}
头插:1、待插入的新结点的pNext指向原来的第一个结点;
new->pNext = pHeader->pNext;
2、 原来的第一个结点的前一个节点此时变成了带插入的新的结点,将原来第一个结点的pPrev指向待插入的结点;
pHeader->pNext->pPrev = new;
3、 头结点的pNext指向新插入的结点;
pHeader->pNext = new;
4、新插入的结点的pPrev指向头结点;
new->pPrev = pHeader;遍历:
正向遍历:
正向遍历和单链表的遍历是一样的,做稍微的修改。
struct node * display_link(struct node *pHeader){
struct node *p = pHeader;
while(p->pNext != NULL)
{
p = p->pNext; //跳过头结点;
printf("%d\n",p->data);}
return p;
}
前向遍历:
从双链表的尾节点开始往前面开始遍历。
1、获取链表中最后一个尾节点的地址;(关键一步)
2、用while循环依次p->pPrev != NULL中间插入:
int insert_mid(struct node *pHeader,int num,struct node *new)
{
struct node *p = pHeader;
while(p->pNext != NULL)
{
p = p->pNext;
if(p->data == num)
{
//已经是尾节点!
if(p->pNext == NULL)
{
p->pNext = new;
new->pPrev = p;
}
else
{
new->pNext = p->pNext;
p->pNext->pPrev = new; //如果不单独考虑尾节点的情况,在这儿会有段错误,毕竟p->pNext = NULL;
p->pNext = new;
new->pPrev = p;
}
return 0;
}
}
}
删除:
链表:头结点-->Node1-->Node2-->Node3-->NULL;
删除:Node3;
伪代码:
1、遍历链表到Node2这个结点;
2、明确指针的指向关系:
(1)将Node2的前面一个结点(即Node1)的pNext指向Node2的后面一个结点;
(2)将Node2的后面一个结点(即Node3)的pPrev指向Node2的前面一个结点(即Node1);
(3)释放Node2这个结点所分配的堆空间;
int delete_node(struct node *pHeader,int num)
{
struct node *p = pHeader;
while(p->pNext != NULL)
{
p = p->pNext; //跳过头结点;
if(p->data == num)
{
if(p->pNext == NULL) //判断尾节点;
{
p->pPrev->pNext = NULL;
}
else
{
p->pPrev->pNext = p->pNext;
p->pNext->pPrev = p->pPrev;
}
free(p);
p = NULL;
return 0;
}
}
printf("delete error!\n");
}
总结:
以上就是带头结点的单链表和双链表的基本操作。
插入(头插、尾插、中间插入)、删除、逆序【双链表不需要逆序】、遍历;
学习方法:
1、可以画图想出逻辑上的算法;
2、根据逻辑算法写出伪代码;
3、编码;
注意的是:是否跳过了头结点,对于尾节点要特殊考虑一下。