1.链表的概念与结构
在上回的顺序表中有提到,顺序表在物理上和逻辑上是连续的,而链表则是在逻辑上连续,在物理上并不连续。打个比方就是,建筑A前面有一个指路牌写着建筑B,向着指路牌的方向走可以找到建筑B,建筑B前也可以有一个指路牌指向下一个地点,也可以没有,但是建筑A不能直接走到下一个地点。
建筑和指路牌共同组成链表的结点,指路牌则是一个指针,指向下一个位置的地址。相比于顺序表,链表灵活了不少,结点是随机分布的,但是在访问的时候不能任意访问,因为地址总是存在上一个结点中。
2.单链表的实现
链表的基础结构如下,
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType x;
//指向下一个节点的指针
struct SListNode* next;
}SLTNode;
接下来咱将实现单链表的几个接口
2.1创建节点
SLTNode* SLTCreate(SLTDataType x)
{
//开辟一块空间
SLTNode* newnode = (SLTNode * )malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("create newnode fail!");
return NULL;
}
//单个节点的下一个节点指向空
newnode->next = NULL;
//赋值
newnode->x = x;
return newnode;
}
2.2插入数据
在进行尾插时,顺序表在有容量的时候直接插入,没有容量的时候就开辟空间;链表则是直接创建一个节点,不用判断容量。
//从列表尾部插入数据
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
//创建一个新的结点
SLTNode* newnode = SLTCreate(x);
//如果传入的结点是空,则新节点变成头
if (*pphead == NULL)
{
(*pphead) = newnode;
}
else
{
SLTNode* ptail = *pphead;
//遍历访问找到最后一个节点,最后ptail指向最后一个节点
while (ptail->next)
{
ptail = ptail->next;
}
//把ptail和新节点连接
ptail->next = newnode;
}
}
在进行头插时,顺序表需要挪动数据,而链表只需要将新节点与头结点连接,新节点置为头结点。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode*newnode = SLTCreate(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
//新节点指向头结点
newnode->next = *pphead;
//新节点进化成新节点
*pphead = newnode;
}
}
2.3删除数据
尾删就看起来有点复杂了,毕竟顺序表只需要在size--.
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(*pphead && pphead);
SLTNode* ptail = *pphead;
SLTNode* prev = *pphead;
//遍历找尾,保留尾节点前一项
while (ptail->next != NULL)
{
prev = ptail;
ptail = ptail->next;
}
//释放尾节点
free(ptail);
ptail = NULL;
//前一项置为空
prev->next = NULL;
}
与头插类似,改变头结点的指向
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead && pphead);
SLTNode* next = *pphead;
//保存下一个节点
next = next->next;
//释放头结点
free(*pphead);
//新头结点登基
*pphead = next;
next = NULL;
}
2.4查找+插入
很经典的遍历查找.
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);
//经典遍历查找
while (phead != NULL)
{
if (phead->x == x)
{
return phead;
}
phead = phead->next;
}
return phead;
}
两钟种基于查找的插入,
//在指定数据位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos , SLTDataType x)
{
assert(*pphead && pphead);
assert(pos);
//如果指定位置是头,则变成头插,因为遍历不可能找到头的前一项
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
//否则遍历查找到pos的前一项
else
{
//新节点
SLTNode* newnode = SLTCreate(x);
SLTNode* pcur = *pphead;
//遍历找到pos前一项
while (pcur->next != pos)
{
pcur = pcur->next;
}
//改变节点的指向
newnode->next = pos;
pcur->next = newnode;
}
}
两者相比较,前者需要遍历找到pos前一项,改变节点时不用考虑顺序;后者如果不遍历找到pos后一项,那么只需要注意节点的连接顺序,因为一旦pos节点先和新节点连接,pos将找不到下一个节点。
//在指定位置之后插入数据
void SLTInsertback(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTCreate(x);
//先把新节点和pos的下一个节点连接,以防丢失
newnode->next = pos->next;
//pos节点和新节点连接
pos->next = newnode;
}
2.5插入+删除
指定位置删除,和指定位置前插入,有个点很类似,就是当指定位置为头的时候直接调用之前的删除/插入接口,而不为头的时候需要找到前一项,毕竟头不能在长个头罢。
//删除指定位置的结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(*pphead && pphead);
assert(pos);
//如果指定位置在头,则头删
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
SLTNode* pcur = *pphead;
//找到指定位置的前一项
while (pcur->next != pos)
{
pcur = pcur->next;
}
//将前一项和pos后一项连接,即使pos是最后一项
pcur->next = pos->next;
//释放pos
free(pos);
pos = NULL;
}
}
很好操作的后删~
//删除指定位置之后的结点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos->next);
//保存删除的结点
SLTNode* after = pos->next;
//pos与after的下一项连接
pos->next = after->next;
//释放需要删除的结点
free(after);
after = NULL;
}
2.6销毁
void SListDesTroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur != NULL)
{
pcur = pcur->next;
free(*pphead);
*pphead = pcur;
}
free(pcur);
}
//这么删
// data 1 2 3
// 1 *pphead pcur
// 2 free *pphead pcur
// 3 free *pphead pcur
3.基于单链表再实现通讯录
顺序表都来了一手通讯录,单链表也得来一手!不得不说,在这一块,遍历用的相当频繁,那么int 遍历=0;
基本操作来一手,
#define NAME_MAX 20
#define TEL_MAX 15
#define GENDER_MAX 5
#define ADD_MAX 100
#define AGE_MAX 4
//contact相当于创建节点的结构体类型
typedef SLTNode contact;
typedef struct PersonInfo
{
char name[NAME_MAX];
char tel[TEL_MAX];
char age[AGE_MAX];
char gender[GENDER_MAX];
char address[ADD_MAX];
}peoinfo;
简单修改一下类型,
typedef struct PersonInfo SLTDataType;
typedef struct SListNode
{
SLTDataType* x;
struct SListNode* next;
}SLTNode;
接下来就是接口的实现.
3.1初始化通讯录
void InitContact(contact** con)
{
//初始化传入的数据
*con = (contact*)malloc(sizeof(contact));
if (con == NULL)
{
perror("malloc fail");
return;
}
(*con)->next = NULL;
(*con)->x = (peoinfo*)malloc(sizeof(peoinfo));
if ((*con)->x == NULL)
{
perror("malloc fail");
free((*con)->x);
(*con)->x = NULL;
return;
}
// 这个name是一个char数组
//((*con)->x)->name = 0;
//这样就可以对这个char数组完成初始化赋值了
strcpy(((*con)->x)->name, "");
strcpy(((*con)->x)->address, "");
strcpy(((*con)->x)->tel, "");
strcpy(((*con)->x)->gender, "");
strcpy(((*con)->x)->age, "");
}
3.2添加通讯录数据
遍历x1
void AddContact(contact** con)
{
//创建一个节点
contact* info;
InitContact(&info);
printf("请输入联系人的姓名\n");
scanf("%s", info->x->name);
printf("请输入联系人的电话号码\n");
scanf("%s", info->x->tel);
printf("请输入联系人的年龄\n");
scanf("%s", info->x->age);
printf("请输入联系人的性别\n");
scanf("%s", info->x->gender);
printf("请输入联系人的住址\n");
scanf("%s", info->x->address);
//如果通讯录为空
if ((*con)->x->name[0] == '\0')
{
*con = info;
}
else
{
// 尾插
contact* tail = *con;
while (tail->next != NULL)
tail = tail->next;
tail->next = info;
}
}
3.3展示通讯录
遍历x2
void ShowContact(contact* con)
{
assert(con);
contact* ptail = con;
printf("%s %s %s %s %s\n", "姓名", "电话", "年龄", "性别", "地址");
while (ptail != NULL)
{
printf("%3s %4s %5s %4s %3s",
ptail->x->name,
ptail->x->tel,
ptail->x->age,
ptail->x->gender,
ptail->x->address);
printf("\n");
ptail = ptail->next;
}
}
3.4删除联系人数据
遍历x3
//通过名字删除联系人
void DelContact(contact** con)
{
assert(*con && con);
printf("请输入想要删除的联系人名字\n");
char name[NAME_MAX];
scanf("%s", name);
//指针一前一后
contact* ptail = *con;
contact* prev = ptail;
while (ptail != NULL)
{
//要删除的如果是通讯录第一个数据
if (strcmp((*con)->x->name, name) == 0)
{
(*con) = (*con)->next;
free(prev);
prev = NULL;
ptail = *con;
prev = ptail;
printf("删除成功!\n");
return;
}
//如果不是通讯录第一个数据
if (strcmp(ptail->x->name, name) == 0)
{
prev->next = ptail->next;
free(ptail);
ptail = prev->next;
printf("删除成功!\n");
return;
}
else
{
prev = ptail;
ptail = ptail->next;
}
}
printf("联系人信息不存在\n");
}
3.5查找联系人
遍历x4
void FindContact(contact* con)
{
assert(con);
printf("请输入想要寻找的联系人姓名\n");
char name[NAME_MAX];
scanf("%s", name);
contact* ptail = con;
//遍历查找
while (ptail != NULL)
{
if (strcmp(ptail->x->name, name) == 0)
{
printf("%s %s %s %s %s\n", "姓名", "电话", "年龄", "性别", "地址");
printf("%3s %4s %5s %4s %3s\n",
ptail->x->name,
ptail->x->tel,
ptail->x->age,
ptail->x->gender,
ptail->x->address);
return ;
}
ptail = ptail->next;
}
printf("联系人信息不存在\n");
}
3.6修改联系人信息
遍历x5
void ModifyContact(contact** con)
{
assert(*con && con);
printf("请输入想要修改的联系人姓名\n");
char name[NAME_MAX];
scanf("%s", name);
contact* ptail = *con;
//通过ptail遍历循环找到再修改,不改变*con
while (ptail != NULL)
{
if (strcmp(ptail->x->name, name) == 0)
{
printf("请输入要修改的联系人的姓名\n");
scanf("%s", ptail->x->name);
printf("请输入要修改的联系人的电话号码\n");
scanf("%s", ptail->x->tel);
printf("请输入要修改的联系人的年龄\n");
scanf("%s", ptail->x->age);
printf("请输入要修改的联系人的性别\n");
scanf("%s", ptail->x->gender);
printf("请输入要修改的联系人的住址\n");
scanf("%s", ptail->x->address);
return;
}
ptail = ptail->next;
}
printf("联系人信息不存在\n");
}
3.7销毁通讯录
遍历x7
void DestroyContact(contact** con)
{
assert(*con && con);
contact* prev = *con;
contact* ptail = *con;
while (ptail != NULL)
{
ptail = ptail->next;
//释放通讯录中的结构体指针
free(prev->x);
free(prev);
prev = ptail;
}
}
接口摆在这里,至于动起来,还请参见之前的文章。
4.链表的分类
链表其实一共有八种,也就是1、2、3中任选一个总共有八种情况,最常用的两种为单链表和双向链表。

单向就是链表只能通过上一个结点找到下一个节点,不能找到上一个结点;双向则既能找到下一个节点也能拐弯回头找到上一个结点。
循环就是看链表尾节点指向的是否为链表上的任一结点,如果指向空则为不循环.
在上面咱实现的是单链表,即不带头单向不循环链表。可咱在单链表部分有提到过头结点,为什么说单链表无头呢?
实际上,单链表中的头不是真正意义上的头,只是为了称呼上的方便,而真正的头中不存放数据,不会因为节点的变动而消失,一直在那里。同时这个头还有一个别称——哨兵位。

双向链表默认是指双向循环带头链表,第一个节点的前一项指向头,最后一个节点的后一项也指向头,如果不带头则指向空。

5.双向链表的实现
既然双向链表也是常用的链表之一,那么咱也浅实现一下来对比单链表。
结构上看,双向链表比单链表要多一个指向上一个位置的结点。
typedef int LTDataType;
typedef struct ListNode
{
LTDataType x;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
5.1初始化头结点
相比于单链表,双向链表需要先初始化一个头结点,对于双向链表来说,空链表指只有一个头结点,而单链表没有节点。
//创建一个节点
LTNode* CreateNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("create is failed");
return;
}
newnode->x = x;
}
//初始化链表
LTNode* LTInit()
{
//创建一个头结点
LTNode* phead = CreateNode(0);
phead->next = phead->prev = phead;
return phead;
}
5.2插入数据
在进行尾插时,最好控制的就是新节点的指向,所以放在第一步进行。之后就是改变尾节点d3的下一项和头结点的上一项,这里需要讲究顺序,需要先改变d3指向new(因为可以通过head找到尾节点),然后再让head的前一项指向new

//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode*newnode = CreateNode(x);
//先处理新节点的前后指向
newnode->next = phead;
newnode->prev = phead->prev;
//原来的尾节点指向新节点
phead->prev->next = newnode;
//头结点的上一项指向新节点
phead->prev = newnode;
}
头插时,需要注意是插入在头结点后面,第一个节点之前。操作同上,先改变新节点的前后指向,再将d1指向new,head指向new(当然如果保存了第一个节点的话就不需要考虑顺序了)

void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = CreateNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}
5.3判断链表是否为空
双向链表为空时,只有一个头结点,也就是头结点的前后指针都指向空。对于单链表,判空似乎没什么意义,因为当链表为空时,assert就会报错。
bool LTEmpty(LTNode* phead)
{
assert(phead);
if (phead->next == phead->prev)
return true;
else
return false;
}
5.4删除数据
尾删时,先保存要删除的结点,然后将头节点和新的尾节点连接,最后释放节点。

void LTPopBack(LTNode* phead)
{
//没有初始化链表和空链表时报错
assert(phead&&phead->next != phead);
//保留要删除的结点
LTNode* del = phead->prev;
//头的前指针指向删除节点的前一项,也就是新的尾节点
phead->prev = del->prev;
//新尾节点指向头结点
del->prev->next = phead;
free(del);
del=NULL;
}
头删的原理和尾删的原理相同,
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->prev = phead;
free(del);
}
5.5查找数据+插入/删除
在查找这一块,双向链表也是遍历查找。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
//创建一个指针指向第一个节点
LTNode* pcur = phead->next;
//遍历查找
while (pcur != phead)
{
if (pcur->x == x)
{
return pcur;
}
pcur = pcur->next;
}
return pcur;
}
在指定位置之后插入数据时,因为这是循环链表,所以可以把pos当做哨兵位,于是就相当于在头插。
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = CreateNode(x);
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
删除指定位置的数据时,pos已知,只需要通过pos改变d1和d3的指向,d1、d3就能连接。

void LTErase(LTNode* pos)
{
assert(pos);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
5.6销毁链表
销毁链表时从尾节点开始删除,就相当于尾删,尾删之后再保留新的尾节点进行删除,遍历删除,直到只剩下哨兵位。
void LTDestroy(LTNode* phead)
{
assert(phead);
//保留尾节点
LTNode* del = phead->prev;
//遍历删除
while (del != phead)
{
//相当于w尾删
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = phead->prev;
}
free(phead);
phead=NULL;
}
5.7双向链表和单链表的比较
双向链表比单链复杂,一半用于单独储存数据,而且在实现时,遍历的次数少,时间复杂度低,使用代码的时候有很多优势。
单向链表结构简单,一般不会用于单独储存数据,实际中更多是作为其他数据结构的子结构,而且这种结构在笔试面试中比较常见。
1999





