前言
本篇博客还是与数据结构有关,这次讲的是单链表和双链表。为了方便讲解还是和顺序表一样采取通讯录的载体,分为两大类讲。仔细来说的话这里的单向链表指的是单向不带头不循环链表,而双向链表指的是双向带头循环链表。这两个正好是链表两个极端,一个结构简单但是用起来不太方便,一个结构复杂但是非常好用,真是合适的一对。那么接下来进入正题吧。
一. 单链表实现通讯录
1. 单链表的结构
单链表的结构就是数据内容和下一个节点的指针,存放数据内容的空间由“malloc”函数申请。增加通讯录需要用到的宏定义和主要使用的库函数,这里的通讯录宏定义和上一篇《用到顺序表的通讯录》中的和宏定义相同,所以不再描述。最后的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#pragma once
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 11
#define ADDR_MAX 100
//用户数据
typedef struct PersonInfo
{
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
}PeoInfo;
//结构体声明
typedef struct SListNode
{
PeoInfo date;
struct SListNode* next;
}contact;
2. 单链表中的主要函数
(1)初始化通讯录
//初始化通讯录
void InitContact(contact** con);
将结构体的指针初始化为空,代码如下:
//初始化通讯录
void InitContact(contact** con)
{
assert(con);
*con = NULL;
}
这里用到二级指针是因为要修改到的实参是一级指针,当然也可以通过返回值完成初始化,但是为了和后面的函数接口相对应,所以同样使用了二级指针。一致化的话当其他程序员调用的时候才不容易犯错。
(2)销毁通讯录数据
//销毁通讯录数据
void DestroyContact(contact** con);
销毁数据,也就需要遍历链表一次,然后依次释放掉空间。所以申请了两个指针,一个指向下一个节点,一个用来释放掉当前节点。
void DestroyContact(contact** con)
{
assert(con);
contact* cur = *con;
while(cur)
{
cur = cur->next;
free(*con);
*con = cur;
}
}
我在这里只重新申请了一个指针指向前的一个内容“cur”,通过“con”来判断是否需要释放内存,但是申请两个更好,因为用“*con”很容易出错。
(3)添加通讯录的联系人
//添加通讯录数据
void AddContact(contact** con);
如果通讯录中没有数据,第一次添加联系人的话就需要修改原来的一级指针,所以这里传入的是二级指针。然后分为两种:1)传入的是空链表 ,2)节点链表来进行增加联系人。增加联系人需要购入新的节点,所以分装一个申请空间的函数,最后返回节点指针。
contact* BuyNode()
{
contact* newnode = (contact*)malloc(sizeof(contact));
if(newnode == NULL)
{
perror("BuyNode() fail");
exit(1);
}
printf("请输入联系人姓名:");
scanf("%s", newnode->date.name);
printf("请输入联系人性别:");
scanf("%s", newnode->date.sex);
printf("请输入联系人年龄:");
scanf("%d", &(newnode->date.age));
printf("请输入联系人电话:");
scanf("%s", newnode->date.tel);
printf("请输入联系人地址:");
scanf("%s", newnode->date.addr);
newnode->next = NULL;
return newnode;
}
//添加通讯录数据
void AddContact(contact** con)
{
assert(con);
if(*con == NULL)
{
*con = BuyNode();
}
else
{
contact* cur = *con;
while(cur->next)
{
cur = cur->next;
}
cur->next = BuyNode();
}
printf("添加联系人成功\n");
}
和之前一样如果申请内存失败就报错并直接结束程序。申请玩节点之后就遍历链表连接到最后的节点之后。
(4)展示通讯录数据
//展示通讯录数据
void ShowContact(contact* con);
先提出展示通讯录的数据,是因为有了这个函数方便检查链表的增删是否成功。整体来说该函数就是遍历链表打印节点数据就可以。代码如下:
//展示通讯录数据
void ShowContact(contact* con)
{
if(con == NULL)
{
printf("通讯录为空\n");
return;
}
contact* cur = con;
while(cur)
{
printf("%-5s|%-5s|%-5s|%-13s|%s\n", "姓名", "性别", "年龄", "电话", "地址");
printf("%-5s|%-5s|%-5d|%-13s|%s\n", cur->date.name
, cur->date.sex
, cur->date.age
, cur->date.tel
, cur->date.addr);
cur = cur->next;
}
}
注意打印的格式,然后不要忘了把节点向后推一位。
(5)查找通讯录中的联系人
//查找通讯录数据
contact* FindContact(contact* con);
因为查找不需要修改节点,所以传入一级指针足够。进入函数之后首先输入需要查找人的姓名,然后再遍历链表查找节点,找到了就返回该节点指针,到最后没找到节点就返回空指针。
//查找通讯录数据
contact* FindContact(contact* con)
{
assert(con);
contact* cur = con;
char name[NAME_MAX];
printf("请输入需要查找的联系人姓名:");
scanf("%s", name);
while(cur)
{
if(strcmp(name, cur->date.name) == 0)
{
printf("%-5s|%-5s|%-5s|%-13s|%s\n", "姓名", "性别", "年龄", "电话", "地址");
printf("%-5s|%-5s|%-5d|%-13s|%s\n", cur->date.name
, cur->date.sex
, cur->date.age
, cur->date.tel
, cur->date.addr);
return cur;
}
cur = cur->next;
}
printf("未找到该联系人\n");
return NULL;
}
这里查找到了就顺便打印了联系人信息。
(6)删除通讯录中的联系人
//删除通讯录数据
void DelContact(contact** con);
这个函数可以用到前面查找节点的函数,来实现删除一个指定联系人的节点的效果。删除的时后首先要判断链表是否为空,为空就没办法删除,如果判断有数据才能删除。然后调用查找函数查找联系人,如果返回的是第一个节点则需要单独删除并改变头结点指针位置,反之就遍历链表到这个节点的前一个节点,先改变链表的链接然后释放该节点。实例化代码如下:
//删除通讯录数据
void DelContact(contact** con)
{
assert(con && *con);
contact* cur = *con;
contact* tmp = FindContact(cur);
if(cur == tmp)
{
tmp = tmp->next;
free(cur);
*con = tmp;
return;
}
if(tmp != NULL)
{
while(cur->next != tmp)
{
cur = cur->next;
}
cur->next = tmp->next;
}
printf("删除联系人成功\n");
}
删除联系人成功就打印一下。
(7)修改联系人信息
//修改通讯录数据
void ModifyContact(contact* con);
这个就更简单了,先调用查找函数,然后直接在该指针位置修改内容即可。代码如下:
//修改通讯录数据
void ModifyContact(contact* con)
{
contact* cur = FindContact(con);
if(cur != NULL)
{
printf("请输入修改后联系人的姓名:");
scanf("%s", cur->date.name);
printf("请输入修改后联系人的性别:");
scanf("%s", cur->date.sex);
printf("请输入修改后联系人的年龄:");
scanf("%d", &(cur->date.age));
printf("请输入修改后联系人的电话:");
scanf("%s", cur->date.tel);
printf("请输入修改后联系人的地址:");
scanf("%s", cur->date.addr);
}
}
到这里,主要函数就全部声明完毕了。
3. 通讯录主体函数以及代码运行结果
主体函数也搭建一个自由操作的框架,供使用者选择功能,并使用它。搭建的方式和《用到顺序表的通讯录》中的主体结构相似,内容如下:
typedef enum Select
{
exit_,
addcontact,
delcontact,
showcontact,
findcontact,
modifycontact
}Select;
void menu()
{
printf("*******************************************\n");
printf("***** 0.exit_ 1.addcontact *****\n");
printf("***** 2.delcontact 3.showcontact *****\n");
printf("***** 4.findcontact 5.modifycontact *****\n");
printf("*******************************************\n");
printf("请输入您需要的功能:");
}
void TestContact()
{
contact* con;
InitContact(&con);
Select select = exit_;
do
{
menu();
scanf("%d", &select);
switch(select)
{
case exit_:
printf("退出通讯录\n");
break;
case addcontact:
AddContact(&con);
break;
case delcontact:
DelContact(&con);
break;
case showcontact:
ShowContact(con);
break;
case findcontact:
FindContact(con);
break;
case modifycontact:
ModifyContact(con);
break;
default:
printf("输入错误请重新输入\n");
break;
}
} while (select);
DestroyContact(&con);
}
int main()
{
TestContact();
return 0;
}
通过枚举常量确定函数功能。
(1)添加联系人
为了方便测试,联系人数据简化为11和22.
(2)展示通讯录
选择功能3,展示通讯录。
(3)查找联系人
选择功能4,查找联系人22。
查找联系人33.
(4)修改联系人信息
选择功能5,将22改成33。
(5)删除联系人
选择功能2,删除11并打印通讯录。
(0)退出通讯录
输入0,退出通讯录。
二. 双链表
双链表实现通讯录也和前面单链表一样定义就好,这里用简单的方法一个int值简单的介绍一下双链表的结构和使用方法。
1. 双链表的结构
和单链表不同双链表会有两个指针分别链接前一个节点和后一个节点,然后会多一个不存放数据的空节点作为头结点,这样可以更快的找到节点的头和尾。这里简单介绍结构,数据部分由整形代替。包括头文件的代码如下:
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType date;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
2. 双链表中主要的函数
(1)初始化链表和增加链表节点
//初始化链表
LTNode* LTInit();
这里将初始化练表和增加链表节点的函数放在一起是因为在设计的时候将函数会重置申请的空间中的内容,然后返回节点回去,所以头结点接直接申请接收。如果是增加节点赋值就直接在插入节点的函数中实现就行。代码如下:
//初始化链表以及申请节点
LTNode* LTInit()
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if(newnode == NULL)
{
perror("LTInit() malloc fail");
exit(1);
}
newnode->date = 0;
newnode->next = newnode;
newnode->prev = newnode;
return newnode;
}
申请的节点都指向自己,并且初始化。
(2)销毁链表
//销毁链表
LTNode* LTDestroy(LTNode* phead);
因为是循环链表而且带头结点,所以从头结点之后的节点开始分别遍历释放。因为是双向链表,所以可以通过后一个节点释放前一个节点就能达到效果。最后释放头结点就行,代码如下:
LTNode* LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while(cur != phead)
{
cur = cur->next;
free(cur->prev);
}
free(phead);
return NULL;
}
(3)打印链表
//打印链表
void LTPrint(LTNode* phead)
打印链表就遍历链表,打印信息直到回到头结点就行,代码如下:
//打印链表
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("phead<->");
while(cur != phead)
{
printf("%d<->", cur->date);
cur = cur->next;
}
printf("phead\n");
}
(4)判断链表是否为空
//判断链表是否为空
bool LTEmpty(LTNode* phead)
建立这样的函数,在删除节点的时候就能直接判断有无节点可以删除。传入头结点然后用头结点找下一个节点的地址,如果节点指向自己,那么就表示链表中只有头结点。代码如下:
//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
if(phead->next == phead)
{
return true;
}
else
{
return false;
}
}
如果是空就返回真,非空就返回假,但是在之后调用函数的时候要取反。因为非空才能删除。
(5)查找节点
//查找x的节点
LTNode *LTFind(LTNode* phead,LTDataType x);
通过输入整形x,来遍历链表查找节点,最后返回节点位置。代码如下:
//查找x的节点
LTNode *LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* cur = phead->next;
while(cur != phead)
{
if(cur->date == x)
{
//printf("找到了\n");
return cur;
}
cur = cur->next;
}
printf("没找到\n");
return NULL;
}
为了方便检查,所以还会打印查找结果。
(6)在pos位置之前(后)插入数据
//在pos位置之后插入数据
void LTInsertNext(LTNode* pos, LTDataType x);
//在pos位置之前插入数据
void LTInsertPrev(LTNode* pos, LTDataType x);
传入pos指针,然后申请节点并赋值,先把节点接到链表上,然后再将该节点的前后节点指向它。
如果是要插入到pos之前,代码如下:
//在pos位置之前插入数据
void LTInsertPrev(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTInit();
newnode->date = x;
newnode->next = pos;
newnode->prev = pos->prev;
newnode->prev->next = newnode;
pos->prev = newnode;
}
如果是要插入到pos之后,代码如下:
//在pos位置之后插入数据
void LTInsertNext(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTInit();
newnode->date = x;
newnode->next = pos->next;
newnode->prev = pos;
newnode->next->prev = newnode;
pos->next = newnode;
}
需要注意的是,节点链接的顺序不能搞反了,不然会链接不上前面的节点。另外需要再节点内为数据部分赋值。
(7)删除pos位置
//删除pos
LTNode* LTErase(LTNode* pos);
先链接pos左右的位置,然后释放pos位置空间即可,代码如下:
//删除pos
LTNode* LTErase(LTNode* pos)
{
assert(pos);
LTNode* cur = pos->next;
pos->prev->next = cur;
cur->prev = pos->prev;
free(pos);
return NULL;
}
其实可以不用申请“cur”指针的,这里我习惯性的多写了一点。返回值其实也可以不用。
(8)尾插、头插
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
直接分别调用之前写的pos前插入和pos后插入。尾插就是在头结点之前插入节点,头插就是在头结点之后插入。代码如下:
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTInsertPrev(phead, x);
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTInsertNext(phead, x);
}
(9)头删,尾删
//头删
void LTPopFront(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
先判断链表是否为空,然后找到后节点和前节点删除即可,代码如下:
//头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTErase(phead->next);
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTErase(phead->prev);
}
3. 测试双向链表代码
测试代码如下:
void TestLineList()
{
LTNode* ptail = LTInit();
LTPushBack(ptail, 1);
LTPushBack(ptail, 2);
LTPushBack(ptail, 3);
LTPushBack(ptail, 4);
LTPrint(ptail);
LTPushFront(ptail, 5);
LTPushFront(ptail, 6);
LTPushFront(ptail, 7);
LTPrint(ptail);
LTPopBack(ptail);
LTPrint(ptail);
LTPopFront(ptail);
LTPrint(ptail);
LTNode* find = LTFind(ptail, 1);
if(find != ptail && find != NULL)
{
LTInsertNext(find, 8);
LTPrint(ptail);
LTInsertPrev(find, 9);
LTPrint(ptail);
LTErase(find);
LTPrint(ptail);
}
ptail = LTDestroy(ptail);
}
首先尾部插入1、2、3、4节点打印链表,然后分别头插5、6、7并打印链表。分别进行一次尾删和头删,最后查找节点1。找到了且不是头结点就在节点前后分别插入9、8,并删除节点1。结束后销毁链表,执行结果如下:
结果无误。
作者结语
到这里单向链表和双向链表的实现就完成了,写的有点着急。中途有的部分又不知道怎么讲清楚,所以整体感觉比较空。配套的图是手扣的,比较差,鼠标画图确实不好使。不过实现的代码都可以直接套用,没有问题。相信对初学者还是有所帮助了,大佬勿嘘。