线性表的链式存储

前言

紧承上篇顺序存储,这一篇来说说链式存储。链式存储在线性表中运用比较多。
我们先来说说顺序存储的优缺点,以此引出单链表的定义。

顺序存储结构的评价:

   优点:
   			  (1)是一种随机存取结构,存取任何元素的时间是一个常数,速度快
			  (2)结构简单,逻辑上相邻的元素在物理上也是相邻的
			  (3)不需要使用指针,节省存储空间
   缺点:
              (1)插入和删除元素要移动大量元素,消耗时间
			  (2)需要一块连续的空间
			  (3)插入元素可能发生“溢出”
			  (4)自由区的存储空间不能被其他数据占用(共享),存在浪费空间的问题  

一、单链表的结构类型定义

1.定义:线性表的每个结点分散的存储在内存空间中,先后依次用一个指针串联起来
2.单链表分为:带表头结点和不带表头结点
3.一般形式:

			①不带表头结点的单链表: 
					头指针   首结点							  尾结点 
					head--->a1_______ --->a2_____  --->...--->an____
						  	data  next 
			其中:data为数据域,next为指针域/链域
			当head==NULL时,为空表;否则为非空表,此时在首结点*head中会存放数据
			②带表头结点的单链表
			a.非空表:
				头指针      表头结点	    首结点			  尾结点 
					head--->///_______ --->a1_____  --->...--->an____
						  				data  next
			b.空表:
					头指针      表头结点
					head--->///______
			注:表头结点不用来存放任何数据
			其中:head指向表头结点,head->data不放元素,head->next指向首结点a1,当head->next==NULL,为空表,否则为非空表

4.结点结构:
(1)eg1:C语言的“结构”类型

struct node
{
	ElemType data;//data为抽象元素类型
	struct node *next;//next为指针类型 
};
//指向结点的指针变量head,p,q说明:struct node *head,*p,*q 

(2)eg2:用typedef定义指针类型

typedef struct node
{
	ElemType data;//data为抽象元素类型
	struct node *next;//next为指针类型
}node,*Linklist
//指针变量head,p,q的说明:Linklist head,p,q 

二、单链表的算法:生成先进先出单链表、后进先出单链表

1.生成单链表(链式队列)

(1)先进先出单链表(插入到表尾):先进来的元素通过变化表头指针可以先被删除

/*eg1:输入一列整数,以0为结束标志,生成“先进先出”单链表。若输入:1,2,3,4,5则生成
head–>///–>1____–>2–>3–>4_–>5
ps:从1开始输入
首先定义结点所占空间大小

#define LENG sizeof(struct node)//结点所占的单元数 
struct node//定义结点类型 
{
	int data;//data为整型 
	struct node *next;//next为指针类型 
}; 

#include<stdio.h>
#include<malloc.h>
struct node *creat1()
{
	struct node *head,*tail,*p;//变量说明
	int e;
	head = (struct node*)malloc(LENG);//生成表头结点
	tail = head;//尾指针指向表头
	scanf ("&d",&e);//输入第一个数
	while (e!=0)//不为0
	{
		p = (struct node*)malloc(LENG);//生成新结点
		p->data = e;//装入输入的元素e
		tail->next = p;//新结点连接到表尾
		tail = p;//尾指针指向新结点
		scanf ("&d",&e);//再输入一个数	
	}
	tail->next = NULL;//尾结点的next置空
	return head;//返回头指针 
}

2.后进先出单链表(插入到表头):后进来的元素通过变化表头指针可以先被删除

eg:输入一列整数,以0为结束标志,生成“后进先出”单链表。输入:1,2,3,4,则生成
head–>///–>4–>3__–>2__–>1__
ps:从4开始输入 ,输入顺序与上面相反
首先定义结点所占空间大小

#define LENG sizeof(struct node)//结点所占的单元数 
struct node//定义结点类型 
{
	int data;//data为整型 
	struct node *next;//next为指针类型 
}; 

#include<stdio.h>
#include<malloc.h>
struct node *creat2()	//生成“后进先出”单链表
{
	struct node *head,*p;
	int e; 
	head = (struct node*)malloc(LENG);//生成表头结点
	head->next = NULL;//置为空表
	scanf ("&d",&e);//输入第一个数
	while (e!=0);//不为0
	{
		p = (struct node *)malloc(LENG);//生成新结点
		p->data = e;//输入数
		p->next = head->next;//新结点指针指向原来的首结点
		head->next = p;//表头结点的指针指向新结点
		scanf ("%d",&e);//再输入一个数 
	} 
	return head;//返回头指针 
}	

3.插入一个结点

(1)eg1:在已知p指针的结点后插入一个元素x

f = (struct node *)malloc(LENG);//生成		
f->data = x;//装入元素x
f->next = p->next;//新结点指向p的后继
p->next = f;//新结点成为p的后继	

(2)eg2:在已知p指针的结点前插入一个元素x

f = (struct node*)malloc(LENG);//生成
f->data = x;//装入元素x
f->next = p;//新结点成为p的前驱
q->next = f;//新结点成为q的后继 
		

三、表头结点的作用

1.不带表头结点

例:假定有一个无表头结点的递增排序的链表,插入一个新结点,使其仍然递增。
	设新插入结点为f,插在qp之间(q在p前)
	
 (分析):①理想情况:p,q均不为空
 			f->next = p;
			 q->next = f;
			 操作后q不可能为空 
	注意:每次扫描单链表之前,要做如下初始化 
			q = NULL;
			p = Head;
			
			②特殊情况: p,q可能为NULL
 			(1)同时为空 :往空表插入第一个结点 
 			(2)仅p为空:尾部插入,即数据插入到链表的尾部 
 			(3)仅q为空:首部插入 ,即插入的数据作为单链表的第一个结点
 				ps:不带表头结点,qp为空情况较多,造成算法复杂 

流程图如下:在这里插入图片描述

(1)递增有序单链表的算法 (不带表头)

struct node* creat3_1 (struct node *head,int e)
{
	q = NULL;
	p = head;//q,p扫描,查找插入位置
	while (p&&e>p->data)//未扫描完(p不为空),且e大于当前结点
	{
		q = p;
		p = p->next;//q,p后移,查下一个位置 
	} 
	f = (struct node*)malloc(LENG)//生成新结点
	f->data = e;//装入元素e 
	
	f->next = p;
	q->next = f;//插入新结点  
}

(2)应用:输入一系列整数,以0结尾,要形成递增的单链表,反复调用该函数即可

int main()
{
	struct node *head;//定义头指针
	head = NULL;//置为空表
	scanf ("%d",&e);//输入整数
	while (e!=0)//不为0,未结束
	{
		head = creat (head,e);//插入递增有序单链表
		scanf ("%d",&e);//输入整数 
	}

2.带表头:一定有一个不为空的头结点,q永远不为空

附流程图:
在这里插入图片描述
生成带头结点的递增有序单链表(不包括0)

//递增有序单链表的算法 (带表头) 
void creat3_2 (struct node *head,int e)
{
	q = head;
	p = head->next;//q,p扫描,查找插入位置
	while (p&&e>p->data)//未扫描完(p不为空),且e大于当前结点
	{
		q = p;
		p = p->next;//q,p后移,查下一个位置 
	} 
	f = (struct node*)malloc(LENG)//生成新结点
	f->data = e;//装入元素e 
	
	f->next = p;
	q->next = f;//插入新结点 
}

//主函数算法
int main()
{
	head = (struct node*)malloc(LENG);//生成表头结点
	head->next = NULL://置为空表
	scanf ("%d",&e);//输入整数
	while (e!=0)
	{
		creat3_2 (head,e);//插入递增有序单链表head
		scanf ("%d",&e);//输入整数 
	} 
 } 

四、单链表的插入删除

1.单链表的指定位置插入新元素

(1)输入:头指针L、位置i、数据元素e
输出:成功返回OK,否则返回ERROR
即在指定位置i上插入一个新元素,使得插入元素成为链表的第i个元素,需用指针p从头扫描该单链表,并对所访问的结点进行计数。

(2)问题 :计数是在第i个位置上结束吗
若在此结束,p指向第i个位置,新元素就要插入到p指向的结点之前
而我们之前分析过,如果插入到某个结点之前,就需要另一个辅助指针q来指向p的前驱结点

简化方法:p指向第i-1个结点时就停止,此时新元素插入到p指向的结点之后即可

扫描的过程:
执行:p = L
当p不为空,执行:p = p->next (i-1)次
定位到第i-1个结点
ps:当i<1或p为空时插入位置错误

算法:单链表的指定位置插入新元素

int insert (Linklist &L,int i,ElemType e)
{
	p = L;
	j = 1;
	while (p && j<i)//扫描定位 
	{
		p = p->next;//p后移,指向下一个位置
		j++; 
	}
	if (i<1 || p==NULL)//判断插入点位置合法性,插入点错误
		return ERROR; 
	
	f = (LinkList)malloc(LENG);//生成新结点
	f->data = e;//装入元素e
	f->next = p->next;
	p->next = f;//插入新结点
	return OK; 	
}

算法三大部分:
① 扫描定位
② 判断插入点位置合法性
③在p指定的位置后插入新结点

2.在单链表中删除一个结点

(1)删除算法1:在带表头结点的单链表中删除元素值为e的结点

int Delete (Linklist head,ElemType e)
{
	struct node *q,*p;
	q = head;
	p = head->next;//q,p扫描
	while (p && p->data!=e)//查找元素为e的结点
	{
		q = p;//记住前一个结点
		p = p->next;//查找下一个结点 
	}
	if (p)//有元素为e的结点
	{
		q->next = p->next;//删除该结点
		free (p);//释放结点所占空间
		return YES; 
	}
	else 
		return NO;//没有删除结点 
 } 

(2)删除算法2:在带表头结点的单链表中删除指定位置的元素
扫描的过程:
执行:p = L
当p不为空,执行:p = p->next (i-1)次
定位到第i-1个结点
ps:当i<1或p->next为空时删除位置错误

int Delete (Linklist &L,int i,ElemType &e)
{
	p = L;
	j = 1;
	while (p->next && j<i)//循环结束时p不为空
	{
		p = p->next;//p后移,指向下一个位置
		j++; 
	}
	if (i<1 || p->next==NULL)//删除点错误
		return ERROR;
	
	q = p->next;//q指向删除结点
	p->next = q->next;//从链表中摘出
	e = q->data;//取走数据元素值
	free (q);//释放结点空间
	return OK; 
}

五、单链表的合并

eg:将两个有序单链表La和Lb合并为有序单链表Lc(该算法利用原单链表(含表头结点)的结点)
La–>///–>2–>5__
Lb–>///–>3–>8__–>20
需要用到三个指针,pa扫描La,pb扫描Lb,pc指向合并后链表Lc的当前表尾结点

输入:两单链表的头指针
输出:合并后的单链表的头指针

struct node *merge(struct node *La,struct node *Lb)
{
	struct node *pa,*pb,*pc;
	pa = La->next;//pa指向表La的首结点
	pb = Lb->next;//pb指向表Lb的首结点
	pc = La;//使用La当代头结点,pc为尾指针
	free (Lb);//释放Lb的头结点 
	
	//比较pa和pb指向结点的数据域大小 
	while (pa && pb)//两表均不为空
	{
		if (pa->data <= pb->data)//取La的一个结点
		{
			pc->next = pa;//插在表Lc的尾结点之后
			pc = pa;//变为Lc的新尾结点
			pa = pa->next;//移向La下一个结点 
		}
		else
		{
			pc->next = pb;//插在表Lc的尾结点之后
			pc = pb;//变为Lc的新尾结点
			pb = pb->next;//移向Lb下一个结点 
		} 
	}
	if (pa)
		pc->next = pa;//若pa不为空则插入表La的剩余段
	else
		pc->next = pb;//若pb不为空则插入表Lb的剩余段
	return La;
}

六、循环链表和双向链表的概念

1.循环链表:最后一个结点next指向链表第一个结点,形成环

1.1 一般形式:
(1)带表头结点的非空循环单链表

H–> ///–> a1->a2__–>…–>an__
依次为:头指针 表头结点 首结点 尾结点

有:H->next≠H,H≠NULL

(2)带表头结点的空循环单链表
有:H->next==H,H≠NULL

1.2 只设尾指针的循环链表
(1)非空表
///–> a1->a2__–>…–>an__<-tail
依次为:表头结点 首结点 尾结点 尾指针

有:tail指向表尾结点
tail->data == an
tail->next 指向表头结点
tail->next->next 指向首结点
tail->next->next->data ==a1

(2)空表
///__ <-tail
依次为:表头结点 尾指针
tail->next == tail

问:只设尾指针的循环链表有什么用处?
举例:两循环链表首尾相连
①只设头指针:必须通过头指针扫描第一个循环链表,直至尾结点,将尾结点next指向第二个链表的首结点
再扫描第二个循环链表至尾结点,将尾结点next指向第一个循环链表的表头结点。
分析知:若要收尾相连,则必须从头至尾扫描链表。假设表长分别为m、n。
时间复杂度:O(m+n)

②只设尾指针:不需扫描整个链表,对指针进行相应变化即可
设p2为第二个链表头指针
p2 = tail->next
tail2->next = tail1->next
tail1->next = p2->next
free (p2) 将p2指向表头结点释放
时间复杂度O(1)

1.3 循环链表算法举例

eg:求以head为头指针的循环单链表的长度,并依次输出结点的值
注:由于是循环链表,需注意循环终止条件

int length (struct node *head)
{
	int leng = 0;//长度变量初值为0
	struct node *p;
	p = head->next;//p指向首结点
	while (p!=head)//p未移回到表头结点
	{
		printf ("%d",p->data);//输出
		leng++;//计数
		p = p->next;//p移向下一结点 
	}
	return leng;//返回长度值 
}

2.双向链表:为方便访问结点的前驱结点

2.1 双向链表的结点结构

第i项的构成:prior data next
前驱a(i-1) ai 后继a(i+1)

结点类型定义

struct Dnode
{
	ElemType data;//data为抽象元素类型
	struct Dnode *prior,*next;//prior,next为指针类型 
};
//或者
typedef struct Dnode
{
	ElemType data;//data为抽象元素类型
	struct Dnode *prior,*next;//prior,next为指针类型
}*DLList//DLList为指针类型

2.2 双向链表的一般形式
(1)非空表
L–> ///–> a1->a2–>…–>an
头指针 表头结点 首结点 尾结点
有:L为头指针,L指向表头结点,L->next指向首结点

L->next->data==a1

L->prior指向尾结点
L->prior->data==an
L->next->prior ==L->prior->next ==NULL

(2)空表
L–> ///
头指针 表头结点
有: L->next = L->prior == NULL

2.3 双向循环链表
(1)空表
有: L->next = =L->prior == L

(2)非空表
设p指向a1
有:p->next指向a2,p->next->prior指向a1
所以,p = =p->next->prior
同理,p == p->prior->next

(3)已知指针p指向结点B,删除B(设分别有结点ABC)
执行:
p->prior->next = p->next;//结点A的next指向结点C
p->next->prior = p->prior;//结点C的prior指向结点A
free §;//释放结点B占有的空间

(4)已知指针p指向结点C,在A、C之间插入结点B,已知指针f指向结点B
执行:
f->prior = p->prior;//结点B的prior指向结点A
f->next = p;//结点B的next指向结点C
p->prior->next = f;//结点A的next指向结点B
p->prior = f;//结点C的prior指向结点B

3.应用

举例:用单链表来做一元多项式的加法
C(x) = A(x) =B(x) 结点:coef(系数) expn(指数) next
算法步骤:
1.pa、pb分别指向首元素结点,产生C(x)的空链表,pc指向表头结点
2.pa不为空且pb不为空,重复下列操作
2.1 若pa->expn = pb->expn
a. 若pa->coef+pb->coef不为零 ,产生新结点,添加至pc后,pc指向新结点。pa、pb后移
b. 若pa->coef+pb->coef为零,pa、pb后移
2.2 若pa->expn < pb->expn,根据pa产生新结点,添加到pc后,pc指向新结点,pa后移
2.3 若pa->expn > pb->expn,根据pb产生新结点,添加到pc后,pc指向新结点,pb后移
3.pa为空,取pb剩余结点产生新结点,pb为空,取pa剩余结点产生新结点,依次添加到pc的后面

注:这里就不给详细代码啦,感兴趣可以自己试试哦

总结

链表的内容比较多,这里给出了一些基本操作。过几天会再出一篇关于栈的基本用法。
ps:代码非原创
如有错误,欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬茶@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值