目录
一、链表有关概念
- 结点的组成:
示意图:
数据域:用来存储数据元素的值
指针域:存放的是该结点的直接后继结点地址,以便将各个独立的结点连接起来。
- 单链表
链表:用链式存储结构表示的线性表称为链表。
单链表:每个结点只有一个指针域的链表称为单链表。
- 开始结点、头结点、尾结点、头指针
1、开始结点:链表中存储第一个数据元素(a1)的结点。
2、尾结点: 链表中存储最后一个数据元素(an)的结点
尾结点没有后继结点,所以其指针域为的值NULL
3、头结点:再开始结点前附加的一个结点。
4、头指针:指向链表中第一个结点(头结点或是无头结点的开始结点)的指针。
- (链表)结点的构造
typedef char DataType;//定义结点的数据类型
typedef struct node{ //结点类型名称定义
DataType data; //结点数据域
struct node *next;//结点的指针域 (之所以是结构体类型指针,是因为指针域里存放的是整个结构体的地址)
}ListNode; //结构体类型标识符
typedef ListNode* LinkList;//表示结构体的地址域 (其实可有可无,可以用 ListNode **x表示,这里知识为了方便区分)
ListNode *p; //指向结点的指针
LinkList head; //指向链表的头指针
注意事项:
- 这里的"ListNode *"等价于"LinkList",只是说用途不同,所以同一个指针类型,用不同名字以示区分。
- LinkList类型的指针变量 head 表示它是单链表的头指针。
- ListNode *类型的指针变量 p 表示它是指向某一结点的指针。
- 结点的生成和释放
- 结点的生成:
建立新结点时,需要向系统申请一个指定大小和类型的存储空间,来生成一个新结点,新结点必须要用指向结点的指针来指向。
例如:p = ( ListNode * )malloc( sizeof ( ListNode )
(指向结点的指针)= (指定类型)+ (指定大小)
2.结点的释放:
当用户申请的某个存储空间(比如 p 指向的空间)不需要时,可使用free(p);释放 p 指向的结点空间,以便其他应用使用,不至于造成空间的浪费。
- 单链表示意图:
- 不带 头结点 的单链表:
2.带 头结点 的单链表:
3.带头结点和不带头结点的区别:
不带头节点:
此时头指针指向第一个节点
h->a1->a2->a3->…… // 头指针存放的是第一个节点的地址,即h,也就是说(*h)表示的是第一个节点
带头结点:
此时头指针指向头结点
h->headnode->a1->a2->a3->…… // 头指针存放的是头结点的地址,也就是说(*h)表示的是头结点
总的来说,就是带头结点时不管是否为空表,头指针的值都不会变化,都指向头结点。而不带头结点则需要根据不同情况来修改头指针的值。
不带头节点写代码不方便,需要专门写一段代码来解决第一个节点。
所以操作不统一,有所不便,所以绝大数时候使用带头结点的方式较为方便。
二、链表的基本操作:
以 // ListNode *p,*newNode, LinkList head实际上都是该对象的地址)
(一)链表的建立
1、头插法:
算法分析:
头插法建立单链表,是从一个空表开始,重复读入数据,生成新结点,将读入的数据存放在新结点的数据域中,然后将新结点插入当前链表的表头上,直到读入具体的结束标志为止。
算法核心:
>向系统申请新结点的存储空间:
newNode = ( ListNode *)malloc( sizeof ( ListNode ) );
>将读入的数据存储到新结点的数据域中:
newNode ->data = ch;
>将头指针 head (head指针一开始为空NULL,是为了第一个结点作为尾结点没有直接后继结点的特征)存放到新结点的指针域中:
newNode ->next = head;(head->next)
>头指针指针域存放新结点,相当于新结点连在了头指针后面:
head = newNode
之后的结点(重复以上操作,不过从第二个结点开始就如图下示例)
算法示例图:
算法实现:
//头插法建立单链表
LinkList CreatListF(void) //因为返回的是单链表,所以函数类型也是单链表
{
DataType newdata; //新结点存储的数据
LinkList head; //头指针(可以视为整个单链表)
ListNode *newNode; //工作指针(新结点)双重指针
head = NULL; //初始链表内容为空
printf("请输入链表各结点的数据(字符型):\n");
while((newdata = getchar()) != '\n')
{
newNode = (ListNode *)malloc(sizeof(ListNode));//向系统申请新结点的存储空间
newNode->data = newdata;//将读入的数据存储到新结点的数据域中
newNode->next = head; //将头指针 head 地址(包含数据域和指针域)存放到新结点的指针域中
head->next = newNode; //头指针指针域存放新结点地址
}
return head;(返回整个单链表)
}
2、尾插法:
算法分析:
需要借助一个辅助指针rear,指向当前链表最后一个节点,每次处理辅助指针指向的节点和新增的节点的关系即可。
算法核心:
>建立一个头结点,将头指针和辅助指针,都指向头结点:
rear = head (可以按照 rear->next = head ->next 来理解)
>向系统申请新结点的存储空间:
head = ( ListNode *)malloc( sizeof ( ListNode ) );
newNode = ( ListNode *)malloc( sizeof ( ListNode ) );
>将读入的数据存储到新结点的数据域中:
newNode ->data = newdata;
>将将新结点插在表未
rear ->next = newNoda;(相当于 (rear->next)->next = newNode:头结点的指针域存放的是新结点的地址)
>尾指针指向新表尾:
rear = newNode;
(之后结点(第2个及以后结点位置 按照2~4步骤执行))
算法示例图:
算法实现:
//尾插法建立单链表
LinkList CreatListRH(void)
{
DataType newdata;
LinkList head;
ListNode *rear, *newNode;//工作指针(尾指针 和 指向新结点的指针)
head = (ListNode *)malloc(sizeof(ListNode));
rear->next = head->next; //尾指针指向头结点
printf("请输入链表各结点的数据(字符型):\n");
while((newdata = getchar()) != '\n')
{
newNode = (ListNode *)malloc(sizeof(ListNode));//向系统申请新结点的存储空间
newNode->data = newdata;//将读入的数据存储到新结点的数据域中
rear->next = newNode; //表尾结点的指针域存放新结点的地址
rear = newNode; // 将尾指针指向新表尾(相当于 尾指针的地址和新结点一样,相当于二者指向同一存储空间)
}
return head;
}
(二)求单链表的长度:
- 带头结点的单链表长度:
算法分析:
首先,求长度必然会涉及遍历,遍历就需要终止条件(最后一个结点指针域为NULL就是一个终止条件)
那么,①设置一个计数器count,并设置初值为:0。②设置一个移动指针’p’用来遍历链表。③若p->next != NULL,就使得 p 指向下一个结点(即 p = p->next),同时计数器 count + 1,直到终止条件达成(p->next == NULL)。
最后,返回的count值就是该单链表的长度。
算法实现:
// 求带头结点的单链表长度
int LengthListH(LinkList head)
{
ListNode *p = head; //移动指针指向头结点 *p = head->next
int j = 0;
while(p->next){
p = p->next; //使 p 指向下一个结点
j++;
}
return j;
}
2.不带 头结点的单链表长度
算法分析:
它和带 头结点的区别是,它的开始结点前面并没有头结点,所以它是直接指向开始结点,所以要考虑当前结点是否为空的情况。
当表不空时候,与带头结点操作基本相同,只是开始时 指针p指向的是开始结点,所以计数器j 的初值是1;当表空时直接返回0。
算法实现:
//求不带头结点的单链表长度
int LengthList(LinkList head)
{
ListNode *p = head;// p 指向开始结点
int j;//计数器
if(p == NULL){
return 0;//处理空表
}
j =1;//处理非空表
while(p->next){
p = p->next;//使 p 指向下一个结点
j++;
}
return j;
}
(三)链表的查找(以带头结点为例):
- 按序号在带头结点的单链表中查找:
算法分析:
首先,设置一个计数器j,并设置初值为0,从指针 p 指向链表的头结点开始顺序遍历,计数器随之增加变化。
接着,当 j == i(形参:传输过来的查询结点序号)时,指针 p 所指向的结点就是要找的第 i 个结点.
然鹅,当指针 p 的值为NULL(已经遍历到了最后一个结点),且 j != i时,则表示找不到第 i 个结点。
算法实现:
//按序号在带头结点的单链表中查找
ListNode *GetNode(LinkList head, int i )
//之所以函数类型是 ListNode *,是因为 使用指针 p(ListNode *类型) 找到结点后,通过 p 将找到的结点信息返回
{
int j = 0;//计数器
ListNode *p = head;//从头结点开始扫描
while(p->next != NULL && j < i){
p = p->next;
j++;
}
if(i == j)
return p;//找到了第 i 个结点
else
return NULL;
}
2.按值在带头结点的单链表中查找:
算法分析:
从开始结点出发,依次遍历单链表中各个结点的值和定值key(传输的形参)作比较,若在链表中找到与key值相同的结点,返回该结点的存储地址,否则返回NULL;
算法实现:
//按值在带头结点的单链表中查找
ListNode *LocateNode(LinkList head, DataType key)
{
ListNode *p = head->next;//这里也可以写成 ListNode *p = head;,这样的话下面的判断条件里就是(p->next &&……)
while(p && p->data != key){//p 等价于p != NULL
p = p->next;
}
return p;
}
(四)链表的插入(以带头结点为例)
算法分析:
要想将新结点插入链表中,那就是将值为x的新结点 *newnode 插入单链表head的第i个结点ai的位置上。
首先,从开始结点开始遍历,找到第i-1个结点(方便对第i和第i-1个结点进行相关操作),相当于进行了 p = GetNode(head, i-1)的代码执行。
(图1)
然后,生成一个值(你想插入的值)为x的新结点 *newnode,即进行了如下代码执行:newnode = (ListNode *)malloc (sizeof(ListNode));
//为新结点申请链表结点类型大小的空间,做到空间高效使用
Newnode->data = x;
(图2)
最后,先将第i个结点及以后的结点,连接在新结点 *newnode 后面(相当于先将 *newnode 的指针域存放第i个结点的地址),再将新结点 *newnode接在第i- 1的结点后面(即将新结点 *newnode的地址存放在第i- 1的结点的指针域里)
(图3、4)
算法示例图:(直接放书上例题了,懒得画了……)
算法实现:
//链表的插入(以带头结点为例)
int InsertList(LinkList head, DataType x, int i)
{
ListNode *p, *newnode;
newnode = GetNode(head, i-1);//寻找第 i-1 个结点
if(p == NULL)
{
printf("未能找到第%d个结点", i-1);
return 0;
}
newnode = (ListNode*)malloc(sizeof(ListNode));//找到第 i-1个结点的情况下
newnode->data = x;
newnode->next = p->next;//将第i个结点及以后的结点,连接在新结点 *newnode 后面
p->next = newnode;//再将新结点 *newnode接在第i- 1的结点后面
return 1;
}
(五)链表的删除(以带头结点为例):
算法分析:
为了删除单链表上第 i个结点。
首先,依旧是从开始结点出发,依次遍历,找到第 i-1个结点(选择这个结点是因为它的指针域里存放的值第i个结点的地址,方便两头操作),是遍历指针 p 指向它。即实现代码操作:p = GetNode(head, i-1)。
然后,使得辅佐指针 *rear指向第i个结点(被删除的结点):
rear = p->next;
接着,将第i+1个结点(被删除结点的直接后继结点)的地址存放在第 i-1个结点的指针域里(相当于第i-1个结点跨过第i个,直接连接第i+1个结点)
p->next = rear->next;
最后释放被删除结点的空间(free(rear))。
算法示例图:(直接放书上例题了,懒得画了……)
算法实现:
//链表的删除(以带头结点为例
int DeleteList(LinkList head, int i)
{
ListNode *p, *rear;
p = GetNode(head, i-1);
if(p == NULL)
{
printf("未能找到第%d个结点", i-1);
return 0;
}
rear = p->next;//辅佐指针 *rear指向第i个结点(被删除的结点)
p->next = rear->next;//第i+1个结点(被删除结点的直接后继结点)的地址存放在第 i-1个结点的指针域里
free(rear);//释放被删除结点的空间
return 1;
}
三、拓展 循环和双向链表:
四、项目实践(以带头结点的单链表为基础):
(一)目标:编写一个记录职工姓名信息的简易系统:
(二)目标分解:
1、建立一个记录职工姓名信息的单链表结构体类型和单链表类型。
2、应用1的结构体建立单链表存储信息。
3、在2的基础上设计并输出所有职工的姓名信息。
4、实现删除职工信息的功能。
5、实现查找某职工姓名的功能,查找成功则返回位置信息。
(三)项目完善:是否在上述功能编写运行无误后,给项目完善改进,如给职工信息添加性别、年龄、工号、家庭住址、电话号码等信息。然后根据工号查找职工信息,避免姓名重复带来的查找错误。
(四)代码示例(简易版)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1、建立一个记录职工姓名信息的单链表结构体类型和单链表类型。
typedef struct node
{
char data[10]; //结点是数据域为字符串(姓名)
struct node* next; //结点的指针域
}ListNode;
typedef ListNode* LinkList; //定义单链表类型
ListNode* LocateNode(LinkList head, char *key);
// 2、应用1的结构体建立单链表存储信息。(此处用头插法建立 带头结点的单链表)
LinkList CreatList()
{
char ch[100];//暂存姓名信息
LinkList head, p;//这几个都是野指针,使用的时候是需要进行动态地址申请,或直接指向一个对象地址。
head = (ListNode*)malloc(sizeof(ListNode));//LinkList 其实就是代替*ListNode
head->next = NULL;
while(1)
{
printf("\n输入 # 结束输入\n");
printf("请输入具体的职工姓名:");
scanf("%s", ch);
if(strcmp(ch, "#"))
{
if(LocateNode(head, ch) == NULL)//避免姓名重复
{
strcpy(head->data, ch);
p = (ListNode*)malloc(sizeof(ListNode));
p->next = head;
head = p; //指针指向同一地址
}
else
printf("\n错误!输入的名称出现重复\n");
}
else
break;
}
return head;
}
// 3、在2的基础上设计并输出所有职工的姓名信息。
void PrintList(LinkList head)
{
ListNode* p;
p = (ListNode*)malloc(sizeof(ListNode));
p = head;
while(p)
{
printf("%s\n", p->data);
p = p->next;
}
printf("\n");
}
// 4、实现删除职工信息的功能。
void DeleteList(LinkList head, char *key)
{
ListNode *p, *rear, *front = head;
p = (ListNode*)malloc(sizeof(ListNode));
p = LocateNode(head, key);
if(p == NULL)
{
printf("查找位置错误");
exit(0);
}
while(front->next != p) // p 为要删除的结点, front 为p的前一个结点
{
front = front->next;
}
rear = front->next;
front->next = rear->next; //释放结点
free(rear);
}
// 5、实现查找某职工姓名的功能,查找成功则返回位置信息。
ListNode* LocateNode(LinkList head, char *key)
{
int count = 0;
ListNode *p = head;
while(p != NULL && strcmp(p->data, key) != 0)//遍历扫描,直至 p 为 NULL,或p->data 为 key 止
{
p = p->next;
count++;
}
return p;
}
//主函数
int main(int argc, char *argv[])
{
char ch[10];
LinkList head;
head = CreatList();
PrintList(head);
printf("请输入要删除的职工信息: ");
scanf("%s", ch);
DeleteList(head, ch);
PrintList(head);
printf("请输入要查找的职工信息: ");
scanf("%s",ch);
if(LocateNode(head, ch))
{
printf("找到该职工信息了");
}
else
printf("未能找到该职工信息!");
return 0;
}