线性表
线性表
:线性表是具有相同数据类型的n个(n>=0)个数据元素的有限序列,其中n为表长,当n==0是线性表为空表,若是使用 L
命名为线性表,其结构为:
L=(a1,a2,a3,a4,....,an);
其中a1为表头元素
,an为 表尾元素
除第一个元素外,每一个元素 有且仅有一个直接前驱
,除最后一个元素外,每一个元素 有且仅有一个直接后继
,这就是线性表的 逻辑特性。
注意:线性表仅仅是一种逻辑结构,表示元素之间一对一的相邻关系。后面所要讲的顺序表
和链表
值得是存储结构,切勿将其混淆。
线性表的顺序表示
线性表的顺序储存结构
:指的是用一段 地址连续的存储单元一次存储线性表的数据元素
让我们先来看看如何定义顺序表
#define MAXSIZE 20
typedef int Elemtype;//具体类型由实际情况而定
typedef struct {
Elemtype data[MAXSIZE];
int length;//线性表当前表长
}Sqlist;
注意:线性表中的下标一般是从0开始的,但是我们平时说的位序一般时从1开始的
不难看出顺序存储的核心即使其中所定义的data数组,我这里采取的时 静态分配 的方式,同样也可以用 动态分配也就是malloc那种形式的分配内存,那样更加灵活
一下为动态内存分配的方式
typedef struct{
Elemtype* data;
int length;
}Seqlist;
其初始动态分配语句为:
L.data=(Elemtype*)malloc(sizeof(Elemtype)*Maxsize);
值得注意的时动态内存分配并不是链式储存,同样还是顺序存储结构,物理顺序依然和逻辑顺序保持一致,只是分配空间大小可以在运行时动态决定
主要特点:
1.随机访问
2.由于逻辑上相邻的元素物理上也是相邻,所以增加和删除要移动大量元素
3.储存密度高
基本操作的实现:
王道书上要求的操作有:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3P3UFzLO-1653239281977)(D:\刷题图片\image-20220515223453653.png)]
首先定义一下函数状态码等
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;//定义函数类型,其值是返回的是函数状态码,如ok等
顺序表的初始化:
//链表的初始化
Status Initlist(Sqlist* L) {
L->length = 0;
return OK;
}
顺序表求表长:
//输出顺序线性表长
int ListLength(Sqlist L) {
return L.length;
}
顺序表按值查找:
//按值查找操作,在线性表L中找到指定元素并且返回其位置
int LocateElem(Sqlist L, Elemtype e) {
if (L.length == 0) {
return -1;
}
int i = L.length;
int j;
for (j = 0; j < i; ++j) {
if (L.data[j] == e) {
return j + 1;
}
}
return -1;
}
顺序表按位查找:
//找出数组中第i个元素的位置,注意i是指位置,所以下标从0开始要向后+1
//值用e返回
int 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;
}
顺序表插入(熟练掌握):
//插入操作,在表中的i位置插入指定元素e
Status ListInsert(Sqlist* L, int i, Elemtype e) {
if (L->length== MAXSIZE) {
return ERROR;
}
if (i<1 || i>(L->length + 1)) {
//如果插入元素比第一个位置还小或者比最后一个元素的后一位还大时不成立
return ERROR;
}
if (i <= L->length) {
//要先把原有所有元素往后移动一位
for (int j =L->length-1; j >= i-1; --j) {
L->data[j+ 1] = L->data[j];
}
}
L->data[i - 1] = e;
L->length++;
return OK;
}
顺序表删除(熟练掌握):
//线性表的删除操作。删除表L中的第i个元素并且用e返回其值
Status ListDelete(Sqlist* L, int i, Elemtype* e) {
if (L->length == 0 || i < 1 || i>L->length||e == NULL) {
return ERROR;
}
*e = L->data[i - 1];
if (i < L->length) {
//如果不删除最后一个位置则要操作,否则不用操作
for (int j = i; j <L->length; ++j) {
//每个元素从删除位置向前移
L->data[i - 1] = L->data[i];
}
}
L->length--;
return OK;
}
顺序表中数据依次输出:
//输出操做,按前后顺序依次输出线性表L中得到所有元素值
void PrintList(Sqlist L){
int i;
for (i = 0; i < L.length; ++i) {
printf("%d", L.data[i]);
printf("\n");
}
return OK;
}
顺序表中判空:
//判断链表是否为空
Status IsEmpty(Sqlist L) {
if (L.length == 0) {
return TRUE;
}
else
{
return FALSE;
}
}
清空:
//清空链表
Status ClearList(Sqlist* L) {
L->length = 0;
return OK;
}
两个顺序表的合并
//线性表的合并
void unionL(Sqlist* L, Sqlist L1) {
int lengthl = ListLength(*L);
int lengthl1 = ListLength(L1);
Elemtype e;
if (lengthl == MAXSIZE) {
//说明已满不能插入
return ERROR;
}
for (int i = 1; i <= lengthl1; i++) {
//遍历要加入的表l1
GetElem(L1, i, &e);//找到相对应元素放入e
if (LocateElem(*L, e) == -1) {
//说明没有该元素
ListInsert(L, ++i, e);//插入
}
}
}
以上即为王道上的基本操作,具体题目还要随机应变
王道课后编程题晚点出。。
算法评价:
这里就插入和删除和修改操作进行评价
1.插入操作
最好时间复杂度:在表尾插入元素不用向后移,为最好,复杂度O(1)
;
最坏时间复杂度:在表头,为大O(n)
;
平均时间复杂度:如果是在第一个点插入,n个元素向后移,若是在第二点插入为n-1个元素向后移,假设所有可能插入位置概率相同即为k=1/(n+1)
也就可以轻易退出公式是为 (1/(n+1))*(n-i+1)
其中i从1到n+1,所以平均次数为 n/2
,因此时间复杂度为大O(n)
2.删除操作
最好时间复杂度:在表尾插入元素不用向前移,为最好,复杂度O(1)
;
最坏时间复杂度:在表头,后面都要前移,为大O(n)
;
平均时间复杂度:跟上面的最坏求法一样,就可以得出时间复杂度为 大O(n)
;
3.顺序查找操作(按值查找)
如果是通过下标查找当然是可以直接找到,按值则要遍历
最好时间复杂度:在表头查找元素,为最好,复杂度O(1)
;
最坏时间复杂度:在表尾,所有元素遍历一遍,为大O(n)
;
平均时间复杂度:跟上面的最坏求法一样,就可以得出时间复杂度为 大O(n)
;
总结:
无论是顺序查找(按值)还是删除插入,最好最坏和平均时间复杂度都一样
习题易错点:
1.顺序存储支持随机存取
这里还是说明一下…
随机存取就是直接存取,可以通过下标直接访问的那种数据结构,与存储位置无关,例如数组。非随机存取
就是顺序存取了,不能通过下标访问了,只能按照存储顺序存取,与存储位置有关,例如链表。
好像没啥好错的地方了,就到这里吧
线性表的链式表示
上一节我们所讲的顺序存储其实是有缺陷的,最大的缺点就是数据的增删的时候可能会移动大量的数据,然后链式存储则可以呵呵好解决这个问题
当我们用链式来存储线性表的时候,不需要地址连续的存储单元,即 不要求逻辑相邻的元素在物理位置上也相邻,它通过
链
建立起数据元素之间的逻辑关系,因此插入或者删除时候不需要移动元素只需要修该指针,但是也会因此失去 随机存取的特点只能进行 顺序存取
单链表的实现
首先要说明的是单链表的实现
线性表中的单链表最为重要的特点我认为还是 如果要寻找目标元素只能找他的前一个结点元素,为了方便单链表的创建增删改等存在一般会初始化一个头结点,真能给单链表带来两个好处:
1.由于第一个数据结点得到位置被存放在头结点的指针域中,所以就算是第一个位置也和其他的操作的位置无差
2.无论链表是否为空,其头指针指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也得到了统一
单链表的ADT定义
typedef int Elemtype;
//定义链表ADT(单链表)
typedef struct LNode {
struct Node* next;//记录下一个节点的地址
Elemtype data;
}LNode, * LinkList;//定义一个指向结构体的指针LinkList
每个节点都有一个数据域和指针域,数据域保存着你需要的数据类型,指针域会指向下一个节点地址
定义一个结构体指针是因为malloc动态分配时候是开辟的空间返回一个空类型指针变量,意思就是指针指向的地方的起始位置开始开辟出了你自己定义的一段空间一般用
sizeof
分配
在实现让方法之前先看看我们定义的状态码等信息
//定义状态码
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20//存储空间默认值
typedef int Status;//函数类型,返回的是状态码时候使用
初始化
Status InitList(LinkList* L) {
*L = (LinkList)malloc(sizeof(LNode));//产生头结点,并且让L指向它
if (!(*L)) {
//如果没创建成功
return ERROR;
}
//注意这里L为二级指针所以解引用之后是个指向Lnode的指针
(*L)->next = NULL;//初始状态下头节点的next为空
return OK;
}
初始化其实就是创建头结点的过程
单链表头插法
//头插法
//用到随机数种子创建n个随机数放松分别按头插法方式插入
void CreateListHead(LinkList* L, int n) {
//建立带头结点的单链表
*L = (LinkList)malloc(sizeof(LNode));
(*L)->next = NULL;
srand(time(0));
LinkList tmp;
for (int i = 0; i < n; ++i) {
tmp = (LinkList)malloc(sizeof(LNode));
tmp->data = rand() % 20 + 1;//生成20-1的随机数
tmp->next = (*L)->next;(1)
(*L)->next = tmp;(2)
}
}
其实还是很简单的,主要关键点是(1)和(2)步骤,因为是头插法也就是说把原来头结点和头结点的next节点中间插入新来的节点,第一个操作时让新来的节点指向原来头节点的下一个节点然后再让头结点指向新创建的节点
单链表的尾插法
//尾插法
void CreateListTail(LinkList* L, int n) {
srand(time(0));//初始化随机数种子
*L = (LinkList)malloc(sizeof(LNode));
LinkList p, q;
q = *L;//q用于指向尾部节点
for (int i = 0; i < n; ++i) {
p = (LNode*)malloc(sizeof(LNode));
p->data = rand() % 20 + 1;
q->next = p;
q= p;
}
q->next = NULL;
}
如果是尾插,那肯定要创建一个变量一直指向现在最后一个元素的位置,然后新元素加入,让之前监控指针指向的下一个节点赋值上新的结点,最后让监控结点指向当前最后一个元素位置
判空
//当表存在是看看是否为空表
Status IsEmpty(LinkList L) {
if (L->next == NULL) {
return TRUE;
}
return FALSE;
}
单链表的插入
//单链表的插入
Status ListInsert(LinkList* L, int i, Elemtype e) {
LinkList temp = *L;
int point = 1;
//寻找节点进行插入
while (temp && point < i) {
temp = temp->next;
point++;
}
if (temp == NULL || point > i) {
printf("无法插入");
return ERROR;
}
LinkList s;
s = (LinkList)malloc(sizeof(LNode));
s->data = e;
s->next = temp->next;
temp->next = s;
return OK;
}
单链表的删除
//删除元素
Status ListDelte(LinkList* L, int i, Elemtype* e) {
//遍历寻找对应元素
LinkList tmp;
LinkList p = *L;
int j=0;//计数,p从头结点开始没向后移一次就+1
while (p && j < i-1) {
//遍历寻找到对应元素
//这里的i-1的不懂可以推一推
//主要是要找到删除节点的前移节点,i-1主要是最后有个j++的动作
//说明他已经是i-1个元素了就退出
p = p->next;
j++;
}
//判断一下没找到的情况,一个是j》i一个数超出p范围了
if (p == NULL || p->next == NULL||j>i) {
printf("不合法超出表长");
return ERROR;
}
tmp = p->next;
p->next = tmp->next;
*e = tmp->data;
free(tmp);
return OK;
}
单链表的删表
Status ClearList(LinkList* L)
{
LinkList p, q;
p = (*L)->next;
while (p) {
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
return OK;
}
按序号查找
//找到对应位置元素
Status GetElem(LinkList L, int i, Elemtype* e)
{
LinkList p;
p = L->next;
int calc = 0;//计数器
while (p&&calc<i) {
p = p->next;
calc++;
}
if (!p || calc > i) {
return ERROR;
}
*e = p->data;
return OK;
}
按值查找
//返回对应元素位置
int LocateELem(LinkList L, Elemtype e) {
LinkList p;
int i = 0;//记住位置
p = L->next;
while (p) {
++i;
if (p->data == e) {
return i;
}
p = p->next;
}
re
单链表主要功能实现即上面这些了,其他相关操作也类似以上操作
双链表的实现:
就是再单链表的基础上加个前驱结点,其实相较于单链表操作更加简单了,由于王道书上就指出了增加和删除的操作,这里也就简单实现下,其他操作都和单链表类似
状态码等信息定义
#define OK 1
#define ERROR 0
#define True 1
#define False 0
typedef int Elemtype;
//函数状态码
typedef int Statue;
双链表的定义
//定义adt
typedef struct Node {
Elemtype data;
struct Node* prior, * next;
}DNode,*DLinkList;
初始化
//函数的初始化
Statue InitList(DLinkList* L) {
if ((*L)) {
printf("链表已经存在,无需再初始化了");
return ERROR;
}
//开始创建
*L = (DNode*)malloc(sizeof(DNode));
if (!(*L)) { return ERROR; }
(*L)->prior = NULL;
(*L)->next = NULL;
return OK;
}
增加操作
// 在链表LL的第ii个位置插入元素ee,返回值:ERROR-失败;OK-成功。
int InsertList(DLinkList L, unsigned int ii, Elemtype* ee) {
if (L == NULL || ii < 1 || ee == NULL) {
return ERROR;
}
int count=0;
DLinkList tmp = L;
while ((count < ii-1) && (tmp != NULL)) {
tmp = tmp->next;
count++;
}
if (tmp == NULL) {
printf("超过了表长");
return ERROR;
}
DNode* nNode = (DNode*)malloc(sizeof(DNode));
if (nNode == NULL)
return ERROR;
//考虑到结构体整体拷贝的情况
memcpy(&nNode->data, ee, sizeof(Elemtype));
nNode->next = tmp->next;
nNode->prior = tmp;
if (nNode->next == NULL) {
nNode->next->prior = nNode;
}
tmp->next = nNode;
return OK;
}
双链表的删除
Statue DeleteNode(DLinkList LL, unsigned int ii) {
if ((LL == NULL) || (ii < 1)) {
return ERROR;
}
int count = 0;
DLinkList tmp = LL;
while ((tmp!= NULL)&&(count<ii-1))
{
tmp = tmp->next;
count++;
}
if (tmp->next == NULL) {
return ERROR;
}
DNode* rem = tmp->next;//要删除1结点
tmp->next = rem->next;
if (rem->next != NULL) {
tmp->next->prior = tmp;
}
free(rem);
return OK;
}
怎么说的有手就行,重点是又水了一天hhh
循环单链表的实现:
循环单链表的基本算法实现几乎和单链表一致,所不同的时候就是
表尾
的判断其中会导致操作不同,所要牢记的一点是正是因为循环单链表是一个环,所以初始化的时候next指针要指向自己,同时任何位置上的插入和删除都是等价的,无需判断是否在表尾
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bPIkYJOT-1653239281978)(C:\Users\达芬奇\AppData\Roaming\Typora\typora-user-images\image-20220522001359274.png)]
这里主要实现一下几个和单链表有差别的地方
循环双链表的定义
typedef int ElemType;
//为什么交cs呢circularsingle,真是取名鬼才
typedef struct CsNode
{
ElemType data; // 存放结点的数据元素。
struct CsNode* next; // 指向下一个结点的指针。
}CsNode, * CLinkList;
初始化
//初始化链表,失败返回null,成功直接返回头结点
CsNode* InitList1()
{
CsNode* head = (CsNode*)malloc(sizeof(CsNode)); // 分配头结点。
if (head == NULL) return NULL; // 内存不足,返回失败。
head->next = head; // 头结点的next指针指向自己。 // xxx
return head;
}
摧毁链表
//摧毁链表
void DestroyList(CLinkList LL) {
CsNode* head = LL;
LL = LL->next;
CsNode* tmp;
while (LL != head) {
//跟单链表不一样不是到null是到头结点未结束所以要从第二个结点开始遍历
tmp = LL;
LL = LL->next;
free(tmp);
}
free(head);
return;
}
清空链表
void ClearList(CLinkList LL) {
if (LL == NULL) {
return;
}
//清空链表指的是释放所有数据节点但不包括头结点
//所以我们指向下一个
CsNode* tmp = LL->next;
CsNode* tmp2;
//保留头结点
while (tmp->next != LL) {
tmp2 = tmp;
tmp = tmp->next;
free(tmp2);
}
//当到头结点出来了
//所以指向自己即可
tmp->next=tmp;
return;
}
插入结点
//插入结点(失败-1,成功-1)
int InsertList(CLinkList LL, int i, ElemType* ee) {
if ((LL != NULL) || (ee != NULL)) {
return -1;
}
if (i < 1) {
return -1;
}
//日常记忆结点留一个
CsNode* tmp;
if (i = 1) {
//这种情况下就是指向第一个数据节点,前一个就是头结点
tmp = LL;
}
else {
tmp = LL->next;//指向第一个数据节点
int count = 1;//第一个
while ((tmp != LL) || (count < i - 1))
{
//当count最后++等于i-1退出循环
tmp = tmp->next;
count++;
}
if (tmp == LL) {
return NULL;
}
CsNode* tmp2 = (CsNode*)malloc(sizeof(CsNode));
if (tmp2 == NULL) {
return -1;
}
memcpy(&tmp2->data, ee, sizeof(ElemType));
tmp2->next = tmp->next;
tmp->next = tmp2;
return 1;
}
}
删除结点
//删除结点中的第i个结点,删除成功但会1,失败则是-1
int DeleteNode(CLinkList LL, unsigned int i) {
if (LL == NULL) {
return -1;
}
if (i < 1) {
return -1;
}
//记忆节点
CsNode* tmp;
if (i == 1) {
tmp = LL;
}
else
{
tmp = LL->next;
int count = 1;
while ((tmp!=LL)&&(count<i-1)) {
tmp = tmp->next;
count++;
}
}
if (tmp == LL)
return -1;
CsNode* tmp2 = tmp->next;
tmp->next = tmp->next->next;
free(tmp2);
return -1;
}
其他操作基本没啥差别,其实循环单链表唯一一个要注意的即是while中的判断要最后的结点不是指向null之前停下而是指向head的时候结束,还有就是插入和删除的时候要判断,插入或者删除的地址是不是第一个地址要额外进行判断,因为其点进入不了while循环
循环双链表的实现:
这里就不实现了,因为真的和循环单链表的操作实在是太相似了
它和循环单恋表的区别
1.多了个头结点
prior
指针指向前驱结点2.初始化的时候,其头结点的
prior
与next
域的值都要等于链表L,也就是都指向自己3.尾结点的
next
指针指向的是head
结点(与循环单链表一样)4.头结点的
prior
指针指向尾结点
静态链表的实现:
静态链表顾名思义就是用静态数组的方式来模拟一个链表的实现,这个在没有指针类型的高级机器语言会出现,但是用途感觉还是很少的,由于是借助于一个固定长度的数组来描述线性表的链式存储结构,灵活度比较低的。
和链表的一样,
adt
中含有数据域data
和指针域next
,但是静态链表中的指针常常被称之为游标
,也就是cur
,和顺序表相同初始化之前要预先分配一块连续的内存空间。
静态链表图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CJcUg7Iy-1653239281978)(C:\Users\达芬奇\AppData\Roaming\Typora\typora-user-images\image-20220522235316773.png)]
基本思想
由于王道第二章对这个静态链表描述较少,这里做一点点补充吧。
首先就是第一点,通常会将静态链表的0位置的结点设置为整个链表的头结点,用他来对已经申请的结点进行管理。
头结点的特点
数据域中不存数据,游标的值尾第一个申请结点所对应的数组下标,若是该静态链表还未申请结点来存放数据,那么其游标的值设为-1表示该静态链表为空
那么未被分配空间的结点要怎么办呢?
我们这里会让数组位置1空出来,来作为未被分配结点的管理结点pool,其数据域同样不放入数据,其cur指向的点为未被分配数据的第一个结点,也就是,王道图为更加简化版没有设置一个管理未被分配区(也被称为静态链表的备用区),也就是说pool这个结点充当一个备用区的头结点的作用,其中不包含数据且其游标值为第一个未申请结点对应的下标值(当表中已经没有未被申请的节点后,其cur设置为0,表示链表已满)
具体实现
定义:
#define MAXSIZE 20
typedef char ELemType;
//静态链表的定义
typedef struct {
ELemType data;
int cur;
}SLinkList[MAXSIZE];
初始化:
//静态链表的初始化
void InitSList(SLinkList space) {
//先要初始化备用空间pool(下标从1-maxsize-1)由于一开始都没分配空间
//然后吧0位置的cur设为-1,因为一开始没有结点含有数据
//还有就是备用区最后一个空间后面再无可以用的空间游标设为0
for (int i = 1; i < MAXSIZE; ++i) {
space[i].cur = i + 1;
}
space[MAXSIZE - 1].cur = 0;
space[0].cur = -1;
}:
申请结点:
//申请结点
int MallocKnot(SLinkList space) {
int i = space[1].cur;//表示从备用区头结点的游标即下一个空数据结点
if (i != 0) {
//说明还有空闲结点
//更改头指针指向表示指向下一个空的结点
space[1].cur = space[i].cur;
}
return i;//返回新申请的空间的数组下标的
}
头插法:
//头插法
int InsertKnot(SLinkList space, ELemType ee) {
int tmp = MallocKnot(space);
if (tmp == 0) {
//申请空间失败
printf("已满");
return 0;
}
//成功
space[tmp].data = ee;
if (space[0].cur == -1) {
space[0].cur = tmp;
space[tmp].cur = -1;
}
else
{
//之前已经有数据节点了
//因为后面的结点完全不知道前面的结点情况
//所以新插入结点的游标直接让头节点游标赋值即可
space[tmp].cur = space[0].cur;
space[0].cur = tmp;
}
return 1;
}
删除结点(头部):
//删除结点(在头部)
int Delete(SLinkList space) {
int tmp = space[0].cur;
space[0].cur = space[tmp].cur;//逻辑上删除
//要重新分配一下备用区pool
space[tmp].cur = space[1].cur;
space[1].cur = tmp;
}
打印链表:
//结点数据显示
void showlist(SLinkList space) {
int i = space[0].cur;
while (i!=-1)
{
printf("%c", space[i].data);
i = space[i].cur;
}
return;
}
总结:
优点
1.在插入和删除操作肘,只需要修改游标
,不需要移动元素,从而改进了在顺序存储结构中的插入和删
除操作需要移动大量元素的缺点
缺点
失去了顺序存储结构随机存取
的特性。