一.按位序插入(带头结点):
1.思路:
在第i个位置上插入一个元素,就需要找到第i-1个结点,用malloc函数申请一个新的结点,把要插入的元素放入该结点:
再把第i-1个结点的指针修改:
特例:把指定元素插入在第一个位置,此时就体现出带头结点的单链表的好处->头结点可视为"第0个"节点
2.代码演示:
#include<stdio.h>
#include<stdlib.h>
//定义单链表结点类型
typedef struct LNode
{
int data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode,*LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建
{
return false;
}
else
{
L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
return true;
}
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据
{
return true;
}
else
{
return false;
}
}
/*在第i个位置插入元素e(带头结点)
->插入元素之前已经初始化了一个空表,有头结点,此时L指向头结点(有数据元素和指向下一个结点的指针) */
bool ListInsert(LinkList &L,int i,int e)
{
if(i<1) //i表示位序,必须是正整数,所以要进行有界判断
{
return false;
}
//走到这儿说明位序i合法
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点:j为0代表头结点
p=L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i-1) //循环找到第i-1个结点
{
/*以i为2为例(和下述图例无关,这个就是用来解释循环),此时p!=NULL,j为0,i-1为1,符合循环条件
此时p为p->next就指向了下一个结点。j++后为1,不符合循环条件,跳出循环,
此时p就是i为2的后一个位置,就把i为2空出来了,就可以插入元素了*/
p = p->next;
j++;
}
if(p==NULL) //说明i值不合法
{
return false;
}
//申请一个新的结点空间
LNode *s = (LNode *)malloc( sizeof(LNode) );
//把参数e存入这个新的结点空间
s->data = e;
//把s指向的结点next指针让他等于p结点next指针指向的位置
s->next = p->next;
//将结点s连到p之后
p->next = s;
return true; //插入成功
}
int main()
{
//声明一个指向单链表的指针
LinkList L;
//初始化一个空表
InitList(L);
return 0;
}
例一:插在表头
i为1时不走第一个if语句和while循环,且此时p指向头结点,不为空,也不走第二个if语句(i值不合法即为0或者负数时会
没有头结点:比如i为0时头结点就是-1,不合法,头结点应该在0位置上,i为负数时同理,因此i值不合法时没有头结点也就使得头结点为NULL)
申请一块新的结点,存入要存的元素,
p->next此时是a1,所以要把p结点(p指向头结点)next指针指向的位置赋值给s指向的结点next指针
i为1时可达到最好的时间复杂度(插在表头),因为不走while循环
注:必须先执行s->next = p->next;,再执行p->next = s;,如果颠倒了,会使得s最终指向自己:
例二:插在表中
i为3时(也代表插入的数据最后在第三个位置上),i-1为2,会走while循环,会循环两次,第一次循环后p指向a1,第二次循环后p指向a2,所以p->next指向a3:
例三:插在表尾
i为5时(也代表插入的数据最后在第五个位置上),i-1为4,会走while循环,会循环4次,第一次循环后p指向a1,第二次循环后p指向a2,第三次循环后p指向a3,第四次循环后p指向a4,所以p->next指向NULL:
这个函数第一部分就是越界判断,第二部分用来循环找到第i-1个结点,第三部分用来插入元素
i为5时可达到最坏的时间复杂度(插在表尾),因为走while循环的次数最多
例四:直接插在比表尾还靠后的位置->显然做不到,因为最多只能先插在表尾,再插比表尾还靠后的位置
i为6时,i-1为5,共走5次while循环,第4次循环后p指向a4,此时p不为NULL,且j为4,i-1为5,符合循环条件,
第5次循环时p就指向了NULL,j++为5,不符合循环条件,跳出循环,此时会走第二个if语句,之后return false结束函数。
while循环结束后p为NULL就代表第i-1个结点不存在->就无法插入第i个结点
3.总结:
二.按位序插入(不带头结点):
1.思路:
注:由于不存在头结点即不存在"第0个"结点,因此插在第一个位置即i为1时需要特殊处理。
2.代码演示:
#include<stdio.h>
#include<stdlib.h>
//定义单链表结点类型
typedef struct LNode
{
int data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode,*LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建
{
return false;
}
else
{
L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
return true;
}
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据
{
return true;
}
else
{
return false;
}
}
/*在第i个位置插入元素e(不带头结点) */
bool ListInsert(LinkList &L,int i,int e)
{
if(i<1) //i表示位序,必须是正整数,所以要进行有界判断
{
return false;
}
//走到这儿说明位序i合法
if( i==1 ) //插入第一个结点的操作与其他结点的操作不同
{
//申请一个新的结点空间
LNode *s = (LNode *)malloc( sizeof(LNode) );
//把参数e存入这个新的结点空间
s->data = e;
//让
s->next = L;
//让头指针指向新结点
L = s;
return true; //表示插入成功
}
LNode *p; //指针p指向当前扫描到的结点
int j=1; //当前p指向的是第几个结点:j为1代表第一个结点(不是头结点)
p=L; //p指向第一个结点(注:不是头结点)
while(p!=NULL && j<i-1) //循环找到第i-1个结点
{
/*以i为2为例,此时p!=NULL,j为0,i-1为2,符合循环条件
此时p为p->next就指向了下一个结点。j++后为1,不符合循环条件,跳出循环,
此时p就是i为2的后一个位置,就把i为2空出来了,就可以插入元素了*/
p = p->next;
j++;
}
if(p==NULL) //说明i值不合法
{
return false;
}
//申请一个新的结点空间
LNode *s = (LNode *)malloc( sizeof(LNode) );
//把参数e存入这个新的结点空间
s->data = e;
//把s指向的结点next指针让他等于p结点next指针指向的位置
s->next = p->next;
//将结点s连到p之后
p->next = s;
return true; //插入成功
}
int main()
{
//声明一个指向单链表的指针
LinkList L;
//初始化一个空表
InitList(L);
return 0;
}
例一:插在表头:
i为1时,走第二个if语句,先利用malloc函数申请一个新的结点,再在这个新的结点中存入要插入的元素:
L一开始指向a1,但结果要求L指向s,
所以需要先把L赋值给s->next即把a1和s里面指向下一个元素的指针相连(新结点的next指针指向L所指向的结点),
最后需要修改头指针L即让L指向新的结点(把s连接到p之后即p=s)。
例二:插在表头之外的位置即i>1
三.指定结点的后插操作:
1.代码演示:
#include<stdio.h>
#include<stdlib.h>
//定义单链表结点类型
typedef struct LNode
{
int data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode,*LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建
{
return false;
}
else
{
L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
return true;
}
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据
{
return true;
}
else
{
return false;
}
}
//后插操作:在p结点之后插入元素e
/*注:单链表的链表指针只能往后寻找,所以给定p,p之后的结点就都可以找出来,
p之前的结点就无法找出来,也就无法往前插,只能后插 */
bool InsertNextNode(LNode *p,int e)
{
if(p==NULL)
{
return false;
}
//申请一个新的结点空间(注:如果此时内存已满,就无法申请出新的结点空间,导致s为NULL)
LNode *s = (LNode *)malloc( sizeof(LNode) );
if(s==NULL) //内存分配失败
{
return false;
}
//用结点s保存数据元素e
s->data = e;
//此时p指向下一个元素(p->next)即y,最终需求s指向y,那么就需要s->next = p->next
s->next = p->next;
//将结点s连接到p之后
p->next = s;
return true;
}
int main()
{
//声明一个指向单链表的指针
LinkList L;
//初始化一个空表
InitList(L);
return 0;
}
2.图解:
申请一个新的结点空间(注:如果此时内存已满,就无法申请出新的结点空间,导致s为NULL),把要插入的元素存入这个新申请的结点空间:
此时p指向下一个元素(p->next)即y,最终需求s指向y,那么就需要s->next = p->next,再把s连接到p之后即p->next = s:
该操作没有循环,显然时间复杂度是O(1)。
3.插入操作的简化:
在第i-1个结点后进行了后插数据元素(最后插入的元素就在第i个位置),因此可简化为:封装的好处
四.指定结点的前插操作:
1.实现方式一:传入头指针
单链表的链表指针只能往后寻找,因此只能后插,不能前插
->想要前插的话就必须引入头指针
该方式时间复杂度为O(n),因为有循环,较麻烦。
2.实现方式二:既然结点不能动,那就动数据元素
以下图为例,先申请一个新的结点,再把这个结点作为p结点的后继结点(就是p结点后面插一个新结点):
关键:既然结点不能动,那就动数据元素
把p结点的数据元素x复制到s结点中,再把要插入的元素放入e,
注:p结点和s结点这些只是名字,真正决定结点的是数据元素和存放的指针,
此时新申请的这块结点s(内部数据元素为x)就相当于p结点,存放e元素的结点就在p结点之前了,实现了前插操作
该方式时间复杂度为O(1),因为没有循环,较高效。
第二个版本:
五.按位序删除(带头结点):
1.思路:
删除表L中第i个位置的元素,要用到free函数,
再找到第i-1个结点,
然后修改第i-1个结点的指针,使其指向第i+1个结点。
2.图解:
3.代码实现:
#include<stdio.h>
#include<stdlib.h>
//定义单链表结点类型
typedef struct LNode
{
int data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个结点
}LNode,*LinkList;
//初始化一个单链表(带头结点)
bool InitList(LinkList &L)
{
L = (LNode *)malloc( sizeof(LNode) ); //分配一个头结点
if(L==NULL) //代表内存不足,分配失败-->意味着带头结点的单链表无法创建
{
return false;
}
else
{
L -> next = NULL; //头结点之后暂时还没有节点,所以指向NULL
return true;
}
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L)
{
if(L->next==NULL) //头结点之后如果指向NULL,代表没有数据
{
return true;
}
else
{
return false;
}
}
/*按位序删除(带头结点)
->插入元素之前已经初始化了一个空表,有头结点,此时L指向头结点(有数据元素和指向下一个结点的指针)*/
bool ListDelete(LinkList &L,int i,int &e)
{
if(i<1)
{
return false;
}
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点:j为0代表头结点
p=L; //L指向头结点,头结点是第0个结点(不存数据)
while(p!=NULL && j<i-1) //循环找到第i-1个结点
{
p = p->next;
j++;
}
//循环结束后p为第i-1个结点
if(p==NULL) //p为NULL,代表第i-1个结点不存在,也就说明i值不合法
{
return false;
}
if(p->next == NULL) //代表第i-1个结点之后已无其他结点
{
return false;
}
//令q指向被删除的结点
LNode *q = p->next;
//用e返回被删除的元素的值
e = q->data;
//将*q结点从链中"断开"
p->next = q->next;
//释放结点的存储空间
free(q);
//删除成功
return true;
}
int main()
{
//声明一个指向单链表的指针
LinkList L;
//初始化一个空表
InitList(L);
return 0;
}
图解:
i为4代表要删除第4个元素并返回,经过while循环p指向第三个元素,p->next指向第四个元素:
第二个if语句和第三个if语句明显不会走,之后把要删除的元素即p->next赋值给q,再把q的值即q->data返回给e:
注:变量e即此次要删除的结点的值最终要返回给ListDelete函数调用处,所以需要&(参数e为引用类型)
根据需求,要把第i+1个元素即q->next与第i-1个元素连接,也就是将*q结点从链中"断开",最后把删除的q释放:
时间复杂度:最好的时间复杂度就是i为1时(即删除第一个结点),不需要走while循环
六.指定结点的删除:
1.思路:
注:单链表的链表指针只能往后寻找,因此要删除的结点的前一个结点的指针就无法利用遍历找到,就需要别的方法:
2.实现方式一:传入头指针
3.实现方式二:偷天换日(类似于结点前插的实现)
p->next为q。题目需求是删除结点p,因此最终把一开始的p里的数据元素x删除即可,根据图里的一系列操作后,最终
数据元素x被删除,也就相当于没有一开始的p了。(p,q之类的就是个名字,真正要操作的是数据元素和内部的指针)
时间复杂度为O(1),因为没有循环。
4.极限情况:删除单链表里的最后一个结点
当执行p->data = p->next->data就会出错,因为p->next为q,而q里没有值即NULL(空指针),p->next->data就出错
-->所以上述代码有bug,即无法删除单链表里的最后一个结点
因此,如果要删除的结点是最后一个结点,
那么只能从表头开始依次寻找要删除的结点(最后一个结点)的前驱(使用传入头指针的方法),
时间复杂度为O(n)。