前言
哈喽楼,大家好哇!!欢迎大家来到我的博客!
今天来讲讲另一种数据结构—链表,并使用链表这一数据结构再来实现一次通讯录的项目。
一、链表的概念及其分类
概念:
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
因为,链表的每个节点的内存地址并不是像数组那样紧密的连续存在的,而是在内存中东一块、西一块的。
所以链表是仅在逻辑结构上连续,而在物理结构上非连续的线性表。
而链表就类似于我们日常生活中的火车,当在淡季时,火车的车厢会较少;而当像中国过年时的春运那样的旺季时,车厢则会增加几节。像这样只需将一节车厢去除/加上,并不会影响其他车厢,正是得益于每节车厢都是独立存在的。而链表就相当于火车这样的结构,链表中的一个个节点则类似于一节节的车厢。
然后再让我们想象一下,由于每节车厢都独立存在,且每节车厢的门都处于上锁的状态,需要不同的钥匙才能打开各节车厢的门,若我们每次只能拿一把钥匙我们该如何从火车头走到火车尾?
最为简单的做法就是,我们只需要在每节车厢放置下节车厢的门的钥匙,这要不就可以畅通无阻的走到车尾。
而这样的结构就类似于单链表,链表节点中的next指针便是前往下一个节点的钥匙。
在设想一下,如果此时可以同时拿两把钥匙,但我们我们要从火车上某节车厢回到上节车厢,甚至是火车头时,有该怎么做呢?
显然,此时我们只需要再在每节车放置上节车厢的钥匙不就可以解决了!
而像这样的结构便类似于另一种链表—双向链表,他分别存在指向下一个节点的next指针与指向上一个节点的prev指针。
与顺序表不同的是,链表中的每一节点都是单独申请的内存空间,节点中除了数据还有指向其他节点的指针。
链表除了上述的单向链表和双向链表外,还分为是否存在头节点,是否循环。这里的头节点是指链表中的一个哨兵位的概念,这个哨兵位是一个用于引领链表的存放无意义数据的节点,至于这个节点的存在意义会在的双向链表中进行说明。
由此可见,链表总共可以分为8(2 x 2 x 2)种。
二、单链表
单链表中的节点主要由数据和指向链表下一个节点的指针两部分组成:
typedef int SLDataType;//定义单链表中的数据类型
typedef struct SListNode//定义单链表
{
SLDataType data;//数据
struct SListNode* next;//下一个节点的地址
}SLTN;
那么接下来就开始手撕不带头非循环的单向链表的代码吧!
SepList.h
首先是头文件的包含与链表节点类型的声明
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;//定义单链表中的数据类型
typedef struct SListNode//定义单链表
{
SLDataType data;//数据
struct SListNode* next;//下一个节点的地址
}SLTN;
然后是链表的增删查改的一些方法的声明,至于为什么再有写方法中要传递二级指针,这是由于我们在特定的情况下是需要改变链表的头节点的(形参是实参的一份临时拷贝),只有传二级指针才可以改变头节点:
//打印单链表
void SListPrint(SLTN* phead);
//尾插
void SListPushBack(SLTN** pphead, SLDataType x);
//头插
void SListPushFront(SLTN** pphead, SLDataType x);
//尾删
void SListPopBack(SLTN** pphead);//可能为空链表,需传二级指针
//头删
void SListPopFront(SLTN** pphead);
//查找指定节点
SLTN* SListFind(SLTN* phead, SLDataType x);
//在指定位置之前插入节点
void SListInset(SLTN** pphead, SLTN* pos, SLDataType x);
//在指定位置后插入节点
void SListInsetAfter(SLTN* pos, SLDataType x);
//删除指定位置节点
void SListErase(SLTN** pphead, SLTN* pos);
//删除指定位置后的节点
void SlistEraseAfetr(SLTN* pos);
//链表的销毁
void SListDestr(SLTN** pphead);
SepList.c
接下来,就是链表内的有关方法的实现。
首先是链表的头插与尾插操作,此处我们通过使用malloc函数为每个节点动态分配内存空间,并将创建好的节点与原链表相连接,可见尾插在插入第一个节点的时候需要改变链表的头节点,而头插则每次都需要改变链表的头节点,这便是传参是传二级指针的原因之一:
//申请链表新节点并输入数据
SLTN* SListBuyNode(SLDataType x)
{
SLTN* newnode = (SLTN*)malloc(sizeof(SLTN));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SListPushBack(SLTN** pphead, SLDataType x)
{
assert(pphead);
SLTN* newnode = SListBuyNode(x);//申请新节点并输入数据
if (*pphead == NULL)//空链表
{
*pphead = newnode;
}
else//非空链表
{
SLTN* ptail = *pphead;
while (ptail->next)//找到链表末尾
ptail = ptail->next;
ptail->next = newnode;//使原链表的末尾的next指向新节点
}
}
//头插
void SListPushFront(SLTN** pphead, SLDataType x)
{
assert(pphead);
SLTN* newnode = SListBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;//将链表的头部赋值位新节点
}
使用动画解释该过程,尾插:
头插:
然后便是对链表的尾删与头删操作,而链表节点数为一和为多是两种不同的情况:
//尾删
void SListPopBack(SLTN** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)//链表仅有一个节点
{
free(*pphead);
*pphead = NULL;
}
else//链表有多个节点
{
SLTN* ptail = *pphead;
SLTN* prev = *pphead;
while (ptail->next)
{
prev = ptail;//找到链表末尾的前一个节点
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
prev->next = NULL;//将前一个节点的next置为NULL
}
}
//头删
void SListPopFront(SLTN** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)//链表只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else//链表有多个节点
{
SLTN* next = (*pphead)->next;
free(*pphead);
*pphead = NULL;
*pphead = next;
/*SLTN* ptmp = *pphead;
*pphead = (*pphead)->next;
free(ptmp);
ptmp = NULL;*/
}
}
使用动画解释该过程,尾删:
头删:
然后就是在查找指定节点位置的位置,并在指定位置的前后插入节点,删除指定位置或其后的节点:
查找指定节点
SLTN* SListFind(SLTN* phead, SLDataType x)
{
assert(phead);
SLTN* pcur = phead;
while (pcur)
{
if (pcur->data == x)//找到
{
return pcur;
}
pcur = pcur->next;
}
return NULL;//未找到
}
//在指定位置之前插入节点
void SListInset(SLTN** pphead, SLTN* pos, SLDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTN* newnode = SListBuyNode(x);
if (pos == *pphead)//若指定位置为头节点
{
newnode->next = *pphead;
*pphead = newnode;
//SListPopFront(pphead);
}
else
{
SLTN* prev = *pphead;
while (prev->next != pos)//找到指定节点的前一个节点
prev = prev->next;
newnode->next = pos;
prev->next = newnode;
}
}
//在指定位置后插入节点
void SListInsetAfter(SLTN* pos, SLDataType x)
{
assert(pos);
SLTN* newnode = SListBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除指定位置节点
void SListErase(SLTN** pphead, SLTN* pos)
{
assert(pphead && *pphead);
assert(pos);
if (pos == *pphead)//指定位置为头节点
{
SListPopFront(pphead);
}
else
{
SLTN* prev = *pphead;
while (prev->next != pos)//找到指定节点的前一个节点
prev = prev->next;
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除指定位置后的节点
void SlistEraseAfetr(SLTN* pos)
{
assert(pos && pos->next);//指定位置可能为尾节点
SLTN* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
最后则是销毁链表,既然我们向内存动态申请了空间,那我们肯定还要归还:
//链表的销毁
void SListDestr(SLTN** pphead)
{
assert(pphead && *pphead);
SLTN* pcur = *pphead;
while (pcur)
{
*pphead = pcur->next;
free(pcur);
pcur = *pphead;
}
}
三、单链表实现通讯录
然后就是使用链表再实现一个通讯录的项目,此处与前面的使用顺序表【数据结构—顺序表(C语言实现)】实现的过程与逻辑大致相同,这里直接上代码:
当然SepList.h也需要进行一定修改:
#include <string.h>
#include <Windows.h>
#include "SListContact.h"
typedef PeoInfo SLDataType;
SListContact.h
#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 ContactLoad(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);
SepListContact.c
#include"SListNode.h"
#include "SListContact.h"
//读取通讯录历史数据
void ContactLoad(contact** con)
{
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL)
{
perror("fopen fail");
return;
}
PeoInfo info;
while (fread(&info, sizeof(PeoInfo), 1, pf))
SListPushBack(con, info);
printf("通讯录历史数据读取成功!\n");
}
//初始化通讯录
void InitContact(contact** con)
{
assert(con);
ContactLoad(con);
}
//添加通讯录数据
void AddContact(contact** con)
{
assert(con);
PeoInfo info;
printf("请输入添加的联系人姓名:>");
scanf("%s", info.name);
printf("请输入添加的联系人性别:>");
scanf("%s", info.sex);
printf("请输入添加的联系人年龄:>");
scanf("%d", &info.age);
printf("请输入添加的联系人电话:>");
scanf("%s", info.tel);
printf("请输入添加的联系人地址:>");
scanf("%s", info.addr);
SListPushBack(con, info);
printf("添加成功!\n");
}
//通过名字查找联系人
SLTN* FindByName(contact* con, char name[])
{
contact* pcur = con;
while (pcur)
{
if (0 == strcmp(pcur->data.name, name))
return pcur;
pcur = pcur->next;
}
return NULL;
}
//删除通讯录数据
void DelContact(contact** con)
{
assert(con);
char name[NAME_MAX];
printf("请输入要删除的联系人姓名:>");
scanf("%s", name);
contact* pos = FindByName(*con, name);
if (pos == NULL)
{
printf("您要删除的联系人不存在!\n");
return;
}
SListErase(con, pos);
printf("删除成功!\n");
}
//展示通讯录数据
void ShowContact(contact* con)
{
assert(con);
contact* ptail = con;
printf("姓名 性别 年龄 电话 地址\n");
while (ptail)
{
printf("%-8s%-8s%-8d%-16s%s\n", \
ptail->data.name, \
ptail->data.sex, \
ptail->data.age, \
ptail->data.tel, \
ptail->data.addr);
ptail = ptail->next;
}
}
//查找通讯录数据
void FindContact(contact* con)
{
assert(con);
char name[NAME_MAX];
printf("请输入要查找的联系人姓名:>");
scanf("%s", name);
contact* find = FindByName(con, name);
printf("姓名 性别 年龄 电话 地址\n");
printf("%-8s%-8s%-8d%-16s%s\n", \
find->data.name, \
find->data.sex, \
find->data.age, \
find->data.tel, \
find->data.addr);
}
//修改通讯录数据
void ModifyContact(contact** con)
{
assert(con);
char name[NAME_MAX];
printf("请输入要修改的联系人姓名:>");
scanf("%s", name);
contact* find = FindByName(*con, name);
if (find == NULL)
{
printf("您要修改的联系人不存在!\n");
return;
}
printf("请输入修改的联系人姓名:>");
scanf("%s", find->data.name);
printf("请输入修改的联系人性别:>");
scanf("%s", find->data.sex);
printf("请输入修改的联系人年龄:>");
scanf("%d", &find->data.age);
printf("请输入修改的联系人电话:>");
scanf("%s", find->data.tel);
printf("请输入修改的联系人地址:>");
scanf("%s", find->data.addr);
system("cls");
printf("修改成功!\n");
}
//保存通讯录数据
void ContactSave(contact* con)
{
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL)
{
perror("fopen fail");
return;
}
contact* ptail = con;
while (ptail)
{
fwrite(&ptail->data, sizeof(PeoInfo), 1, pf);
ptail = ptail->next;
}
printf("保存成功!\n");
}
//销毁通讯录数据
void DestroyContact(contact** con)
{
ContactSave(*con);
SListDestr(con);
}
main.c
#include "SListNode.h"
void menu()//打印通讯录菜单
{
printf(" ---------------通讯录--------------- \n");
printf("|####################################|\n");
printf("|#### 1.增加联系人 2.删除联系人 ####|\n");
printf("|#### 3.修改联系人 4.查找联系人 ####|\n");
printf("|#### 5.展示联系人 0.退出通讯录 ####|\n");
printf("|####################################|\n");
printf(" ------------------------------------ \n");
}
int main()
{
PeoInfo* con = NULL;
InitContact(&con);
int input;
do
{
menu();
printf("请输入您的选择:>");
scanf("%d", &input);
getchar();
system("cls");
switch (input)
{
case 1:
AddContact(&con);
Sleep(1000);
system("cls");
break;
case 2:
DelContact(&con);
Sleep(1000);
system("cls");
break;
case 3:
ModifyContact(&con);
Sleep(1000);
system("cls");
break;
case 4:
FindContact(con);
Sleep(1000);
break;
case 5:
ShowContact(con);
Sleep(1000);
break;
case 0:
printf("退出中....\n");
Sleep(500);
DestroyContact(&con);
break;
default:
printf("输入错误请重新输入!\n");
Sleep(500);
break;
}
} while (input);
return 0;
}
总结
最后,各位觉得博客有用的话,欢迎各位点赞。
如有错误,欢迎各位大佬指出。
而在下篇博客,将实现另一种形式的链表—双向链表。