第二章——线性表的链式表示和实现

1.链式存储结构定义:

结点在存储器的位置上是任意的,即逻辑相邻的数据元素在物理上不一定相邻。

2.单链表、双链表、循环链表:

单链表:结点只有一个指针域链表
双链表:结点有两个指针域的链表
循环链表:首尾相连的链表

3.头指针、头结点、首元结点:

头指针:指向链表中第一个结点的指针
头结点:在链表的首元结点之前附加的一个结点
首元结点:是指链表中存储第一个元素的结点

4.引入头结点的好处:

1.由于第一个结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置上的操作一致,无需进行特殊处理。
2.无论链表是否为空,其头指针都指向头结点的非空指针,因此空表和非空表的处理也得到了统一。

4. 链表的特点

1.结点在存储器中的位置是任意的,逻辑上相邻的数据元素物理上不一定相邻
2.访问时是通过头指针进入链表,并通过每个结点的指针域依次向后 扫描其余结点,所以寻找第一个结点和最后一个结点所花时间并不相同。
附:顺序表->随机存取 链表->顺序存取

存储学生、学号、姓名、成绩的单链表结点类型定义如下:
typedef struct student{
	char num[8];	//数据域
	char name[8];	//数据域
	int score;		//数据域
	struct student*next;	//指针域
}LNode,*LinkList
5.单链表的基本操作:
5.1单链表的初始化
Status InitLis_L(LinkList &L)
{
	L=(LinkList)malloc(sizeof(LNode));//使用malloc动态分配一个新节点
	L->next=NULL;
	return OK;
}
5.2判断单链表是否为空
Status ListEmpty(LinkList &L)
{
	if(L->next)	//非空
		return 0;
	else 
		return 1;
}
5.3单链表的销毁
//单链表的销毁:链表销毁后是不存在
Status DestoryList(LinkList &L)
{
	Lnode *p;
	while(L)
	{
		p=L;
		L=L->next;
		free(p);
	}
}
5.4单链表的清空

清空后的单链表仍然存在,但是链表中已没有元素,成为空链表(头指针和头结点仍然还在)

/*
依次释放所有的结点,并将头结点的指针域置空
*/
Status ClearList(LinkList &L)
{
	LNode *p;
	LNode *q;
	p=L->next;//此时p的位置在首元结点
	while(p)
	{
		q=p->next;
		free(p);
		p=q;
	}
	L->next=NULL;//将L链表中的头结点置空
	return OK;
}
5.5单链表的表长
//算法思想:从首元结点开始,依次计数所有的结点
Status ListLength_L(LinkList L)
{
	LinkList p;
	p=L->next;   
	int i=0;
	while(p)
	{
		i++;
		p=p->next;
	}
	return i;
}
5.6单链表的取值

取单链表中第i个元素的值

/*
算法思想:
1.从第一个结点(L->next)出发顺链扫描,用指针p指向当前扫描到的结点,p的初始值是p=L->next;
2.j做计数器,累计当前扫描过的结点数,j的初值为1;
3.当p指向扫描到下一结点时,计数器j就加1;
4.当j==i时,p所指的结点就是要找的第i个结点;
*/
Status GetElem(LinkList L,int i,ElemType &e)
{
	LNode *p;
	p=L->next;    //初始化p,此时p指向首元结点
	int j=1;	  //初始化计数器为1
	while(p&&j<i)  //顺链域向后扫描,直到p为空或者p指向第i个元素
	{
		p=p->next;  
		j++;
	}
	if(j>i||!p) return ERROR;  //i值不合法,i>n或者i<=0
	e=p->data;   //取第i个结点的数据域
	return OK; 
}
5.7单链表的按值查找

根据指定数据获取该数据所在的位置(地址)

/*
1.从第一个结点开始和e相比较
2.如果找到一个其值与e相等的数据元素,则返回其在链表中的位置或者地址
3.如果查遍整个链表都没有找到其值和e相等的元素,则返回0或者“NULL”
*/
LNode *LocateElem_L(LinkList L,ElemType e)
{
	LNode *p;
	p=L->next;
	while(p&&p->data!=e)
	{
		p=p->next;
	}
	return p;
}
5.8单链表的插入结点

在第i个结点前插入值为e的新节点

/*
算法步骤:
1.首先找到i-1个位置的结点,并记为p;
2.生成一个新的数据域为e的新节点s;
3.插入新结点:s->next=p->next      p->next=s;
*/
Status ListInsert_L(LinkList &L,int i,ElemType e)
{
	LNode *p=L->next;
	LNode *s;
	int j=1;
	while(p&&j<i-1)    //查找第i-1个结点,p指向该节点
	{
		p=p->next;
		j++;
	}
	if(!p||j>i-1) return ERROR;
	s=(LinkList)malloc(sizeof(LNode));//生成新节点s;
	s->data=e;
	s->next=p->next;//将结点*s的指针域指向第i个结点
	p->next=s;   //将结点*p的指针域指向结点*s
}
5.9单链表的删除第i个结点
/*
算法思想:
1.首先要找到删除结点的前驱结点,将其前驱结点的位置设为P,保存要删除结点的值
2.让p->next指向第i+1的结点
*/
Status ListDelete_L(LinkList &L,int i,ElemType &e)
{
	LNode *p=L->next;
	int j=1;
	while(p&&j<i-1) //查找第i-1个元素
	{
		p=p->next;
		j++;
	}
	if(!p||j>i-1) return ERROR;  //查找位置不合法
	LNode *q=p->next;//临时保存被删结点的地址
	p->next=p->next->next;//改变删除结点前驱结点的指针域
	e=q->data;
	free(q);  //释放删除结点的空间
	return OK;
}
6. 单链表的时间效率分析

当学完单链表的插入和删除操作后,和顺序表做了一个简单的比较,发现顺序表是随机存取,但是它插入和删除元素的时候需要大量的移动元素,所以它的时间复杂度为O(n),但是链表虽然插入和删除操作只需修改前后指针即可,不需要移动,但需要按照顺序先查找元素所在的前一个位置结点,然后再执行删除和插入的操作,其算法时间复杂度也是O(n),那为何说链表的效率高呢?
答:

①因为O(n)的内涵不同,一个是读的效率O(n),一个是写的效率O(n);数组擅长读取而链表擅长写入。在写入场景中,数组链表的复杂度是定位写入复杂度之和,都是O(n),但写入比定位的O(n)慢很多,所以两个表面看起来一样的O(n)的实际时间还是差很多。
② 当我们要知道插入和删除的位置时,链式存储的优越性就表现出来了,假如我们要在a10与a11之间插入10个元素,那么顺序存储每插入一个元素后面的元素就要移动一次位置,每次都是O(n)。而链式存储,只需要第一次时找到要插入的那个位置,后面的就只是赋值移动指针而已,时间复杂度为O(1)。
因此,可以得出一个结论:对于插入或者删除操作越频繁的操作,单链表的效率优势就越是明显。

7. 单链表的建立
7.1.单链表的建立——头插法

元素插在链表的头部,也叫前插法

/*
算法思想:
1.从一个空表开始,重复的读入数据
2.生成新的结点,将读入数据存放到新的结点的数据域中
3.从最后一个结点开始,依次将各结点插入到链表的前端
*/
void CreateList(LinkList &L,int n)
{
	L=(LinkList)malloc(sizeof(LNode));
	L->next=NULL;
	for(int i=0;i<n;i++)
	{
		LNode *p=(LinkList)malloc(sizeof(LNode));
		scanf("%d",p->data);    //输入元素值赋给新结点*p的数据域
		p->next=L->next;        //将新结点*p插入到头结点之后
		L->next=p;
}

附:采用头插法建立单链表,读入数据顺序和生成的链表中元素的顺序是相反的。每个结点插入时间是O(1),则总的时间复杂度是O(n)。

7.1.单链表的建立——尾插法

元素插在链表的尾部,也叫后插法,同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r来指向链表的尾结点。

/*
算法步骤:
1.创建只有头结点的空链表;
2.尾指针r初始化,指向头结点
3.根据创建链表包括元素的个数n,循环n次执行以下操作:
①生成一个新结点*P,
②输入元素值赋给新结点*p的数据域
③新结点*p插入到尾结点*r之后
④尾指针r指向新的尾结点*p
*/

void CreateList_R(LinkList &L,int n)
{
	L=(LinkList)malloc(sizeof(LNode));
	L->next=NULL;
	LNode *r=L;
	for(int i=0;i<n;i++)
	{
		LNode *p=(LinkList)malloc(sizeof(LNode));
		scanf("%d",p->data);
		p->next=NULL;
		r->next=p;//将新结点*p插入到尾结点*r之后
		r=p;   //r指向新的尾结点*p
	}
}
8. 循环链表

循环链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环。循环链表可以从表中任意一个结点开始遍历整个链表,使得操作效率更高。

8.1.如何将带尾指针的循环链表合并

分析操作:
①p存表头结点
②tb表头连接到ta表尾
③释放tb表头结点
④修改指针

void Connect(LinkList Ta,LinkList Tb)
{
	LNode *p=Ta->next;//创建一个新的结点存Ta表的头结点
	Ta->next=Tb->next->next;//让Tb的首元结点接在Ta表的尾结点的后面
	free(Tb->next);//将Tb的头结点释放掉
	Tb->next=p;//让Tb的尾结点和Ta的头结点相连
	return Tb;
}

时间复杂度是O(1)

9. 双向链表
9. 1引入双向链表的原因

单链表查找某结点的后继结点比较容易,时间复杂度是O(1),但是查找某结点的前驱结点要从表头开始出发,时间复杂度是O(n)。而双向链表可以克服单链表这一缺点。

双向链表:在单链表的每一个结点里面再增加一个指向其直接前驱的指针域prior,这样链表就形成了有两个方向不同的链,故称为双向链。

9. 2循环双向链表

双向链表的查询操作和单链表基本一致,但是删除和插入有了很大的变化,不仅要修改后继指针的位置,还需要修改前驱指针的位置。
头结点的前驱指针指向链表的最后一个结点。
最后一个结点的后继指针指向头结点。

9. 3双向链表的插入算法

①在带头结点的双向链表L中的第i个元素之前插入元素e

Status ListInsert_DuL(DuLinkList &L,int i,ElemType &e)
{
    DuLNode *p=(DuLinkList)malloc(sizeof(DuLNode));
	if(!(p=GetElem_DuL(L,i))) return ERROR;
	DuLNode *s=(DuLinkList)malloc(sizeof(DuLNode));
	s->data=e;
	s->prior=p->prior;
	p->prior->next=s;
	p->prior=s;
	s->next=p;
	return OK;
}

②在带头结点的双向链表L中的第i个元素之后插入元素e

    s->next=p->next;
	p->next->prior=s;
	s->prior=p;
	p->next=s;

以上代码顺序并不是唯一,但是也不是任意的。

9. 4双向链表的删除操作

删除带头结点的双向链表L中的第i个元素、

//双向链表的删除
Status ListDelete_DUL(DuLinkList &L,int i,ElemType &e)
{
	DuLNode *p=(DuLinkList)malloc(sizeof(DuLNode));
	if(!(p=GetElem_DuL(L,i))) return ERROR;
	e=p->data;
	p->prior->next=p->next;//修改被删结点的前驱节点的后继指针
	p->next->prior=p->prior;//修改被删结点的后继节点的前驱指针
	free(p);
	return OK;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值