(a1,a2,… ai-1,ai,ai+1,…an)
a1称为起始结点, an称为终端结点;
ai-1称为 ai的直接前趋,ai+1称为 ai的直接后继。
线性表(Linear List) :由n(n≧0)个数据元素(结点)a1,a2, …an组成的有限序列。
其中数据元素的个数n定义为表的长度。当n=0时称为空表,常常将非空的线性表(n>0)记作:
(a1,a2,…an)
这里的数据元素ai(1≦i≦n)只是一个抽象的符号,其具体含义在不同的情况下可以不同。
(A,B,C、…、Z)
例2、某校从1978年到1983年各种型号的计算机拥有量的变化情况。
(6,17,28,50,92,188)
例3、一副扑克的点数
(2,3,4,…,J,Q,K,A)
比较复杂的线性表中,一个数据元素可以由若干数据项组成。
在这种情况下常把数据元素称为记录。
同一线性表中的数据元素必须具有相同特性。相邻数据元素之间存在序偶关系。通常记为:
(a1,a2,… ai-1,ai,ai+1,…an)
线性表的逻辑特征是:
在非空的线性表,有且仅有一个开始结点a1,它没有直接前趋,而仅有一个直接后继a2;
有且仅有一个终端结点an,它没有直接后继,而仅有一个直接前趋a n-1;
其余的内部结点ai(2≦i≦n-1)都有且仅有一个直接前趋a i-1和一个直接后继a i+1。
算法
PS:利用两个线性表LA和LB分别表示两个集合A和B,现要求一个新的集合A=A∪B。
方法:从LB中依次取得各个元素,与LA中各元素比较,若不在LA中,则将其加入到LA中。
void union(List &La,List Lb) {
La-len=listlength(La);
Lb-len=listlength(Lb);
for(i=1; i<=Lb-len; i++)
{ getelem(Lb,i,e);
if(!locateelem(La,e,equal))
listinsert(La,++La-len,e);
}
}
PS:巳知线性表LA和线性表LB中的数据元素按值非递减有序排列,现要求将LA和LB归并为一个新的线性表LC,且LC中的元素仍按值非递减有序排列。
分析: LC中的数据元素来自LA和LB,可以先设LC为空表,然后将LA或LB中的元素逐个插入到LC中即可。
关键是如何来插入:可设两个指针i和j 分别指向LA和LB中某个元素,设i当前所指元素为a, j当前所指元素为b。则当前应插入到LC中的元素c为:
当a<=b时: c=a
当a>b 时: c=b
void mergelist(list la,list lb,list &lc)
initlist(lc);
i=j=1;k=0;
la-len=listlength(la);
lb-len=listlength(lb);
while((i<=la-len)&&(j<=lb-len)){
getelem(la,i,ai); getelem(lb,j,bj);
if(ai<=bj) {listinsert(lc,++k,ai);++i;}
else {listinsert(lc,++k,bj);++j;}
}
while(i<=la-len){
getelem((la, i++, ai);
listinsert(lc, ++k, ai);
}
while(j<=lb-len){
getelem((lb,j++,bj);
listinsert(lc,++k,bi);
}
}
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素,用这种存储形式存储的线性表称其为顺序表。
用物理上的相邻实现数据元素之间的逻辑相邻关系是既简单,又自然的。因为内存中的地址空间是线性的,因此, 只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存取。
假设线性表的每个元素需占用L个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置。则线性表中第i+1个数据元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间满足下列关系:
LOC(a i+1)=LOC(a i)+L
线性表的第i个数据元素ai的存储位置为:
LOC(ai)=LOC(a1)+(i-1)*L
顺序表具有按数据元素的序号随机存取的特点。
在程序设计语言中,一维数组在内存中占用的存储空间就是一组连续的存储区域,因此,用一维数组来表示顺序表的数据存储区域是再合适不过的。
由于线性表的长度是可变的,因此,数组的容量必须能够动态增长。
顺序表的结构定义如下:
typedef struct{
ElemType *elem; //存储空间基地址
int length; //当前长度
int listsize; //当前分配的存储容量
} SqList;
# define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
初始化操作
Status InitList_Sq (SqList &L) {
//构造一个空的线性表L
L.elem=(ElemTpye * )malloc(LIST_INIT_SIZE * sizeof(ElemTpye) ); //申请存储空间
if (!L.lelem) exit (OVERFLOW); //存储分配失败
L.length = 0; // 空表长度为0
L.listsize = LIST_INIT_SIZE ; //初始存储容量
return OK;
}
线性表的插入操作
线性表的插入运算是指在表的第i(1≦i≦n+1)个位置上,插入一个新结点e,
使长度为n的线性表
(a1,…a i-1,ai,…,an)
变成长度为n+1的线性表
(a1,…a i-1,e,ai,…,an)
顺序表上完成这一运算则通过以下步骤进行:
(1) 将ai~an 顺序向后移动,为新元素让出位置;
(2) 将 e 置入空出的第i个位置;
(3) 修改顺序表的当前长度length
插入算法
Status listInsert_sq(Sqlist &L, int i, Elemtype e ){
if (i<1||i>L.length+1) return ERROR; //验证i的合法性
if (L.length>=L.listsize) { //空间不够,追加
newbase =(ElemType*)realloc(L.elem, (L.listsize+LISTINCREMENT) * sizeof (ElemType))
if (!newbase) exit(OVERFLOW);
L.elem=newbase;
L.listsize+=LISTINCREMENT;
}
q=&(L.elem[i-1]); //用q指示插入位置
for ( p=&(L.elem[L.length-1]); p>=q; --p ) *(p+1)=*p;
//依次将第i至第n个元素向后移动一位
*q=e; //插入元素e
++L.length; //元素个数增加1
return OK;
}
现在分析算法的复杂度。
这里的问题规模是表的长度,设它的值为n。该算法的时间主要化费在循环的结点后移语句上,所需移动结点的次数不仅依赖于表的长度,而且还与插入位置有关。
当i=n+1时,结点后移语句将不进行;这是最好情况,其时间复杂度O(1);
当i=1时,结点后移语句将循环执行n次,需移动表中所有结点,这是最坏情况,其时间复杂度为O(n)。
由于插入可能在表中任何位置上进行,因此需分析算法的平均复杂度
在长度为n的线性表中第i个位置上插入一个结点,令Eis(n)表示移动结点的期望值(即移动的平均次数),则在第i个位置上插入一个结点的移动次数为n-i+1。故
Eis(n)= Σ pi(n-i+1)
不失一般性,假设在表中任何位置(1≦i≦n+1)上插入结点的机会是均等的,则
p1=p2=p3=…=p n+1=1/(n+1)
因此,在等概率插入的情况下,
Eis(n)= Σ(n-i+1)/(n+1)=n/2
也就是说,在顺序表上做插入运算,平均要移动表上一半结点。当表长 n较大时,算法的效率相当低。虽然Eis(n)中n的的系数较小,但就数量级而言,它仍然是线性阶的。因此算法的平均时间复杂度为O(n)。
删除
线性表的删除运算是指将表的第i(1≦i≦n)结点删除,使长度为n的线性表:
(a1,…a i-1,ai,a i+1…,an)
变成长度为n-1的线性表
(a1,…a i-1,a i+1,…,an)
算法步骤:
(1)判断合法性。
(2)找到被删元素的位置,将其值赋给e(用e返回值)
(3)将第i+1至第n个元素依次向前移动一个位置。
(4)修改表长L.length。
删除算法
Status listDelete_sq(Sqlist &L, int i, Elemtype e ){
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;
}
该算法的时间分析与插入算法相似,结点的移动次数也是由表长n和位置i决定
若i=n,前移语句将不执行,无需移动结点;算法的时间复杂度为O(1)
若i=1,则前移语句将循环执行n-1次,需移动表中除开始结点外的所有结点。时间复杂度为O(n)。
删除算法的平均性能分析与插入算法相似。在长度为n的线性表中删除一个结点,令Ede(n)表示所需移动结点的平均次数,删除表中第i个结点的移动次数为n-i,
故
Ede(n)= Σ pi(n-i)
(pi表示删除表中第i个结点的概率)
概率的假设如下,
p1=p2=p3=…=pn=1/n
由此可得:
Ede(n)= Σ(n-i)/n=(n-1)/2
即在顺序表上做删除运算,平均要移动表中约一半的结点,平均时间复杂度也是O(n)。
线性表的顺序表示的特点:
用物理位置上的邻接关系来表示结点间的逻辑关系。可以随机存取表中的任一结点。
插入和删除操作会移动大量的结点。 要求占用连续的空间,存储分配空间不灵活。
为克服顺序表的缺点,可以采用链接方式来存储线性表。通常将链接方式存储的线性表称为链表(线性链表)。
线性链表:用一组任意的存储单元存放线性表的数据元素。
链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息,这个信息称为指针(pointer)或链(link)。这两部分组成了链表中的结点结构:
链表正是通过每个结点的链域将线性表的n个结点按其逻辑次序链接在一起的。由于上述链表的每一个结只有一个链域,故将这种链表称为单链表(Single Linked)。
单链表中每个结点的存储地址是存放在其前趋结点next域中。
而开始结点无前趋,故应设头指针head指向开始结点。
终端结点无后继,故终端结点的指针域为空,即NULL(图示中也可用^表示)。
整个链表的存取必须从头指针开始。
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。例如:若头指针名是head,则把链表称为表head。
用C语言描述的单链表:
typedef struct LNode{
ElemType data; //存储元素的值
struct LNode *next; // 存储后续结点的地址
} LNode, *LinkList;
LinkList L; // L是LinkList类型的变量,
表示单链表的头指针
有时在单链表的第一个数据元素结点之前附设一个结点,称之头结点。
说明:头结点的next域指向链表中的第一个数据元素结点。
对于头结点数据域的处理:
a.加特殊信息,如数据域为整型,则在该处存放链表长度信息。
b.置空
总结:
带头结点:链表置空 Lnext = NULL;
判断是否为空的条件 if (L->next == NULL)
不带头结点:则置空 L=NULL;
判空条件 if (L==NULL)
单链表的基本操作
初始化
查找(定位,按值查找)
求长度
取元素
插入元素
删除元素
建立链表
集合的并运算
有序链表的合并(归并)
单链表的简单操作
LinkList Initiate_L () { LNode * t;
t= (LNode *) malloc (sizeof (LNode));
//产生一个头结点 t->next=NULL; //设置后继指针为空 retutn (t);
}
建立空表: LinkList L=Initiate_L ();
求表长:(返回元素的个数)
思路:从第一个结点开如数有多少个结点。设指针p为“指点工具”。P最开始指向第一个结点(p=head->next),数一个结点P便向后移一位(p=p->next);直到最后一个结点(此时p->next==NULL)。
另:还需设一个计数器j,记录已数元素个数。
单链表的简单操作
求长度(返回元素的个数)
int LinkLength_L (LinkList L)
{
int j;
LinkList p; //指针p为指点工具,j为计数器
p=L->next; j=0;
while (p) { j++; p=p->next;}
return(j);
}
查找(按值查找,若找到值为x的元素则返序号,否则返回0)
int LinkLocate_L (LinkList L, ElemType x)
{ int i; LinkList p;
p=L->next; i=1;
while ( p!=NULL && p->data != x )
{ p= p->next; i++; }
if (!p) { printf ("Not found! \n");
return(0);
}
else { printf ("i=%d\n",i); return (i); }
}
取元素(按序号查找)
单链表是非随机存取结构。因此只能从链表的头指针出发,顺链域next逐个结点往下搜索(设计数器j),直至找到第i个结点为止(j=i)。
Status GetElem_L(LinkList L, int i, ElemType &e)
{ LinkList p;
p=L->next; int j=1;
while (p && j<i) { p=p->next; ++j; }
if (!p || j>i) return ERROR;
e=p->data;
return OK;
}
插入操作:在单链表第i个位置上(第i个元素之前,第i-1个元素之后)插入值为e的元素。
在第i个元素之前插入,先找到第i-1个结点。
算法思路:
(1)找到第i-1个结点;若存在则继续操作,否则结束
(2)申请、填装新结点;
(3)将新结点插入。结束。
Status ListInsert_L(LinkList &L, int i, ElemType e)
{
LinkList p,s;
p=L; int j=0;
while (p && j<i-1) { p=p->next; ++j;} //p指向第i-1个结点
if (!p || j> i-1) return ERROR;
s = (LinkList) malloc( sizeof (LNode));
s->data = e; //申请空间,构造新结点
s->next = p->next;
p->next = s; //修改指针,链入新结点
return OK;
}
删除元素操作
设q指向单链表中某结点,删除*q。
首先要找到*q的前驱结点*p,然后完成指针的操作即可
1.找到第i-1个结点;若存在继续2,否则结束;
2.若存在第i个结点则继续3,否则结束;
3.删除第i个结点,释放空间。 p->next = q->next;
Status ListDelete_L(LinkList &L, int i, ElemType &e)
{ LinkList p,q;
p=L; int j=0;
while (p->next && j<i-1) { p=p->next; ++j;} //找前驱
if (!(p->next) || j> i-1) return ERROR;
q=p->next; // q指向待删结点
p->next = q->next; // 修改指针,删除元素
e=q->data; // 备份删除的结点的值
free(q); // 释放空间
return OK;
}
Status DelNode(LinkList &L, ElemType x)
{ LNode *p,q;
p=L; //设p为待删结点的前驱
while(p->next&&p->next->data!=x )
p=p->next;
if(p->next==NULL) return ERROR;// 查找失败
q=p->next; //q指向要删除的元素
p->next=q->next;
free(q);
}
如何从线性表得到单链表?
链表是一个动态的结构,它不需要予分配空间,因此生成链表的过程是一个结点“逐个插入” 的过程。
根据插入新元素的位置主要有两种方法
后插法
前插法
建立链表(头插法建表)
在链表表头插入新结点,结点次序与输入次序相反。
例如:逆位序输入 n 个数据元素的值,建立带头结点的单链表。
操作步骤:
一、建立一个“空表”;
二、输入数据元素an,
建立结点并插入;
三、输入数据元素an-1,
建立结点并插入;
四、依次类推,直至输入a1为止。
建立链表(头插法建表)
在链表表头插入新结点,结点次序与输入次序相反。
void CreateList_L(LinkList &L, int n)
{ LNode *s;
L=(LinkList)malloc(sizeof(LNode)); //做头节点
L->next = NULL;
for (int i=n; i>0; --i) {
s=(LinkList)malloc(sizeof(LNode));
scanf("%d",&s->data);
s->next = L->next; L->next=s;
}
}
尾插法建表:将新结点插到链表尾部,须增设一个尾指针rear,使其始终指向当前链表的尾结点。
LinkList CreateList() /*尾插法建表,返回表头指针*/ { LinkList head, p, rear; head = (LinkList)malloc(sizeof(LNode)); /*生成头结点*/ rear = head; /*尾指针初值指向头结点*/ scanf(“%f”,&x); while(x != $) { p = (LinkList)malloc(sizeof(LNode)); /*生成新结点*p */ p->data = x; rear->next = p; /*新结点插入表尾*/ rear = p; /*尾指针指向新的表尾*/ scanf(“%f”, &x); } rear->next = NULL; return head; }
集合并运算
void UnionList_L(LinkList &La, LinkList Lb)
{ LinkList p , q; int x;
p=Lb->next; //p作Lb链表的指点工具
while (p) {
x=p->data;
q=La->next; // q作La链表的指点工具
while (q && q->data !=x) q=q->next; //在La中找x
if (!q) { //若La中无x,则构造、插入
q=(LinkList)malloc(sizeof(LNode));
q->data = x; q->next = La->next;
La->next = q; }
p=p->next;
}
}
插入位置在La表的首元素之前;
时间复杂度: O(m*n)
合并有序链表
void MergeList_L(LinkList La, LinkList Lb, LinkList &Lc)
{ LinkList pa ,pb , pc;
pa = La->next; pb= Lb->next;
Lc = pc = La; //pc为Lc的尾指针
while (pa && pb) {
if (pa->data <= pb->data){
pc->next=pa; pc=pa; pa=pa->next; }
else { pc->next=pb; pc=pb; pb=pb->next; }
}
pc->next=pa ? pa : pb;
free(Lb);
}
线性表的静态单链表存储结构
用一维数组来描述线性链表。数组的一个分量表示一个结点,用数组的另一个分量游标(cur)代替指针指示结点类数组中的相对位置。
这种存储结构仍需预先分配一个较大的空间。因此相对于用指针描述的动态链表,这种用数组描述的链表叫静态链表。
插入和删除操作不需要移动元素,仅需修改指针。故仍具有链式存储结构的主要优点。
循环链表——是一种首尾相接的链表。
循环链表最后一个结点的next指针不为 0 (NULL),
而是指向了表头结点。在循环链表中没有NULL
为简化操作,在循环链表中往往加入表头结点。
特点:循环链表中,从任一结点出发都可访问到表中
所有结点;而在单链表中,必须从头指针开始,
否则无法访问到该结点之前的其他结点。
在用头指针表示的单链表中,找开始结点a1的时间是O(1),然而要找到终端结点an,则需从头指针开始遍历整个链表,其时间是O(n)
实际中多采用尾指针rear表示单循环链表。
开始结点a1的存储位置: (rear–>next) —>next
终端结点an的存储位置 :rear
查找时间都是O(1)。
循环条件:
非循环链表循环:p或p—>next是否为空
循环链表循环: 判断它们是否等于某一指定指针,如头指针或尾指针等。 p->next != H
双向链表(Double Linked List)
在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链,故称为双向链表。
前驱方向 (a)结点结构 后继方向
typedef struct DuLNode{
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode, *DuLinkList;
DuLinkList d,p;
判空条件:
head->next==head
或 head->prior==head
插入、删除作有很大不同。需要同时修改两个方向的指针。
插入
s->next = p->next; p->next = s;
s->next->prior = s; s->prior = p;
删除
p->next = p->next->next;
p->next->prior = p;
一元多项式的表示和相加
n阶多项式Pn(x)有n+1项。
系数 a0, a1, a2, …, an
指数 0, 1, 2, …, n。按升幂排列
在计算机中,可以用一个线性表来表示
P=(a0,a1, … , an)
1. 第一种表示方法
Pn=(a0,a1, … , an)
适用于指数连续排列、 “0”系数较少的情况。
但对于指数不全的多项式,如P (x) = 3 + 5x50 + 14x20000, 会造成系统空间的巨大浪费。
2. 第二种表示方法
一般情况下,一元多项式可写成:
Pn(x)=p1xe1+p2xe2+…+pmxem
其中:pi是指数为ei的项的非零系数,
0≤e1 ≤e2 ≤… ≤em ≤n
二元组表示 ((p1,e1),(p2,e2), … ,(pm,em))
例:P999(x) = 7x3 - 2x12 -8x999
表示成: ((7,3),(-2,12),(-8,999))
一元多项式的抽象数据类型定义
ADT Polynomial {
数据对象:
D={ai|ai ∈ TermSet, i=1,2,…,m, m≥0 TermSet中的每个元素包含一个表示系数的实数和表示指数的整数}
数据关系:
R1={<ai-1,ai>|ai-1,ai ∈ D,且ai-1中的指数值<ai中的指数值, i=2,…n}
基本操作:
}ADT Polynomial
抽象数据类型(Polynomial)的实现
typedef struct {
float coef;
int expn;
}term, ElemType;
//term用于本ADT, ElemType为LinkList的 数据对象名
typedef LinkList polynomial;
多项式链表的相加
AH = 1 - 10x6 + 2x8 +7x14
BH = - x4 + 10x6 - 3x10 + 8x14 +4x18
两个多项式的相加
结果多项式另存
扫描两个相加多项式,若都未检测完:
若当前被检测项指数相等,系数相加。若未变成 0,则将结果加到结果多项式。
若当前被检测项指数不等,将指数小者加到结果多项式。
若有一个多项式已检测完,将另一个多项式剩余部分复制到结果多项式。