参考课程:数据结构与算法基础(青岛大学-王卓),笔记中截图都来自王老师的课件
参考书目:《数据结构(C语言版)》严蔚敏 吴伟民 著
第二章 线性表
线性表的类型定义
线性表是具有相同特性
的数据元素的一个有限
序列
(
a
1
,
a
2
,
a
3
,
…
,
a
i
−
1
,
a
i
,
,
a
i
+
1
…
,
a
n
)
(a_1,a_2,a_3,…,a_{i-1},a_i,,a_{i+1}…,a_n)
(a1,a2,a3,…,ai−1,ai,,ai+1…,an)
a
1
a_1
a1称之为线性起点或者起始结点,
a
i
a_i
ai称之为数据元素
,
a
n
a_n
an称为线性终点或者终端结点。同时
a
i
−
1
a_{i-1}
ai−1叫做
a
i
a_i
ai的直接前趋
;
a
i
+
1
a_{i+1}
ai+1叫做
a
i
a_i
ai的直接后继
。当n=0时线性表称为空表。
同一线性表中的元素必定具有相同特性,数据元素之间的关系是线性
关系。在非空的线性表中,有且仅有一个开始结点,其没有直接前趋但是有且只有一个直接后继;同时非空线性表中有且仅有一个终端结点,其没有直接后继但是有且仅有一个直接前趋。
案例引入:
线性表 P = ( p 0 , p 1 , p 2 , … , p n ) P=(p_0,p_1,p_2,…,p_n) P=(p0,p1,p2,…,pn)可以用数组来存储,下标表示项的幂指数,值表示项的系数。按照这个规则对于 R n ( x ) = P n ( x ) + Q n ( x ) R_n(x)=P_n(x)+Q_n(x) Rn(x)=Pn(x)+Qn(x)的运算可以用线性表表示为 R = ( ( p 0 + q 0 ) , ( p 1 + q 1 ) , … , ( p n + q n ) ) R=((p_0+q_0),(p_1+q_1),…,(p_n+q_n)) R=((p0+q0),(p1+q1),…,(pn+qn))
但是对于一些稀疏多项式如 S ( x ) = 1 + 3 x 10000 + 2 x 2000000 S(x)=1+3x^{10000}+2x^{2000000} S(x)=1+3x10000+2x2000000这样的稀疏多项式如果还用普通数组存储将会造成存储空间很大的浪费——解决方法是只存系数不为0的项,如下所示
P n ( x ) = p 1 x e 1 + p 2 x e 2 + … + p m x e m P_n(x)=p_1x^{e_1}+p_2x^{e_2}+…+p_mx^{e_m} Pn(x)=p1xe1+p2xe2+…+pmxem的稀疏多项式可以用线性表 P = ( ( p 1 , e 1 ) , ( p 2 , e 2 ) , … , ( p m , e m ) ) P=((p_1,e_1),(p_2,e_2),…,(p_m,e_m)) P=((p1,e1),(p2,e2),…,(pm,em))表示
线性表的顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像(用一组地址连续
的存储单元一次存储线性表的数据元素)。顺序存储:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
顺序存储的好处是,如果知道某个元素的存储位置,就可以计算得到其他元素的存储位置。假设线性表的每个元素需占 l l l个存储单元,那么第 i i i个元素的地址跟第1个元素的地址关系如下: L O C ( a i ) = L O C ( a 1 ) + l ∗ ( i − 1 ) LOC(a_{i})=LOC(a_1)+l*(i-1) LOC(ai)=LOC(a1)+l∗(i−1)
线性表的顺序存储方式和数组的形式很像,但也有区别:
C语言中可用下述类型定义来描述顺序表(Sequence List):
//------线性表的动态分配顺序存储结构------
#define LIST_INIT_SIZE 100 // 线性表存储空间的初始分配量
typedef struct{
ElemType *elem;
int length;
}SqList;
其中,数组指针elem
表示线性表的基地址,length
表示线性表的当前长度。
多项式 P n ( x ) = p 1 x e 1 + p 2 x e 2 + … + p m x e m P_n(x)=p_1x^{e_1}+p_2x^{e_2}+…+p_mx^{e_m} Pn(x)=p1xe1+p2xe2+…+pmxem的顺序表存储结构类型的定义:
#define MAXSIZE 1000 // 多项式可能达到的最大长度
typedef struct{
float p; // 系数
int e; // 指数
}Polynomial;
typedef struct{
Polynomial *elem; // 存储空间的基地址
int length; // 多项式中当前项的个数
}SqList; // 多项式的顺序存储结构类型为SqList
补充:类C语言有关操作
1.在上述循序表定义中出现了
ElemType *elem;
或者ElemType elem[];
。这个代码中的ElemType
是指后面的elem
的类型请况。如果后面的elem
是一个只有int
型的数据,那么可以用int elem[]
直接代替;但是如果elem
中混合有多种基础数据类型,那么可以通过构造一个结构体来解决这一问题。(例如上述多项式顺序表中定义的Polynomial
结构体)2.数组的静态分配&动态分配
左侧的代码是数组的静态分配,右侧是数组的动态分配。
*data
指向的就是数组的第一个元素的地址。// 内存的动态分配 SqList L; L.data=(ElemType*)malloc(sizeof(ElemType)*MaxSize);
malloc(m)
函数:开辟m字节长度的地址空间,并返回这段空间的首地址(参数m要为整数)。sizeof(ElemType)*MaxSize
是进行计算需要总字节的计算方法。在通过
malloc(m)
开辟空间以后(假设开辟800个字节的空间),还需要确定这800个字节空间用来存储什么类型的数据(按照什么规则进行分配),例如是存储800个字符还是200个整数还是100个double?这时就需要通过前面的(ElemType*)
(强制类型转换)来进行规定。这里是转换为了指向Elemtype
的指针
类型,与SqList定义结构体中data类型一致。3.释放内存:
free(p)
函数,参数是一个指针变量,释放指针p所指变量的存储空间,即彻底删除一个变量。上述操作需要在程序前加载头文件
#include <stdib.h>
顺序表示意图:
顺序表基本操作的实现
首先定义一些预定义常量:
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1 // 不可行的
#define OVERFLOW -2 // 溢出
typedef int Status;
【算法2.1】线性表L的初始化(参数用引用类型)
Status InitList_Sq(SqList &L){ // 构造一个空的顺序表L
L.elem=(ElemType*)malloc(LIST_INIT_SIZE*sizeof(ElemType)); // 为顺序表分配空间
if (!L.elem) exit(OVERFLOW); // 存储分配失败
L.length=0; // 空表长度为0
return OK;
}
需要解释的是:在上述代码中参数传递部分采用的是c++中的引用类型作参数,如下所示,m和a使用的空间一样,对形参m的任何操作都相当与直接对实参a的操作
下面是一些顺序表中基本简单操作的实现
// 求线性表L的长度
int GetLength(SqList &L){
return L.length;
}
// 判断线性表L是否为空
int IsEmpty(SqList &L){
if (L.length==0) return 1;
else return 0;
}
// 销毁顺序表
Status DestoryList(Sqlist &L){
free(L.elem);
L.elem=NULL;
L.length=0;
L.listsize=0;
return OK;
}
// 清空顺序表
void ClearList(SqList &L){
L.length=0;
}
【算法2.2】顺序表的取值(根据位置i获取相应位置数据元素)
int GetElem(SqList L,int i,ElemType &e){
if (i<1||i>L.length) return ERROR;
e=L.elem[i-1];
return OK;
}
【算法2.3】顺序查找(查找与指定值e相同的数据元素的位置)
int LocateElem(SeqList L, ElemType e){
int i;
for(i=0;i<L->length;i++){
if(L.elem[i]==e) return i+1;
}
return 0;
}
对于表长为n的顺序表,在查找过程中,数据元素比较次数最少为1,最多为n,元素比较次数的平均值为(n+1)/2,称之为平均查找长度ASL(Average Search Length)
时间复杂度为O(n)。
对于含有n个记录的表,查找成功时, A S L = ∑ i = 1 n P i C i ASL= \sum_{i=1}^{n}P_iC_i ASL=∑i=1nPiCi,其中 P i P_i Pi第i个记录被查找到的概率; C i C_i Ci是第i个记录被查找的概率。
【算法2.4】顺序表数据元素的插入
Status ListInsert_Sq(SqList &L,int i,ElemType e){
if (i<1||i>L.length+1) return ERROR;
if (L.length==MAXSIZE) return OVERFLOW;
q=&(L.elem[i-1]);
for (p=&(L.elem[L.length-1]);p>=q;--p){
*(p+1)=*p;
}
*q=e;
++L.length;
return OK;
}
长度为6的顺序表一共有7个位置可以进行插入(最后末尾也可以插入)。假设在第i个元素之前插入的概率为pi,则在长度为n的线性表中插入一个元素所需移动元素次数的期望值为: E i n s = ∑ i = 1 n + 1 p i ( n − i + 1 ) E_{ins}=\sum_{i=1}^{n+1}p_i(n-i+1) Eins=∑i=1n+1pi(n−i+1)若假定在线性表中任何一个位置上进行插入的概率都相等,(一共有n+1个位置可以插入)则移动元素的期望值为:
E i n s = ∑ i = 1 n + 1 p i ( n − i + 1 ) = 1 n + 1 ∑ i n + 1 ( n − i + 1 ) = n 2 E_{ins}=\sum_{i=1}^{n+1}p_i(n-i+1)=\frac{1}{n+1}\sum_{i}^{n+1}(n-i+1)=\frac{n}{2} Eins=∑i=1n+1pi(n−i+1)=n+11∑in+1(n−i+1)=2n
插入操作需要移动表中一半的数据元素,插入操作的时间复杂度为O(n),当n较大时算法效率比较低
【算法2.4】顺序表数据元素的删除
Status ListDelete_Sq(SqList &L,int i,ElemType &e){
if (i<1||i>L.length) return ERROR;
p=&(L.elem[i-1]);
e=*p; // 删除的元素保留在e中
for (j=i;j<L.length-1;j++){
L.elem[j-1]=L.elem[j];
}
L.length--;
rerturn OK;
}
假设删除第i个元素的概率为pi , 则在长度为n的线性表中删除一个元素所需移动元素次数的期望值为: E d e l = ∑ i = 1 n p i ( n − i ) E_{del}=\sum_{i=1}^np_i(n-i) Edel=∑i=1npi(n−i)u若假定在线性表中任何一个位置上进行删除的概率都是相等的,则移动元素的期望值为:
E d e l = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 E_{del}=\frac{1}{n}\sum_{i=1}^n(n-i)=\frac{n-1}{2} Edel=n1∑i=1n(n−i)=2n−1
删除数据元素大约需要移动表中一半的元素,时间复杂度为O(n)
顺序表的总结:
上述查找、插入、删除算法的空间复杂度都是O(n),空间复杂度都是S(n)=O(1),没有占用辅助空间。
顺序表的优点:
- 存储密度大(结点本身所占存储量、结点结构所占存储量)
- 可以随机存取表中任一元素
顺寻表的缺点:
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
线性表的链式表示和实现
线性表的链式存储结构的特点是用一组任意的
存储单元存储线性表的数据元素(可以连续也可以不连续)。为了表示每个数据元素
a
i
a_i
ai和其直接后继
a
i
+
1
a_{i+1}
ai+1之间的逻辑关系,对于
a
i
a_i
ai除了存储本身的信息,还需要存储一个指示其直接后继的信息(直接后继的存储位置),这两部分信息组成数据元素
a
i
a_i
ai的存储映像
,称为结点
。结点包括两个域:其中存储数据元素信息的域称为数据域
,存储直接后继的存储位置的域称为指针域
。
记录第一个数据元素的指针称为头指针
单链表可以用头指针的名字命名,方便表述。
n个结点构成的链结程一个链表
,即线性表的链式存储结构。
结点只有一个指针域的链表也成为线性链表或者单链表(只包含一个指针域)。
结点有两个指针域的链表称为双链表,首位相接的链表称为循环链表。
头结点:是在链表的手元结点之前附设的一个结点
【如何表示空表?】
无头结点时,头指针为空时表示空表。有头结点时,头结点的指针域为空表示空表。
【设置头结点的好处?】
1.便于首元结点的处理。2.便于空表和非空表的统一处理
【头结点的数据域中存储的是什么?】
头结点的数据域可以为空,也可以存放线性表的长度等附加信息,但是此结点不能计入链表长度值。
链表(链式存储结构的特点):(1)结点在存储器中的位置是任意的,即逻辑上相邻数据元素在物理上不一定相邻。(2)访问只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以顺找第一个结点和最后虽然一个结点所花的时间不等。这种存取元素的方法称为顺序存储
。
顺序表是随机存取,链表是顺序存取
单链表是由表头唯一确定的,若头指针名为L,则可以把链表称为表L。
// ------线性表的单链表存储结构------
typedef struct LNode{
ElemType data;
ElemType* next;
}LNode;
例如存储学生学号、姓名、成绩的单链表结点类型定义如下:
typedef struct student{
char num[8];
char name[8];
int score;
}ElemType;
typedef struct LNode{
ElemType data; // 数据域
struct LNode *next; // 指针域
}LNode,*LinkList;
单链表基本操作的实现
【算法2.5】单链表的初始化(构造一个头结点指针域为空的空表)
- (1)生成新结点作为头结点,用头指针L指向头结点
- (2)将头结点的指针域设置为NULL
Status InitList L(LinkList &L){ // cpp引用型变量
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
return OK;
}
【补充链表简单操作的算法】
// 判断链表是否为空
int ListEmpty(LinkList L){
if (L->next) return 0; // if(L->next == NULL) 头结点指针域是否为空
else return 1;
}
// 单链表的销毁(从头结点开始,依次释放所有结点)
int DestroyList(LinkList &L){
LNode *p; // 或LinkList p;
while (L!=NULL){
p=L;
L=L->next;
free(p);
}
return OK;
}
// 单链表的清空(链表存在,但是链表中无元素。依次释放所有结点)
int ClearList(LinkList &L){
LNode *p,*q;
p=L->next; //首元结点开始
while(p){ // while(p!=NULL)
q=p->next;
free(p);
p=q;
}
L->next=NULL;
return OK;
}
// 求单链表表长
int ListLength_L(LinkList L){
LNode *p=L->next; // p指向首元结点
int count=0;
while (p){
count++;
p=p->next;
}
return count;
}
【算法2.6】取值(取单链表中第i个数据元素)
ElemType GetElem_L(ListLink L,int i,ElemType &e){
LNode *p=L->next;
int j=1;
while(P && j<i){
p=p->next;
++j;
}
if (!p || j>i) return ERROR; // 第i个元素不存在
e = p->data; // 用e返回查找第i个元素
return OK;
}
【算法2.7】按值查找(返回地址或者位次)
// 返回地址
LNode *LocateElem_L(LinkList L,ElemType e){
// 查找链表中值为e的数据元素
LNode *p=L->next;
while (p && p->data!=e){
p=p->next;
}
return p;
}
// 返回位次
int LocateElem_L(LinkList L,ElemType e){
LNode *p=L->next;
int i;
while (p && p->data!=e){
p=p->next;
i++;
}
if (p) return i; // p不为空,说明找到了,返回i
else return 0; // p为空,说明没找到,返回0
}
时间复杂度:链表只能顺序存储,所以查找的时间复杂度为O(n)。
【算法2.8】插入,在第i个结点前插入值为e的新结点
思考的问题会使得指向 a i a_i ai的指针丢失,所以一定要先接后面再接前面
int ListInsert_L(LinkList &L,int i,ElemType e){
LNode *p=L;
int j=0;
while (p && j<i-1){ // 寻找第i-1的结点
p=p->next;
++j;
}
if (!p || j>i-1) return ERROR; // i大于表长+1或小于1,插入位置非法
s=(LNode*)malloc(sizeof(LNode)); // 生成新结点s
s->data=e; // 设置结点数据域
s->next=p->next;
p->next=s;
return OK;
}
时间复杂度和查找一致,也为O(n)。
【算法2.9】删除单链表中的第i个结点
int ListDelete_L(LinkList &L,int i,ElemType &e){
LNode *p,*q;
int j=0;
p=L;
while (p->next && j<i-1){
p=p->next;
++j;
}
if (!(p->next) || j>i-1) return ERROR;
q=p->next;
p->next=q->next;
e=q->data;
free(q);
return OK;
}
时间复杂度和查找一致,也为O(n)。
【算法2.10】单链表的建立:头插法
从空表开始,生成新的结点,从最后一个结点开始,依次将各个系欸但插入到链表的前端
// 头插法创建链表
void CreateList_H(LinkList &L,int n){
int i;
LNode *p;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
for (i=n;i>0;i--){
p=(LNode*)malloc(sizeof(LNode)); // 生成新的结点
scanf(&p->data);
p->next=L->next;
L->next=p;
}
}
时间复杂度为O(n)。
【算法2.11】单链表的建立:尾插法
从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。初始时,r和L均指向头结点,每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。
// 尾插法创建链表
void CreateList_R(LinkList &L,int n){
int i;
LNode *p,*r;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
r=L; // 尾指针r指向头结点
for (i=0;i<n;i++){
p=(LNode*)malloc(sizeof(LNode));
scanf(&p->data);
p->next=NULL;
r->next=p; // 插入到表尾
r=p; // r指向新的尾结点
}
}
时间复杂度也是O(n)。
第二章线性表的剩余内容笔记将会再下次发出,整理不易,给作者点个赞吧 φ(゜▽゜*)♪