线性表
线性表的定义:
- 由n(n≥0)个数据元素(a1,a2,…, an)构成的有限序列。
- 记作: L=(a1,a2,…,an)
a i-1是ai的直接前驱 ai+1是a i的直接后继
a1 ──首元素 - 其没有前驱
an──尾元素 - 其没有后继 - 表长:线性表中元素的个数
- 空表:不含元素的线性表
线性表的基本操作:
1.InitList(&L) //构造空表L。
2.LengthList(L) //求表L的长度
3.GetElem(L,i,&e) //取元素ai,由e返回ai
4.PriorElem(L,ce,&pre_e) //求ce的前驱,由pre_e返回
5.InsertElem(&L,i,e) //在元素ai之前插入新元素e
6.DeleteElem(&L,i) //删除第i个元素
7.EmptyList(L) //判断L是否为空表
线性表的选择及算法:
- 例一:顺序表的插入操作
设 L.elem[0…maxleng-1]中有length个元素,在 L.elem[i-1]之前插入新元素e,(1<=i<=length+1)
- 算法一 :用指针指向被操作的线性表,静态分配
#define maxleng 100
typedef struct
{ ElemType elem[maxleng];//下标:0,1,...,maxleng-1
int length; //表长
} SqList;
SqList L;
int Insert1(SqList *L,int i,ElemType e)
{ if (i<1||i>L->length+1) return ERROR //i值不合法
if (L->length>=maxleng) return OVERFLOW //溢出
for (j=L->length-1;j>=i-1;j--;) //先移动后面元素(直到移完第i-1个元素)
L->elem[j+1]=L->elem[j]; //向后移动元素
L->elem[i-1]=e; //插入新元素
L->length++; //长度变量增1
return OK; //插入成功
}
过程如图示:
- 算法二:用引用参数表示被操作的线性表,静态分配
//L的定义同上
Status Insert2(SqList &L,int i,ElemType e) //此处的L不再是指针
{ if (i<1||i>L.length+1) return ERROR //i值不合法
if (L.length>=maxleng) return OVERFLOW //溢出
for (j=L.length-1;j>=i-1;j--;)
L.elem[j+1]=L.elem[j]; //向后移动元素
L.elem[i-1]=e; //插入新元素
L.length++; //长度变量增1
return OK //插入成功
} //总结:及将‘*L’、‘->’改为‘&L’、‘.’
- 算法三:用引用参数表示被操作的线性表,动态分配
#define LIST_INIT_SIZE 100 //初始长度
#define LISTINCREMENT 10 //增量
typedef struct
{ ElemType *elem;//存储空间基地址
int length; //表长
int listsize; //当前分配的存储容量
//(以sizeof(ElemType)为单位
} SqList;
SqList L;
int Insert3(SqList &L,int i, ElemType e)
{int j;
if (i<1 || i>L.length+1) //i的合法取值为1至n+1
return ERROR;
if (L.length>=L.listsize) /*溢出时扩充*/
{
ElemType *newbase;
newbase=(ElemType *) realloc(L.elem,
(L.listsize+LISTINCREMENT)*sizeof(ElemType));
if (newbase==NULL) return OVERFLOW; //系统空间已满,扩充失败
L.elem=newbase; //更新线性表
L.listsize+=LISTINCREMENT; //更新当前长度
}
//向后移动元素,空出第i个元素的分量elem[i-1]
for(j=L.length-1;j>=i-1;j--)
L.elem[j+1]=L.elem[j];
L.elem[i-1]=e; /*新元素插入*/
L.length++; /*线性表长度加1*/
return OK; //同静态分配
}
- realloc函数
- 用法:指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)。其中-新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;如果新的大小小于原内存大小,可能会导致数据丢失)
- 头文件:#include <stdlib.h> 有些编译器需要#include <malloc.h>,在TC2.0中可以使用alloc.h头文件
- 返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
- 注:不用时需要free
关于顺序结构的评价:
- 优点:
(1)是一种随机存取结构,存取任何元素的时间是一个
常数,速度快;
(2)结构简单,逻辑上相邻的元素在物理上也是相邻的;
(3)不使用指针,节省存储空间。 - 缺点:
(1)插入和删除元素要移动大量元素,消耗大量时间;
(2)需要一个连续的存储空间;
(3)插入元素可能发生“溢出”;
(4)自由区中的存储空间不能被其它数据占用(共享)。
线性表的链式存储结构
1.单链表:
- 1)不带表头结点的单链表:
- 带表头结点的单链表:(默认类型)
单链表的定义:
struct node //也可用typedef
{ ElemType data; //data为抽象元素类型
struct node *next; //next为指针类型
};
指向结点的指针变量head,p,q说明:
struct node *head,*p,*q;
生成单链表:
1.后进先出表:(head后插入)
head=malloc(sizeof(struct node)) //前面也可无强制类型转换,等式会自动转换成左侧数据类型
tail=head
p1=malloc(sizeof(struct node))
tail->next=p1 //可以结合scanf函数 给p的data域赋值
tail=p1
p2=malloc(sizeof(struct node))
tail->next=p2 //也可不用tail 写成p1->next=p2
tail=p2
..........
tail->next=NULL; //or pn=NULL
2.先进后出表:(head前插入)
head=(struct node *)malloc(LENG); //生成表头结点
head->next=NULL; //置为空表
p=(struct node *)malloc(LENG);//生成新结点
scanf(“%d”,&e);
p->data=e; //输入数送新结点的data
p->next=head->next; //新结点指针指向原首结点
head->next=p; //表头结点的指针指向新结点
scanf(“%d”,&e); //再输入一个数
return head; //返回头指针
}
各类基本操作:
1.插入:
f=(struct node *)malloc(LENG); //生成
f->data=x; //装入元素x
f->next=p->next; //新结点指向p的后继
p->next=f; //新结点成为p的后继
2.删除:
q->next=p->next;
free(p);
- 已基本操作为基础的功能实现
- 例:假定有一个无表头结点的递增排序的链表,插入一个新结点,使其仍然递增。
- 算法1:生成不带头结点的递增有序单链表:① 空表插入;② 尾部插入;③ 首部插入;④ 一般插入。
struct node * creat3_1(struct node *head,int e)
{ q=NULL; p=head; //q,p指针(不是节点)去扫描,查找插入位置
while (p && e>p->data) //未扫描完,且e大于当前结点
{ q=p; p=p->next;} //q,p后移,查下一个位置
f=(struct node *)malloc(LENG); //生成新结点
f->data=e; //装入元素e
if (p==NULL){
f->next=NULL;
if (q==NULL) //(1)对空表的插入
head=f;
else q->next=f;} //(2)作为最后一个结点插入
else if (q==NULL) //(3)作为第一个结点插入
{f->next=p; head=f;}
else
{f->next=p; q->next=f;} //(4)一般情况插入新结点
return head; }
- 算法2:生成带头结点的递增有序单链表
此时q不可能为空 省去算法1中关于q的if语句
void creat3_2(struct node *head,int e)
{ q=head;
p=head->next; //q,p扫描,查找插入位置
while (p && e>p->data) //未扫描完,且e大于当前结点
{ q=p;
p=p->next; //q,p后移,查下一个位置
}
f=(struct node *)malloc(LENG); //生成新结点
f->data=e; //装入元素e
f->next=p; q->next=f; //插入新结点
//此处同时包含(2)作为最后一个结点插入 因为语句f->next=p 不影响结果
} 可见 加头节点是明智的!(一般链表都默认拥有)
- 算法3:在单链表的指定位置插入新元素(指定位置-不用pq定位)
也不用考虑首位插入之类 f->next=p->next; p->next=f; 可包含全部
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;
}
- 例:在单链表中删除一个结点
- 算法1:在带表头结点的单链表中删除元素值为e的结点(可有多个)
int Delete1(Linklist head, ElemType e)
{ struct node *q,*p;
int result=Yes;
q=head; p=head->next; //q,p扫描
while(p)
{
while(p&&p->data!=e) //查找元素为e的结点
{
q=p; //记住可能要删元素的前一个结点p
p=p->next; //查找下一个结点
}
if (p) //有元素为e的结点
{
q->next=p->next; //删除该结点
free(p); //释放结点所占的空间
p=q->next; //p指向下一个结点
result=Yes;
}
}
return result;
- 算法2:在单链表中删除指定位置的元素
int Delete2( 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指向删除结点 用于free
p->next=q->next; //从链表中摘出
e=q->data; //取走数据元素值 记录
free(q); //释放结点空间
return OK;
}
关于单链表评价:
- 优点:
- 找出节点后,插入和删除元素相对于顺序表更快
- 不需要预分配空间,元素个数不受限制
- 缺点:
- 查找某一结点的数据元素相比顺序表更慢
2.静态链表:
- 及用数组描述的链表
typedef struct //静态链表的定义
{
int data; //静态链表节点中的数据
int cur; //静态链表节点中的游标
}component;
component a[100];
关于静态链表的评价:
- 优点
- 在插入和删除操作肘,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
- 缺点
- 没有解决连续存储分配(数组)带来的表长难以确定的问题。
- 失去了顺序存储结构随机存取的特性。
循环链表:
-
带表头结点的非空循环单链表
-
带表头结点的空循环单链表
-
只设尾指针的循环链表
-
只设尾指针的循环空表
- 使用循环链表时,也比较提倡使用尾指针。可节省一些操作(如:两循环链表首位相连)中找到尾节点的时间,从而减少时间复杂度。
- 循环链表的一些算法:
- 求以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; //返回长度值
}
关于循环链表的评价:
- 优点:
- 在单链表的基础上进一步改进,在遍历的时候可以从任意节点开始,增加了遍历的灵活性。
- 缺点:
- 没有解决单链表查找元素较慢的问题
双向链表:
- 双向链表的结构
1.非空双向链表
2.空表
3.双向循环链表
4.空表
- 循环链表的基本操作
定义:
typedef struct Dnode
{ ElemType data; //data为抽象元素类型
struct Dnode *prior,*next; //prior,next为指针类型
}*DLList //DLList为指针类型
1.插入
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
2.删除
p->prior->next=p->next; //结点A的next指向结点C
p->next->prior=p->prior; //结点C的prior指向结点A
free(p); //释放结点B占有的空间
插入:
删除:
双向链表的评价:
- 优点
- 在单链表的基础上进行改良,查找元素可以双向查找,一定程度上提升了查找某元素所需的时间
- 缺点
- 需要记录前缀点(prior)增加了额外内存空间的开销