线性表
线性表的定义
- 线性表(list):由零个或多个数据元素组成的有限序列
- 关键:
- 首先它是一个序列,也就是说元素之间是有个先来厚道的
- 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继
- 另外,线性表强调是有限的,事实上,无论及算你发展到多强大,它所处理的元素都是有限的
- 数学定义:
- 若线性表积为(a1,……,ai-1,ai,ai+1……an)则表中ai - 1领先于ai,ai领先于ai+1,称ai - 1是ai的直接前驱元素,ai+1是ai的直接后继元素
- 所以线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表
抽象数据类型
-
数据类型:指一组性质相同的值的集合及定义在此集合上的一些操作的总称
-
抽象:是指抽取出事物具有的普遍性的本质。它要求抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,,它隐藏了繁杂的细节
-
抽象数据类型:是指一个数学模型及定义在该模型上的一组操作
-
抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和现实无关
-
”抽象“的定义在于数据类型的数学抽象数据特性
-
而且,抽象数据类型不仅仅指那些已经定义并实现的数据类型,还可以是计算机编程者在设计软件程序时自定义的数据类型
-
抽象数据类型的标准格式
ADT 抽象数据类型名 Data 数据元素之间逻辑关系的定义 Operation 操作 endADT
线性表的抽象数据类型
-
线性表的抽象数据类型定义:
-
ADT 线性表(List)
-
Data
线性表的数据对象集合为{a1,a2,a3,a4,…an},每个元素的类型均为DataType。其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素,除了最后一个元素an外,每个元素有且只有一个直接后继元素。数据元素之间的关系时一对一的关系
-
Operation
*InitList(L):初始化操作,建立一个空的线性表L
ListEmpty(L):判断线性表是否为空表,若线性表为空返回true,否则返回false
*ClearList(L):将线性表清空
*GetElem(L,i,e):将线性表L中的第i个位置元素值返回给e
LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素中序号表示成功,否则,返回0表示失败
*ListInsert(L,i,e):在线性表L中第i个位置插入新元素e
**ListDelete(L,i,e):删除线性表L中第i个位置元素,并用e返回其值
ListLength(L):返回线性表L的元素个数
-
endADT
-
对于不同的应用,线性表的基本操作是不同的,上述操作时最基本的,对于实际问题中设计的关于线性表更复杂的操作,完全可以用这些基本操作的组合来实现
-
线性表的顺序存储结构
-
线性表有两种物理存储结构:顺序存储结构和链式存储结构
-
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
-
物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中
#define MAXSIZE 20 typedef int ElemType; typedef struct{ ElemType data[MAXSIZE]; int length; //线性表当前长度 }Sqlist;
顺序储存结构封装需要三个属性:
- 存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置
- 线性表的最大存储容量:数组的长度MaxSize
- 线性表的当前长度:length
注意:数组长度与线性表的当前长度需要区分一下:数组的长度是存放线性表的存储空间的总长度,一般初始化后不变。而线性表的当前长度时线性表中元素的个数,会是变化的
-
地址计算方法:假设ElemType占用的是c个存储单元(字节),那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是(LOC表示获得存储位置的函数):LOC(ai + 1) = LOC(ai) + c
-
*所以对于第i个元素ai的存储位置可以有a1推算得出:LOC(ai) = LOC(a1) + (i-1)c
-
初始化操作
//InitList void InitList(Sqlist* L) { L->length = 0; }
-
获取长度操作
//ListLength Status ListLength(Sqlist L) { return L.length; }
-
获取元素操作
-
实现GetElem的具体操作:
//GetElem函数 #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;
}
**这里返回值类型Status是一个整型,约定返回1代表OK,返回0代表ERROR**
-
-
插入操作
-
插入算法的思路:
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 将要插入元素填入位置i处
- 线性表长+1
-
代码
//ListInsert //初始条件:顺序线性表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; }
-
-
删除操作
-
删除算法的思路
- 如果删除位置不合理,抛出异常
- 去除删除元素
- 从删除元素位置开始遍历到最后一个元素,分别将它们都向前移动一个位置
- 表长-1
-
实现代码
//ListDelete //初始条件:线性表L已存在,1<=i<=Listlength(L) //操作结果:删除L的第i个元素,并用e返回其值,L的长度-1 Status ListDelete(Sqlist *L,int i,int *e) { int k; if (L->length < 1) { return ERROR; } if (i<1 || i>L->length) { return ERROR; } *e = L->data[i - 1]; if (i < L->length) { for (k = i - 1; k < L->length; k++) { L->data[k] = L->data[k + 1]; } } L->length--; return OK; }
-
分析:插入和删除的时间复杂度
- 最好的情况:插入和删除操作刚好要求在最后一个位置操作,因为不需要移动任何元素,所以此时的时间复杂度为O(1)
- 最坏的情况:如果要插入和删除的位置是第一个元素,那就意味着要移动所有的元素向后或者向前,所以这个时间复杂度为O(n)
- 至于平均情况,就取中间值O((n-1)/2)
- 平均情况复杂度简化后还是O(n)
-
-
线性表顺序存储结构的优缺点
- 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1)。而在插入删除时,时间复杂度都是O(n)
- 这说明,他比较适合元素个数比较稳定,不经常插入和删除元素,而更多的操作时存取数据的应用
- 优点:
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任意位置的元素
- 缺点:
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 容易造成存储空间的碎片
线性表的链式存储结构
定义
-
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以存在内存中未被占用的任意位置
-
比起顺序存储结构每个数据元素只需要存储一个位置就可以了。现在链式存储结构中,除了要存储数据元素信息外,还要存储它的后继元素的存储地址(指针)
-
存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这俩部分信息组成数据元素称为存储映像,称为结点(Node)
-
n个结点链接成一个链表,即为线性表(a1,a2,a3,……,an)的链表存储结构
-
因为此链表的每个结点中只包含一个指针域,所以叫做单链表
-
我们把链表中的第一个节点的存储位置叫做头指针,最后一个结点指针为空(NULL)
头指针与头结点的异同
-
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
-
头结点
-
头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但可以用来存放链表的长度)
-
有了头结点,对在第一个元素结点前插入结点和删除第一个结点其操作与其它结点的操作就统一了
-
头结点不一定是链表的必须要素
-
单链表存储结构
-
c语言中可以用结构指针来描述单链表
typedef struct Node{ ElemType data;//数据域 struct Node* Next;//指针域 }Node; typedef struct Node* LinkList;
结点由存放数据元素的数据域和存放后继结点地址的指针域组成
单链表的读取
-
获得链表第i个数据的算法思路
- 声明一个结点p指向链表第一个结点,初始化j从1开始;
- 当j<i,就遍历链表,让p的指针向后移动,不断指向下一个结点,j+1;
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,返回结点p的数据
-
GetElem
Status GetElem(LinkList L,int i,ElemType *e) { int j; LinkList p; j = 1; p = L->Next; while (p && j < i) { p = p->Next; ++j; } if (i < 1 || !p) { return 0; } *e = p->data; return 1; }
单链表的插入
-
思路:
- 让i->next和p->next的指针做一点改变
- s->next = p->next;
- p->next = s;
- 顺序不可换
- 让i->next和p->next的指针做一点改变
-
单链表第i个数据插入结点的算法思路
- 声明一个结点p指向链表头结点,初始化j从1开始
- 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 若查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给s->data
- 单链表的插入刚才俩个标准语句
- 返回成功
-
代码:
//GetElem Status GetElem(LinkList L,int i,ElemType *e) { int j; LinkList p; j = 1; p = L->Next; while (p && j < i) { p = p->Next; ++j; } if (i < 1 || !p) { return 0; } *e = p->data; return 1; } //============================== //ListInsert 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 (j > i || !p) { return 0; } s = new Node[1]; s->data = e; s->Next = p->Next; p->Next = s; return 1; }
单链表的删除
-
思路:
- p -> next = p->next->next;
- 也可以: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结点
-
代码
//ListDelete Status ListDelete(LinkList *L,int i,ElemType *e) { int j; LinkList p, q; j = 1; p = *L; while (p&&j < i) { p = p->Next; j++; } if (!p || j > i) { return 0; } q = p->Next; p->Next = q->Next; *e = q->data; delete[]q; return 1; }
效率比较
- 对整个算法来说,他们的时间复杂度都是O(n)
- 在i未知的情况下,单链表数据结构在插入和删除上与线性表的顺序结构没有太大优势
- 但如果从i位置开始插入连续10个元素,对于顺序结构意味着每次插入都需要n-i个位置
- 而单链表我们只需要在第一次时,找到第i个位置的指针,此时为O(n)接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)
单链表的整表创建
- 对于顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解
- 对于单链表,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成
- 创建单链表的过程是一个动态生成链表的过程,从空表的初始状态起,依次建立个元素结点并逐个插入链表
- 单链表整表创建的算法思路如下
- 声明一结点p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即建立一个带头结点的单链表
- 循环实现后继点的赋值和插入
头插法建立单链表
-
头插法从一个空表开始生成新节点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,知道结束为止
-
简单来说,就是把新加进的元素放在表头后的第一个位置:
- 先让新结点的next指向头结点之后
- 然后让表头的next指向新结点
-
代码:
//CreateListHead void CreateListHead(LinkList *L,int n) { LinkList p; int i; srand(time(0)); *L = new Node; (*L)->Next = NULL; for (i = 0; i < n; i++) { p = new Node; p->data = rand() % 100 + 1; p->Next = (*L)->Next; (*L)->Next = p; } }
尾插法建立单链表
-
代码:
//CreateListTail void CreateListTail(LinkList *L,int n) { LinkList p, r; int i; srand(time(0)); *L = new Node; r = *L; for (i = 0; i < n;i++) { p = new Node; p->data = rand() % 100 + 1; r->Next = p; r = p; } r->Next = NULL; }
单链表的整表删除
-
单链表整表删除的算法思路如下:
- 声明结点p和q
- 将第一个结点赋值给p,下一个结点赋值给q
- 循环执行p和将q赋值给p的操作
-
代码:
//ClearList Status ClearList(LinkList *L) { LinkList p, q; p = (*L)->Next; while (p) { q = p->Next; delete[]p; p = q; } (*L)->Next = NULL; return 1; }
单链表结构与顺序存储结构优缺点
- 存储分配方式
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
- 时间性能:
- 查找
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在计算出某位置的指针后,插入和删除时间仅为O(1)
- 查找
- 空间性能:
- 顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出
- 单链表不需要分配存储空间,只有有就可以分配,元素个数也不受限制
静态链表
-
线性表的静态链表存储结构
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mJY7JtQP-1615698699897)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimage.bubuko.com%2Finfo%2F201809%2F20180921101929015935.png&refer=http%3A%2F%2Fimage.bubuko.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1617889385&t=8395467a97f995cb74c2be05ce73a5fe)]
#define MAXSIZE 1000 typedef struct { ElemType data;//数据 int cur;//游标(cursor) }Conponent, StaticLinkList[MAXSIZE];
-
对静态链表进行初始化相当于初始化数组:
//InitList Status InitList(StaticLinkList space) { int i; for (i = 0; i < MAXSIZE; i++) { space[i].cur = i + 1; } space[MAXSIZE - 1].cur = 0; return OK; }
-
【注】:
- 我们对数组的第一个和最后一个元素做特殊处理,它们的data不存放数据
- 我们通常把使用的数组元素称为备用链表
- 数组的第一个元素,即下标为0的那个元素的cur就存放备用链表的第一个结点的下标
- 数组的最后一个元素,即下标为MAXSIZE-1 的cur则存放第一个由数值的元素的下标,相当于单链表中的头结点作用
-
静态链表的插入操作
-
首先是获得空闲分量的下标
//Malloc_SLL int Malloc_SLL(StaticLinkList space) { int i = space[0].cur; if (space[0].cur) { space[0].cur = space[i].cur; } return i; }
-
ListInsert
//ListInsert 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]是要插入位置的前一位,所以应将它的游标赋值给L[j] L[k].cur = j;//将L[j]的前一个元素的游标指向L[j] return OK; } return ERROR; }
-
-
静态链表的删除操作
-
ListDelete.c
//ListDelete Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i<1||i>ListLength(L))
{
return ERROR;
}k = MAXSIZE; for(j = 1;j <= i-1;j++){ k = L[k].cur; } j = L[k].cur; L[k].cur = L[j].cur; Free_SLL(L,j); return OK;
}
- **Free_SLL** ~~~c //Free_SLL void Free_SLL(StaticLinkList space,int k) { space[k].cur = space[0].cur; space[0].cur = k; }
-
ListLength
//ListLength int ListLength(StaticLinkList L) { int j = 0; int i = L[MAXSIZE - 1].cur; while(i){ i = L[i].cur; j++; } return j; }
-
-
静态链表优缺点
- 优点:
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点
- 缺点:
- 没有解决连续存储分配(数组)带来的表长难以确定的问题
- 失去了顺序存储结构随机存取的特性
- 优点:
快慢指针
-
**原理:设置俩个指针*search、 *mid都指向单链表的头结点。其中 search的移动速度时 mid的2倍。当search指向末尾结点的时候,mid就在中间了
-
代码:
Status GetMidNode(LinkList L,ElemType *e) { LinkList search,mid; mid = search = L; while(search -> next !=NULL){ if(search->next->next != NULL) { search = search->next->next; mid = mid->next; } else { search = search->next; } } *e = mid->data; return OK; }
循环链表
-
循环链表和单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为NULL,现在则是head->next是否等于head
-
代码
-
初始化部分:ds_init
void ds_init(node **pNode, int item) { node *temp; node *target; while (1) { if (item == 0) { return; } if ((*pNode) == NULL) { *pNode = new node; if (!(*pNode)) { exit(0); } (*pNode)->data = item; (*pNode)->next = *pNode; } else { for (target = (*pNode); target->next != (*pNode); target = target->next) { temp = new node; if (!temp) { exit(0); } temp->data = item; temp->next = *pNode; target->next = temp; } } } }
-
插入部分:ds_insert
//ds_insert void ds_insert(node **pNode,int i,int item) { node *temp; node *target; node *p; int j = 1; if (i == 1) { temp = new node; if (!temp) { exit(0); } temp->data = item; for (target = (*pNode); target->next != (*pNode); target = target->next) { } temp->next = (*pNode); target->next = temp; (*pNode) = temp; } else { target = (*pNode); for (j = 1; j < i - 1; j++) { target = target->next; } temp = new node; if (!temp) { exit(0); } temp->data = item; temp->next = target->next; target->next = temp; } }
-
删除部分:ds_delete
void ds_delete(node **pNode,int i) { node *target; node *temp; int j; if (i == 1) { for (target = (*pNode); target->next != (*pNode); target = target->next); temp = *pNode; *pNode = (*pNode)->next; target->next = *pNode; delete[]temp; } else { target = *pNode; for (j = 1; j < i - 1; j++) { target = target->next; } temp = target->next; target->next = temp->next; delete[]temp; } }
-
返回结点所在位置:ds_search
int ds_search(node *pnode,int elem) { node *target; int i = 1; for (target = pnode; target->data != elem && target->next != pnode; i++) { target = target->next; } if (target->next == pnode && target->data != elem) { return 0; } else { return i; } }
-
约瑟夫问题
-
n个人围圈报数,报出m出列,剩下的时几号
#include<iostream> using namespace std; typedef struct node { int data; node *next; }node; node* create(int n) { node *p = NULL, *head; head = new node; p = head; node *temp; int i = 1; if (n != 0) { while (i <= n) { temp = new node; temp->data = i++; p->next = temp; p = temp; } p->next = head->next; } delete[]head; return p->next; } int main() { int n = 41; int m = 3; int i; node *p = create(n); node *temp; while (p != p->next) { for (i = 1; i < m - 1; i++) { p = p->next; } cout << p->next->data << "->"; temp = p->next; p->next = temp->next; delete[]temp; p = p->next; } cout << p->data << endl; system("pause"); return 0; }
循环链表的特点
-
循环链表的特点时无须增加存储量,仅对链接方式稍作改变,即可使得表处理更加方便
-
例题:实现将俩个线性表(a1,a2.……an)和(b1,b2……bn)连接成一个线性表的运算
-
分析:
- 若单链表或头指针标识的单循环链表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1连到an后面,执行时间为O(n)
- 若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间为O(1)
-
判断单链表中是否有环
- 有环的定义是,链表的尾结点指向了链表中的某个结点
- 判断链表是否有环,有俩种方法
- 使用pq俩个指针p总是向前走,但q每次都从头开始走,对于每个结点,看p走的步数是否和q一样,当p从6走到3时,用了6步,此时若q从head出发,则指向走俩步就到3,因而步数不等,出现矛盾,存在环
- 使用p,q指针,p每次向前走一步,q每次向前走两步,若在某个时候p==q,则存在环
双向链表
-
双向链表结点结构
typedef struct DualNode { int data; struct DualNode *prior; struct DualNode *next; }DualNode, *DuLinkList;
-
双向链表也可以有循环链表
-
插入操作代码实现:
s->next = p; s->prior = p->prior; p->prior->next=s; p->pripr=s;
代码:
void DualInsert(DuLinkList *L, int n, int elem) { DuLinkList p; DuLinkList temp; p = *L; for (int i = 0; i < n; i++) { p = p->next; } temp = new DualNode; temp->data = elem; temp->next = p; temp->prior = p->prior; p->prior->next = temp; p->prior = temp; }
-
删除操作代码实现:
p->prior->next = p->next; p->next->prior = p->prior; delete[]p;
代码
void DualDelete(DuLinkList *L, int n) { DuLinkList p; p = *L; for (int i = 0; i < n; i++) { p = p->next; } p->prior->next = p->next; p->next->prior = p->prior; delete[]p; }
双向链表
-
双向链表结点结构
typedef struct DualNode { int data; struct DualNode *prior; struct DualNode *next; }DualNode, *DuLinkList;
-
双向链表也可以有循环链表
-
插入操作代码实现:
s->next = p; s->prior = p->prior; p->prior->next=s; p->pripr=s;
代码:
void DualInsert(DuLinkList *L, int n, int elem) { DuLinkList p; DuLinkList temp; p = *L; for (int i = 0; i < n; i++) { p = p->next; } temp = new DualNode; temp->data = elem; temp->next = p; temp->prior = p->prior; p->prior->next = temp; p->prior = temp; }
-
删除操作代码实现:
p->prior->next = p->next; p->next->prior = p->prior; delete[]p;
代码
void DualDelete(DuLinkList *L, int n) { DuLinkList p; p = *L; for (int i = 0; i < n; i++) { p = p->next; } p->prior->next = p->next; p->next->prior = p->prior; delete[]p; }