-
前言
本博客为本人在学习数据结构路途上的知识整理,如觉得对有你有所帮助,还希望留下一个赞。由于博主只是一名大一新生,如果博文中出现错误,欢迎指正。如果想要转载,附上链接就行。
注:
本文中的颜色标记
- 红色部分为重点内容
- 蓝色部分为注释
目录
目录
带尾指针的循环链表的合并(合并链表a与链表b,将b链接在a后)
一、线性表的链式存储结构
1、链式存储的基本概念
定义
线性表中各个数据元素在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 线性表的链式表示又称为非顺序映像或链式映像。
存储方式
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。
- 链表中元素的逻辑次序和物理次序不一定相同。
与链式存储有关的术语
1、结点:数据元素的存储映像。由数据域和指针域两部分组成
- 数据域存储数据
- 指针域存储指向下一个数据的指针
结点示意图
2、链表:n个结点由指针链组成个链表。
- 它是线性表的链式存储映像,称为线性表的链式存储结构
链表示意图
3、单链表、双链表、循环链表:
- 结点只有一个指针域的链表,称为单链表或线性链表
- 结点有两个指针域的链表,称为双链表
- 首尾相接的链表称为循环链表
4、头指针、头结点和首元结点
- 头指针:是指向链表中第一个结点的指针
- 首元结点:是指链表中存储第一个数据元素a1的结点
- 头结点:是在链表的首元结点之前附设的一个结点
带头结点和不带头结点的链式存储结构的区别
- 不带头结点的链式存储结构示意图
- 带头结点的链式存储结构示意图
- 单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名,若头指针名是L,则把链表称为表L
1、如何表示空表?
- 无头结点时,头指针为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
2、在链表中设置头结点有什么好处?
- 便于首元结点的处理首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
- 便于空表和非空表的统一处理无论链表是否为空,头指针都是指向头结点的非空指针因此空表和非空表的处理也就统一了
3、头结点的数据域内装的是什么?
- 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入连表长度值 。
链表(链式存储结构)的特点
- 结点在存储器中的位是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
- 这种存取元素的方法被称为顺序存取法
2、线性表链式存储结构的实现(单链表)
创建链表
#include <stdio.h>
#include <stdlib.h>
//引用头文件
typedef int ElemType;//自定义数据元素类型,这里在链表中存储的数据元素类型为int
typedef struct Lnode
{
ElemType data;//数据域
Lnode* next;//指针域
}Lnode,*LinkList;
//将这个数据结构用typedef定义为Lnode,Lnode的指针Lnode*定义为LinkList,增加代码可读性,方便创建指针
3、单链表的操作(有头结点的单链表)
单链表的初始化
bool InitList_L(LinkList L)
步骤:
- 生成新结点作头结点,用头指针L指向头结点。
- 将头结点的指针域置空
bool InitList_L(LinkList L)
{
L = (LinkList)malloc(sizeof(Lnode));
if (!L)
return false;//判断是否开辟空间失败,失败则返回false
L->next = NULL;//将next指针置空
return true;//初始化完成返回true
}
建立单链表
一、头插法
- 元素查找链表头部,也叫前插法。
步骤:
- 从一个空表开始,重复读入数据
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将客结点插入到链表的前端
图示:
bool CreateListH(LinkList L, int n)
{
L = (LinkList)malloc(sizeof(Lnode));
if (!L)
return false;
L->next = NULL;
//建立一个带头结点的单链表
LinkList p = NULL;
for (int i = n; i > 0; --i)
{
p = (LinkList)malloc(sizeof(Lnode));//生成新结点p
scanf("%d",p->data);//输入元素值;
p->next = L->next;
L->next = p;
//插入到表头
}
return true;
}
二、尾插法
- 元素查找链表尾部,也叫后插法。
步骤:
- 从一个空表开始,将新结点逐个插入到链表的尾部,尾指针指向链表的尾结点。
- 初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点。将新结点插入到尾结点后,r指向新结点。
图示:
bool CreateListR(LinkList L, int n)
{
L = (LinkList)malloc(sizeof(Lnode));
if (!L)
return false;
L->next = NULL;
//建立一个带头结点的单链表
LinkList r = L;//创建尾指针r指向头结点
LinkList p = NULL;
for (int i = 0; i < n; ++i)
{
p = (LinkList)malloc(sizeof(Lnode));
if (!p)
return false;
p->next = NULL;
//生成新结点p
scanf("%d", p->data);//输入元素值
r->next = p;//插入到表尾
r = p;//r指向新的尾结点
}
}
判断链表是否为空
bool ListEmpty(LinkList L)
空表:链表中无元素,称为空链表(头指针和头结点仍然在)
步骤:
- 判断头结点指针域是否为空
bool ListEmpty(LinkList L)
{
if (L->next)//为空表的话next指针为NULL(0)
return false;
else
return true;
}
销毁单链表
void DestoryList(LinkList L)
- 销毁:链表不存在,链表所在的空间都被释放
步骤:
- 从头指针开始,依次释放所有结点
void DestoryList(LinkList L)
{
LinkList p;//创建缓存指针,放置地址丢失
while (L)//当L非空时一直循环,直到指向NULL
{
p = L;//缓存指向这个结点的指针
L = L->next;//将指向这个结点的指针指向下一个结点
free(p);//释放这个结点的空间
p = NULL;//将p置空,防止出现野指针
}
}
清空单链表
void ClearList(LinkList L)
- 清空:链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然存在)
步骤:
- 依次释放所有结点,并将头结点指针域设置为空
void ClearList(LinkList L)
{
LinkList p, q;
p = L->next;
while (p)
{
q = p->next;
free(p);
p = q;
}//步骤与销毁单链表一致,只不过跳过了头结点,从首元结点开始销毁
L->next = NULL;//销毁完寿元结点之后的结点后将头结点的指针域置空
}
获得单链表的表长
int ListLength(LinkList L)
步骤:
- 从首元结点开始,遍历所有结点,依次计数
int ListLength(LinkList L)
{
LinkList p;
p = L->next;//将p指向首元结点
int cnt = 0;//创建计数器
while (p)//直到p指向尾结点的指针域即p为空的情况下停止
{
cnt++;//没经过一个结点,计数器cnt加1
p = p->next;//将p从这个结点转变为指向下一个结点
}
return cnt;
}
查找第i个结点的值
bool GetElemI(LinkList L, int i,ElemType* e)//用e来获得第i个元素的值
步骤:
- 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的点,p初值p=L->next。
- 用 j 做计数器,累计当前扫描过的结点数,初值为1。
- 当p指向扫描到的下一结点时,计数器 j 加1。
- 当 j == i 时,p所指的结点就是要找的第 i 个结点。
bool GetElemI(LinkList L, int i,ElemType* e)//用e来获得第i个元素的值
{
LinkList p = L->next;
int j = 1;
//初始化
while (p&& j < i)//向后遍历,直到p指向第i个元素或p为空
{
p = p->next;
j++;
}
if (!p || j > i)
return false;//第i个元素不存在则返回false
*e = p->data;//把第i个元素存进e中
return true;
}
查找值为e的结点的位置
LinkList LocateELem(LinkList L, ElemType e)
步骤:
- 从第一个结点起,依次和e相比较。
- 如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置"或地址。
- 如果查遍整个链表都没有找到其值和e相等的元素,则返回0或"NULL"。
//查找值为e的元素的位置
LinkList LocateELem(LinkList L, ElemType e)
{
LinkList p = L->next;
while (p && p->data != e)//向后遍历直到找到值为e的数据元素的地址或p指向NULL
p = p->next;
return p;
}
在指定位置插入结点
步骤:
- 首先找到ai-1的存储位置p。
- 生成一个数据域为e的新结点s。
- 插入新结点:
①新结点的指针域指向结点ai
②结点ai-1的指针域指向新结点
①s->next= p->next
②p->next=s;
步骤①和②不能互换!否则会丢失ai的地址
bool ListInsert(LinkList L, int i, ElemType e)
{
LinkList p = L;
int j = 0;
while (p && j < i - 1)
{
p = p->next;
j++;
}//寻找第i-1个结点,p指向i-1结点
if (!p || j > i - 1 || i < 1)
return false;//如果i>表长或者小于1,插入位置非法,返回fales报错
LinkList s = (LinkList)malloc(sizeof(Lnode));//生成新结点s
s->data = e;//将结点s的数据域置为e
s->next = p->next;
p->next = s;
//将结点s插入L中
return true;
}
删除第i个结点
bool ListDeleteI(LinkList L, int i)
步骤:
- 首先找到ai-1的存储位置p,
保存要删除的ai的值。 - 令p->next指向ai+1。
- 释放结点ai的空间。
bool ListDeleteI(LinkList L, int i)
{
LinkList p = L;
int j = 0;
//初始化
while (p->next&& j < i - 1)
{
p = p->next;
j++;
}//寻找第i个结点,并令p指向其前驱
if (!(p->next)||j > i - 1)
return false;//如果删除位置非法,返回false报错
LinkList q = p->next; //临时保存被删结点的地址以备释放
p->next = q->next;//将删除结点的前驱结点的指针域指向删除结点的后继结点
free(q);
q = NULL;//释放删除结点的空间
return true;
}
4、循环链表
定义:
- 一种头尾相接的链表(即:表中最后一个结点的指针域指向头结点,整个链表形成一个环)
图示:
优点:
- 从表中任一结点出发均可找到表中其他结点
注意事项:
- 由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。
带尾指针的循环链表的合并(合并链表a与链表b,将b链接在a后)
- 尾指针:一般而言,我们对链表的操作都是从链表的尾开始的,因此我们在构建链表时,一般会创建一个尾指针指向尾结点来方便我们对链表进行操作。
步骤:
- 创建p指针存储a表头结点。
- 将b表首元结点接到a表尾结点。
- 释放b表头结点。
- 修改指针。
①p=Ra->next; ②Ra->next=Rb->next->next; ③free(Rb->next); ④Rb->next=p;
LinkList Connect(LinkList Ra, LinkList Rb)//假设Ra、Rb都是非空的单循环链表
{
LinkList p = Ra->next;//①p存表头结点
Ra->next = Rb->next->next;//②Rb表头连结Ra表尾
free(Rb->next);//③释放Tb表头结点
Rb->next = p;//④修改指针
return Rb;
}
5、双向链表
定义:
- 在单链表的每个结点单再增加一个指向其直接前驱的指针域prior,这样链表中就形成了有两个方向不同的链,故称为双向链表。
创建双向链表
typedef struct DuLNode{
ElemType data;
DuLNode* prior, * next;
} DuLNode, * DuLinkList;
双向链表结构的对称性:
p->prior -> next = p =p->next -> prior(设指针p指向某一结点)
双向循环链表
和单链的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点。
双向链表的操作
- 在双向链表中有些操作(如:ListLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,其他则与单链表的操作相同,两者的操作的时间复杂度均为Q(n)。
二、顺序表和链表的比较
1、链式存储结构的优缺点
链式存储结构的优点:
- 结点空间可以动态申请和释放。
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
链式存储结构的缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
存储密度:结点数据本身所占的存储量和整个结点结构中所占的存储量之比
即:存储密度=结点数据本身占用的空间 / 结点占用的空间总量
例:
一般地,存储密度越大,存储空间的利用率就越高。
显然,顺序表的存储密度为1(100%),而链表的存储密度小于1。
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
2、顺序表的优缺点
优点:
- 存储密度大(结点本身所占存储量/结点结构所占存储量)。
- 可以随机存取表中任一元素。
缺点:
- 在插入、删除某一元素时,需要移动大量元素。
- 浪费存储空间。
- 属于静态存储形式,数据元素的个数不能自由扩充。