一、链表的概念
1. 链式存储结构:
结点在存储器中的位置是任意的,即逻辑上的相邻的数据元素在物理上不一定相邻
2. 线性表的链式表示又称为非顺序映像或链式映像
3. 存储单元可以是连续的、不连续的、也可以是零散的,访问时只能通过指针依次扫描
4.一个结点由数据域和指针域构成,头指针记录第一个元素的地址,前一个结点的指针域保存下一个结点的地址,单链表的结点只有一个指针域
5. 单链表是由头指针唯一确定的,因此单链表可以用头指针的名字来命名
6. 头指针:指向链表中第一个结点的指针
首元节点 :链表中存储第一个数据元素a1的结点
头结点:在首元结点之前附设的一个结点
7. 两种形式
1)不带头结点
头指针直接存放第一个有数据元素的结点的地址
2)带头结点
头指针存放头结点的地址
8. 如何表示空表?
1)无头:头指针为空时,header==NULL
2)有头:头指针的指针域为空,header->next==NULL(next是头结点的指针域)
9. 带头结点好处
1)处理首元结点时与其他结点一致,无需特殊处理
2)便于统一处理空表和非空表
10. 头结点包含什么?
指针域存放首元结点的地址,而数据域中可以为空,也可存放线性表的长度,因为它不是数据元素,所以统计表长时不能将它统计进去
二、单链表的定义
1. 结点分为数据域和指针域,数据域可以是任意类型的数据,int,float,char等等,而指针域是指向下一个结点,结点是结构体类型,因此指针域应是结构体类型
解释:图中定义了一个结构体类型的结点,包括任意类型的数据域和结构体类型的指针域,加上typedef是为结构体起了别名,后续操作中不用再写struct Lnode了,而*LinkList是定义了一个指向结构体Lnode的指针类型,所以在定义头指针时,可以写成Lnode*header,也可以写成LinkList header
如果想要定义一个单链表,就可以定义为Lnode*header,或者是LinkList header了,因为头结点的名字代表整个链表,定义结点指针也可以使用这两种方式
三、单链表的相关算法
即构造一个空的单链表,即有一个头结点,头结点的指针域为空
1.单链表的初始化
算法步骤:
1)生成新结点作为头结点,用头指针header指向头结点
2)将头结点的指针域置空
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
3)看看是否初始化成功
int main()
{
LinkList header;//定义头结点,也可用Lnode*header定义
//定义变量,调用状态函数
int a=InitList_L(header);
cout<<a<<endl;//如果打印了1,说明初始化成功
system("pause");
return 0;
}
2. 几个简单算法
1)判断链表是否为空
空表是链表中无元素,但头指针和头结点仍然存在
算法思路:判断头结点指针与是否为空
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//判断链表是否为空
int ListEmpty(LinkList &header)
{
if(header->next==NULL)
{
return 1;
}
else
return 0;//非空返回0
}
3)看看是否初始化成功
int main()
{
LinkList header;//定义头结点,也可用Lnode*header定义
//定义变量,调用状态函数
int a=InitList_L(header);
cout<<a<<endl;//如果打印了1,说明初始化成功
cout<<"-----------------------------------------------------"<<endl;
//看看链表是否为空
int b=ListEmpty(header);
cout<<a<<endl;//如果打印了1,说明不为空,打印0,则为空
system("pause");
return 0;
}
2)单链表的销毁
销毁后链表不存在,包括头结点和头指针
算法思路:从头指针开始,依次释放所有结点
代码实现 :
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//销毁链表,没有设置两个指针,而是让头指针也跟着定义的新指针移动
int DestoryList_L(LinkList& header)
{
//设置一个可移动的指针,让它依次指向每一个结点
Lnode* pCurrent= header;//一开始都指向头结点
//销毁链表要从头结点开始,所以让pCurrent先指向头结点,即让把头指针赋值给pCurrent
//利用循环
while (pCurrent != NULL)
{
header = pCurrent;//循环第一次时,都指向头结点,二者谁赋给谁是一样的,这条也可不加
//让pCurrent向后移动
pCurrent = pCurrent->next;
//释放
delete pCurrent;
}
delete header;//将头结点和头指针释放
return 0;
}
3)看看是否初始化成功
int main()
{
LinkList header;//定义头结点,也可用Lnode*header定义
//定义变量,调用状态函数
int a=InitList_L(header);
cout<<a<<endl;//如果打印了1,说明初始化成功
cout<<"-----------------------------------------------------------"<<endl;
int b=DestoryList_L(header);
cout<<b<<endl;//如果返回0,说明销毁成功
system("pause");
return 0;
}
3)单链表的清空
链表仍存在,但链表中无元素,成为空链表,但头指针和头结点仍然存在
算法思路:依次释放所有结点,并将头结点指针域设置为空
先给链表中结点附上值,更方便观察是否清空
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//创建一个链表
void test()
{
//创建6个独立的结点
Lnode node1={10,NULL};
Lnode node2={20,NULL};
Lnode node3={30,NULL};
Lnode node4={40,NULL};
Lnode node5={50,NULL};
Lnode node6={60,NULL};
//连接起来
node1.next=&node2;
node2.next=&node3;
node3.next=&node4;
node4.next=&node5;
node5.next=&node6;
//node6指针域为空
//设置辅助指针进行遍历
Lnode*p=&node1;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}
//清空链表
void ClearList(LinkList &header)
{
//因为在清空中,头指针和头结点要保留,所以就让头指针始终指着头结点,另外设置两个指针移动
Lnode*pCurrent=header->next;//pCurrent指着首元结点
Lnode*pNext=pCurrent->next;//pNext指着首元结点后面的那个结点
while(pCurrent!=NULL)
{
//清空一个结点前先得获得里面保存的下一个结点的地址
pNext=pCurrent->next;
delete pCurrent;
//让pCurrent指向下一个结点
pCurrent=pNext;
}
//将头结点指针域置空
header->next =NULL;
}
3)看看是否赋值成功
int main()
{
test();
system("pause");
return 0;
}
4) 求单链表的表长
注意:头结点不算入其中
算法思路:从首元结点开始,设置一个计数器,依次计数所有结点
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//创建一个链表
void test()
{
//创建6个独立的结点
Lnode node1={10,NULL};
Lnode node2={20,NULL};
Lnode node3={30,NULL};
Lnode node4={40,NULL};
Lnode node5={50,NULL};
Lnode node6={60,NULL};
//连接起来
node1.next=&node2;
node2.next=&node3;
node3.next=&node4;
node4.next=&node5;
node5.next=&node6;
//node6指针域为空
//设置辅助指针进行遍历
Lnode*p=&node1;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}
//计算链表表长
int ListLength_L(LinkList&header)
{
//设置一个指针,让它先指向首元结点
Lnode*pCurrent=header->next;
int i=0;//设置计数器
while(pCurrent!=NULL)//遍历链表
{
i++;//结点不为空时
pCurrent=pCurrent->next;//移动指针
}
return i;//返回元素个数,即表长
}
3)看看是否赋值成功
int main()
{
//定义头指针
Lnode*header;
InitList_L(header);
test();
int i=ListLength_L(header);
cout<<"表长为:"<<i<<endl;
system("pause");
return 0;
}
3. 几个较难算法
1)取第i个元素值
算法思路:设置一个可移动的指针,让它指向首元结点,逐渐移动,扫描每一个结点
设置一个计数器 j, 让它的初值为1,指针每移动到下一个结点,就加一
当j与i 相同时,说明找到了第i个值
还要考虑特殊情况,比如i不能小于j,i也不能大于最大值,即指针不为空
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//创建一个链表
void test()
{
//创建6个独立的结点
Lnode node1={10,NULL};
Lnode node2={20,NULL};
Lnode node3={30,NULL};
Lnode node4={40,NULL};
Lnode node5={50,NULL};
Lnode node6={60,NULL};
//连接起来
node1.next=&node2;
node2.next=&node3;
node3.next=&node4;
node4.next=&node5;
node5.next=&node6;
//node6指针域为空
//设置辅助指针进行遍历
Lnode*p=&node1;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}
//取第i个值
int GetElem(LinkList&header,int i,int &e)//int &e为返回值
{
//设置指针指向首元结点
Lnode*pCurrent=header->next;
int j=1;//计数器
while(pCurrent!=NULL&&i>j)//j==i时退出循环
{
j++;
pCurrent=pCurrent->next;
}
if(pCurrent==NULL||j>i)//
{
return;
}
e=pCurrent->data;
return e;
}
3)看看是否赋值成功
int main()
{
//定义头指针
Lnode*header;
InitList_L(header);
int e=GetElem(header,2,e);
cout<<"第i个值为:"<<e<<endl;
system("pause");
return 0;
}
2) 按值查找
根据指定数据获取该数据所在的位置(地址)
算法思路:从首元结点开始,依次与要找的值比较
如果找到一个与待找值相同的数据元素,就返回它在链表中的位置(地址)
如果查遍整个链表都没有,就返回0或NULL
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//创建一个链表
void test()
{
//创建6个独立的结点
Lnode node1={10,NULL};
Lnode node2={20,NULL};
Lnode node3={30,NULL};
Lnode node4={40,NULL};
Lnode node5={50,NULL};
Lnode node6={60,NULL};
//连接起来
node1.next=&node2;
node2.next=&node3;
node3.next=&node4;
node4.next=&node5;
node5.next=&node6;
//node6指针域为空
//设置辅助指针进行遍历
Lnode*p=&node1;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}
//按值查找,返回值的地址
Lnode*LocateELem_L(LinkList &header,int e )
{
//设置指针指向首元结点
Lnode*pCurrent=header->next;
//利用循环查找
while(pCurrent->data!=e&&pCurrent!=NULL)
{
pCurrent=pCurrent->next;//移动指针
}
return pCurrent;
}
3)看看是否赋值成功
int main()
{
Lnode*header;
InitList_L(header);
test();
//定义变量接收
Lnode*p=LocateELem_L(header,20);
cout<<p<<endl;
system("pause");
return 0;
}
算法思路:
根据指定数据获取该数据位置序号
代码实现:
//初始化一个单链表
#include<iostream>
using namespace std;
//生成新结点作为头结点,用头指针header指向头结点
1)定义结点
typedef struct Lnode
{
int data;//假设数据是int类型
struct Lnode*next;//指针域
}Lnode,*LinkList;//Lnode为别名,*LinkList为指向结构体的指针类型
2)设置一个状态函数,进行初始化
int InitList_L(LinkList &header)//LinkList &header是函数引用,可以修改main中的实参,header既是原名,也是别名
{
//为结点开辟空间
header=new Lnode();//new出来后返回的是内存空间的首地址,所以要用指针接收
//将头结点的指针域置空
header->next=NULL;
return 1;
}
//创建一个链表
void test()
{
//创建6个独立的结点
Lnode node1={10,NULL};
Lnode node2={20,NULL};
Lnode node3={30,NULL};
Lnode node4={40,NULL};
Lnode node5={50,NULL};
Lnode node6={60,NULL};
//连接起来
node1.next=&node2;
node2.next=&node3;
node3.next=&node4;
node4.next=&node5;
node5.next=&node6;
//node6指针域为空
//设置辅助指针进行遍历
Lnode*p=&node1;
while(p!=NULL)
{
cout<<p->data<<endl;
p=p->next;
}
}
//按值查找,返回值的地址
int LocateELem_L(LinkList &header,int e )
{
//设置指针指向首元结点
Lnode*pCurrent=header->next;
int j=1;//设置计数器
//利用循环查找
while(pCurrent->data!=e&&pCurrent!=NULL)
{
pCurrent=pCurrent->next;//移动指针
j++;
}
if(pCyrrent->data==e)
{
rerurn j;
}
else
return 0;
}
3)看看是否赋值成功
int main()
{
Lnode*header;
InitList_L(header);
test();
//定义变量接收
int j=LocateELem_L(header,20);
cout<<j<<endl;
system("pause");
return 0;
}
3)插入结点
在第i个结点前插入值为e的新结点
算法思路: 首先要找到第i-1个结点,让指针Prev指向它,让指针pCurrent指向要插入的结点
插入新结点时,先让新结点的指针域指向原i结点的数据域,再让i-1结点的指针域指向 新节点的数据域(写代码时一定要按这个顺序,先后再前)
代码实现:
//在值为oldval的位置插入一个新的数据newval(因为插入后oldval的位置会变化),函数的参数首先要获得这个链表,这个链表的数据类型也就是第一个结点的数据类型,也就是struct LinkNode*类型,头结点起名为header,获得头结点就获得了整个链表
void InsertByValue_LinkList(struct LinkNode*header,int oldval,int newval)
{
if(NULL==header)//判断一下链表是否为空链表
{
return;
}
//利用双指针,一次移动两个指针,插入的结点一定会在这两个指针中间
//设置两个辅助指针变量
struct LinkNode *pPrev=header;//让前一个指针默认指向头结点
struct LinkNode *pCurrent=pPrev->next;//pPrev->next就是next,都是指针
while(pCurrent!=NULL)//不为空的原因和上面一样
{
if(pCurrent->data==oldval)//说明pCurrent已经指向了oldval
{
break;
}
//如果没有找到oldval,则同时移动两个指针
pPrev=pCurrent;// pPrev移动到了pCurrent刚才的位置
pCurrent=pCurrent->next;
//这个过程结束后只有两个结果,1是pCurrent找到了oldval,2是pCurrent最后指向了空,因此这是要判断一下
if(pCurrent==NULL)
{
return;//没有就直接返回吧
}
//创建新结点
Lnode *newnode=new Lnode();
newnode->data=newval;//将刚才输入的值赋给新结点
newnode->next=NULL;//因为现在只创建了一个新结点,新结点相对头结点来说是最后一个,所以后面为空
//新结点插入到链表中
newnode->next=pCurrent;
pPrev->next=newnode;
}
}
4) 删除第i个结点
算法思路:首先找到i-1中存储的i的位置,保存要删除的值
令p指向i-1,再让p->next指向i+1
关键一步:p->next=p->next->next,意思是把i+的地址赋给i
代码实现:
//将线性表中第i个数据元素删除
int ListDelete_L(LinkList &header,int i,int &e)
{
//设置指针和计数器,指针的目的还是想找第i-1个数据元素
Lnode*pCurrent=header;//从头结点开始,header是头指针
int j=0;
while(pCurrent->next!=NULL&&j<i-1)//当链表不为空并且还没找到i-1个节点时,要继续向后找
{
j++;
pCurrent=pCurrent->next;
}
if(pCurrent->next==NULL||j>i-1)
{
return -1;
}
//设置一个临时指针用来保存被删节点的地址
Lnode*q=pCurrent->next;//此时pCurrent已经指到了i-1的位置
//改变i-1结点的指针域
pCurrent->next=q->next;
//保存删除节点的数据域
e=q->data;
delete q;
return 0;
}
四、建立单链表
1 . 头插法
算法时间复杂度为O(n)
1)从一个空表开始,重复读入数据
2)生成新结点,将读入数据存放到新结点的数据域中
3)从最后一个结点开始,依次将各结点插入到链表前端
算法思路:
void CreateList_H(LinkList &L,int n)//表示要输入n个元素
{
//创建头结点
Lnode*header=new Lnode();
header->next=NULL;//建立一个带有头结点的单链表
//头插法形象表示如上图
for(int i=n;i>0;i--)//先插入最后一个数,然后依次向前插
{
//创建新结点
p=new Lnode();//即将插入的新结点
cin>>p->data;//依次输入数字,存入各结点的数据域中
p->next=header->next;//将新结点与后面的结点连接起来
header->next=p;//将新结点与头结点连接起来
}
}
2. 尾插法
算法时间复杂度为O(n)
//函数定义
//定义函数来初始化链表,初始化后需要返回链表中的结点,即返回对应指针
//当链表为空时,只有一个头结点,头结点后面为空
struct LinkNode* Init_LinkList()
{
//创建头结点,申请动态空间,使用malloc,返回对应指针,不能在堆上和和栈上创建,不然函数使用完后节点就消失了,只能用malloc函数,而c++上优先使用new和delete
//因为想到使用malloc函数会返回指针类型,所以声明时的数据类型就写的是struct LinkNode*,而不是struct LinkNode
//指向头节点的指针就代表着头结点
struct LinkNode * header=malloc(sizeof(struct LinkNode));
header->data=-1;//header在此处是指针,指向头结点,而header->data则是获取头结点中的数据,又因为头结点是没有数据的,我们可以任意给他一个值,反正也不会使用它
header->next=NULL;//初始时只有头结点,后面没有节点,而header->next表示下一个节点的地址,还没有设置,哪里来的地址呢,因此设为空
//等待用户输入
//设置一个尾部指针
struct LinkNode *pRear=header;//rear是尾的意思,刚开始默认这个尾部指针和头指针是一回事
int val=-1;//相当于初始化data
while(1)//利用循环不断创建新节点,不断赋值
{
printf("请输入插入的数据:\n");
scanf("%d",&val);
if(val==-1)//为了让用户输入一个与初始值不同的值
{
break;
}
//判断结束后,不符合时用户就可以进行第二次输入
//插入数据时,是在头结点后依次向后插,但再插入的过程中尾部一直发生变化,这样在每次插入时,都需要找到尾部结点,有点麻烦。那能不能在插入的过程中先设置一个指针,让指针始终指向尾部,先让指针指向头结点,因为初始后面没有节点,随着逐渐插入结点,这个指针也跟着向后移动,直到真正的尾部
//数据有了,此时要创建新结点来接受这些数据了
//动态链表中创建节点就是创建指针
struct LinkNode *newnode=malloc(sizeof(struct LinkNode));
newnode->data=val;//将刚才输入的值赋给新结点
newnode->next=NULL;//因为现在只创建了一个新结点,新结点相对头结点来说是最后一个,所以后面为空
//将新结点插入到链表中
//因为pRear初始时指向头结点,如果要连接下一个结点,必须获得下一个节点的位置,然后让pRear再指向第二个结点,依此类推,这个过程和遍历有点像,但不是遍历,而是不断让pRear指向新尾部,直到真正的尾部
pRear->next=newnode; //newnode是指向新结点的指针,随着结点插入,newnode不断更新,新结点的地址不断更新,pRear->next,初始next是头结点的next,这样可以获得下一个结点的地址,newnode不断赋值给pRear->next,pRear这个指针也就不断去寻找新的尾部
//因为newnode初始化是空的,因此这里给不给值都没关系
//更新尾部指针指向
pRear=newnode;//因为newnode指针始终指向新创建的结点,如果想让pRear这个尾部指针移动,就把newnode的值赋给pRear
//小总结:上面这两行代码代表不同含义。第一行是让头结点的next指向newnode,而pRear又是指向头结点的next,也就相当于是next的内容,也就是新结点的地址,就是newnode,目的是获取新节点的地址。而第二行
是为了改变尾部指针的指向,让它不断去寻找新的尾部,直到真正的尾部。
}
//结束之后,我们要获得这个链表,就返回它的头结点就行了
return header;
}