目录
一、链表的概念
- 链表是一种基本的数据结构,用于存储一系列元素。与数组不同,链表的元素在内存中不是连续存放的,而是由一系列节点组成,每个节点包含数据和一个或多个指向下一个节点的指针(或引用)。
- 链表的物理地址并不相同,就像一节一节的火车一样,通过指针进行连接。每个节点独立存在,并不会影响其他的节点。
(每个节点的修改,重新连接并不会影响整体的的链表结构)
1.链表的特点
- 动态大小:链表的大小可以动态变化,可以根据需要增加或者删除节点,而无需事先定义容量。而顺序表的本质是数组,数组的空间大小是相对固定的,如果对数组进行头插或者尾插等操作时可能会需要扩容,而扩容就会造成空间上的浪费。
-
非连续存储:链表的节点在内存中不必连续存储,每个节点通过指针链接到下一个节点,因此可以在内存中任意位置分配。
-
节点结构:链表由节点组成,每个节点包含数据部分和一个或多个指针部分(指向下一个节点或前一个节点)。
-
插入和删除效率:在链表中,插入和删除操作通常比数组更高效,因为只需调整指针,而不需要移动其他元素。
-
访问效率:链表的随机访问效率较低。要访问某个特定位置的节点,必须从头节点开始逐个遍历,时间复杂度为 O(n)。
-
多样性:链表有多种变体,如单链表、双向链表、循环链表等,适用于不同的应用场景。
二、链表详解
1.链表的组成
链表的组成:节点。
- 链表是由一个个节点组成的
- 每个节点由含有两个部分:存储数据的部分和存储下一个节点地址的部分
2.链表的分类
-
单向和双向
-
带头和不带头
-
循环和非循环
(这三种类别的链表可以相互组合,例如双向带头链表、双向循环链表、带头双向循环链表.........)
三、单向链表的实现
1.单向链表的结构
- 结构体的创建
typedef int SLDataType; //这样做能便于数据类型的修改
typedef struct SListNode
{
SLDataType data;//存放数据
struct SListNode* next;//指向下一个指针
}SLTNode;
2.创建新节点
- 因为链表的很多操作都需要传建一个新链表(也就是开辟一个新空间),所以我们可以把他写成一个函数,方便其他函数的使用
SLTNode* SLTBuyNode(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//新建一个链表
newnode->data = x;
newnode->next = NULL;
return newnode;
}
3.单向链表的尾插、头插
- 单向链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//如果链表为空
if (*pphead==NULL)
{
*pphead = newnode;
return;
}
//链表不为空,新建一个链表找尾节点
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail=ptail->next;
}
ptail->next = newnode;
}
- 单向链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead);
//链表为空或者不为空,情况都是一样
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
注意:在对链表进行增删等操作时,我们需要用到二级指针。因为考虑到传值或者传址操作,普通的数据在函数调用时需要用一级指针来进行传址;同样地,因为链表本身是一级指针,所以要二级指针来进行传址操作
4.单向链表的尾删、头删
- 单向链表的尾删
void SLTPopback(SLTNode** pphead)
{
assert(pphead);
//链表不能为空
assert(*pphead);
//如果链表只有一个节点
if (*pphead == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//如果链表有多个节点
SLTNode* ptail = *pphead;
SLTNode* prve = NULL;
//找到NULL的前一个节点
while (ptail->next)
{
prve = ptail;
ptail = ptail->next;
}
prve->next = NULL;
free(ptail);
ptail = NULL;
}
- 单向链表的头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
//链表不能为空
assert(*pphead);
//让第二个节点成为头节点,释放第一个节点
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
5.单向链表的查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
6.单向链表在指定位置前 / 后的插入
- 在指定位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead);
assert(pos);
//链表不能为空
assert(*pphead);
SLTNode* newnode = SLTBuyNode(x);
//如果在头节点前插入
if (*pphead == pos)
{
//用头插
SLTPushFront(pphead,x);
return;
}
//不在头节点
SLTNode* pcur = *pphead;
//到pos的前一个
while (pcur->next != pos)
{
pcur = pcur->next;
}
newnode->next = pos;
pcur ->next = newnode;
}
- 在指定位置后插入
void SLTInsertAfter(SLTNode*pos, SLDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
7.单向链表删除指定节点 / 指点节点后面的节点
- 删除指定节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//节点不能为空
assert(*pphead);
//如果删除的是头节点
if (pos == *pphead)
{
//执行头删操作;
SLTPopFront(pphead);
return;
}
//不是头节点
SLTNode* prve = *pphead;
while (prve->next!=pos)
{
prve = prve->next;
}
prve->next = pos->next;
free(pos);
pos = NULL;
}
- 删除指定节点后面的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
//pos->next不能为空
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
8.单向链表的销毁
void SListDesTroy(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
}
9.整体代码展示
- SeList.h
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLTNode;
//链表打印
void SLTPrint(SLTNode *phead);
//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x);
//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
//链表的尾删
void SLTPopback(SLTNode** pphead);
//链表的头删
void SLTPopFront(SLTNode** pphead);
//链表的查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x);
//在指定位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
//在指定位置后插入
void SLTInsertAfter(SLTNode*pos, SLDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//删除链表
void SListDesTroy(SLTNode** pphead);
- SeList.c
#include"Selist.h"
void SLTPrint(SLTNode * phead)
{
SLTNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->data);
pcur=pcur->next;
}
printf("NULL\n");
}
SLTNode* SLTBuyNode(SLDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
//如果链表为空
if (*pphead==NULL)
{
*pphead = newnode;
return;
}
//链表不为空,新建一个链表找尾节点
SLTNode* ptail = *pphead;
while (ptail->next)
{
ptail=ptail->next;
}
ptail->next = newnode;
}
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
assert(pphead);
//链表为空或者不为空,情况都是一样
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopback(SLTNode** pphead)
{
assert(pphead);
//链表不能为空
assert(*pphead);
//如果链表只有一个节点
if (*pphead == NULL)
{
free(*pphead);
*pphead = NULL;
return;
}
//如果链表有多个节点
SLTNode* ptail = *pphead;
SLTNode* prve = NULL;
//找到NULL的前一个节点
while (ptail->next)
{
prve = ptail;
ptail = ptail->next;
}
prve->next = NULL;
free(ptail);
ptail = NULL;
}
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
//链表不能为空
assert(*pphead);
//让第二个节点成为头节点,释放第一个节点
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
assert(pphead);
assert(pos);
//链表不能为空
assert(*pphead);
SLTNode* newnode = SLTBuyNode(x);
//如果在头节点前插入
if (*pphead == pos)
{
//用头插
SLTPushFront(pphead,x);
return;
}
//不在头节点
SLTNode* pcur = *pphead;
//到pos的前一个
while (pcur->next != pos)
{
pcur = pcur->next;
}
newnode->next = pos;
pcur ->next = newnode;
}
void SLTInsertAfter(SLTNode*pos, SLDataType x)
{
assert(pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//节点不能为空
assert(*pphead);
//如果删除的是头节点
if (pos == *pphead)
{
//执行头删操作;
SLTPopFront(pphead);
return;
}
//不是头节点
SLTNode* prve = *pphead;
while (prve->next!=pos)
{
prve = prve->next;
}
prve->next = pos->next;
free(pos);
pos = NULL;
}
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
//pos->next不能为空
assert(pos->next);
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
void SListDesTroy(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
}
- text.c
#include"SeList.h"
//void text1()
//{
// SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
// node1->data = 1;
// SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
// node2->data = 2;
// SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
// node3->data = 3;
//
// node1->next = node2;
// node2->next = node3;
// node3->next = NULL;
// SLTNode* plist = node1;
// SLTPrint(plist);
//}
void text2()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPrint(plist);
SLTPushFront(&plist, 4);
SLTPrint(plist);
SLTPopback(&plist);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLTNode* Find=SLTFind(plist, 2);
if (Find)
{
printf("找到了!\n");
}
else
{
printf("未找到!\n");
}
SLTInsert(&plist, Find, 2);
SLTPrint(plist);
SLTInsertAfter(Find, 3);
SLTPrint(plist);
SLTErase(&plist, Find);
SLTPrint(plist);
SLTNode* Find1 = SLTFind(plist, 1);
SLTEraseAfter(Find1);
SLTPrint(plist);
}
int main()
{
//text1();
text2();
return 0;
}
四、带头双向循环链表的实现
1.带头双向循环链表的结构
注意:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。 带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”。
“哨兵位”存在的意义:
- 遍历循环链表避免死循环。
- 减少代码的复杂性
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;//指向上一个节点
struct ListNode* next;//指向下一个节点
}LTNode;
2.带头双向循环链表创建新节点
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
3.带头双向循环链表的初始化及销毁
- 带头双向循环链表的初始化,也就是先创造一个哨兵位
LTNode* LTInit(LTNode* phead)
{
phead = LTBuyNode(-1);
phead->next = phead->prev = phead;//前一个节点和后一个节点都指向自己
}
- 带头双向循环链表的销毁
void LTDestory(LTNode* phead)
{
LTNode* del = phead->next;
while (del != phead)
{
LTNode* next = del->next;
free(del);
del = next;
}
free(phead);
phead = NULL;
}
在这里我们可以发现,为什么单向链表需要用到二级指针,而带头双向循环链表就不需要呢?
- 单向链表是没有哨兵位的,所以需要借助外部创建好的结构体指针变量来访问,因为是一级指针,所以要用到二级指针来进行对单向指针的增删操作。
- 而带头双向循环链表中已经有一个哨兵位了,所以我们不需要用外来的结构体指针变量来访问,直接通过哨兵位进行后续的操作。
4带头双向循环链表的尾插、头插
- 带头双向循环链表的尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
- 带头双向循环链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode->next;
phead->next = newnode;
}
5.带头双向循环链表的尾删、头删
- 带头双向循环链表的尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
//双向链表不能只有一个哨兵位
assert(phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
- 带头双向循环链表的头删
void LTPopFront(LTNode* phead)
{
assert(phead);
//双向链表不能只有一个哨兵位
assert(phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
}
6.带头双向循环链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
//phead是哨兵位,为-1。要从哨兵位的下一个开始找
LTNode* find = phead->next;
while (find != phead)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
7.带头双向循环链表在指定位置后插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
8.带头双向循环链表在指定位置后删除
void LTErase(LTNode* pos)
{
LTNode* del = pos;
del->next->prev = del->prev;
del->next = del->next->next;
}
9.整体代码展示
- List.h
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
LTDataType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
//双向链表的初始化和销毁
LTNode* LTInit(LTNode* phead);
void LTDestory(LTNode* pphead);
//双向链表的打印
void LTPrint(LTNode* phead);
//双向链表的尾插
void LTPushBack(LTNode* phead,LTDataType x);
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x);
//双向链表的尾删
void LTPopBack(LTNode* phead);
//双向链表的头删
void LTPopFront(LTNode* phead);
//查找双向链表
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos后插入
void LTInsert(LTNode* pos, LTDataType x);
//在pos后删除
void LTErase(LTNode* pos);
- List.c
#include"list.h"
LTNode* LTBuyNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->next = newnode->prev = newnode;
return newnode;
}
LTNode* LTInit(LTNode* phead)
{
phead = LTBuyNode(-1);
phead->next = phead->prev = phead;
}
void LTDestory(LTNode* phead)
{
LTNode* del = phead->next;
while (del != phead)
{
LTNode* next = del->next;
free(del);
del = next;
}
free(phead);
phead = NULL;
}
void LTPrint(LTNode* phead)
{
//phead是哨兵位,不需要打印
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
void LTPushBack(LTNode* phead,LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead;
newnode->prev = phead->prev;
phead->prev->next = newnode;
phead->prev = newnode;
}
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = LTBuyNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode->next;
phead->next = newnode;
}
void LTPopBack(LTNode* phead)
{
assert(phead);
//双向链表不能只有一个哨兵位
assert(phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
void LTPopFront(LTNode* phead)
{
assert(phead);
//双向链表不能只有一个哨兵位
assert(phead->next != phead);
LTNode* del = phead->next;
phead->next = del->next;
del->next->prev = phead;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
//phead是哨兵位,为-1。要从哨兵位的下一个开始找
LTNode* find = phead->next;
while (find != phead)
{
if (find->data == x)
{
return find;
}
find = find->next;
}
return NULL;
}
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = LTBuyNode(x);
newnode->next = pos->next;
newnode->prev = pos;
pos->next->prev = newnode;
pos->next = newnode;
}
void LTErase(LTNode* pos)
{
LTNode* del = pos;
del->next->prev = del->prev;
del->next = del->next->next;
}
- text.c
#include"List.h"
void text1()
{
//初始化双向链表
LTNode* plist1=NULL;
LTNode* plist=LTInit(plist1);
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
LTPushFront(plist, 4);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTNode* find = LTFind(plist, 3);
if (find)
{
printf("找到了!");
}
else
{
printf("未找到!");
}
printf("\n");
LTInsert(find, 4);
LTPrint(plist);
LTErase(find);
LTPrint(plist);
}
int main()
{
text1();
return 0;
}
五、单向链表和带头双向循环链表的区别
1.结构
- 单向链表:每个节点包含一个数据域和一个指向下一个节点的指针(即“下一个”指针)。链表的头节点指向第一个存储的节点。
- 带头双向循环链表:每个节点包含一个数据域、一个指向下一个节点的指针(“下一个”指针)和一个指向前一个节点的指针(“前一个”指针)。带头的双向链表通常有一个头节点,头节点不存储有效数据,而是用于简化对链表的操作。
2.访问方式
- 单向链表:只能从头节点进行遍历,不能从后往前遍历。
- 带头双向循环链表:既可以从后往前,也可以从前往后遍历。
3.空间占用
- 单向链表:每个节点只有一个指向下一个节点的指针,存在较小的内存消耗
- 带头双向循环链表:每个节点既有指向下一个节点的指针,也有指向前一个节点的指针,所以内存消耗较大
4.插入和删除操作
- 单向链表:插入和删除操作需要考虑前驱节点,对于删除操作,通常需要从头开始遍历找到前驱节点。
- 带头双向循环链表:由于每个节点都有对前驱节点的引用,可以更方便地执行插入和删除操作,无需前向遍历。
六、顺序表和链表的优缺点
优点 | 缺点 | |
顺序表 |
1. 物理空间是连续的,方便用下标随机访问。 2. CPU高速缓存命中率会更高。 |
1. 由于需要物理空间连续,空间不够需要扩容。其次扩容机制还存在一定的空间浪费。 2. 头部或者中部插入、删除、挪动数据效率低 O(N)。 |
链表 | 1、按需申请和释放空间。 2. 任意位置插入、删除数据效率高 O(1)。 |
1. 不支持下标的随机访问。有些算法不适合在它上面进行。如:二分、排序等 |