一,引子
在数据的逻辑结构中,有种常见而且简单的结构是线性结构,即数据元素之间构成一个有序的序列。
用一个例子来说:
前面我们举过一个一元多项式的例子,我们继续用那个例子来说,前面我们讲的是计算,那我们这节课来讲一讲存储。我们该如何将一个一元多项式的表达式存储到电脑里面呢?
可能大家有各种各样的想法,我们来整理一下:
1,用顺序存储结构直接表示一元多项式
我们发现,数组的下标正好对应了其一个个的指数,而正好也可以将系数放到对应下标的空间当中。
这种方法对于一般情况来说还是挺不错的,但是如果指数很大,但是其间有很多零项,很多下标对应的数据都是空,就会造成空间的浪费。因此,在多项式比较稀疏的情况下,最后只存储非零项,避免空间的浪费。
2,用顺序存储结构表示其非零项
每一个非零项涉及两个信息,指数和系数,因此,可以将一个多项式看成是一个二元组的集合。为了计算方便,我们还可以按照指数下降的顺序组织这个二元组。所以,我们可以把多项式看成是二元组的有序序列。我们可以用一个结构数组来存储这个有序序列。数组的大小与非零项的多少有关。
这样的话,对于稀疏的多项式表达能节省大量空间。但是如果多项式不是很稀疏,则空间节省的有事就没有了,甚至需要的空间更多。
经过上面的两种方法我们知道,顺序存储结构就是数组,那数组表示有什么问题吗?
对于这两种方法来说,问题就是灵活性不够,无法事先知道有多少需要处理的数据,所以无法确定数组的大小,所以会浪费大量的空间。
3,用链表来存储非零项
用链表表示的话就非常简单了,数据域就是两个元素,然后加上一个指针域,用链表来表示,空间的利用效率更高,但是操作难度有了很大的提高。
故,我们综上做一个总结:数据结构的设计往往需要在算法可理解性与时间、空间效率之间做出折中,针对具体问题选择合适的数据结构及设计相应的算法。
二,线性表
一,定义:
线性表是由同一类型的数据元素构成的有序序列的线性结构。线性表中元素的总个数称为线性表的长度。当一个线性表中没有元素(长度为0)时,称为空表。表的起始位置称为表头,表的结束位置称为表尾。
二,线性表的抽象数据类型描述为:
类型名称:线性表
数据对象集:线性表是n(n>=0)个元素构成的有序序列,其中后面一个元素称为前面一个元素的直接后继,反过来就是直接前驱。直接前驱和直接后继反映了元素之间一对一的邻接逻辑关系。
操作集:
三,线性表的顺序存储实现
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素。所以用一维数组来存放再合适不过了。
考虑到线性表有一些基本的操作,因此表的长度是动态可变的。所以数组的容量要足够大,但是实际大小可能又没有那么多,因此需要用一个变量Last记录当前线性表中最后一个元素在数组中的位置。即Last起一个指针的作用(实际上是数组下标,是一个变量),始终指向线性表的最后一个元素,表空时Last=-1.
1,代码实现:
typedef int position; typedef struct LNode* PtrToLNode; struct LNode { ElementType Data[MAXSIZE];//ElementType 任何数据类型 position Last; }; typedef PtrToLNode List;
由于LNode是一个包含数组的结构,通常使用结构指针,传递效率更高,这样我们可以利用List定义线性表。
2,基本操作-初始化
即构造一个空表,首先动态分配表结构所需的存储空间,然后将表中Last指针置为-1,表示表中没有数据元素。
List MakeEmpty() { List L; L = (List)malloc(sizeof(struct LNode)); L->Last = -1; return L; }
3,基本操作-查找
#define ERROR -1 Position Find(List L,ElementType X) { position i = 0; while (i <= L->Last && L->Data[i] != X) i++; if (i > L->Last) return ERROR; else return i; }
若找到就返回存储下标,若没找到,则返回错误信息ERROR。
4,基本操作-插入
bool Insert(List L, ElementType X, int i) { //在L的指定位序i前插入一个新元素X,位序i元素的数组位置下标是i-1 position j; //判断表满没有 if (L->Last == MAXSIZE - 1) { printf("表满"); return false; } //检查插入位序的合法性,是否在1~n+1.n为当前元素个数,即Last+1. if (i<1 || i>Last + 2) { printf("位序不合法"); reutrn false; } for (j = L->Last; j >= i - 1; j++) { L->Data[j + 1] = L->Data[j]; } L->Data[i - 1] = X; L->Last++; return true; }
5,基本操作-删除
bool Delete(List L, int i) { position j; if (i<1 || i>Last + 2) { printf("位序%d不存在元素",i); reutrn false; } for (j = i; j <= L->Last; j++) { L->Data[j = 1] = L->Data[j]; } L->Last--; return true; }
四,线性表的链式存储实现
由于顺序表的存储特点是用物理上的相邻实现了逻辑上的相邻,他要求用连续的存储单元顺序存储线性表中各元素,因此,对于一些基本操作的执行效率会有很大影响。
而对于链式存储结构而言,它不需要用地址连续的存储单元来实现,因为它不要求逻辑上相邻的两个元素物理上也相邻,它是通过链来建立起数据元素之间的逻辑关系。
为了访问链表,必须先找到链表的第一个数据单元,因此实际应用中常用一个称为“表头”的指针指向链表的第一个单元,并用它表示一个具体的链表。
注意:无论是顺序存储还是链式存储,我们都用一致的接口定义线性表,因为这两种具体的存储方式都是对同一个抽象概念的实现。
1,代码实现:
typedef struct LNode* PtrTolNode; struct LNode { ElementType Data; PtrToLNode Next; }; typedef PtrToLNode Position; typedef PtrToLNode List;
2,基本操作-求表长
int length(List L) { position p; int cnt = 0;//初始化计数器 p = L;//p指向表的第一个结点 while (p) { p = p->Next; cnt++; } return cnt; }
3,基本操作-查找
在线性表抽象类型说明中,我们提到线性表的查找有两种,即按序号查找(FindKth)和按值查找(Find)两种。
按序号查找
ElementType FindKth(List L,int K) { position p; int cnt; p = L; while (p && cnt < K) { p = p->Next; cnt++; } if (cnt == k) return p->Data; else return ERROR; }
按值查找
List Find(List L,ElementType X) { List p = L; while (p && p->Data != x) { p = p->Next; } return p; }
4,基本操作-插入
List Insert(ElementType X, List PtrL) { List p, s; //新结点插入在表头 if (i == 1) { s = (List)malloc(sizeof(struct LNode)); s->Data = X; s->Next = PtrL; return s;//返回新表头指针 } //查找第i-1个结点 p = FindKth(i - 1, PtrL); //第i-1个不存在,不能插入 if (p == NULL) { printf("参数i错"); return NULL; } //若找到,便创建新结点 else { s = (List)malloc(sizeof(struct LNode)); s->Data = X; s->Next = p->Next; p->Next = s;//新结点插入在i-1个结点的后面 return PtrL; } }
5,基本操作-删除
List Delete(int i, List PtrL) { List p, s; if (i == 1) { s = PtrL; if (PtrL != NULL) PtrL = PtrL->Next; else return NULL; free(s); return PtrL; } p = FindKth(i - 1, PtrL); if (P == NULL) { printf("第%d个结点不存在", i - 1); return NULL; } else if (p->Next == NULL) { printf("第%d个结点不存在", i); return NULL; } else { s = p->Next; p->Next = s->Next; free(s); return PtrL; } }
综上可知:
1)在单链表上插入,删除一个结点,必须知道其前驱结点。
2)单链表不具有按序号随机访问的特点,只能从头指针开始一个个顺序进行。
五,广义表
1,定义:
1)是线性表的推广
2)与线性表类似,也是由n个元素组成的有序序列,
3)广义表的元素可以是单元素,也可以是另一个广义表
2,
广义表不仅跟线性表一样可以表达简单的线性顺序关系,而且可以表达更为复杂的非线性多元关系。
3,
由于广义表的元素可以有不同的结构(单元素和广义表),因此不适合采用顺序存储方式表示,通常采用链式存储结构,也就是用由结点组成的链表来表示广义表,结点对应每个元素,如果该元素还是一个广义表,则通过该结点引申出另一个链表。
4,代码实现:
typedef struct GNode* GList; struct GNode { int Tag;//标志域:0表示该节点是单元素,1表示该节点是广义表 union { //子表指针域Sublist与单元素数据域Data公用,即公用存储空间 ElementType Data; GList Sublist; }URegion; GList Next;//指向后继结点 };
多维数组:
六,多重链表
存在结点属于多个链的链表叫做“多重链表”。一般来说,多重链表中每个结点的指针域会有多个。像上面所说的广义表的实现中,有的包含了两个指针域,但包含两个指针域的链表并不一定是多重链表,例如:双向链表,尽管有多个指针域,但是每个结点还是都属于同一个链表。
七,堆栈
1,定义:
堆栈是一种线性结构,也是一个特殊的线性表。
2,概念:
在堆栈中,通常把数据插入称为入栈,而数据删除叫做出栈,由于这一特性,最后入栈的数据将呗最先弹出,所以堆栈也被称为后入先出表。
3,堆栈的抽象数据类型:
类型名称:堆栈(Stack)
数据对象集: 一个有0个或多个元素的有穷线性表。
操作集:对于一个具体长度为正整数MaxSize的堆栈S ∈Stack吗
4,代码实现:
typedef struct SNode* Stack; struct SNode { ElementType Data[MAXSIZE];//存放元素的数组 int Top;//栈顶指针 int MAXSIZE;//堆栈最大容量 };
5,顺序栈的创建
Stack CreateStack(int MaxSize) { Stack S = (Stack)malloc(sizeof(struct SNode); S->Data = (ElementType*)malloc(MaxSize * sizeof(ElementType)) S->Top = -1;//标志着栈为空 S->MaxSize = MaxSize; return S; }
6,入栈操作-push
bool IsFull(Stack S) { return (S->Top == S->MaxSize - 1); } bool Push(Stack S,ElementType X) { if (IsFull(S)) { printf("堆栈满"); return false; } else { S->Data[++(S->Top)] = X; return true; } }
7,出栈操作-Pop
ElementType Pop(Stack PtrS) { if (PtrS->Top == -1) { printf("堆栈空"); return ERROR; } else { return (PtrS->Data[(PtrS->Top)--]); } }
8,堆栈的链式存储实现
栈的链式存储结构(链栈)与单链表相似,但其操作受限制,操作只能在链栈的栈顶进行。栈顶指针Top就是链表的头指针。
9,代码实现:
typedef struct SNode* Stack; struct SNode { ElementType Data; struct SNode* Next; };
10,基本操作-创建,判断,传值,删除
Stack CreateStack() { //构建一个堆栈的头节点, 返回该节点指针 Stack S; S = (Stack)malloc(sizeof(struct SNode)); S->Next = NULL; return S; } bool IsEmpty(Stack S) { //判断堆栈是否为空 return (S->Next == NULL); } void Push(ElementType X, Stack S) { //将元素压入栈 struct SNode* TmpCell; TmpCell = (struct SNode*)malloc(sizeof(struct Snode)); TmpCell->Element = X; TmpCell->Next = S->Next; S->Next = TmpCell; } ElementType Pop(Stack S) { PtrToSNode FirstCell; ElementType TopElem; if (IsEmpty(S)) { printf("堆栈空"); return NULL; } else { FirstCell = S->Next; TopElem = FirstCell->Data; S->Next = FirstCell->Next; free(FirstCell); return TopElem; } }
八,队列:
一,定义:
队列也是一个有序线性表,但队列的插入和删除操作分别在线性表的两个不同的端点进行的。准确来说,队尾进数据,队头出数据。
二,抽象数据类型:
类型名称:队列(queue)
数据对象集:一个有0个或者多个元素的有穷线性表
操作集:
三,队列的顺序存储实现
1,队列最简单的表示方法就是用数组。用数组存储队列有许多种具体的方法。一般可以选择将队列头放数组下标小的位置,而将队列尾放在数组下标大的地方,并用两个变量Front和Rear分别指示队列的头和尾。
注意:
随着出队入队的进行,会使整个队列整体向后移动,这样就出现了上面第二张图的现象,队尾指针已经移到了最后,再有元素入队就会出现溢出,而事实上此时队中并未真的满员,这种现象叫做假溢出。
为了解决队尾溢出而实际上数组仍然有空余空间的问题,一般在队列的顺序存储结构种采用循环队列的方式:Rear和Front到达数组端点时,能折回到数组开始处,即相当于将数组头尾相接,想象成一个环状。
采用循环队列解决了我们前面提到的假溢出问题,但在循环队列中,如何根据Front和Rear判断当前队列是否空或者满?
在目前所说的操作情况下,当Front和Rear的值相等时,队列可能为空也可能是满的,我们无法判别。
根本原因就是:我们是根据Rear和Front之间的差距来判断队列元素个数的,而Rear和Front之间的差距最多只有n种情况(n为数组大小)而队列元素有n+1种情况(0~n),所有仅仅依靠这两个是无法区分n+1种情况的。
可见,无法区分队列是空还是满的原因是Front和Rear提供的信息量不够。解决方法有两种:
方法一:另外增设一个变量,比如:记录当前队列元素个数的size,或者用一个变量Flag记录最后一次操作是入队还是出队。
方法二:少用一个元素空间,就是上图中的第三个图,这种情况就视为队列满。此时的状态就是队尾指针加1就会从后面赶上头指针,因此,队满的条件就是:(Rear+1)%数组长度等于Front。队空的条件仍然是:Rear==Front。
下面我们采用第二种方法实现循环队列,
#define MaxSize <存储数据元素的最大个数> struct QNode { ElementType Data[MaxSize]; int Rear; int Front; }; typedef struct QNode* Queue;
2,基本操作-创建
Queue CreateQueue(int MaxSize) { Queue Q = (Queue)malloc(sizeof(struct QNode)); Q->Data = (ElementType)malloc(MaxSize * sizeof(ElementType)); Q->Front = Q->Rear = 0; Q->MaxSize = MaxSize; return Q; }
3,基本操作-入队列
void AddQ(Queue PtrQ, ElementType item) { if ((PtrQ->Rear + 1) % MaxSize == PtrQ->Front) { printf("队列满"); return; } PtrQ->Rear = (PtrQ->Rear + 1) % MaxSize; PtrQ->Data[PtrQ->Rear] = item; }
4,基本操作-出队列
ElementType DeleteQ(Queue PtrQ) { if (PtrQ->Front == PtrQ->Rear) { printf("队列空"); return ERROR; } else { PtrQ->Front = (PtrQ->Front) % MaxSize; return PtrQ->Data[PtrQ->Front]; } }
四,队列的链式存储实现
1,代码实现:
struct Node { ElementType Data; struct Node* Next; }; struct QNode {//链队列结构 struct Node* Rear;//指向队尾结点 struct Node* Front;//指向队头结点 }; typedef struct QNode Queue; Queue PtrQ;
2,采用链式存储的入队和出队操作实际就是在一个链表的尾部插入结点或者在头部删除结点。以出队操作为例:
ElementType DeleteQ(Queue PtrQ) { struct Node* FrontCell; ElementType FrontElem; if (PtrQ->Front == NULL) { pritnf("队列空"); return ERROR; } FrontCell = PtrQ->Front; if (PtrQ->Front == PtrQ->Rear) {//若队列只有一个元素 PtrQ->Front == PtrQ->Rear = NULL;//删除后队列置为空 } else { PtrQ->Front = PtrQ->Front->Next; } FrontElem = FrontCell->Data; free(FrontCell);//释放被删结点空间 return FrontElem;//返回该节点的值 }
关于线性结构的内容就讲到这里,这里面都是一些基础的内容,我会在B站的视频讲解中,提出几个例子来讲解,供大家学习理解,希望大家捧场支持,都是免费的,只希望大家关注点赞支持。