线性表——链表
在前面的顺序表中我们提到了顺序表的优缺点,其中最大的不足是插入和删除元素需要移动大量的数据,这就对时间有一定的消耗。为了解决这个不足,本章将介绍一个成员——链表,接下来就开始本章的介绍。
1. 链式存储结构
链式存储结构 特点: 用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。
在链式结构中,除了要存储数据外,还需要存储下一个数据元素的地址,这样才能找到下一个元素,不然就是一盘散沙。我们把存储数据元素信息的域称为 数据域 ,存储后继元素地址的域称为 指针域 。这两个域在一起组成链表的一个元素,称之为 结点 。
头指针是指向头结点的指针(若没有头结点就直接指向第一个结点),只用来存储地址。
头结点是单链表 第一个结点前附设的一个结点(有时候也称为“ 哨兵 ”),目的是方便对链表的操作,它一样有数据域和指针域,而数据域可以不存储任何信息,当然也可以存储线性表长度或其他信息。
单链表像火车车厢一样,车厢(结点)之间相互连接,有头就要有尾,最后一个结点不用指向其他元素,在指针域中放入NULL 即可,也相当于指向 NULL,以此收尾。
头结点通常不是必须的,具体根据实际需求使用,而头指针是必须的,并且不为空。还一种情况,当头指针为空时,说明整个链表为空。
图解:
2. 单链表的实现(无头结点)
2.1 结点的定义
typedef int LinkDataType;
typedef struct LinkNode //单链表
{
int val; //数据域的信息
struct LinkNode* next; //指针域的信息
}LNode;
2.2 单链表的查找
链表查找不需要对修改数据,所以直接传参,就不用再取地址(使用二级指针)。
LNode* LinkFind(LNode* phead, LinkDataType x) //查找
{
/* 注意返回值类型为LNode* */
LNode* cur = phead;
while (cur)
{
if (cur->val == x)
{
return cur; //找到了返回结点地址
}
else
{
cur = cur->next;
}
}
return NULL; //找不到返回空
}
2.3 单链表的插入
单链表的插入需要创建新的结点,我们可以对此操作进行封装。
2.3.1 新结点的创建
LNode* CreateNode(LinkDataType x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode)); //为新结点开辟空间
if (newnode == NULL) //检查是否开辟成功
{
perror("malloc fail!");
return -1;
}
newnode->val = x; //数据域存入x
newnode->next = NULL; //指针域暂时指向NULL
}
2.3.2 头部插入(头插)
这里我们创建的是一个 LNode*
类型的指针:LNode* plist = NULL;所以我们就要使用二级指针来对一级指针进行操作。若外部直接创建的是LNode
类型的变量,就说明创建的是一个带有头结点的链表。
就像在外部定义了 int a = 10 ;由于形参是实参的一份临时拷贝,形参的改变并不会影响实参,所以不会直接传递 int 类型,而会传递 int* ;因此,遇见 int* a = NULL;就传递 int**
void LinkPushFront(LNode** pphead, LinkDataType x) //头插
{
assert(pphead); //检查pphead是否为空
LNode* newnode = CreateNode(x); //创建新结点
newnode->next = *pphead; //新结点的指针域存放原链表第一个结点的地址
*pphead = newnode; //*pphead再指向第一个结点
}
图解:
2.3.3 尾部插入(尾插)
尾插就需要判断链表是否为空
void LinkPushBack(LNode** pphead, LinkDataType x) //尾插
{
assert(pphead);
LNode* newnode = CreateNode(x); //新结点
if (*pphead == NULL)
{
*pphead = newnode; //如果链表为空就直接指向新结点
}
else
{
LNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next; //找尾(最终会在最后一个结点跳出循环)
}
tail->next = newnode; //原链表最后一个结点指向新结点
}
}
图解:
2.3.4 任意位置(前)插入
这部分再在任意位置之 前 插入,需要结合链表查找的返回值进行。另外,关于pos,有些地方使用的int
类型,这里根据需要自行变换。
void LinkInsert(LNode** pphead, LNode* pos, LinkDataType x) //任意位置前插入
{
//检查1:
assert(pphead);
assert(*pphead); //检查链表是否为空
assert(pos); //pos是链表的一个有效结点(pos不为空)
//检查2:
assert((!pos && !(*pphead)) || (pos && *pphead)); //pos 和 *pphead 保持一致(都为空或都不为空)
/* 检查1,检查2 根据需要任选其一即可 */
if (*pphead == pos) //pos为第一个结点时
{
LinkPushFront(pphead, x); //头插
}
else
{
LNode* prev = *pphead;
while (prev->next != pos) //寻找 pos 的位置
{
prev = prev->next;
}
//插入:
LNode* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
图解:
2.3.5 任意位置(后)插入
任意位置后插入就不用考虑头插的情况,也不用将链表传过来。
void LinkInsertAfter(LNode* pos, LinkDataType x) //任意位置后插入
{
assert(pos);
LNode* newnode = CreateNode(x);
newnode->next = pos->next; //新结点指向原链表指向的位置
pos->next = newnode; //原链表指向新结点的位置
}
图解:
2.4 单链表的删除
2.4.1 头部删除(头删)
void LinkPopFront(LNode** pphead) //头删
{
assert(pphead);
assert(*pphead);
//方法1:
LNode* tail = (*pphead)->next; //新建指针指向第二个结点
free(*pphead); //释放第一个结点空间
*pphead = tail; //头指针指向原链表第二个结点
//方法2:
//LNode* tail = *pphead; //新建指针指向第一个结点
//*pphead = (*pphead)->next; //头指针指向第二个结点
//free(tail); //释放第一个结点空间
}
2.4.2 尾部删除(尾删)
void LinkPopBack(LNode** pphead) //尾删
{
assert(pphead);
assert(*pphead);
//方法1:
//1.一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.多个结点
else
{
LNode* tail = *pphead;
LNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
/* 跳出循环 prev 停在倒数第二个结点,tail 停在最后一个结点 */
free(tail); //删除结点
tail = NULL;
prev->next = NULL;
}
//方法2:
// /* 先删除,再判断链表的情况 */
//LNode* tail = *pphead;
//LNode* prev = NULL;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//if (prev == NULL)
//{
// *pphead = NULL;
//}
//else
//{
// prev->next = NULL;
//}
//方法3:
//(尽量使用以上两种)
//LNode* tail = *pphead;
//while (tail->next->next != NULL)
//{
// tail = tail->next;
//}
/* tail在倒数第二个结点跳出循环 */
//free(tail->next);
//tail->next = NULL;
}
2.4.3 任意位置(前)删除
需结合查找功能使用。
void LinkErase(LNode** pphead, LNode* pos) //任意位置前删除
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
LinkPopFront(pphead); //头删
}
else
{
LNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next; // pos 前的结点直接指向 pos 后的结点
free(pos);
pos = NULL;
}
}
2.4.4 任意位置(后)删除
需结合查找功能使用。
void LinkEraseAfter(LNode* pos) //任意位置后删除
{
assert(pos);
assert(pos->next); //检查pos是否为最后一个结点
//方法1:
LNode* tail = pos->next->next;
free(pos->next);
pos->next = tail;
//方法2:
//LNode* tail = pos->next;
//pos->next = pos->next->next;
//free(tail);
//tail = NULL;
}
2.5 单链表的打印
void LinkPrint(LNode* phead)
{
LNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
2.6 单链表的销毁
void LinkDestory(LNode** pphead) //销毁
{
assert(pphead);
LNode* cur = *pphead;
while (cur)
{
LNode* Next = cur->next;
free(cur);
cur = Next;
}
*pphead = NULL;
}
3. 功能综合
以下综合会比较长,可以另外通过接口实现会更加清楚,可自行更改。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LinkDataType;
typedef struct LinkNode //单链表
{
int val; //数据域的信息
struct LinkNode* next; //指针域的信息
}LNode;
LNode* LinkFind(LNode* phead, LinkDataType x) //查找
{
/* 注意返回值类型为LNode* */
LNode* cur = phead;
while (cur)
{
if (cur->val == x)
{
return cur; //找到了返回结点地址
}
else
{
cur = cur->next;
}
}
return NULL; //找不到返回空
}
LNode* CreateNode(LinkDataType x)
{
LNode* newnode = (LNode*)malloc(sizeof(LNode)); //为新结点开辟空间
if (newnode == NULL) //检查是否开辟成功
{
perror("malloc fail!");
return -1;
}
newnode->val = x; //数据域存入x
newnode->next = NULL; //指针域暂时指向NULL
}
void LinkPushFront(LNode** pphead, LinkDataType x) //头插
{
assert(pphead); //检查pphead是否为空
LNode* newnode = CreateNode(x); //创建新结点
newnode->next = *pphead; //新结点的指针域存放原链表第一个结点的地址
*pphead = newnode; //*pphead再指向第一个结点
}
void LinkPushBack(LNode** pphead, LinkDataType x) //尾插
{
assert(pphead);
LNode* newnode = CreateNode(x); //新结点
if (*pphead == NULL)
{
*pphead = newnode; //如果链表为空就直接指向新结点
}
else
{
LNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next; //找尾(最终会在最后一个结点跳出循环)
}
tail->next = newnode; //原链表最后一个结点指向新结点
}
}
void LinkInsert(LNode** pphead, LNode* pos, LinkDataType x) //任意位置前插入
{
//检查1:
assert(pphead);
assert(*pphead); //检查链表是否为空
assert(pos); //pos是链表的一个有效结点(pos不为空)
//检查2:
assert((!pos && !(*pphead)) || (pos && *pphead)); //pos 和 *pphead 保持一致(都为空或都不为空)
/* 检查1,检查2 根据需要任选其一即可 */
if (*pphead == pos) //pos为第一个结点时
{
LinkPushFront(pphead, x); //头插
}
else
{
LNode* prev = *pphead;
while (prev->next != pos) //寻找 pos 的位置
{
prev = prev->next;
}
//插入:
LNode* newnode = CreateNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
void LinkInsertAfter(LNode* pos, LinkDataType x) //任意位置后插入
{
assert(pos);
LNode* newnode = CreateNode(x);
newnode->next = pos->next; //新结点指向原链表指向的位置
pos->next = newnode; //原链表指向新结点的位置
}
void LinkPopFront(LNode** pphead) //头删
{
assert(pphead);
assert(*pphead);
//方法1:
LNode* tail = (*pphead)->next; //新建指针指向第二个结点
free(*pphead); //释放第一个结点空间
*pphead = tail; //头指针指向原链表第二个结点
//方法2:
//LNode* tail = *pphead; //新建指针指向第一个结点
//*pphead = (*pphead)->next; //头指针指向第二个结点
//free(tail); //释放第一个结点空间
}
void LinkPopBack(LNode** pphead) //尾删
{
assert(pphead);
assert(*pphead);
//方法1:
//1.一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//2.多个结点
else
{
LNode* tail = *pphead;
LNode* prev = NULL;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
/* 跳出循环 prev 停在倒数第二个结点,tail 停在最后一个结点 */
free(tail); //删除结点
tail = NULL;
prev->next = NULL;
}
//方法2:
// /* 先删除,再判断链表的情况 */
//LNode* tail = *pphead;
//LNode* prev = NULL;
//while (tail->next != NULL)
//{
// prev = tail;
// tail = tail->next;
//}
//free(tail);
//tail = NULL;
//if (prev == NULL)
//{
// *pphead = NULL;
//}
//else
//{
// prev->next = NULL;
//}
//方法3:
//(尽量使用以上两种)
//LNode* tail = *pphead;
//while (tail->next->next != NULL)
//{
// tail = tail->next;
//}
/* tail在倒数第二个结点跳出循环 */
//free(tail->next);
//tail->next = NULL;
}
void LinkErase(LNode** pphead, LNode* pos) //任意位置前删除
{
assert(pphead);
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
LinkPopFront(pphead); //头删
}
else
{
LNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next; // pos 前的结点直接指向 pos 后的结点
free(pos);
pos = NULL;
}
}
void LinkEraseAfter(LNode* pos) //任意位置后删除
{
assert(pos);
assert(pos->next); //检查pos是否为最后一个结点
//方法1:
LNode* tail = pos->next->next;
free(pos->next);
pos->next = tail;
//方法2:
//LNode* tail = pos->next;
//pos->next = pos->next->next;
//free(tail);
//tail = NULL;
}
void LinkPrint(LNode* phead)
{
LNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
void LinkDestory(LNode** pphead) //销毁
{
assert(pphead);
LNode* cur = *pphead;
while (cur)
{
LNode* Next = cur->next;
free(cur);
cur = Next;
}
*pphead = NULL;
}
int main()
{
LNode* plist = NULL;
LinkPushBack(&plist, 1);
LinkPushBack(&plist, 2);
LinkPushBack(&plist, 3);
LinkPushBack(&plist, 4);
LinkPrint(plist);
LNode* pos = LinkFind(plist, 1);
LinkErase(&plist, pos);
LinkPrint(plist);
pos = LinkFind(plist, 3);
LinkErase(&plist, pos);
LinkPrint(plist);
return 0;
}
4. 链表和顺序表的选择
顺序表的优点主要是访问方便,而链表主要是在空间的扩容上和对数据的增删上占优势。
- 当线性表需要大量对数据的增加和删除,同时对查找要求不高时使用链表;反之使用顺序表。
- 当我们对元素的个数及变化范围不确定时,尽量使用单链表。