数据结构看书笔记(三)--线性表
线性表的定义:
线性表(List):零个或多个数据元素的有限序列。
分析:若将线性表几位(a1,a2,a3……,ai-1,ai,ai+1,……an),则表中ai-1为ai的直接前驱元素,ai+1为ai的直接后继元素。
线性表元素的个数n(n>=0)定义为线性表的长度,当n=0是称为空表。
在复杂的线性表中,一个数据元素可以由若干个数据项组成。
线性表的抽象数据类型定义如下:
ADT 线性表(List) Data 线性表的数据对象集合为{a1,a2,……,an},每个元素的类型均为DataType。 其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,出了最后 一个元素an外,有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。 Operation InitList(*L): 初始化操作,建立一个空的线性表 ListEmpty(L):若线性表为空,返回true,否则返回false. ClearList(*L):清空 GetElem(L,i,*e):将线性表L中的第i个位置元素返回给e. LocateElem(L,e):查找e在线性表中的位置,如果查找成功,则返回位置,否则返回0表示失败. ListInsert(*L,i,e):在线性表L中的第i个位置插入新的元素,并赋值为e. ListDelete(*L,i,*e):将线性表L中的第i个位置元素删除,并用e返回该元素的值。 ListLength(L):返回线性表的长度。
线性表的顺序存储结构:
顺序存储的定义:线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。(可以用一维数组来实现顺序存储结构)
实现顺序存储结构的一般用一维数组来实现。
定义:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqlList;
数据长度与线性表长度的区别:
数组的长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的。
线性表的长度是线性表中数据元素的个数,随线性表插入和删除操作的进行,线性表的长度是变化的。
地址计算方法:
分配的数组空间要大于等于当前线性表的长度。
注:存储器中的每个存储单元都有自己的编号,这个编号称为地址。
计算公式:
LOC(ai+1)=LOC(ai)+c
LOC(ai)=LOC(ai)+(i-1)*c
顺序结构的插入与删除:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; 获得元素操作: Status GetElem(SqlList L,int i,ElemType *e) { if(L.length==0||i<1||i>L.length) return ERROR; *e = L.data[i-1]; return OK; }
插入操作:
如果插入位置不合理,抛出异常;
如果线性表长度大于等于数组长度,则抛出异常或者动态增加容量;
从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
将要插入元素填入位置i处;
表长加1;
Status ListInsert(SqlList *L,int i,ElemType e) { int k; if(L->length==MAXSIZE) return ERROR; if(i<1||i>L->length+1) 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; }
删除操作:
如果删除位置不合理,抛出异常;
取出删除元素;
从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一位;
表长减1.
Status ListDelete(SqlList *L,int i,ElemType *e) { int k; if(L->length==0) return ERROR; if(i<1||i>length-1) return ERROR; *e = L->data[i]; if(i<L->length-1) { for(k=i;k<L->length;k--) L->data[k-1] = L->data[k]; } L->length--; return OK; }
线性表结构的优缺点:
优点:
无须为表示表中之间的逻辑关系而增加额外的存储空间
可以快速的存取表中任一位置的元素
缺点:
插入和删除操作需要移动大量元素
当线性表长度变化较大时,难以确定存储空降的容量
造成存储空间的“碎片”
线性表的链式存储结构:
顺序存储结构的缺点:插入和删除需要移动大量元素,这显然就需要耗费时间。
线性表链式存储结构定义:为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系,对元素ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息。
我们把存储数据元素信息的域称为数据域,把存储后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为节点(Node).
链表中第一个结点的存储位置叫做指针。
为了更方便对链表进行操作,会在单链表的第一个节点前附设一个结点,称为头结点。
头结点和头指针的区别:
头指针:
头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
头指针具有标识作用,所以常用头指针冠以链表的名字
无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
头结点:
头结点是为了操作系统的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可以存放链表的长度)。
有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。
头结点不一定是链表必须要素。
ADT定义:
typedef struct Node { ElemType data; struct Node *next; }Node; typedef struct Node *LinkList;
//结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
单链表的读取
声明一个指针p指向第一个结点,初始化j从1开始;
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
若到链表末尾p为空,则说明第i个结点不存在;
否则查找成功,返回结点p的数据。
Status GetElem(LinkList L,int i,ElemType *e) { int j; LinkList p; p = L->next; j = 1; while(p&&j<i) { p = p->next; ++j; } if(!p||j>i) return ERROR; *e = p->data; return OK; }
核心思想,工作指针后移
单链表的插入与删除:
插入:
主要操作
s->next = p->next;
p->next = s;
思路:
声明一指针p指向链表头结点,初始化j从1开始;
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
若到链表末尾p为空,则说明第i个结点不存在;
否则查找成功,在系统中生成一个空结点s;
将数据元素e赋值给s->data;
单链表的插入标准语句s->next=p->next;p->next = s;
返回成功;
实现代码:
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; }
删除:
主要操作:
q=p->next; p->next = q->next;
思路:
声明一个指针p指向第一个结点,初始化j从1开始;
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
若到链表末尾p为空,则说明第i个结点不存在;
否则查找成功,将欲删除的结点p->next赋值给q;
单链表的删除的标准语句:p->next = q->next;
将q结点中的数据赋值给e,作为返回;
释放q结点;返回成功。
实现代码:
Status ListDelete(LinkList *L,int i,ElemType *e) { int j; LinkList p,q; p = *L; j = 1; while(p&&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; }
注:对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显。
单链表的整表创建:
单链表的整表创建的算法思路:
声明一指针p和计数器变量i;
初始化一空链表L;
让L的头结点的指针指向NULL,即创建一个带头结点的单链表;
循环:
生成一个新的结点赋值给p;
随机生成一个数字赋值给p的数据域p->data;
将p插入到头结点与前一新结点之间。
实现代码算法如下:
/*头插法*/
/*头插法*/ void CreateListHead(LinkList *L,int n) { LinkList p; int i; srand(time(0)); *L = (LinkList)malloc(sizeof(Node)); (*L)->next = NULL; for(i = 0;i<n;i++) { p = (LinkList)malloc(sizeof(Node)); p->data = rand()%100+1; p->next = (*L)->next; (*L)->next = p; } }
/*尾插法*/ void CreateListTail(LinkList *L,int n) { LinkList p,r; int i; srand(time(0)); *L = (LinkList)malloc(sizeof(Node)); r = *L; for(i=0;i<n;i++) { p = (LinkList)malloc(sizeof(Node)); p->data = rand()%100+1; r->next = p; r = p; } r->next = NULL; }
单链表的整表删除:
思路:
声明一结点p和q;
将第一个结点赋值给p;
循环:
将下一个结点赋值给q;
释放p;
将q赋值给p;
实现代码如下:
Status ClearList(LinkList *L) { LinkList p,q; p = (*L)->next; while(p) { q = p->next; free(p); p = q; } (*L)->next = NULL; return OK; }
单链表结构和顺序存储结构优缺点:
存储分配方式:
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能:
查找:
顺序存储结构O(1)
单链表O(n)
插入和删除:
顺序表O(n)
单链表O(1)
空间性能:
顺序存储结构需要预分配存储空间,分配大了,浪费,小了容易发生上溢
单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
静态链表:
定义:用数组描述的链表叫做静态链表
#define MAXSIZE 1000 typedef struct { ElemType data; int cur; //相当于next指针 }Component,StaticLinkList[MAXSIZE];
初始化数组状态:
Status InitList(StaticLinkList space) { int i; for (i = 0;i<MAXSIZE-1;i++) space[i].cur = i+1; space[MAXSIZE-1].cur = 0; return OK; }
静态链表的插入和删除操作:
插入操作:
/*获取最近一个可以分配的结点的下标,否则返回0*/ int Malloc_SLL(StaticLinkList space) { int i = space[0].cur; /*当前数组第一个元素的cur存的值*/ /*返回第一备用空闲的下标*/ if(space[0].cur) space[0].cur = space[i].cur;/*由于要拿出一个分量来使用了,所以应把它的下一个分量用来作为备用*/ return i; }
//插入算法 Status ListInsert(StaticLinkList L,int i,ElemType e) { int j,k,l; k = MAXSIZE - 1; if(i<1||i>ListLength(L)+1) return ERROR; j = Malloc_SLL(L); if(j)//需要重点理解的块 { L[j].data = e; for(l=1;l<=i-1;l++) k = L[k].cur; L[j].cur = L[k].cur; L[k].cur = j; return OK; } return ERROR; }
删除操作:
Status ListDelete(StaticLinkList L,int i) { int j,k; if(i<1||i>ListLength(L)) return ERROR; k = MAXSIZE - 1; for(j = 1;j<=i-1;j++) k = L[k].cur; j = L[k].cur; L[k].cur = L[j].cur; Free_SSL(L,j); return OK; }
//释放被删除结点 void Free_SSL(StaticLinkList space,int k) { space[k].cur = space[0].cur; space[0].cur = k; }
//求静态链表中元素个数,(这个方法优点问题) int ListLength(StaticLinkList L) { int j = 0; int i = L[MAXSIZE-1].cur; while(i) { i = L[i].cur; j++; } return j; }
//个人认为比较正确的算法 int ListLength(StaticLinkList L) { int i,j,k; j = 0; k = L[0].cur; i = L[MAXSIZE-1].cur; while(i!=k) { i= L[i].cur; j++; } return j; }
静态链表的优缺点:
优点:
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中插入和删除操作所需要的移动大量数据元素的缺点。
缺点:
没有解决连续存储分配带来的表长难以确定的问题
失去了顺序存储结构的随机存取的特性
循环链表:
定义:将单链表中终端节点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circle linked list)。
解决了如何从一个结点出发能够访问全部结点的问题。
合并两个循环链表(主要语句代码):
p = rearA->next;//保存A头结点 rearA->next = rearB->next->next;//连接A的尾指针指向B的第一个数据元素 q = rearB->next;//保存B的头结点 rearB->next = p;//使B的尾指针指向A的头指针 free(q); //释放q,即释放B的头指针
双向链表:
定义:双向链表(double linked list)是在单链表的每个结点中,再设置一个指向器前驱结点的指针域。
ADT定义:
typedef struct DulNode { ElemType data; struct DulNode *prior; struct DulNode *next; }DulNode,*DulLinkList;
可以知道的一点就是:
p->next->prior = p = p->prior->next;
插入操作:
s->prior = p; s->next = p->next; p->next->prior = s; p->next = s;
删除操作:
p->prior->next = p->next; p->next->prior = p->prior; free(p);
本章内容结构
线性表
顺序存储结构
链式存储结构
单链表
静态链表
循环链表
双向链表