- 线性表(List):由0个或多个元素组成的有限序列。
- 首先它是一个序列,也就是说元素之间是有个先来后到的顺序
- 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继
- 线性表是有限的,事实上无论计算机发展到多强大,它所处理的元素都是有限的
- 线性表用数学语言来定义:若将线性表记为(a1,a2,……,ai-1,ai,ai+1,……,an-1,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素
- 线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表。
- 数据类型:指一组性质相同的值得集合及定义在此集合上的一些操作的总称,例如,整型,浮点型,字符型。
- 在C语言中,按照取值的不同,数据类型可以分为两类:
- 原子类型:不可以再分解的基本类型,例如整型、浮点型、字符型等。
- 结构类型:由若干个类型组合而成,是可以再分解的,例如整型数组是由若干个整型数据组成的
- 抽象:是指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事务的一个概括。抽象是一种思考问题的方式,它隐藏了繁杂的细节。
- 我们对已有的数据类型进行抽象,就有了抽象数据类型。
- 抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作。
- 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。比如1+1=2这样一个操作,在不同CPU的处理上可能不一样,但由于其定义的数学特性相同,所以在计算机编程者看来,他们都是相同的。
- 描述抽象数据类型的标准格式:
ADT 抽象数据类型名
Data
数据元素之间逻辑关系的定义
Operation
操作
endADT
11. 线性表的抽象数据类型定义:
ADT 线性表(List)
Data
线性表的数据对象集合为{a1,a2,……,an},每个元素的类型均为DataType。
其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。
数据元素之间的关系是一对一的关系。
Operation
InitList(*L):初始化操作,建立一个空的线性表L。
ListEmpty(L):判断线性表是否为空表,若线性表为空,返回ture,否则返回false。
ClearList(*L):将线性表清空。
GetElem(L,i,*e):将线性表L中第i个位置的元素值返回给e。
LocatieElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回0表示失败。
ListInsert(*L,i,e):在线性表L中第i个位置插入新元素e。
ListDelete(*L,i,*e):删除线性表L中第i个位置元素,并用e返回其值。
ListLength(L):返回线性表L的元素个数。
endADT
12. 对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂的操作,完全可以用这些基本操作的组合来实现。
13. 线性表的顺序存储结构:指的是用一段地址连续的存储单元依次存储线性表的数据元素。
14. 线性表顺序存储的结构代码:
#define MAXSIZE 20 typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length;//线性表当前长度 } Sqlist;
这里我们对数组进行了一个封装,增加了当前长度的变量。
15. 顺序存储结构封装需要三个属性:
- 存储空间的启示位置,数组data的存储位置就是线性表存储空间的存储位置。
- 线性表的最大存储容量,数组的长度MAXSIZE。
- 线性表的当前长度,length。
数组长度与线性表的当前长度需要区分一下:数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度是线性表中元素的个数,是会变化的。
16. 假设ElemType占用的是c个存储单元(字节),那么线性表中第i+1个数据元素和第i个元素的存储位置的关系是:LOC(ai+1)=LOC(ai)+c。(LOC表示获得存储位置的函数)
第i个数据元素ai的存储位置可以由a1推算得出:LOC(ai)=LOC(a1)+(i-1)*c,通过这个公式,我们可以随时计算出线性表中任意位置的地址,不管它是第一个还是最后一个,
都是相同的时间。那么它的存储时间性能当然就为O(1),我们通常称之为随机存储结构。
17. 实现GetElem的具体操作,即将线性表L中的第i个位置元素值返回。就程序而言就非常简单了,我们只需要把数组第i-1下标的值返回即可。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//Status是函数类型,其值是函数结果状态代码,如OK等 //初始条件:顺序线性表L已存在,1<=i<=ListLength(L) //操作结果:用e返回L中第i个元素的值 Status GetElem(Sqlist L, int i, ElemType *e) { if(L.length==0 || i<1 || i>L.length) { return ERROR; } *e = L.data(i-1); return OK; }
18. 实现插入算法多的思路
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 将要插入元素填入位置i处
- 线性表长+1
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//Status是函数类型,其值是函数结果状态代码,如OK等
//初始条件:顺序线性表L已存在,1<=i<=ListLength(L)
//操作结果:在L的第i个元素开始,后面的元素向后移一位,将e的值插入,L的长度加1
Status ListInsert(SqList *L, int i, ElemType e) { int k; if (L->length == MAXSIZE) {//顺序线性表已满 return ERROR; } if (i<1 || i>L->length+1) {//当i不在范围内时 return ERROR; } if (i <= L->length) {//若插入数据位置不在表尾 //将要插入位置后数据元素向后移动一位 for (k=L->length-1; k>=i-1; k--){ L->data[k+1]=L->data[k]; } } L->data[i-1]=e;//将新元素插入 L->length++; return OK; }
19. 删除算法的思路
- 如果删除位置不合理,抛出异常
- 取出删除的元素
- 从删除元素位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置
- 表长减1
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; //Status是函数类型,其值是函数结果状态代码,如OK等 //初始条件:顺序线性表L已存在,1<=i<=ListLength(L) //操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 Status ListInsert(SqList *L, int i, ElemType e) { int k; if (L->length == 0) { return ERROR; } if (i < 1 || i > L->length) { return ERROR; } *e = L->data[i-1]; if (i < L->length) { for(k = i ; k< L->length; k++) { L->data[k-1] = L->data[k]; } } L->length--; return OK; }
20. 插入和删除操作的时间复杂度
- 最好的情况:插入和删除操作刚好要求在最后一个位置操作,因为不需要移动任何元素,所以此时的时间复杂度为O(1)
- 最坏的情况:如果要插入和删除的位置是第一个元素,那就意味着要移动所有的元素向前或者向后,所以这个时间复杂度为O(n)
- 平均情况就取中间值O((n-1)/2)
- 按照时间复杂度的简化规则,平均情况复杂度简化之后是O(n)
21. 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入或删除操作时,时间复杂度是O(n)
这就说明,它比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作是存取数据的应用
22. 线性表顺序存储结构的优缺点
优点:无须为表示表中元素之间的逻辑关系而增加额外的存储空间
可以快速地存取表中任意位置的元素
缺点:插入和删除操作需要移动大量元素
当线性表长度变化较大时,难以确定存储空间的容量
容易造成存储空间的“碎片”
23. 单链表中,我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。
这两部分信息组成数据元素称为存储映像,称为结点(Node)。n个结点链接成一个链表即为线性表的链式存储结构。
因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
24. 单链表的结构
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)
25. 头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
26. 头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)
- 有了头结点,对在第一个元素结点前插入结点和删除第一结点的操作与其它结点的操作就统一了
- 头结点不一定是链表的必须要素
27. 单链表与空链表图示
typedef struct Node { ElemType data;//数据域 struct Node *Next;//指针域 } Node;
typedef struct Node *LinkList;
28. 获得链表第i个数据的算法思路:
- 声明一个结点p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j+1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,返回结点p的数据
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; //Status是函数类型,其值是函数结果状态代码,如OK等 //初始条件:单链表L已存在,1<=i<=ListLength(L) //操作结果:用e返回L中第i个数据元素的值 Status GetElem (LinkList L, int i, ElemType *e) { int j; LinkList p;//p是指针变量 p = L->next; j = 1; while (p && j<i) { p = p->next; ++j; } if (!p || j>i ) { return ERROE; } *e = p->data; return OK; }
这个算法的时间复杂度取决于i的位置,当i=1时,则不需要遍历,而i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度为O(n)。
由于单链表的结构中没有定义表长,所以不知道要实现要循环多少次,也就不方便使用for来控制循环
这个算法的核心思想叫做“工作指针后移”,这也是很多算法的常用技术。
29. 单链表的插入
插入数据时,将结点p的指针域p->next赋值给s结点的指针域s->next
将s结点的地址赋值给p结点的指针域p->next
s->next = p->next;
p->next = s;
注意着两句代码顺序不能改变,如果改变了顺序,先执行p->next = s; 则执行s->next = p->next;时,p已经被s覆盖了,这里会形成一个死循环
30. 单链表第i个数据插入结点的算法思路
- 生命一节点p指向链表头结点,初始化j从1开始
- 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,在系统中生成一个空节点s
- 将数据元素e赋值给s->data
- 单链表的插入(刚才两个标准语句)
- 返回成功
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; /* Status 是函数类型,其值是函数结果状态码,如OK等 初始条件:单链表已存在,1<=i<=ListLength(L) 操作结果:在L中第i个位置之前插入新的元素e,L的长度加1 */ Status ListInsert (LinkList *L, int i, ElemType e) { int j; LinkList p,s; p = *L; j = 1; while (p && j<i) { p = p->Next; j++; } if (!p || j>i) { return ERROR; } s = (LinkList)malloc(sizeof(Node)); s->data = e; s->Next = p->Next; p->Next = s; return OK; }
31. 单链表的删除
假设元素a2的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可。
我们实际要做的只有一步
p->next = p->next->next 或 q = p->next; p->next = q->next;
32. 单链表第i个数据删除结点的算法思路
- 声明结点p指向链表第一个结点,初始化j=1;
- 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,将欲删除结点p->next赋值给q;
- 单链表的删除标准语句p->next = q->next;
- 将q结点中的数据复制给e,作为返回;
- 释放q结点
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; /* Status 是函数类型,其值是函数结果状态码,如OK等 初始条件:单链表已存在,1<=i<=ListLength(L) 操作结果:删除L中的第i个数据元素,并用e返回其值,L的长度-1 */ Status ListInsert (LinkList *L, int i, ElemType *e) { int j; LinkList p,q; p = *L; j = 1; while (p->Next && j>i) { p = p->Next; j++; } if (!p || j>i) { return ERROR; } q = p->Next; p->Next = q->Next; *e = q->data; free(q); return OK; }
33. 单链表与顺序存储
如果我们希望从第i个位置开始,连续插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个位置,所以每次都是O(n)
而单链表中进行这些操作时,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)
显然,对于插入或删除数据越平凡的操作,单链表的效率越是明显