线性表
线性表的基本概念
线性表的定义
线性表是由\(n(n>=0)\) 个相同类型的数据元素组成的有限序列,标记为:
\(L = (a_{1},a_{2},...,a_{i},...,a_{n})\)
- 线性表中元素的个数n定义为线性表的长度,当\(n=0\)时为空表。
- 当\(n>0\)时,线性表的逻辑结构如图所示:
线性表的几个概念:
逻辑特征:
- 若至少含有一个元素,则只有唯一的起始元素
- 若至少含有一个元素,则只有唯一的尾元素
- 除了起始元素外,其他元素有且仅有一个前驱元素
- 除了尾元素外,其余元素有且只有一个后继元素
线性表中的每个元素有唯一的序号(逻辑序号),同一个线性表中可以有多个相同的元素,但他们的序号是不同的。
线性表的基本运算
1、初始化InitList(L)。其作用是建立一个空表L(即建立线性表的构架,但不含任何数据元素)。
2、销毁线性表DestroyList(L)。其作用是释放线性表L的内存空间。
3、求线性表的长度ListLength(L)。其作用是返回线性表L的长度。
4、求线性表中第i个元素GetElem(L,i,e)。其作用是返回线性表L的第i个数据元素。
5、按值查找LocateElem(L,x)。若L中存在一个或多个值与x相等的元素,则其作用是返回第一个值为x的元素的逻辑序号。
6、插入元素ListInsert(L,i,x)。其作用是在线性表L的第i个位置上增加一个以x为值的新元素
7、删除元素ListDelete(L,i)。其作用是删除线性表L的第i个元素ai。
8、输出元素值DispList(L)。其作用是按前后次序输出线性表L的所有元素值
我们通过以下实例进行分析:
1、利用两个线性表LA,LB分别表示两个集合\(A\&B\),要求一个新集合\(A=A∪B\)
void Union(SqList &La,SqList Lb)
{
ElemType e;
int la_len,lb_len;
int i;
la_len = ListLength(La)//ListLength 返回长度
lb_len = ListLength(Lb)//ListLength 返回长度
for(i = 1;i <= lb_len ;i++)
{
GetElem(Lb,i,e)//取Lb中第i个数据元素赋给e
if(!LocateElem(La,e,equal))
ListInsert(La,++la_len,e);//// La中不存在和e相同的元素,则插入之
}
}
线性表LA和LB,其元素均按非递减有序排列,编写一个算法将它们合并成一个线性表LC,且LC的元素也是按非递减有序排列。
- 思路:扫描La,Lb的元素,比较当前元素的值,将较小的元素值赋给Lc,最后在处理没有插入的值
void MergeList(SqList La,SqList Lb,SqList &Lc)
{
int i = 1,j = 1, k = 0;
int la_len = ListLength(La),lb_len = ListLength(Lb);
ElemType ai,bj;
InitList(Lc);
//比较大小
while(i<=la_len&&j
线性表的顺序存储和实现
顺序表的定义
-
顺序表的定义和特点
定义:将将线性表中的元素相继存放在一个连续的存储空间中,即构成顺序表。
存储:它是线性表的顺序存储表示,可利用一维数组描述存储结构。
特点:元素的逻辑顺序与物理顺序一致。
访问方式:可顺序存取,可按下标直接存取。 -
顺序表的连续存储方式
\(LOC(i) = LOC(i-1) + l = a + i * l,LOC是元素存储位置,l是元素大小\)
在介绍存储结构之前,我们先给出相应的类型重定向:
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
typedef int Status; // Status值是函数结果状态代码,如OK等
假定线性表的数据元素的类型为ElemType,类型声明如下:
// ------ 线性表的动态分配顺序存储结构 --------
#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
#define LIST_INCREMENT 10 // 线性表存储空间的分配增量
typedef int ElemType; //元素的数据类型
typedef struct {
ElemType *elem; // 存储空间基址
int length; // 当前长度
int listsize; // 当前分配的存储容量
} SqList;
同样的,静态存储结构类型声明如下:
// ------ 线性表的静态分配顺序存储结构 --------
#define MaxSize 100
typedef int ElemType; //假设顺序表中所有元素为int类型
typedef struct
{
ElemType data[MaxSize]; //存放顺序表的元素
int length; //顺序表的实际长度
} SqList; //顺序表类型
- 由于顺序表采用数组存放元素,数组具有随机存取特性,所以顺序表具有随机存取特性。
基本运算实现
- 初始化线性表算法
Status InitList_Sq(List &L)
{
L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType));
if(!L.elem)return ERROR; //健壮性,不过一般不会失败
L.length = 0; //长度置为0
L.listsize = LIST_INIT_SIZE; //最大长度
return OK;
}
- 销毁线性表算法
Status DestroyList(SqList &L)
{
free(L.elem);
L.elem = NULL; //指针置空,防止野指针
L.length = 0;
L.listsize = 0;
return OK;
}
- 求线性表长度
int GetLength(SqList L)
{
return L.length;
}
- 求第i个元素
Status GetElem(SqList L, int i, ElemType &e)
{
if(i<1||i>L.length)
return ERROR; //后面可以改为:i>Getlength(L);
e = *(L.elem + i -1); //公式,随机存取
return OK;
}
- 按值查找
在顺序表L找第一个值为e的元素,找到后返回其逻辑序号,否则返回0(由于线性表的逻辑序号从1开始,这里用0表示没有找到值为e的元素)。
int LocateElem(SqList L, ElemType e)
{
ElemType *p;
int i = 1;
p = L.elem;
while(i<=L.length && (*p++ != e))//核心代码
++i;
if(i<=L.length)
return i;
else
return 0;
}
- 插入算法
将新元素e插入到顺序表L中逻辑序号为i的位置(如果插入成功,元素e成为线性表的第i个元素)。
i的合法值为1≤i≤L.Length+1。当i无效时返回0(表示插入失败)。
有效时将L.elem[i-1..L.length-1]后移一个位置, 在L.elem[i-1]处插入e,顺序表长度增1,并返回1 (表示插入成功)
Status ListInsert_Sq(SqList &L, int i, ElemType e)
{
ElemType *p;
if(i < 1 || i > L.length + 1)
return ERROR;
//这里不再考虑内存不足再使用relloc申请空间的情况
ElemType *q = &(L.elem[i-1]); //q为插入位置
for(p = &(L.elem[L.length - 1]);p>=q;--p)
*(p+1) = *p; //插入位置及之后的元素右移
*q = e;
++L.length; //表长+1
return OK;
}
- 删除算法
删除顺序表L中逻辑序号为i的元素
i的合法值为1≤i≤L.Length。在i无效时返回0 (表示删除失败)。
有效时将L.elem[i..length-1]前移一个位置,顺序表长度减1,并返回1(表示删除成功)
Status ListDelete_Sq(SqList &L, int i, ElemType &e)
{
ElemType *p,*q;
if(i<1 || i>L.length)//位置不合法
return ERROR;
p = &(L.elem[i-1]);//p 为删除元素的位置
e = *p; //存储其值
q = L.elem +L.length - 1; //表尾元素的位置
for(++p;p<=q;p++) //从下一个位置开始,逐步覆盖
*(p-1) = *p; //元素左移
--L.length;
return OK;
}
拓展实例
- 假设有一个顺序表L,其中元素为整数且所有元素值均不相同。设计一个算法将最大值元素与最小值元素交换。
用maxi和mini记录顺序表L中最大值元素和最小值元素的下标,初始时maxi=mini=0。
i从1开始扫描所有元素:当L.elem[i]>L.elem[maxi]时置maxi=i;否则若L.elem[i]扫描完毕时,L.elem[maxi]为最大值元素,L.elem[mini]为最小值元素,将它们交换。
void SwapMaxMin(SqList &L)
{
int i,maxi,mini;
maxi = mini = 0; //存储的是下标
for(i = 1;iL.elem[maxi])
maxi = i;
else if(L.elem[i]
- 从线性表中删除自第i个元素开始的k个元素,其中线性表用顺序表L存储。
将线性表中a_{i}~a_{i+k-1}元素(对应\(L.elem[i-1..i+k-2]\)的元素)删除,即将\(a_{i}+k~a_{n}\)(对应\(L.elem[i+k-1..n-1]\))的所有元素依次前移k个位置。
int Deletek(SqList &L,int i,int k)
{
int j;
if(i<1||k<1||i+k-1>L.length)
return 0;
for(j = i+k-1;j
- 已知线性表(a1,a2,…,an)采用顺序表L存储,且每个元素都是互不相等的整数。设计一个将所有奇数移到所有的偶数前边的算法(要求时间最少,辅助空间最少)
并没有要求有大小次序
置i=0,j=n-1,在顺序表L中从左向右找到偶数L.elem[i],从右向左找到奇数L.elem[j],将两者交换;循环这个过程直到i等于j为止。
void Move(SqList &L)
{
int i = 0,j = L.length - 1;
while(i
- 已知一个整数线性表采用顺序表L存储。设计一个尽可能高效的算法删除其中所有值为负整数的元素(假设L中值为负整数的元素可能有多个)。
每次删除都需要移动大量元素,会消耗大量的时间,可以换一个角度考虑
采用整体重建顺序表的算法思路,仅仅将插入元素的条件设置为“元素值≥0”即可。
void De(SqList &L)
{
int i,k = 0;
for(i = 0;i < L.length;i++)
{
if(L.elem[i]>=0) //非负数插入
{
L.elem[k] = L.elem[i];
k++;
}
}
L.length = k; //重置长度
}
- 设将\(n(n>1)\)个整数存放到一维数组R中。试设计一个时间和空间两方面尽可能高效的算法,将R中整数序列循环左移\(p(0<p<n)\)个位置,即将R中的数据序列\((X_{0},X_{1},…,X_{n-1})\)变换为\((X_{p},X_{p+1},…,X_{n-1},X_{0},X_{1},…,X_{p-1})\)
设\(R=(X_{0},X_{1},…,X_{p},X_{p+1}, …,X_{n-1)}\),记\(A=(X_{0},X_{1},…,X_{p-1})\) (共有p个元素),\(B=(X_{p},…,X_{n-1})\)(共有n-p个元素)
设计逆置算法reverse(R),用于原地逆置数组R,则A原地逆置后A’变为\((X_{p-1},…,X_{1},X_{0})\),B原地逆置后B’变为\((X_{n-1},…,X_{p-1},X_{p})\)
就是说\(A’B’=(X_{p-1},…,X_{1},X_{0},X_{n-1},…,X_{p-1},X_{p})\),再将A’B’原地逆置变为\((X_{p},X_{p-1},…,X_{n-1},X_{0},X_{1},…,X_{p-1})\)即为所求,即进行3次逆置操作:\(reverse(R)=reverse(reverse(A),reverse(B))\)
void reverse(int R[],int m,int n) //将R[m..n]逆置
{
int i,tmp;
for (i=0;i<(n-m+1)/2;i++)
{
tmp=R[m+i]; //将R[m+i]与R[n-i]进行交换
R[m+i]=R[n-i];
R[n-i]=tmp;
}
}
int ListReverse(int R[],int n,int p) //循环左移
{
if (p<=0 || p>=n)
return 0;
else
{
reverse(R,0,p-1);
reverse(R,p,n-1);
reverse(R,0,n-1);
return 1;
}
}
线性表的链式存储和实现
线性链表
存储方式
线性表的单链表存储方法:用一个指针表示结点间的逻辑关系。
因此单链表的一个存储结点包含两个部分,结点的形式如下:
data:为数据域,用于存储线性表的一个数据元素,也就是说在单链表中一个结点存放一个数据元素。
next:为指针域或链域,用于存放一个指针,该指针指向后继元素对应的结点,也就是说单链表中结点的指针用于表示后继关系。
- 特点
每个数据元素由结点构成
线性结构
结点可以连续,可以不连续存储。
结点的逻辑顺序与物理顺序可以不一致
表可扩充。
链表中第一个元素结点称为首结点;最后一个元素称为尾结点,其指针域(next)为空(NULL)。
- 结点类型声明
typedef int ElemType;
typedef int Status;
#define OK 1
#define ERROR 0
// ------ 结点的C语言描述 --------
typedef struct node {
ElemType data;
struct node *next;
} LNode, *LinkList;
带头节点的单链表示意图
常规算法设计
- 初始化单链表算法
创建一个空的单链表,它只有一个头结点,由L指向它。该结点的next域为空,data域未设定任何值。
Status InitList(LinkList &L)
{
L = (LinkList)malloc(sizeof(LNode));
if(!L)
return ERROR;
L->next = NULL; //指针域为空
return OK;
}
- 销毁单链表算法
一个单链表中的所有结点空间都是通过malloc函数分配的,在不再需要时需通过free函数释放所有结点的空间
借助一个遍历指针
Status DestroyList(LinkList &L)
{
LinkList p; //临时指针
while(L)
{
p = L->next
free(L);
L = p;
}
return OK;
}
- 求单链表长度算法
设置一个整型变量i作为计数器,i初值为0,p初始时指向第一个数据结点。
然后沿next域逐个往后查找,每移动一次,i值增1。当p所指结点为空时,结束这个过程,i之值即为表长。
int ListLength(LinkList L)
{
int i = 0;//计数器
LinkList p = L->next; // p指向第一个结点
while(p) { // 没到表尾
i++;
p = p->next;
}
return i;
}
- 求单链表中值为e的元素算法
用p从头开始遍历单链表L中的结点,用计数器i统计遍历过的结点,其初值为0。
在遍历时,若p不为空,则p所指结点即为要找的结点,查找成功,算法返回位序i
否则算法返回0表示未找到这样的结点。
int LocateElem(LinkList L, ElemType e)
{
int i = 0;
LinkList p = L->next;
while(p)
{
i++;
if(p->data == e)
return i;
p = p->next;
}
return 0;
}
- 单链表的插入算法
在单链表L中第i个位置,插入值为x的结点。
找前驱结点
先在单链表L中查找第i-1个结点,若未找到返回0;
找到后由p指向该结点,创建一个以x为值的新结点s,将其插入到p指结点之后。
在p结点之后插入s结点的操作如下:
① 将结点s的next域指向p的下一个结点(s->next=p->next)。
② 将结点p的next域改为指向新结点s(p->next=s)。
插入操作的①和②执行顺序不能颠倒。
记忆方法:先把没有指向的指针指向一个位置,进行操作
Status ListInsert_L(LinkList &L, int i, ElemType e)
{
LinkList p, s;
p = L;
int j = 0;
while(p && jnext,j++; //找结点
if(!p||j>i-1) return ERROR;//位置不合法
s = (LinkList)malloc(sizeof(LNode)); // 生成新结点
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
- 单链表的删除算法
先在单链表L中查找第i-1个结点,若未找到返回0
找到后由p指向该结点,然后让q指向后继结点(即要删除的结点)。
若q所指结点为空则返回0,否则删除q结点并释放其占用的空间。
删除p指结点的后继结点的过程:p->next=q->next;
free(q);
即需要找到删除结点的前驱结点
Status ListDelete_L(LinkList &L, int i, ElemType &e)
{
LinkList p, q;
p = L;
int j = 0;
while(p && jnext,j++; //找结点
if(!(p->next)||j>i-1) return ERROR;//位置不合法
q = p->next; //p为前驱,q为当前结点
e = q->data;
free(q);
return OK;
}
拓展实例
- 设计一个算法,通过一趟遍历确定单链表L(至少含两个数据结点)中第一个元素值最大的结点。
用p遍历单链表,在遍历时用maxp指向data域值最大的结点(maxp的初值为p)。当单链表遍历完毕,最后返回maxp。
LNode *MaxNode(LNode *L)
{
LNode *p=L->next, *maxp=p;
while (p!=NULL) //遍历所有的结点
{
if(maxp->datadata)
maxp = p; //当p指向的值大于maxp时,更新maxp
}
return maxp;
}
- 删除一个单链表L(至少含两个数据结点)中第一个元素值最大的结点。
以p遍历单链表,pre指向p结点的前驱结点。
在遍历时用maxp指向data域值最大的结点,maxpre指向maxp结点的前驱结点。
当单链表遍历完毕,通过maxpre结点删除其后的结点,即删除了元素值最大的结点。
用到了同步指针
void DelMaxNode(LNode *&L)
{
LNode *p=L->next,*pre=L,*maxp=p,*maxpre=pre; //初始化
while(p!=NULL)
{
if(maxp->data < p->data)
{
maxp = p;
maxpre = pre;
}
pre = p;
p = p->next;//pre、p同步后移,保证pre始终为p的前驱结点
}
maxpre->next = maxp->next;//删除maxp结点
free(maxp);//释放maxp结点
}
- 将一个单链表L(至少含两个数据结点)中所有结点逆置,并分析算法的时间复杂度。
先将单链表L拆分成两部分,一部分是只有头结点L的空表,另一部分是由p指向第一个数据结点的单链表。
然后遍历p,将p所指结点逐一采用头(前)插法插入到L单链表中,由于头插法的特点是建成的单链表结点次序与插入次序正好相反,从而达到结点逆置的目的。
头插法天然可以实现单链表逆置
一样,还是先处理没有指向的指针,再做其他处理
void ListReverse(LNode *&L)
{
LNode *p = L->next,*q;
L->next = NULL;
while(p!=NULL)
{
q = p->next;
p->next = L->next;//头插法
L->next = p;
p = q;
}
}
- 假设该单链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表中倒数第k个位置上的结点(k为正整数)。若查找成功,算法输出该结点的data域的值,并返回1;否则,只返回0。
单链表只能从前往后遍历,并不能从后往前,所以想要再一次循环内找到相对应的结点,只能采用特殊的方式。
考虑双指针的思路,定义两个指针变量p和q,初始时均指向头结点的下一个结点。p指针沿链表移动; 当p指针移动到第k个结点时,q指针开始与p指针同步移动;当p指针移动到链表尾结点时,q指针所指元素为倒数第k个结点。
详细思路:
①置count=0,p和q指向链表表头结点的下一个结点。
②若p为空,则转向⑤ 。
③若count等于k,则q指向下一个结点;否则,置count=count+1。
④置p指向下一个结点,转向② 。
⑤若count等于k,则查找成功,输出该结点的data域的值,返回1;否则,查找失败,返回0。
⑥算法结束。
int Searchk(LinkList list,int k)
{
LinkList p, q;
int count = 0;
p = q = list->link;
while (p!=NULL && countlink;
}
if(p == NULL)
return 0;
else
{
while(p!=NULL)//没有时返回0
{
q = p->link;//p和q同步后移直到p=NULL
p = p->link;
}
printf("%d",q->data);
return 1;
}
}
循环链表
循环链表是单链表的变形,链表尾结点的next指针不是NULL,而是指向了单链表的前端。
循环链表的判空条件:L->next == L;
循环单链表的特点是:只要知道表中某一结点的地址,就可搜寻到所有其他结点的地址。
在搜寻过程中,没有一个结点的 next 域为空遍历条件:for ( p = L->next; p != L; p = p->next )
循环单链表的所有操作的实现类似于单链表,差别在于检测到链尾
,指针不为NULL,而是回到链头
。
算法设计
- 初始化线性表运算算法
创建一个空的循环单链表,它只有头结点,由L指向它。
Status InitList_cl(LinkList &L)
{
L = (LinkList)malloc(sizeof(LNode));
if(!L)exit(OVERFLOW);
L->next = L;
return OK;
}
- 求线性表的长度算法
int ListLength(LinkList L)
{
int i = 0;
LinkList p = L->next; // p指向第一个结点
while(p != L) { // 没到表尾
i++;
p=p->next;
}
return i;
}
其他
- 设计一个算法求一个循环单链表L中所有值为x的结点个数
int CountNode(LNode *L, ElemType x)
{
int i = 0;
LNode *p = L->next;
while(p!=L)
{
if(p->data == x)i++;
p = p->next;
}
return i;
}
- 有一个非递减有序的循环单链表L,设计一个算法删除其中所有值为x的结点
由于循环单链表L是非递减有序的,则所有值为x的结点必然是相邻的
先找到第一个值为x的结点p,让pre指向其前驱结点
然后通过pre结点删除p结点及其后面连续值为x的结点
int Delallx(LNode *&L, ElemType x)
{
LNode *pre=L,*p=L->next; //pre指向p结点的前驱结点
while (p!=L && p->data!=x) //找第一个值为x的结点p
{
pre = p;
p = p->next;
}
if(p==L)return 0;//没有找到值为x的结点返回0
while(p!=L&&p->data == x)//删除所有值为x的结点
{
pre->next = p->next;
free(p);
p = pre->next;
}
return 1;//成功删除返回1
}
双向链表
双向链表是指在前趋和后继方向都能遍历的线性链表。
双向链表每个结点的结构为:
双向链表中用两个指针表示结点间的逻辑关系
指向其前驱结点的指针域prior。
指向其后继结点的指针域next。
结点指向:
指向其后继结点的指针域next。
p->next 指示结点 p 的后继结点;
p->prior->next指示结点 p 的前趋结点的后继结点,即结点 p 本身;
p->next->prior指示结点 p 的后继结点的前趋结点,即结点 p 本身。
p->prior->next == p == p->next->prior 固有特性
存储声明
typedef struct DuLNode {
ElemType data;
DuLNode *prior, *next;
} DuLNode, *DuLinkList;
双向循环链表
- 双向循环链表长度
int ListLength(DuLinkList L)
{
int i = 0;
DuLinkList p = L->next; // p指向第一个结点
while(p!=L)
{
i++;
p = p->next;
}
return i;
}
- 插入
① 将结点s的next域指向结点p的下一个结点( s->next=p->next)。
② 若p不是最后结点(若p是最后结点,只插入s作为尾结点),则 将p之后结点的prior域指向s(p->next->prior=s)。
将s的prior域指向p结点(s->prior=p)。
④ 将p的next域指向s(p->next=s)。
还是先将没有指向的指针指向该指向的位置
在双向循环链表中,可以通过一个结点找到其前驱结点,所以插入操作也可以改为:在双链表中找到第i个结点p,然后在p结点之前插入新结点。
Status ListInsert(DuLinkList L, int i, ElemType e)
{
DuLinkList p, s;
if(i<1 || i>ListLength(L) + 1) //判断位置
return ERROR;
p = GetElemP(L, i - 1); // 在L中确定第i个元素前驱的位置指针p
if(!p) return ERROR;
s = (DuLinkList)malloc(sizeof(DuLNode));
if(!s) exit(OVERFLOW);
s->data = e;
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s;
return OK;
}
- 删除结点运算算法
若p不是尾结点,则将其后继结点的prior域指向pre结点(p->next->prior=pre)
② 将pre结点的next域改为指向p结点的后继结点(pre-next=p->next)
Status ListDelete(DuLinkList L, int i, ElemType &e)
{
DuLinkList p;
if(i<1) return ERROR;
p = GetElemP(L,i)//确定第i个位置
if(!p) return ERROR;
e = p->data; //保存删除元素
p -> prior -> next = p->next;
p -> next ->prior = p->prior;
free(p);
return OK;
}