链表———线性表笔记整理
代码基本来源:2020王道数据结构(侵删)
文章目录
1.单链表(重点)
1.1单链表的定义
每个结点除了存放数据元素外,还要存储指向下一个结点的指针
- 优点:不要求大片连续空间,改变容量方便
- 缺点:不可随机存取,要耗费一定空间存放指针
1.2单链表的实现
1.2.1 数据对象的定义
LNode * 与 LinkList 本质上都是指向LNode类型的指针
- LNode *:强调指向一个结点
- LinkList :强调指向一个链表
typedef struct LNode{
//定义单链表结点类型
ElemType data;//数据域 每个结点存放一个数据元素
struct LNode *next;//指针域 指向下一个结点
}LNode, *LinkList;
1.3不带头结点的单链表
- 第一个结点存储数据(第一个结点即为链表位序为1的结点)
1.3.1链表的初始化
防止脏数据(指向莫名其妙的区域)
- 参数为引用类型(需要带回变量的值)
bool InitList(LinkList &L){
L = NULL;//空表,暂时还没有任何结点
return L;
}
1.3.2空链表的判断
bool IsEmpty(LinkList L){
return (L == NULL);
/* 等价于下面的代码
if (L == NULL)
return true;
else
return false;
*/
}
1.3.3数据的插入
1.3.3.1按位序插入
- 找到第i-1个结点,将新结点插入其后
- 不存在“第0个”结点,因此i=1时需要特殊处理
- 注意:
- 由于不存在第0个结点,所以j从1开始
bool ListInsert(LinkList &L,int i, ElemType e){
if (i<1)
return false;//插入位置非法
if (i == 1){
//插入第一个结点的操作与其他结点操作不同
LNode *t = (LNode *)malloc(sizeof(LNode));
t->data = e;//填充新结点
t->next = L->next;
L = t;//头指针指向新结点
return ture;
}
LNode *p;//p指向当前扫描到的结点
p = L;//p指向当前的头结点,认为是第0个结点
int j = 1;//当前p指向的是第j个结点
while ( (p != NULL) && (j < i-1)){
//循环找到第i-1个结点
p = p->next;
++j;
}
if (p == NULL) //找不到第i-1个结点
return false;
//找到第i-1个结点,插入结点
LNode *t = (LNode *)malloc(sizeof(LNode));
t->data = e;//填充新结点
t->next = p->next;
p->next = t;
return ture;//插入成功
}
1.4带头结点的单链表(代码实现简单)
- 头结点不存储数据(链表第一个结点不存储数据)
- 头指针指向的结点的下一个结点才开始存放数据
1.4.1链表的初始化
- 返回false:内存不足,分配失败
- 返回true:成功初始化
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));//分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->next = NULL;//头结点之后暂时还没有结点
return true;
}
1.4.2空链表的判断
bool IsEmpty(LinkList L){
if (L->next == NULL)
return true;
else
return false;
}
1.4.3数据的插入
1.4.3.1按位序插入
- 找到第i-1个结点,将新结点插入其后
- 注意
- 头结点可以看做”第0个“结点
- 循环条件的设定以及循环结束后判断p是否为NULL来获知i是否合法
bool ListInsert(LinkList &L,int i, ElemType e){
if (i<1)
return false;//插入位置非法
//下面这段代码相当于查找第i-1个结点的操作
//LNode *p = GetElem(L,i-1)
LNode *p;//p指向当前扫描到的结点
p = L;//p指向当前的头结点,认为是第0个结点
int j = 0;//当前p指向的是第j个结点
while ( (p != NULL) && (j < i-1)){
//循环找到第i-1个结点
p = p->next;
++j;
}
//下面的操作相当于后插操作 可调用后插函数实现
// return InsertNextNode(p,e)
if (p == NULL) //找不到第i-1个结点
return false;
//找到第i-1个结点,插入结点
LNode *t = (LNode *)malloc(sizeof(LNode));
t->data = e;//填充新结点
t->next = p->next;
p->next = t;
return ture;//插入成功
}
1.4.3.2指定结点的前/后插操作(重点在前插实现)
- 对于单链表,给定结点之后的区域是可知区域,而前面的区域是不可知区域
- 给点结点的前插操作和后插操作不相同
- 给定结点的后插操作
bool InsertNextNode(LNode *p,ElemType e){
if (p == NULL)//边界条件判断
return false;
LNode *t = (LNode *)malloc(sizeof(LNode));
if ( s == NULL)//内存不足,分配失败
return false;
t->data = e;//填充结点
t->next = p->next;
p->next = t;//将结点t连到p之后
return true;
}
-
给定结点的前插操作(给定插入的数据)
- 思路一:遍历整个链表,寻找到给定结点的前驱结点(q->next = p)
- 思路二(骚操作):偷心换月
- 将p结点的数据复制到新申请的结点中
- 用p结点存放新的数据
- 改变指针指向(先绿后黄)
bool InsertPriorNode(LNode *p,ElemType e){
if (p == NULL)//边界条件判断
return false;
LNode *t = (LNode *)malloc(sizeof(LNode));
if ( s == NULL)//内存不足,分配失败
return false;
t->data = p-data;//将p中元素复制到t中
p->data = e;//p中元素覆盖为e
t->next = p->next;
p->next = t;//将结点t连到p之后
return true;
}
1.4.4按位序删除
删除表L中第i个位置的元素,并用e返回删除元素的值
- 找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点
- 头结点可看成是“第0个”结点
- 注意:每一次使用指针的时候都需要判断其指向的地方是否为NULL
- 如找到第i-1个结点的时候,由于需要删除的是第i个结点,需要判断第I个结点是否为NULL(易漏)
bool ListDelete(LinkList &L, int i,ElemType &e){
if (i<1)
return false;
//下面这段代码相当于查找第i-1个结点的操作
//LNode *p = GetElem(L,i-1)
LNode *p;//指针p指向当前扫描到的结点
int j = 0;//当前p指向的是第几个结点
p = L;//L指向头结点,头结点假设是第0个结点(不存数据)
while( p != NULL && j<i-1 ){
//寻找第i-1个结点
p = p->next;
++j;
}
if (p == NULL)//找不到第i-1个结点
return false;
if (p->next == NULL)//第i-1个结点后已无其他结点(删除第i个位置的结点非法)
return false;
LNode t = p->next;//临时存放所需删除的结点
e = t->data;//引用 需返回e的值
p->next = t->next;//将所需删除结点在链表中断开
free(t);//释放结点的存储空间
}
1.4.5指定结点的删除(有坑)
- 思路1:传出头指针,循环寻找p的前驱结点(O(n))
- 思路2:偷天换日(类似于结点前插)
- 删除指定结点无法就是修改当前结点的前驱结点指向当前结点的后继结点
- 关键点
- 将当前结点的后继结点的数据覆盖到当前结点上,然后再修改当前结点的指向
- 有坑:不要忘记判断p->next是否为NULL(指点结点为表尾)(每一次使用指针的时候都要想一下是否为NULL)————这种情况只能从头开始依次寻找p的前驱结点O(n)
bool DeleteNode(LNode *p){
if ( p == NULL)
return false;
LNode *q = p->next;//指向当前结点p的后继结点
if (q == NULL){
//删除结点在表尾
Find
}
p->data = p->next->data;//(和后续结点交换数据域)将当前结点p的下一结点的数据覆盖到当前结点
p->next = q->next;//将*q所指向结点从链表断开
free(q);//释放后继结点的存储空间
return true;
}
1.4.6查找
1.4.6.1按位查找
按位查找:获取表L中指向第I个位置结点的指针
- 成功:返回指向该结点的指针
- 失败(注意):返回NULL
- 查找位置不合法(i<0)
- i = 0 返回指向头结点的指针
- i = 1,2,… 返回指向数据结点的指针
- 查找位置不合法(查找位置超出链表长度)
//按位查找 带头结点(第0个结点)
LNode *GetElem(LinkList L,int i){
if (i<0)
return NULL;
LNode *p;//指针p指向当前扫描到的结点
int j = 0;//当前p指向的是第j个结点
p = L;//指针p指向头结点,头结点是第j个结点(不存数据)
while(p != NULL && j < i){
//循环找到第j个结点
p = p->next;
++j;
}
return p;
}
1.4.6.2按值查找
按值查找:在表L中查找具有给定关键字值的元素
- 成功:返回指向该结点的指针怎
- 失败:返回NULL
//按值查找,找到数据域==e的结点
LNode * LocateElem(LinkList L,ElemType){
LNode *p = L->next;
//从第一个结点开始查找数据域为e的结点
while( p != NULL && p-data != e){
p = p->next;
}
return p;//调用程序需判断是否为NULL
}
1.4.7求表的长度
思路:遍历链表时加一个计数器
//求表的长度
int Length(LinkList L){
int len = 0;
LNode *p = L-next;//p指向链表的第一个结点
while (p != NULL){
++len;
p = p->next;
}
return len;
}
1.4.8链表的建立
- 初始化一个单链表
- 每次取一个数据元素,插入到表尾/表头
1.4.8.1尾插法(正向建立链表)
- 初始化单链表(调用初始化函数)
- 设置变量length记录链表长度
- 循环:每次取一个数据元素;调用按位序插入插到链表尾部(length+1);++length
- 问题:每次调用后插函数都从头开始遍历,时间复杂度为O(n^2)
- 解决方法:设立一个尾指针,这样每次操作就不用从头开始遍历找尾部
综上:利用后插的思想,每次将结点插到尾指针所指结点之后(最终方法)
LinkList List_TailInsert(LinkList &L){
int x;//设ElemType为整形
LNode *s,*r = L;//s指向临时插入的新结点,r为表尾指针
scanf("%d",&x);//输入结点的值
while( x != -1 ){
//表示结束的值
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;//指向新的表尾结点
scanf("%d",&x);//读入新的数据
}
r->next = NULL;//尾结点指针置空
return L;//返回表头指针
}
1.4.8.2头插法(逆向建立链表)——对头结点的后插操作
- 初始化单链表(调用初始化函数)
- 循环:每次取一个数据元素;调用后插插到链表头部(指向头结点的指针)
LinkList List_TailInsert(LinkList &L){
int x;//设ElemType为整形
scanf("%d",&x);//输入结点的值
while( x != -1 ){
//表示结束的值
InsertNextNode(&L,x);
scanf("%d",&x);//读入新的数据
}
return L;//返回表头指针
}
1.5代码的注意事项
- 分配内存后,需要判断返回指针是否为NULL(内存可能分配失败-----内存空间不足)
- 数据结构的全部实现不要死记,记住关键算法并用图形助记
- 按值查找:若ElemType是结构类型,则不能直接比较两个结点
- 方法:自定义比较函数——每个成员一一比较
- 前插法建立链表应用:链表的逆置
- 重要操作
- 后插操作/前插操作(骚操作)
- 指定结点的删除(骚操作)
- 若题目要求建立的链表不带头结点,也可以利用头结点的方法
- 建立链表:则利用带头结点的方法建立后在释放头结点
- 在无头结点的链表操作,可以自己先修改一下链表使其有头结点,操作完成后再释放
2.双链表
- 双链表可进可退
- 存储密度更低一点(需要存放指向前驱结点的指针)
2.1双链表的定义
typedef struct DNote{
//定义双链表结点类型
ElemType daa;//数据域
struct DNode *prior,*next;//前驱和后驱指针
}DNote,*DLinkList;
2.2双链表的初始化
bool InitDLinkList{
DLinkList &L}{
L = (DNode *)malloc(sizeof(DNode));//分配一个头结点
if (L == NULL)//内存不足,分配失败
return false;
L->prior = NULL;//头结点的prior结点永远指向NULL
L->next = NULL;//头结点之后暂时还没有结点
return ture;
}
2.3双链表的判空
bool IsEmpty{
DLinkList L}{
if (L->next == NULL)//内存不足,分配失败
return false;
else
return false;
}
2.4双链表的插入(画图助解)
- 在p结点之后插入s结点(后插操作)
- 注意:在表尾插入结点的情况
bool InsertNextNode{
DNode *p,DNode *s}{
if (p == NULL || s == NULL)//非法参数
return false;
s->next = p->next;
if (p->next != NULL)//如果p结点有后继结点
p->next->prior = s;
p-next = s;
s->prior = p;
return false;
}
2.4双链表的删除
2.4.1删除p结点的后继结点
- 注意
- p结点没有后继结点
- p结点的后继结点为表尾结点
bool DeleteNextNode(DNode *p){
if (p == NULL)
return false;
DNode *q = p->next;//指向p的后继结点q
if (p->next == NULL)
return false;
p->next = q->next;
if (q->next != NULL)//q结点不是最后一个结点
q->next->prior = p;
free(q);//释放结点空间
return true;
}
2.4.2销毁链表
- 思路:循环删除头结点的后继结点
- 注意:只有销毁表时才能删除头结点
void DestoryList(DLinkList &L){
//循环释放各个数据结点
while(L->next != NULL)
DeleteNextNode(L);
free(L);//释放头结点
L = NULL;//头指针指向NULL
}
2.5双链表的遍历
2.5.1前向遍历(跳过头结点)
while (p->prior != NULL){
//对结点p做相应处理
p = p->prior;
}
3.循环链表
3.1循环单链表
3.1.1循环单链表的定义
- 循环单链表:表尾结点的next指针指向头结点
- 从一个结点出发可以找到其他任何一个结点
3.1.2循环单链表的优点
- 从尾部找到头部,时间复杂度为O(1)
- 当对链表的操作都是在头部或尾部时,循环单链表很方便(让L指向表尾元素)
- 注意:L指向表尾时,插入、删除时可能需要修改L
3.1.2循环单链表的初始化
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));//分配一个结点
if (L == NULL)//内存不足,分配失败
return false;
L->next = L;//头结点next指向头结点
return true;
}
3.1.3循环单链表的判空
bool IsEmpty(LinkList L){
if (L->next == L)//单手抱紧自己
return true;
else return false;
}
3.1.4循环单链表的表尾结点的判断
bool IsTail(LinkList L, LNode *p){
if (p->next == L)
return true;
else return false;
}
3.2循环双链表
3.2.1循环双链表的定义
- 表头结点的prior 指向表尾结点
- 表尾结点的next 指向头结点
3.2.2循环双链表的初始化
bool InitList(DLinkList &L){
L = (LNode *)malloc(sizeof(LNode));//分配一个结点
if (L == NULL)//内存不足,分配失败
return false;
L->next = L;//头结点next指向头结点
L->prior = L;//头结点prior指向头结点
return true;
}
3.2.3循环单链表的判空
bool IsEmpty(DLinkList L){
if (L->next == L)//单手抱紧自己
return true;
else return false;
}
3.2.4循环单链表的表尾结点的判断
bool IsTail(DLinkList L, LNode *p){
if (p->next == L)
return true;
else return false;
}
3.2.5循环双链表的插入
- 不用判断前驱结点或后继结点是否为NULL
bool InsertNextNode{
DNode *p,DNode *s}{
if (p == NULL || s == NULL)//非法参数
return false;
s->next = p->next;
p->next->prior = s;
p-next = s;
s->prior = p;
return false;
}
3.2.6删除p结点的后继结点
- 不用判断p结点的后继结点
- 是否为NULL
- 是否为表尾结点
bool DeleteNextNode(DNode *p){
if (p == NULL)
return false;
DNode *q = p->next;//指向p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);//释放结点空间
return true;
}
4.静态链表
- 分配一整片连续的内存空间,各个结点集中安置
- 利用数组的方式实现链表的功能(结构数组)
4.1静态链表的结点
#define MaxSize 10//静态链表的最大长度
struct Node{
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
};
4.2静态链表的定义(重点—结构数组的typedef定义)
#define MaxSize 10//静态链表的最大长度
typedef struct {
//静态链表结构类型的定义
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
}SLinkList[MaxSize];
/* 等价以下代码
#define MaxSize 10//静态链表的最大长度
struct Node{//静态链表结构类型的定义
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
};
//SLinkList 定义 “一个长度为MaxSize 的Node 型数组”
typedef struct Node SlinkList[MaxSize];
*/
4.3静态链表的初始化
- 0号结点充当头结点
- 游标为-1表示已经到达表尾
- 游标为-2表示该位置没有元素(用一个特殊的数标记空闲结点)
4.4静态链表的按位序插入
- 找到一个空的结点(遍历:直至遇到第一个游标为-2的元素),存入数据元素
- 修改新结点的next
- 从头结点出发找到位序为i-1 的结点
- 修改i-1 号结点的next
4.4静态链表删除某个结点
- 从头结点出发找到前驱结点
- 修改前驱结点的游标
2静态链表的定义(重点—结构数组的typedef定义)
#define MaxSize 10//静态链表的最大长度
typedef struct {
//静态链表结构类型的定义
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
}SLinkList[MaxSize];
/* 等价以下代码
#define MaxSize 10//静态链表的最大长度
struct Node{//静态链表结构类型的定义
ElemType data;//存储数据元素
int next;//下一个元素的数组下标
};
//SLinkList 定义 “一个长度为MaxSize 的Node 型数组”
typedef struct Node SlinkList[MaxSize];
*/
4.3静态链表的初始化
- 0号结点充当头结点
- 游标为-1表示已经到达表尾
- 游标为-2表示该位置没有元素(用一个特殊的数标记空闲结点)
4.4静态链表的按位序插入
- 找到一个空的结点(遍历:直至遇到第一个游标为-2的元素),存入数据元素
- 修改新结点的next
- 从头结点出发找到位序为i-1 的结点
- 修改i-1 号结点的next
4.4静态链表删除某个结点
- 从头结点出发找到前驱结点
- 修改前驱结点的游标
- 被删除结点next 设为-2