线性表
线性表分为顺序表和链表
顺序表
逻辑上相邻的数据元素存储在相邻的物理存储单元中。
可以根据初始地址、单位长度和序列号而直接找到要查找的元素
#define maxsize
typedef struct //定义 (一个新的) 结构体 (数据类型)
{
ElemType elem[maxsize]; //这里的ElemType指的是int float等
int length; //
}SeqList; // 叫SeqList
SeqList L;或者// 创建一个 SeqList数据类型的变量L (类似 int L)
SeqList *L // 创建一个 SeqList数据类型的指针类型的变量L (类似 int *L)
顺序表的增删改查
查找操作
1.按照序号查找 L.elem(i):
结构体类型的变量 L 中的数据可以 “.”出来 使用 例如 L.elem[1]
结构体类型的指针变量 *L 中的数据则用“->”取出使用 例如L->elem[1]
2.按照内容查找
思路:简单遍历
int 函数名( Seqlist L,ElemType x)
{
int i=1;//初始化循环变量 顺序表的下标是从1开始的这也是和数组的一个区别
while((i<=L.length)&&(L.elem[i]!=x)) // 循环和判断相等放在一起简化代码 而将判断是否相等放在后面可以防止越界
i++;
if(i<L.length) return (i)
else return(-1)//未找到,返回一个一定不在表中的数据
}
插入操作
#define OK 1
#define ERROR 0
int inslist(SeqList *L ,int i,ElemType x)//参数 *L:结构体变量的指针形式 i: 插入位置(第1、2、3...个) x待插入的元素
{
int k;
if((i<1)||i>L->length+1)// 之所以有i>L->length +1 是为了保证顺序表以顺序存储,不会出现NULL间隔,这也是顺序表和数组的不同之处,当然仅从数据类型上看是可以的,但是这样就不符合顺序表这一数据结构的定义了
{
printf("插入位置i不合法");
return (ERROR);//宏定义后,增加了代码的可读性
}
if(L->length>=maxsize-1);//这里的maxsize-1 是因为其在内部的L->element[maxsize]依然是一个数组
{
printf("表已满");
return (ERROR);//宏定义后,增加了代码的可读性
}
for(k=L->length;k>=i;k--)
{
L->elem[k+1]=L->elem[k]; //循环移位,将第i位腾出了
}
L->elem[i]=x; //终于可以插入了
L->length++;//插入后,还要讲长度加一
return(OK);//终于结束了
}
删除操作
int DelList(SeqList *L ,int i, ElemType *e)//只是删除的话,移位补上空位即可,而加入的第三个指针类型的参数是为了取出被删除的元素
{
int k;
if((i<)||i>->length)
{
printf("删除位置不合法");
return(ERROR);
}
*e=L->elem[i];//取出被删除的元素 前期处理/手动滑稽
for(k=i;k<=L->length-1;k++)//移位 覆盖
{
L->elem[k]=L-elem[k+1];
}
L->length--;// 长度减一 算是后期处理吧
retuen (OK);
}
优缺点总结
优点:
1.无须为表示节点间的逻辑关系而增加额外的存储空间 (就是 逻辑结构简单 按顺序放就完了,不然为啥要叫顺序表😃 )
2.可以根据索引 随机方便的 存取任意一个元素
缺点:
1.插入删除不方便 😂 显而易见 每一次插入和删除都要都要把操作位 后面的元素都移动一次 ,简直够了
2.内部存储数据的实际上就是一个数组,所以当然和数组一样都是在编译时就为之分配了确定的内存大小 无法随意扩展 😂哎,静态存储 的老毛病了
链表 😅
采用链式存储结构的线性表被称为链表
- 实现角度:1
- 静态链表: 兼具链表和线性表的有点,采取静态存储,就是创建一个 自定义的结构体类型的数组 数组中每个元素都存储了两个数 data && next (当前数据,和下一关数据的数组下标)
- 动态链表
1😗* 一般而言考试是不会考的 ** - 连接方式
- 单链表
- 双向链表
- 循环链表
单链表
typedef struct Node
{
ElemType data ;
struct Node *next;
}LNode,*LinkList;
在这里讲一下的结构体的定义吧
一般而言,结构体最简单的定义方法是
struct 类型名{
成员列表;
成员列表;
....
};
然后就可以使用 *struct 类型名 变量名* 去定义一个自定义变量了;
还可以
struct 类型名{
成员列表;
成员列表;
....
}变量1,变量2;
最后 因为每一次 使用自定义类型去定义一个变量时都要加上一个struct 很麻烦
所以 为他取了一个别名
即
typedef struct Node(自定义的数据类型)
{
ElemType data ;
struct Node *next;
}LNode(等价于struct Node),*LinkList (等价于 *(struct Node)) ;
这样就可以简化操作了。
单链表的插入
思路:
1.找到待插入的位置。动态链表采用的是动态分配的地址空间,不能根据索引直接获得待插入节点前后的地址,所以需要从头指针开始遍历链表,直至遍历到第i-1个节点。1
2.将第i-1个的节点的next的值赋给指向新的节点的next值 //这一步必须是在前面,23颠倒就找不到后面的节点了将i-1的节点的next值指向当前的新的节点,哎呦😂好像忘记些什么了。
我们还没有申请节点呢,哪来的待插入节点的地址值。/流汗
所以在第三步之前的某一个位置,我们需要先申请一个节点空间。
LNode *s;
s=(Node*)malloc(sizeof(Node));//应对考试的话,只记住要这样申请一下才可以使用就好了,下面的是我自己给出的一些扩展,可以了解一下。如果弄懂了的话,以后就不容易忘了。
这段语句中有一各malloc函数:这是一个申请节点空间的函数,它的参数是要申请的节点空间的大小
它的返回值是一个指向这个空间的指针
还有一个强转(Node *)将指向申请到的空间的指针强转为Node类型的指针,所以即使不阅读源码我们也可以猜到,当初C语言的设计者对于指针的设计使其同时可以存储两个信息,一个是指针所指向的空间,还有一个是空间的分配[^2]
[^2]:即申请到的空间是如何分块的,每一块有多大,举个例子: 比如指定一个数组类型的指针变量
int * a[10]; 那么a就是这个指针变量,他其中存储的是这个数组的首地址,还有每个元素所占的内存大小。
void InsList(LinkList L, int i, ElemType e) //参数接析:L LinkList 类型的指针,存储有链表首地址,i 要插入的位置, e 要插入的元素
{
LNode *pre,*s; //pre 用于上面的第一步,找到需要插入的位置之前的节点,s 即将申请的新节点
int k=0;
pre =L;
while(pre!=NULL&&k<i-1)//循环定位
{
pre=pre->next;
k=k+1;
}
if(k!=i-1) //就是遍历完了没找到,判断是否是越界才跳出循环
{
printf("查入位置不合理");
return ;是的话,就可以直接( ^_^ )/拜拜了
}
//终于定完位了,很快哈,我们可以开始 插入了
s=(Node *)malloc(sizeof(Node));//申请节点
s->data = e; // 将数据加入到节点中,完善节点
s-next=pre->next;//和后面的节点相连了哈,很快哈
pre->next=s;//和前面的节点也相连了哈,没有二百多斤,但也很快哈
}
// 连接就两句
//定位,判断插入位置是否合理就用了一大半的篇幅
单链表的删除 (原理差不多,我略讲了哈)
1.定位 --> 遍历
2.判断待删除节点的合理性
3.将被删除的节点的后面的节点地址赋给被删除节点的前一个节点的next ,被删除节点在这个链中去掉
4.释放这个节点空间
void DelList(LinkList L,int i,ElemType *e)
{
LNode *pre,*s; //pre 用于上面的第一步,找到需要插入的位置之前的节点,s 即将申请的新节点
int k=0;
pre =L;
while(pre!=NULL&&k<i-1)//循环定位
{
pre=pre->next;
k=k+1;
}
if(k!=i-1) //就是遍历完了没找到,判断是否是越界才跳出循环
{
printf("删除位置不合理");
return ;是的话,就可以直接( ^_^ )/拜拜了
}
//终于定完位了,很快哈,我们可以开始 删除了
s=pre->next;//保存被删除节点的位置,方便后面释放空间
pre->next=pre->next->next;//删除 ,就这一步,哎!!!
free(s);//释放空间
return OK;
}
改,查就不展开说了
1. 遍历到指定位置
2. 查,改 就完了
循环链表
就是最后一个的next 不赋值为NULL了,而是赋值为头结点的地址。虽然只是很小的改变但是在以后的操作中会更方便,更适合某些情景
这是有尾指针的两个循环链表的合并
解释一下这里的RA->next=RB->next->next 第一个循环链表的尾指针连接到了第二个循环链表的第一个元素,其实是删除了第二个链表的头结点的,虽然第二条链表的头结点没有画出来,但是有一步free(RB->next)释放了第二条链表头结点的空间
如果没有尾指针则仍然需要遍历才能找到最后的节点。
双向链表
typedef struct DNode
{
ElemType data ;
struct DNode *prior,*next;//这个时候别名还没有取完,所以只能 struct DNode来定义了
}DNode,*DoubeList; 好嘛让我们恭喜别名取完。
就是增加了一个指向前一个节点的指针。考试应该不会考。我画几个简单的增删改查的原理图应该就足够了。
#### 增加
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201128164531592.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3RoZWZseWJyaWQ=,size_16,color_FFFFFF,t_70)
#### 删除
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201128164833539.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3RoZWZseWJyaWQ=,size_16,color_FFFFFF,t_70)
#### 改查 只是遍历之后取出或改变 节点中的数据而已