前言
今天老师上课讲了单链表,听的我是一头雾水,决定自己来好好研究一下到底是如果实现的。且听我慢慢道来。
看完本文你能掌握:
1、单链表的实现
2、单链表的基本操作(添加或删除等等)
单链表
首先,我们要明白单链表的原理是什么,什么是单链表?
单链表是一种链式存取的数据结构。
用一组存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素+ 指针,元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
通俗点来讲,就是每个结点既存放着自己的数据,又存放着下一个节点的地址。
一个节点的结构为下图:
静态链表
接下来,我们主要研究的是用结构体和指针实现的单链表。
首先,我们先来定义一个结构体:
struct Node {
int data;
struct Node* next;
};
然后,我们定义三个结构体变量并赋值。
struct Node Node1 = { 1, NULL };
struct Node Node2 = { 2, NULL };
struct Node Node3 = { 3, NULL };
而我们要实现的功能就是,每个结点的指针都指向下一个结点,让三个节点之间有关联,即为下图所示:
结点1的指针指向结点2,结点2的指针又指向结点3,最后结点3指向NULL,那么我们只需要进行赋值即可实现。
Node1.next = &Node2;
Node2.next = &Node3;
但是我们这样实现的,仅仅是一个静态链表。实际的作用并不大,下面我们再来看看如何实现一个动态链表呢?
动态链表
动态链表是什么呢?提到动态很容易想到我们之前学过的动态申请内存空间。
其实动态链表就是可以动态建立一个链表,实现起来大致有以下几个功能:
1)创建链表(即创建一个链表头部)
2)创建结点
3)插入结点
4)删除结点
5)遍历结点
首先,我们先用一个函数来实现功能一:
创建链表头部其实就是动态申请一块节点的空间并传址给结构体指针变量,然后将该指针作为返回值返回即可。
将该功能写为单独一个函数也是为了便于处理,做到模块化编程。
struct Node* createList()
{
struct Node* headNode = (struct Node*)malloc(sizeof(struct Node));
headNode->next = NULL; //初始化
return headNode;
}
下面,我们再来实现以下功能二:
struct Node* creatNode()
{
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
printf("请输入数字:\n");
scanf("%d", &newNode->data);
newNode->next = NULL; //每次都要进行初始化
return newNode;
}
这样,每次调用该函数,就会产生一个新的结点。
接下来,我们先来看一下功能五,因为这个比较简单一些。为了方便观察,我们遍历的同时把每个结点存储的data
也打印出来。
我们先用图示来看一下怎么实现的:
起初pMove
指向的是head后面的第一个结点,即为headNode->next
指向的内容。然后我们将pMove
里面的内容输出。
接下来我们更改pMove
指向的内容,一步步向后移动,即为将pMove = pMove->next
,这样就可以将整个链表遍历一遍了。
最后,当pMove
指向的内容为NULL
时,就可以结束遍历了。
void printList(struct Node* headNode)
{
struct Node* pMove = headNode->next;
while (pMove)
{
printf("%d", pMove->data);
pMove = pMove->next; //每次打印之后将pMove后移
}
}
接下来,看一下功能三:插入节点。
这里我们只讨论一下头插法。
头插法其实就是将结点插入到链表头和下一个结点之间。
我们先来看图示:
首先,我们先用刚刚写好的函数创建一个新的结点struct Node* newNode = creatNode();
接下来,我们就开始更改插入位置左右两个结点中指针的指向即可。
代码的话就是两句话即可:
newNode->next = headNode->next; //将新建的结点指向下一个
headNode->next = newNode; //链表头指向新建的结点
下面是实现该功能的完整代码:
void insertNodeByHead(struct Node* headNode)
{
struct Node* newNode = creatNode(); //调用刚刚我们写好的函数
newNode->next = headNode->next; //将新建的结点指向下一个
headNode->next = newNode; //链表头指向新建的结点
}
有了上述的几个功能后,其实动态链表的功能基本上就已经实现了,我们可以写在主函数里面试验一下:
int main()
{
struct Node* listNode = createList();
insertNodeByHead(listNode);
insertNodeByHead(listNode);
insertNodeByHead(listNode);
printList(listNode);
return 0;
}
运行结果:
因为我们使用的是头插法,所以输出的数字刚好是颠倒着的。
好了,动态建立链表基本完成了。我们来看最后一个功能:删除结点。
删除结点依据的是用户提供的数值,那么我们就需要用数值与每个结点存储的数值进行比较,直到找到数字或者到了链表最后一位。
首先,我们先建立两个指针posNode
和posNodeFront
,就是指向当前比较的结点和指向当前位置的前一个结点。
起初,两个位置分别指向链表头和第一个结点。
接下来每次判断posNode->data == posData
。
如果不成立的话,那就一直将两个指针指向的结点后移一位,即为
posNodeFront = posNode;
posNode = posNode->next;
成立时我们就可以更改posNodeFront
的指向,然后将删除的那个结点的内存释放。
当然我们也可能遇到遍历完毕之后,没有符合要求的结点,即为不删除任何节点。那我们遍历到posNode == NULL
时即可停止。
上面分析的都是常规情况,但是我们可能会遇到一些极端情况。比如下面两种:
首先我们来看第一种情况,其实这种情况无非就是只有头部和一个结点,和正常情况没有太大区别,代码暂时无需更改。
再来看一下第二种情况,这种情况下在最开始给posNode
和posNodeFront
赋值时,就已经出现了空指针,所以我们最开始进行循环遍历的时候,就要对posNode
进行判断。
只有当posNode != NULL
的时候,才会进行遍历比较。
下面时实现该功能的完整代码:
posNodeFront->next = posNode->next;
free(posNode);
void deleteNode(struct Node* headNode, int posData)
{
struct Node* posNode = headNode->next; //指定数据存在的结点
struct Node* posNodeFront = headNode; //指定数据存在的结点的上一个结点
while (posNode->data != posData && posNode)
{
posNodeFront = posNode;
posNode = posNode->next;
if (posNode == NULL) //若为NULL,即表明posNode已经是最后一位
return;
}
posNodeFront->next = posNode->next;
free(posNode);
}
那么目前为止所有的功能都实现了。下面附上所有的代码吧。
#include<stdio.h>
#include<stdlib.h>
struct Node {
int data;
struct Node* next;
};
struct Node* createList()
{
struct Node* headNode = (struct Node*)malloc(sizeof(struct Node));
headNode->next = NULL; //初始化
return headNode;
}
struct Node* creatNode()
{
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
printf("请输入数字:\n");
scanf("%d", &newNode->data);
newNode->next = NULL;
return newNode;
}
void printList(struct Node* headNode)
{
struct Node* pMove = headNode->next;
while (pMove)
{
printf("%d ", pMove->data);
pMove = pMove->next; //每次打印之后将pMove后移
}
printf("\n");
}
void insertNodeByHead(struct Node* headNode)
{
struct Node* newNode = creatNode(); //调用刚刚我们写好的函数
newNode->next = headNode->next; //将新建的结点指向下一个
headNode->next = newNode; //链表头指向新建的结点
}
void deleteNode(struct Node* headNode, int posData)
{
struct Node* posNode = headNode->next; //指定数据存在的结点
struct Node* posNodeFront = headNode; //指定数据存在的结点的上一个结点
//if (posNode == NULL)
// return;
while (posNode->data != posData && posNode)
{
posNodeFront = posNode;
posNode = posNode->next;
if (posNode == NULL)
return;
}
posNodeFront->next = posNode->next;
free(posNode);
}
int main()
{
struct Node* listNode = createList();
insertNodeByHead(listNode);
insertNodeByHead(listNode);
insertNodeByHead(listNode);
printList(listNode);
deleteNode(listNode, 1);
printList(listNode);
return 0;
}
顺便在最后附上老师给我们讲的一个例题,感觉代码也很不错,只是功能比较单一,只有输入和输出:
// 例9 - 9 动态建立链表
//==========================================================
#include <stdio.h>
#define LEN sizeof(struct STU)
struct STU
{
int sum;
float score;
struct STU* next;
};
/******************************************/
struct STU* Creat() //动态建立链表
{
struct STU* head, *tail, *new;
int n = 0;
head = NULL;
printf("==================================\n");
printf(" 建立动态链表(学号-1结束):\n");
printf("==================================\n");
do
{
new = (struct STU*)malloc(LEN);
if (new == NULL) //申请不到
{
printf("No space\n"); return NULL;
}
printf("输入学号: ");
scanf("%d", &new->num);
if (new->num == -1) break; //输入结束
printf("输入成绩: ");
scanf("%f", &new->score);
if (n == 0) //第一个结点
head = tail = new;
else
{
tail->next = new; //新申请的结点链在最后一个结点的后面
tail = new; //改变表尾指针,指向新的表尾
}
n++;
} while (1);
tail->next = NULL; //千万不能忘!
free(new); //释放new指向的存储单元
return head; //返回链表头指针
}
/**********************************************/
int main()
{
struct STU* Head, * p;
Head = Creat();
printf("=================================\n");
printf(" 输出链表信息:\n");
printf("=================================\n");
p = Head;
while (p)
{
printf("%6d %7.2f\n", p->num, p->score);
p = p->next;
}
return 0;
}