我们首先要知道通讯录的实现是基于单链表的基础上的,所以我们首先要搞懂单链表。(注意:今天的代码量较多),但这不是阻挡我们前进的脚步,冲冲冲!!!
单链表的简要概述
我们知道单链表是和顺序表有一点区别的:顺序表在物理存储结构上是连续的,在逻辑上也是连续的,而单链表只是在逻辑上是连续的,物理上并不连续。单链表是由一个一个的节点(每个节点由一个结构体组成)连接而成的,它们的地址并不连续,只是它每个节点里面存着下一个节点的地址,我们可以由某一个节点来找到它下一个节点,相当于每个节点依次这样链接起来,这就叫做逻辑上是连续的,而物理上不连续。像这样:
那么我们来实现一下这样的单链表。
简单定义和说明
我们将实现链表的增删查改写在SList.c的源文件里面,将各种函数声明写在SList.h 中,将各种函数的测试写在test_1.c的源文件里面。我们来看看我们的头文件里面要实现的函数以及要使用到的头文件。
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义节点
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//链表的打印
void SLTprint(SLTNode* phead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//链表的销毁
void SListDestroy(SLTNode** pphead);
注意:这里节点结构体重命名的原因是节点在后续中要多次用到,简化一下代码,而int重命名的原因是我们可以在此插入字符型数据,结构体数据类型,不止于int类型数据。
函数的实现
尾插函数的实现
我们要知道的是刚开始的时候链表是空的,我们要在一个空链表里面一个一个插入数据,而第一个插入的节点则被当作第一个节点,那么这个链表的起始位置要被改变。所以在形参中我们要传二级指针(要改变实参的地址)。而我们要知道的是再插入节点之前,我们要先申请节点空间才能插入进去。
cpp
//申请节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//申请节点
if (newnode == NULL)
{
perror("malloc fail!");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);//尾插前先申请空间然后才能插入进去
//分为空链表和非空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾节点
SLTNode* ptail = *pphead;
while (ptail->next)//如果这里是空链表的话,会报错,不能对空指针进行解引用
{
ptail = ptail->next;
}
//找到了尾节点
ptail->next = newnode;
}
}
头插函数的实现
和尾插一样,插入之前要先申请一块节点空间再去插入。
cpp
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;//不管是空链表还是非空链表都可以插入,不像尾插要分情况
*pphead = newnode;
}
尾删函数的实现
只有一个节点的链表和有多个节点的链表的情况有点不同,所以我们要分两种情况去讨论一下,当然在删除之前,链表不能为空。
cpp
//尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不能为空
assert(pphead && *pphead);
//判断链表节点有多个还是一个,如果没有分两种情况,程序出问题(结果出问题,但没报错)
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
头删函数的实现
和尾删一样,在删除之前链表不能为空。但不需要分情况。
cpp
//头删
void SLTPopFront(SLTNode** pphead)
{
//前提是链表不为空
assert(pphead && *pphead);
//不管链表是只有一个还是多个,都行通
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找函数的实现
我们需要遍历链表,查找数据,查到了就返回节点位置,没有查到返回空。
cpp
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* pur = phead;
while (pur)
{
if (pur->data == x)
{
return pur;
}
pur = pur->next;
}
//遍历了一遍,没有找到
return NULL;
}
在指定位置之前插入节点函数实现
要实现在指定的节点位置插入节点,我们需要利用上一个查找节点的函数来找到指定位置,然后进行下一步的实现。
cpp
//在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//不能为空链表
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//如果在头节点之前插入,将找不到pos,所以分两种情况
if (*pphead == pos)
{
SLTPushFront(pphead, x);
}//利用头插函数就行
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//将 prev newnode pos 链接起来
prev->next = newnode;
newnode->next = pos;
}
}
在指定位置之后插入节点函数的实现
cpp
//在指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
//不能为空链表
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//将 pos newnode pos->next 链接起来
newnode->next = pos->next;
pos->next = newnode;
}
删除指定位置的节点
cpp
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
//链表不能为空以及pos不能为空
assert(pphead && *pphead);
assert(pos);
//两种情况一种pos为头节点,一种不为头节点
if (*pphead == pos)
{
//头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//将prev pos pos->next 中的pos删除
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
删除指定位置之后的位置节点函数的实现
我们要区分要下删除指定位置的节点函数的代码逻辑。
cpp
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
//pos之后不能为空,要不然怎么删除
assert(pos && pos->next);
SLTNode* del = pos->next;
//pos del del->next 删除pos
pos->next = del->next;
free(del);
del = NULL;
}
链表的销毁
链表节点创建出来,不用了的话我们就要销毁,养成好的代码习惯。
cpp
//链表的销毁
void SListDestroy(SLTNode** pphead)
{
//空链表就不需要销毁
assert(pphead && *pphead);
SLTNode* pur = *pphead;
while (pur)
{
SLTNode* next = pur->next;
free(pur);
pur = next;
}
*pphead = NULL;
}
通讯录的简单定义和说明
我们将通讯录的代码实现要用到的函数实现写到Contact.c的源文件中,将所用到的函数声明写到Contact.h文件中。
Contact.h函数声明如下:
cpp
#pragma once
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 11
#define ADDR_MAX 100
//前置声明
typedef struct SListNode contact;
//用户数据
typedef struct PersonInfo
{
char name[NAME_MAX];//姓名
char sex[SEX_MAX];//性别
int age;//年龄
char tel[TEL_MAX];//电话
char addr[ADDR_MAX];//地址
}PeoInfo;
//初始化通讯录
void InitContact(contact** con);
//添加通讯录数据
void AddContact(contact** con);
//删除通讯录数据
void DelContact(contact** con);
//展示通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);
//修改通讯录数据
void ModifyContact(contact** con);
//销毁通讯录数据
void DestroyContact(contact** con);
初始化通讯录函数的实现
cpp
//初始化通讯录
void InitContact(contact** con)
{
*con = NULL;
}
添加通讯录数据函数的实现
注意:这个函数的实现,可以调用单链表函数来实现。调用尾插或头插都行。
cpp
//添加通讯录数据
void AddContact(contact** con)
{
//尾插和头插两种方法
PeoInfo infor;
printf("请输入姓名!\n");
scanf("%s", infor.name);
printf("请输入性别!\n");
scanf("%s", infor.sex);
printf("请输入年龄!\n");
scanf("%d", &(infor.age));
printf("请输入电话!\n");
scanf("%s", infor.tel);
printf("请输入地址!\n");
scanf("%s", infor.addr);
//采用尾插插入数据,也可采用头插,单链表中有现成的函数
SLTPushBack(con, infor);
}
展示通讯录数据函数的实现
cpp
//展示通讯录数据
void ShowContact(contact* con)
{
printf("姓名 性别 年龄 电话 地址\n");
contact* pcur = con;
while (pcur)
{
printf("%s %s %d %s %s\n", pcur->data.name,pcur->data.sex,
pcur->data.age,pcur->data.tel,
pcur->data.addr);
pcur = pcur->next;
}
}
查找通讯录的数据函数的实现
cpp
//查找通讯录数据
void FindContact(contact* con)
{
char name[NAME_MAX];
printf("请输入要查找联系人的信息!\n");
scanf("%s", name);
contact* pcur = con;
while (pcur)
{
if (strcmp(pcur->data.name, name) == 0)
{
printf("姓名 性别 年龄 电话 地址\n");
printf("%s %s %d %s %s\n", pcur->data.name, pcur->data.sex,
pcur->data.age, pcur->data.tel,
pcur->data.addr);
return;
}
pcur = pcur->next;
}
printf("没有此联系人!\n");
}
删除通讯录函数的实现
我们首先要找到你需要删除的联系人地址,所以我们要封装另一个函数。并且删除函数单链表里面有现成函数,我们可以直接调用。
cpp
//查找联系人数据的指针
contact* Findaddr(contact** con,char*name)
{
contact* pcur = *con;
while (pcur)
{
if (strcmp(pcur->data.name, name) == 0)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//删除通讯录数据
void DelContact(contact** con)
{
assert(con && &con);
char name[NAME_MAX];
printf("请输入你要删除的联系人姓名!\n");
scanf("%s", name);
contact*find=Findaddr(con, name);
if (find == NULL)
{
printf("没有此联系人!\n");
}
else
{
SLTErase(con, find);
printf("删除成功!\n");
}
}
修改通讯录数据函数的实现
cpp
//修改通讯录数据
void ModifyContact(contact** con)
{
//修改之前联系人要存在
char name[NAME_MAX];
printf("请输入要修改的联系人姓名!\n");
scanf("%s", name);
contact* find = Findaddr(con, name);
if (find == NULL)
{
printf("该联系人不存在!\n");
return;
}
else
{
PeoInfo infor;
printf("请输入新的姓名!\n");
scanf("%s", infor.name);
printf("请输入新的性别!\n");
scanf("%s", infor.sex);
printf("请输入新的年龄!\n");
scanf("%d", &(infor.age));
printf("请输入新的电话!\n");
scanf("%s", infor.tel);
printf("请输入新的地址!\n");
scanf("%s", infor.addr);
find->data = infor;
}
}
销毁通讯录数据函数的实现
cpp
//销毁通讯录数据
void DestroyContact(contact** con)
{
assert(con && *con);
SListDestroy(con);
}
通讯录菜单的封装
最终我们将所以函数进行一个封装汇总。
cpp
void menu()
{
printf("********通讯录*******\n");
printf("请选择你的操作:\n");
printf("1.添加联系人 2.删除联系人\n");
printf("3.展示联系人 4.查找联系人\n");
printf("5.修改联系人 0.退出通讯录\n");
}
int main()
{
contact* con;
InitContact(&con);//初始化
int input;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:AddContact(&con);
break;
case 2:DelContact(&con);
break;
case 3:ShowContact(con);
break;
case 4:FindContact(con);
break;
case 5:ModifyContact(&con);
break;
case 0:printf("退出成功!\n");
break;
default:printf("选项错误,请重新选择!\n");
break;
}
} while (input);
DestroyContact(&con);
return 0;
}
总代码汇总
SList.c文件
cpp
#include"SList.h"
//打印
void SLTprint(SLTNode* phead)
{
SLTNode* pur = phead;
while (pur)
{
printf("%d->", pur->data);
pur = pur->next;
}
printf("NULL\n");
}
//申请节点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//申请节点
if (newnode == NULL)
{
perror("malloc fail!");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);//尾插前先申请空间然后才能插入进去
//分为空链表和非空链表
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//找尾节点
SLTNode* ptail = *pphead;
while (ptail->next)//如果这里是空链表的话,会报错,不能对空指针进行解引用
{
ptail = ptail->next;
}
//找到了尾节点
ptail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;//不管是空链表还是非空链表都可以插入,不像尾插要分情况
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
//链表不能为空
assert(pphead && *pphead);
//判断链表节点有多个还是一个,如果没有分两种情况,程序出问题(结果出问题,但没报错)
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
//前提是链表不为空
assert(pphead && *pphead);
//不管链表是只有一个还是多个,都行通
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
//SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
//{
// SLTNode* pur = phead;
// while (pur)
// {
// if (pur->data == x)
// {
// return pur;
// }
// pur = pur->next;
// }
// 遍历了一遍,没有找到
// return NULL;
//}
//在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//不能为空链表
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//如果在头节点之前插入,将找不到pos,所以分两种情况
if (*pphead == pos)
{
SLTPushFront(pphead, x);
}//利用头插函数就行
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//将 prev newnode pos 链接起来
prev->next = newnode;
newnode->next = pos;
}
}
//在指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
//不能为空链表
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
//将 pos newnode pos->next 链接起来
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
//链表不能为空以及pos不能为空
assert(pphead && *pphead);
assert(pos);
//两种情况一种pos为头节点,一种不为头节点
if (*pphead == pos)
{
//头删
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
//将prev pos pos->next 中的pos删除
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
//pos之后不能为空,要不然怎么删除
assert(pos && pos->next);
SLTNode* del = pos->next;
//pos del del->next 删除pos
pos->next = del->next;
free(del);
del = NULL;
}
//链表的销毁
void SListDestroy(SLTNode** pphead)
{
//空链表就不需要销毁
assert(pphead && *pphead);
SLTNode* pur = *pphead;
while (pur)
{
SLTNode* next = pur->next;
free(pur);
pur = next;
}
*pphead = NULL;
}
SList.h文件
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"Contact.h"
//定义节点
typedef PeoInfo SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//链表的打印
void SLTprint(SLTNode* phead);
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//链表的销毁
void SListDestroy(SLTNode** pphead);
Contact.h文件
cpp
#pragma once
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 11
#define ADDR_MAX 100
//前置声明
typedef struct SListNode contact;
//用户数据
typedef struct PersonInfo
{
char name[NAME_MAX];//姓名
char sex[SEX_MAX];//性别
int age;//年龄
char tel[TEL_MAX];//电话
char addr[ADDR_MAX];//地址
}PeoInfo;
//初始化通讯录
void InitContact(contact** con);
//添加通讯录数据
void AddContact(contact** con);
//删除通讯录数据
void DelContact(contact** con);
//展示通讯录数据
void ShowContact(contact* con);
//查找通讯录数据
void FindContact(contact* con);
//修改通讯录数据
void ModifyContact(contact** con);
//销毁通讯录数据
void DestroyContact(contact** con);
Contact.c文件
cpp
#include"SList.h"
//初始化通讯录
void InitContact(contact** con)
{
*con = NULL;
}
//添加通讯录数据
void AddContact(contact** con)
{
//尾插和头插两种方法
PeoInfo infor;
printf("请输入姓名!\n");
scanf("%s", infor.name);
printf("请输入性别!\n");
scanf("%s", infor.sex);
printf("请输入年龄!\n");
scanf("%d", &(infor.age));
printf("请输入电话!\n");
scanf("%s", infor.tel);
printf("请输入地址!\n");
scanf("%s", infor.addr);
//采用尾插插入数据,也可采用头插,单链表中有现成的函数
SLTPushBack(con, infor);
}
//展示通讯录数据
void ShowContact(contact* con)
{
printf("姓名 性别 年龄 电话 地址\n");
contact* pcur = con;
while (pcur)
{
printf("%s %s %d %s %s\n", pcur->data.name,pcur->data.sex,
pcur->data.age,pcur->data.tel,
pcur->data.addr);
pcur = pcur->next;
}
}
//查找通讯录数据
void FindContact(contact* con)
{
char name[NAME_MAX];
printf("请输入要查找联系人的信息!\n");
scanf("%s", name);
contact* pcur = con;
while (pcur)
{
if (strcmp(pcur->data.name, name) == 0)
{
printf("姓名 性别 年龄 电话 地址\n");
printf("%s %s %d %s %s\n", pcur->data.name, pcur->data.sex,
pcur->data.age, pcur->data.tel,
pcur->data.addr);
return;
}
pcur = pcur->next;
}
printf("没有此联系人!\n");
}
//查找联系人数据的指针
contact* Findaddr(contact** con,char*name)
{
contact* pcur = *con;
while (pcur)
{
if (strcmp(pcur->data.name, name) == 0)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//删除通讯录数据
void DelContact(contact** con)
{
assert(con && &con);
char name[NAME_MAX];
printf("请输入你要删除的联系人姓名!\n");
scanf("%s", name);
contact*find=Findaddr(con, name);
if (find == NULL)
{
printf("没有此联系人!\n");
}
else
{
SLTErase(con, find);
printf("删除成功!\n");
}
}
//修改通讯录数据
void ModifyContact(contact** con)
{
//修改之前联系人要存在
char name[NAME_MAX];
printf("请输入要修改的联系人姓名!\n");
scanf("%s", name);
contact* find = Findaddr(con, name);
if (find == NULL)
{
printf("该联系人不存在!\n");
return;
}
else
{
PeoInfo infor;
printf("请输入新的姓名!\n");
scanf("%s", infor.name);
printf("请输入新的性别!\n");
scanf("%s", infor.sex);
printf("请输入新的年龄!\n");
scanf("%d", &(infor.age));
printf("请输入新的电话!\n");
scanf("%s", infor.tel);
printf("请输入新的地址!\n");
scanf("%s", infor.addr);
find->data = infor;
}
}
//销毁通讯录数据
void DestroyContact(contact** con)
{
assert(con && *con);
SListDestroy(con);
}
test_1.c文件
cpp
#include"SList.h"
void menu()
{
printf("********通讯录*******\n");
printf("请选择你的操作:\n");
printf("1.添加联系人 2.删除联系人\n");
printf("3.展示联系人 4.查找联系人\n");
printf("5.修改联系人 0.退出通讯录\n");
}
int main()
{
contact* con;
InitContact(&con);//初始化
int input;
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:AddContact(&con);
break;
case 2:DelContact(&con);
break;
case 3:ShowContact(con);
break;
case 4:FindContact(con);
break;
case 5:ModifyContact(&con);
break;
case 0:printf("退出成功!\n");
break;
default:printf("选项错误,请重新选择!\n");
break;
}
} while (input);
DestroyContact(&con);
return 0;
}