顺序存储
顺序存储结构是存储结构类型中的一种,该结构是把逻辑上相邻的结点存储在物理位置上相邻的存储单元中,结点之间的逻辑关系由存储单元的邻接关系来体现。简单来说就是,用一段连续的地址存放数据元素,数据间的逻辑关系和物理关系相同。
线性结构的特点就好比一串珠子,其特点是第一个节点只有一个后继,没有前驱,最后一个节点是只有一个前驱,没有后继。而其余的节点只有一个前驱和一个后继。说白了线性表就是一串。下方这个图就是线性表的示例图。中间蓝色的节点前方的是就是改点对应的前驱,后边就是改点对应的后继。从下方可以明确看出head没有前驱只有后继,而tail只有前驱没有后继。
顺序存储的优点:存储密度大,空间利用度高,比链式存储节约空间;存储操作上方便操作,顺序支持随机存取,查找会比较容易
缺点:插入或者删除元素时不方便,花费的时间更多
往顺序线性表中插入数据
见下图往B与C之间插入一个M,在插入之前我们需要将CD整体往后移一个位置,为M空出一个位置,再见M放入。
往顺序线性表中删除元素
与上面所说的插入其实挺像的,前者在插入位置后的元素都往后移,后者则是向左移覆盖掉要删除的元素,需要注意的是,要将最后一个元素进行移除,可以参考下图
链式存储
链式存储结构,又叫链接存储结构。在计算机中用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的).它不要求逻辑上相邻的元素在物理位置上也相邻.因此它没有顺序存储结构所具有的弱点,但也同时失去了顺序表可随机存取的优点。
优点:插入或删除时方便些,空间使用灵活;存储密度小,空间利用度低
缺点:查找会相较顺序存储方式复杂一些,花费的时间会更多
往链式线性表中插入数据
(1)往链表的后方添加元素
这里我们先看图,其实就是将想要插入的元素往链表的尾部插入,然后更新一下为节点tail的位置即可。
(2)往链表的头部插入元素
看这个图我想你应该可以理解这句话,首先第一步需要我们的“C”去找组织中的A,第二步是头结点接到新元素C上。
往链式线性表中删除数据
要想移除单向链表中的一个元素,首先我们得找到被移除结点的前驱的位置,比如是pre“A”。当前移除的元素是remove“B”,让pre->next = remove->next, 然后再执行remove->next = NULL。经过上面这些步骤,B就与链表脱离关系了。
我们对比一下这两种存储方式的优缺点
一、从空间性能角度
(1)由下表可以看出顺序存储的存储密度是1(100%)。什么意思呢?就是开辟一段连续的空间,用来存顺序表,这一段空间所有的位置都用来存储我们需要的数据信息,没有空间的浪费。所以利用率达到了100%【也就是存储密度是1】。然而链式结构中的节点不仅存储了数据,还保存了指针,然而指针并没有存储我们用到的数据,所以说,链式存储的存储密度是小于1的。
(2)由下表可以看出顺序存储的容量分配要弱一些。顺序存储需要多少空间要是事先确定,链式存储是动态分配的,更加灵活吗,可以动态的更改分配。
二、从时间性能方面
(1)在时间性能反方面,查找运算用顺序表更方便,链式表相对来讲就差一些。当然,在内容没有顺序的前提下,他们的时间复杂度是一样的。都是从第一个开始匹配。如果存储的顺序表本身是有序的,涉及到二分查找法时,顺序存储要更优。
(2)读取信息(指定某个内容读取出来)顺序表更优。比如说要读取a[5],在顺序表中,直接读取a[5]即可,但是在链式存储中,假设指针在头结点,头结点指向1号节点, 要next()4次才能定位到a[5]。
(3)插入运算,链式结构更优。链式结构在插入数据的时候,只需要将要插入的节点的指针指向要插入节点上一个节点的next节点,在将上一个节点的next节点指向要插入的节点即可。但是在顺序结构中,需要将要插入位置之后的所有元素向后移,再插入新的节点、
(4)删除运算,链式结构更优。链式结构在删除节点是,只需要将要删除节点的上一个节点的指针指向他的下一个节点即可。然而在顺序结构中,需要将要删除节点之后的所有节点前移一个位置。
线性表
线性表,全名为线性存储结构。线性表是n个数据特性相同的元素的组成有限序列,是最基本且常用的一种线性结构(线性表,栈,队列,串和数组都是线性结构),同时也是其他数据结构的基础。
线性表是最简单的数据结构,其主要特征有:
1、 每个线性表只有一个头元素,一个尾元素;
2、 除第一个数据元素外,每个元素都有一个直接前驱;
3、 除最后一个数据元素外,每个元素都有一个直接后继。
前驱和后继
数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)。例如,图 1 显示的这组数据,其中 1、2、3、4 和 5 都是这组数据中的一个元素。
另外,对于具有“一对一”逻辑关系的数据,我们一直在用“某一元素的左侧(前边)或右侧(后边)”这样不专业的词,其实线性表中有更准确的术语:
某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;
图 4 前驱和后继
顺序表:
顺序表,全名顺序存储结构,是线性表的一种。
使用顺序表存储数据之前,除了要申请足够大小的物理空间之外,为了方便后期使用表中的数据,顺序表还需要实时记录以下 2 项数据:
- 顺序表申请的存储容量;
- 顺序表的长度,也就是表中存储数据元素的个数;
需要自定义顺序表,代码如下:
typedef struct Table{
int * head;//声明了一个名为head的长度不确定的数组,也叫“动态数组”
int length;//记录当前顺序表的长度
int size;//记录顺序表分配的存储容量
}table;
注意,head 是声明的一个未初始化的动态数组,不要只把它看做是普通的指针。
接下来开始学习顺序表的初始化,也就是初步建立一个顺序表。建立顺序表需要做如下工作:
- 给 head 动态数据申请足够大小的物理空间;
- 给 size 和 length 赋初值;
#define Size 5 //对Size进行宏定义,表示顺序表申请空间的大小
table initTable(){
table t;
t.head=(int*)malloc(Size*sizeof(int));//构造一个空的顺序表,动态申请存储空间
if (!t.head) //如果申请失败,作出提示并直接退出程序
{
printf("初始化失败");
exit(0);
}
t.length=0;//空表的长度初始化为0
t.size=Size;//空表的初始存储空间为Size
return t;
}
整个顺序表初始化的过程被封装到了一个函数中,此函数返回值是一个已经初始化完成的顺序表。这样做的好处是增加了代码的可用性,也更加美观。与此同时,顺序表初始化过程中,要注意对物理空间的申请进行判断,对申请失败的情况进行处理,这里只进行了“输出提示信息和强制退出”的操作,可以根据你自己的需要对代码中的 if 语句进行改进。
通过在主函数中调用 initTable 语句,就可以成功创建一个空的顺序表,与此同时我们还可以试着向顺序表中添加一些元素,C 语言实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#define Size 5
typedef struct Table{
int * head;
int length;
int size;
}table;
table initTable(){
table t;
t.head=(int*)malloc(Size*sizeof(int));
if (!t.head)
{
printf("初始化失败");
exit(0);
}
t.length=0;
t.size=Size;
return t;
}
//输出顺序表中元素的函数
void displayTable(table t){
for (int i=0;i<t.length;i++) {
printf("%d ",t.head[i]);
}
printf("\n");
}
int main(){
table t=initTable();
//向顺序表中添加元素
for (int i=1; i<=Size; i++) {
t.head[i-1]=i;
t.length++;
}
printf("顺序表中存储的元素分别是:\n");
displayTable(t);
return 0;
}
程序运行结果如下:
可以看到,顺序表初始化成功。
顺序表的基本操作
顺序表插入元素
向已有顺序表中插入数据元素,根据插入位置的不同,可分为以下 3 种情况:
- 插入到顺序表的表头;
- 在表的中间位置插入元素;
- 尾随顺序表中已有元素,作为顺序表中的最后一个元素;
虽然数据元素插入顺序表中的位置有所不同,但是都使用的是同一种方式去解决,即:通过遍历,找到数据元素要插入的位置,然后做如下两步工作:
- 将要插入位置元素以及后续的元素整体向后移动一个位置;
- 将元素放到腾出来的位置上;
例如,在 {1,2,3,4,5}
的第 3 个位置上插入元素 6,实现过程如下:
- 遍历至顺序表存储第 3 个数据元素的位置,如图 1 所示:
图 1 找到目标元素位置
- 将元素 3 以及后续元素 4 和 5 整体向后移动一个位置,如图 2 所示:
图 2 将插入位置腾出
- 将新元素 6 放入腾出的位置,如图 3 所示:
图 3 插入目标元素
因此,顺序表插入数据元素的 C 语言实现代码如下:
//插入函数,其中,elem为插入的元素,add为插入到顺序表的位置
table addTable(table t,int elem,int add)
{
//判断插入本身是否存在问题(如果插入元素位置比整张表的长度+1还大(如果相等,是尾随的情况),或者插入的位置本身不存在,程序作为提示并自动退出)
if (add>t.length+1||add<1) {
printf("插入位置有问题\n");
return t;
}
//做插入操作时,首先需要看顺序表是否有多余的存储空间提供给插入的元素,如果没有,需要申请
if (t.length==t.size) {
t.head=(int *)realloc(t.head, (t.size+1)*sizeof(int));
if (!t.head) {
printf("存储分配失败\n");
return t;
}
t.size+=1;
}
//插入操作,需要将从插入位置开始的后续元素,逐个后移
for (int i=t.length-1; i>=add-1; i--) {
t.head[i+1]=t.head[i];
}
//后移完成后,直接将所需插入元素,添加到顺序表的相应位置
t.head[add-1]=elem;
//由于添加了元素,所以长度+1
t.length++;
return t;
}
注意,动态数组额外申请更多物理空间使用的是 realloc 函数。并且,在实现后续元素整体后移的过程,目标位置其实是有数据的,还是 3,只是下一步新插入元素时会把旧元素直接覆盖。
顺序表删除元素
从顺序表中删除指定元素,实现起来非常简单,只需找到目标元素,并将其后续所有元素整体前移 1 个位置即可。
后续元素整体前移一个位置,会直接将目标元素删除,可间接实现删除元素的目的。
例如,从 {1,2,3,4,5}
中删除元素 3 的过程如图 4 所示:
图 4 顺序表删除元素的过程示意图
因此,顺序表删除元素的 C 语言实现代码为:
table delTable(table t,int add){
if (add>t.length || add<1) {
printf("被删除元素的位置有误\n");
return t;
}
//删除操作
for (int i=add; i<t.length; i++) {
t.head[i-1]=t.head[i];
}
t.length--;
return t;
}
顺序表查找元素
顺序表中查找目标元素,可以使用多种查找算法实现,比如说二分查找算法、插值查找算法等。
这里,我们选择顺序查找算法,具体实现代码为:
//查找函数,其中,elem表示要查找的数据元素的值
int selectTable(table t,int elem){
for (int i=0; i<t.length; i++) {
if (t.head[i]==elem) {
return i+1;
}
}
return -1;//如果查找失败,返回-1
}
顺序表更改元素
顺序表更改元素的实现过程是:
- 找到目标元素;
- 直接修改该元素的值;
顺序表更改元素的 C 语言实现代码为:
//更改函数,其中,elem为要更改的元素,newElem为新的数据元素
table amendTable(table t,int elem,int newElem){
int add=selectTable(t, elem);
t.head[add-1]=newElem;//由于返回的是元素在顺序表中