目录
一、链表概念
链表是什么?链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
简单理解一下便是:链表可以近似的看作成火车,用节点一个一个串起来,需要操作时运用节点进行操作。
其特点如下:
链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成(malloc),每个节点包括两个部分:
一个是存储数据元素的数据域
另一个是存储下一个节点地址的指针域
链表可分为以下几类:
带头 | 不带头 |
单向 | 双向 |
循环 | 不循环 |
一共为8种组合(2*2*2),可用下图表示:
虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构: 单向不带头不循环链表(单链表) 和 双向带头循环链表(双链表)。
原因如下:
1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构。
2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了。
二、单链表的实现
我们在实现链表时其实现目的与顺序表类似,无外乎为:“增删查改”四大功能。
2.1 定义结点
typedef int SLTDataType;//使用目的:方便更改数据类型
typedef struct SListNode
{
struct SListNode* next;
SLTDataType date;
}SLTNode;
注意:在定义节点(写成此结点也无所谓)时不要写成:SLTNode* next;因为编译器把结构体读完时才能达成重写条件。
2.2 具体实现功能如下:
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);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
接下来会带领大家一个一个实现。
2.3 单链表打印
void SLTPrint(SLTNode* phead)
{
SLTNode* p = p;
while (p)
{
printf("%d-> ", p->date);
}
printf("NULL\n");
}
在对链表进行打印时,我们不能像打印顺序表那样那样用for循环进行遍历, 因为链表在内存中并不是像顺序表线性存放,而是靠节点进行联系。在此处我们是否需要assert断言吗?大家可以稍作思考。
答案是不需要,原因如下:若链表为空,不会进入for循环,直接打印NULL。
2.4单链表插入
单链表插入像顺序表一样分为:头插与尾插,接下来会一一实现。
2.4.1 尾插
Node* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
return -1;
}
node->date = x;
node->next = NULL;
return node;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)//此处可猜测一下为什么使用二级指针
{
assert(*pphead);
SLTNode* newnode = SLTBuyNode(x);//因创建新节点方法会被多次使用,于是便单独封装出来
if (*pphead == NULL)
{
*pphead = pphead;
}
else
{
SLTNode* p = *pphead;
while (p)
{
p = p->next;
}
p->next = newnode;
}
}
在进行尾插时要注意以下几点 :
首先,要使用二级指针来接收。在进行测试时发现使用一级指针无法改变实参,要改变实参只能传地址,一级指针传地址要用二级指针来接收。
其次,要考虑头节点为零的情况。在头节点为空时,新开辟的节点便是我们要插入的节点。
最后,为了使以后方便我们便创建了一个方法,需要使用使直接调用即可。
2.4.2头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(*pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头插的代码相较于尾插能简单一点,只需要将新开辟出的节点的指向改成头节点,将头节点改为新开辟的节点即可。
2.5 单链表删除
和插入一样,单链表的删除同样分为头删和尾删。
2.5.1 尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* p = *pphead;
SLTNode* n;
while (p->next)
{
n = p;
p = p->next;
}
free(p);
p = NULL;
n->next = NULL;
//法二
//SLTNode* p = *pphead;
//while(p->next->next)
//{
// p = p->next;
//}
//free(p->next);
//p->next = NULL;
}
}
写尾删注意如下:
1.考虑到只有一个头节点。即该头节点指向空,可直接对其释放。
2.若不为空,可使用两种方法:双指针或创造出的指针向后多指向一次。
3.别忘记释放和置空。
2.5.2 头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
以上便是头删实现无需考虑情况,因为该节点若为头节点那它所指向的即为NULL。只需定义一个指向下一位的节点即可。
2.6 单链表关于指定节点的增与删
2.6.1在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
SLTNode* p = *pphead;
if (pos == *pphead)
{
//调用头插
SLTPushFront(pphead, x);
}
else
{
while (p->next != pos)
{
p = p->next;
}
newnode->next = pos;
p->next = newnode;
}
}
在经行插入数据时,我们要创造头节点以及分析是否要分类讨论。
2.6.2 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这种插入代码比较简单创造出节点后即可快速完成。
2.6.3删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if (pos == *pphead)
{
//调用头删
SLTPopFront(pphead);
}
else
{
SLTNode* p = *pphead;
while (p->next != pos)
{
p = p->next;
}
p->next = pos->next;
free(p->next);
p->next = NULL;
}
}
代码与之前插入类似。
2.6.4 删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
SLTNode* p = pos->next;
pos->next = p->next;
free(p);
p = NULL;
}
2.7 销毁链表
void SListDesTroy(SLTNode** pphead)
{
SLTNode* p = *pphead;
SLTNode* n = *pphead;
while (p)
{
n = p->next;
free(p);
p = n;
}
*pphead = NULL;
}
好了,以上便是我们单链表的学习了,请大家休息片刻,我们随后开始双链表的学习。
三、双链表的实现
接下来,我们来学习双链表。双链表与单链表有相同也有不同,大家可在学习中自行体会。(在该链表使用一级指针而不用二级指针后面会说明)
3.1 双链表初始化
LTNode* LTInit(LTDataType x)
{
LTNode* phead = LTBuyNode(x);
return phead;
}
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
return -1;
}
node->date = x;
node->next = node;
node->prve = node;
return node;
}
这里的LTBuyNode与上文单链表功能类似都是为了便于后面的使用。单、双链表的不同已在上文呈现,这里不过多介绍。
3.2 功能实现
LTNode* LTInit(LTDataType x);//初始化
void LTDestroy(LTNode* phead);//销毁
void LTPrint(LTNode* phead);//打印
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopFront(LTNode* phead);//头删
void LTInsert(LTNode* pos, LTDataType x);//在pos位置之后插入数据
void LTErase(LTNode* pos);//删除pos位置数据
LTNode* LTFind(LTNode* phead, LTDataType x);//查找数据
3.3 头插与尾插
3.3.1 尾插
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prve = phead->prve;
phead->prve->next = newnode;//不可颠倒
phead->prve = newnode;
}
注意:后俩行代码位置不可交换。
3.3.2 头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prve = phead;
phead->next->prve = newnode;
phead->next = newnode;
}
注意点还是一样即:后俩行代码位置不可交换。
3.4 头删与尾删
3.4.1 头删
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->prve;
del->next->prve = phead;
free(del);
del = NULL;
}
注意:断言时要加入后半句,否则会删掉哨兵位。
3.4.2 尾删
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->prve;
phead->prve = del->prve;
del->prve->next = phead;
free(del);
del = NULL;
}
注意点还是一样,断言要加入后半句。
3.5 指定位置插入、修改数据
3.5.1 指定位置后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prve = pos;
pos->next->prve = newnode;
pos->next = newnode;
}
注意:最后两行代码顺序不可修改。
3.5.2 删除pos位置数据
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prve = pos->prve;
pos->prve->next = pos->next;
free(pos);
pos = NULL;
}
3.6 查找数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* p = phead->next;
while (p != phead)
{
if (p->date == x)
{
return p;
}
p = p->next;
}
return NULL;
}
3.6 打印链表
void LTPrint(LTNode* phead)
{
LTNode* p = phead->next;
while (p!=phead)
{
printf("%d->", p->date);
p = p->next;
}
printf("NULL\n");
}
打印方法与单链表类似。
3.7 销毁链表
void LTDestroy(LTNode* phead)
{
LTNode* p = phead->next;
LTNode* n = phead->next;
while (p != phead)
{
n = p->next;
free(p);
p = n;
}
//此时p指向phead,phead还没销毁
free(phead);
phead = NULL;
}
//存在问题传入一级指针后实参不会修改为NULL,需要手动修改
3.8 总结
为什么使用一级指针而不用二级指针?
1.传入数据之前,链表要进行初始化,为了保护哨兵位,使用一级指针即可,若使用二级指针不会改变哨兵位也可以使用,不过还是推荐使用一级指针。
2. 保持接口一致性,减少记忆成本。此链表接口过多,如若一会一级指针,一会二级指针会造成记忆成本。
四、顺序表和双向链表的优缺点分析
不同点 | 顺序表 | 链表(单链表) |
存储空间上 | 物理上⼀定连续 | 逻辑上连续,但物理上不⼀定连续 |
随机访问 | ⽀持O(1) | 不⽀持:O(N) |
任意位置插⼊或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩 容 | 没有容量的概念 |
应用场景 | 元素⾼效存储+频繁访问 | 任意位置插⼊和删除频繁 |
五、代码展示
5.1 单链表
5.1.1slist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"slist.h"
void SLTPrint(SLTNode* phead)
{
SLTNode* p = p;
while (p)
{
printf("%d-> ", p->date);
}
printf("NULL\n");
}
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
return -1;
}
node->date = x;
node->next = NULL;
return node;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)//此处可猜测一下为什么使用二级指针
{
assert(*pphead);
SLTNode* newnode = SLTBuyNode(x);//因创建新节点方法会被多次使用,于是便单独封装出来
if (*pphead == NULL)
{
*pphead = pphead;
}
else
{
SLTNode* p = *pphead;
while (p)
{
p = p->next;
}
p->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* p = *pphead;
SLTNode* n;
while (p->next)
{
n = p;
p = p->next;
}
free(p);
p = NULL;
n->next = NULL;
//法二
//SLTNode* p = *pphead;
//while(p->next->next)
//{
// p = p->next;
//}
//free(p->next);
//p->next = NULL;
}
}
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && *pphead);
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
SLTNode* p = *pphead;
if (pos == *pphead)
{
//调用头插
SLTPushFront(pphead, x);
}
else
{
while (p->next != pos)
{
p = p->next;
}
newnode->next = pos;
p->next = newnode;
}
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;//位置不可颠倒
pos->next = newnode;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && *pphead);
assert(pos);
if (pos == *pphead)
{
//调用头删
SLTPopFront(pphead);
}
else
{
SLTNode* p = *pphead;
while (p->next != pos)
{
p = p->next;
}
p->next = pos->next;
free(p->next);
p->next = NULL;
}
}
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
SLTNode* p = pos->next;
pos->next = p->next;
free(p);
p = NULL;
}
void SListDesTroy(SLTNode** pphead)
{
SLTNode* p = *pphead;
SLTNode* n = *pphead;
while (p)
{
n = p->next;
free(p);
p = n;
}
*pphead = NULL;
}
5.1.2 slist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;//使用目的:方便更改数据类型
typedef struct SListNode
{
struct SListNode* next;
SLTDataType date;
}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);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);
5.2 双链表
5.2.1 list.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
LTNode* LTInit(LTDataType x)
{
LTNode* phead = LTBuyNode(x);
return phead;
}
void LTDestroy(LTNode* phead)
{
LTNode* p = phead->next;
LTNode* n = phead->next;
while (p != phead)
{
n = p->next;
free(p);
p = n;
}
//此时p指向phead,phead还没销毁
free(phead);
phead = NULL;
}
void LTPrint(LTNode* phead)
{
LTNode* p = phead->next;
while (p!=phead)
{
printf("%d->", p->date);
p = p->next;
}
printf("NULL\n");
}
LTNode* LTBuyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
return -1;
}
node->date = x;
node->next = node;
node->prve = node;
return node;
}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prve = phead->prve;
phead->prve->next = newnode;//不可颠倒
phead->prve = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->prve;
phead->prve = del->prve;
del->prve->next = phead;
free(del);
del = NULL;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prve = phead;
phead->next->prve = newnode;
phead->next = newnode;
}
void LTPopFront(LTNode* phead)
{
assert(phead && phead->next != phead);
LTNode* del = phead->next;
phead->next = del->prve;
del->next->prve = phead;
free(del);
del = NULL;
}
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prve = pos;
pos->next->prve = newnode;
pos->next = newnode;
}
void LTErase(LTNode* pos)
{
assert(pos);
pos->next->prve = pos->prve;
pos->prve->next = pos->next;
free(pos);
pos = NULL;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* p = phead->next;
while (p != phead)
{
if (p->date == x)
{
return p;
}
p = p->next;
}
return NULL;
}
5.2.2 list.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prve;
LTDataType date;
}LTNode;
LTNode* LTInit(LTDataType x);//初始化
void LTDestroy(LTNode* phead);//销毁
void LTPrint(LTNode* phead);//打印
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopFront(LTNode* phead);//头删
void LTInsert(LTNode* pos, LTDataType x);//在pos位置之后插入数据
void LTErase(LTNode* pos);//删除pos位置数据
LTNode* LTFind(LTNode* phead, LTDataType x);//查找数据
链表经典练习题:CSDN
完!