线性表的链式存储方式:
用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素与其后继元素的逻辑关系,除了本身数据信息之外,还需要存储一个指示其直接后继的信息。这两部分,称为结点,数据本身信息叫做数据域,存储后继位置信息的叫做指针域。
下图是链式存储结构示意图:
如下图,我们可以很清晰的看到,每个结点是怎么组成的:
我们把链表中的第一个结点的存储位置叫做头指针,整个链表的存储必须从头指针开始;由于最后一个结点没有后继元素,那么最后一个结点指针域为“空”(通常用“ NULL ”或者“ ^ ”表示空指针)。一般情况下,头结点的数据域不存储数据元素,而是存储线性表的长度等公共数据。如图所示:
名词解释:
- 首元结点:是指链表中存储第一个数据元素的结点。
- 头结点:是在首元结点前附设的一个结点,其指针域指向首元结点。
- 头指针:是指向链表中第一个结点的指针。
单链表的存储结构描述:
typedef struct LNode{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
}LNode,*LinkList; //LinKlIST为指向结构体LNode的指针类型
为了提高程序的可读性,对同一结构体指针类型起了两个名字,LinkList 与 LNode *,两者本质上是等价的。通常习惯上用LinkList定义单链表,强调定义的是某个单链表的头指针;用LNode * 定义指向单链表中任意结点的指针变量。例如,若定义LinkList L,则L为单链表的头指针,若定义LNode *p,则p为指向单链表中某个结点的指针,用*p代表该结点。当然也可以使用定义LinkList p,这种定义形式完全等价于LNode *p。
从这个描述中我们可以得出,每个结点都是由存放数据元素的数据域和存放后继结点地址的指针域组成。假设p是指向第i个元素的指针,则该结点的数据域可以用p->data来表示,该结点的指针域可以用p->next来表示。p->next是指向第i+1个结点的指针,即,如果p->data=,则p->next->data=。如图所示:
1.单链表的初始化:
【算法步骤】
- 生成新节点作为头结点,用头指针L指向头结点
- 头结点的指针域置空
Status InitList(LinkList &L)
{
L=new LNode; //生成新结点作为头结点,用头指针L指向头结点
L->next=NULL; //头结点置空
return OK;
}
2.单链表的取值 O(n)
【算法步骤】
- 用指针p指向首元结点,用j做计数器初值赋值为1
- 从首元结点开始依次顺着链域next 向下访问,只要指向当前结点的指针p不为空(NULL),并且没有达到序号i的结点,则执行:
- p指向下一个结点;
- 计数器j++。
- 退出循环时,如果指针为空,或者i值不合法时(i>表长,i<=0)取值失败,返回ERROR;否则取值成功,此时j=i,p指向结点的就是i的结点,用参数e保存当前结点数据域,返回OK
Status GetElem(LinkList L,int i,ElemType &e)
{
LNode *p=L->next; //p指向首元结点,
int j=1; //计数器赋值为1
while(p&&j<i) //当p不为空 j不到i的时候向后扫描
{
p=p->next; //p指向下一个结点
j++; //计数器++
}
if(!p||j>i) //i值不合法 i大于表长或i小于等于0
return ERROR;
e=p->data; //取第i个结点的数据域
return OK;
}
3.单链表的按值查找 O(n)
【算法步骤】
- 用指针p指向首元结点
- 从首元结点开始扫描,只要p不为空或者没有找到与e相等的数据时,执行:p指向下一个结点
- 返回p,若查找成功返回p(地址),否则查找失败,p的值是NULL。
LNode *LocateElem(LinkList L,ElemType e)
{
//在头结点的单链表L中查找值为e的元素
LNode *p=L->next; //初始化,p指向首元结点
while(!p&&p->data!=e) //一直扫描,直到p为空或者找到与e相等的结点时结束 循环
{
p=p->next; //p指向下一个结点
}
return p; //查找成功返回e的结点地址p,查找失败p为NULL
}
4.单链表的插入 O(n)
在链表的第i个位置插入新元素e,我们先上图:
链表就是链子啊,不过在编程的时候抽象了一点,每个链子都是数据元素罢了。我们在现实生活中,如果链子断了,我们为了保证链子够长,必须要加一个新的锚点,那就很显然了:先找一个新的零件(生成新的结点),再把原来链子的右端对上新链子(即s->next = p->next),最后再把左端链子接上新零件就ok了(即p->next=s)。
【算法步骤】
- 声明一个结点p指向链表的第一个结点,初始化j=1
- 当j<i时,遍历链表,p的指针向后移动,j++
- 若链表末尾p为空时,则说明第i个元素不存在
- 否则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给s->data
- s->next=p->next; p->next=s;
- 返回成功
Status ListInsert(LinkList &L,int i,ElemType e)
{
//在带头结点的单链表L中第i个位置插入值为e的新结点
LNode *p=L;
int j=0;
while(p&&(j<i-1)) //查找第i-1个结点,p指向该结点
{
p=p->next;
j++;
}
if(!p||j>i-1)
return ERROR; //i>表长或者i<1
LNode *s=new LNode; //生产新结点*s
s->data=e; //将结点*s的数据域置为 e
s->next=p->next; //将结点*s的指针域指向结点
p->next=s; //将结点*p的指针域指向结点 *s
return OK;
}
5.单链表的删除 O(n)
同链表的插入一样,我们先看图:
要删除第i个结点,我们应该先找到被删除结点的前驱,然后将它的前驱结点指针绕过,指向它的后继就OK了。
即p->next = p->next->next,因为开辟了空间,再删除的时候要把这个空间释放了,所以用一个新的指针q来存储p->next,以备后续释放。
【算法步骤】
- 声明结点p指向空结点,初始化计数器j=0
- 当j<i时,遍历链表,让p的指针向后移动,不断指向下一个结点,j++
- 若链表末尾为空,则说明第i个结点不存在,返回ERROR
- 否则,将待删除的结点地址赋给q,以备释放
- q=p->next; p->next=q->next;
- 释放q,返回OK
Status ListDelete(LinkList &L,int i)
{
//在带头结点的单链表L中,删除第i个元素
LNode *p=L;
int j=0;
while(p->next&&j<i-1) //查找第i-1个结点,p指向该结点
{
p=p->next;
j++;
}
if(!(p->next)||j>i-)
return ERROR;
LNode *q=p->next; //临时保存被删结点以备释放
p->next=q->next; //改变删除结点的前驱结点的指针域
delete q; //释放被删除结点空间
return OK;
}
因为要查找第i个结点的位置,所以就要遍历链表,所以插入、删除操作的时间复杂度为O(n).
6.单链表的创建
单链表的创建即给定n个元素,然后插入到链表当中。
根据结点插入的位置不同,链表的创建方法又分为前插法和后插法。
a、前插法创建单链表 O(n)
顾名思义,前插法,就是把要插入的n个元素,每个都时插入到表头,举一个例子,如果我们要插入5个数字1、2、3、4、5;那么利用前插法插入到链表当中后,他们从头到尾的内容为:5、4、3、2、1。 即输入顺序和线性表中的逻辑顺序是相反的。
- 【算法步骤】
- 创建一个只有头结点的空链表
- 根据待创建链表包括的元素个数n,循环n次执行以下操作:生成一个新结点*p、输入元素值赋给新结点*p的数据域、将新结点*p插入到头结点之后
void CreateList_H(LinkList &L,int n)
{
L=new LNode; //先建立一个带有头结点的空链表
L->next=NULL;
for(int i=0;i<n;i++)
{
LNode *p=new LNode; //生成新结点*p
cin >> p->data; //输入元素赋值给*p的数据域
p->next=L->next;
L->next=p; //将新结点*p插入到头结点之后
}
}
b、后插法创建单链表 O(n)
后插法,将每个元素插入到链表的尾部来创建单链表。每次申请一个新节点,读入值。为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。
【算法步骤】
- 创建一个只有头结点的空链表
- 尾指针r初始化,指向头结点
- 根据需要元素个数n,循环n次执行以下操作:生成一个新结点*p、输入元素值赋给新结点*p的数据域、将新结点*p插入到尾结点*r之后、尾指针r指向新的尾结点*p
void CreateList_R(LinkList &L,int n)
{
L=new LNode;
L->next=NULL;
LNode *r=L; //尾指针r指向头结点
for(int i=0;i<n;i++)
{
LNode *p=new LNode; //生成新结点
cin >> p->data;
p->next=NULL;
r->next=p;
r=p;
}
}
7.单链表的整表删除 O(n)
【算法步骤】
- 声明结点p和q
- 将第一个结点赋值给p
- 循环:将下一个结点赋值给q,释放p,将q赋值给p
Status ClearList(LinkList &L)
{
LNode *p,*q;
p=L->next; //p指向第一个结点
while(p) //没到表尾
{
q=p->next;
delete p;
p=q;
}
L->next=NULL; //头结点指针域为空
return OK;
}
8.递归遍历单链表
【算法步骤】
- 若p为NULL,递归结束返回
- 否则输出p->data,p指向后继结点继续递归
void TraverseList(LinkList p)
{
if(p)
{
cout << p->data << endl;
TraverseList(p->next);
}
}
简单实现单链表:https://blog.csdn.net/lesileqin/article/details/88094845
博客内容借鉴于:
①《数据结构》 作者:严蔚敏
②《大话数据结构》 作者:程杰