前言
临阵磨枪,不快也光。应付考试,大量摘抄课本内容,会陆续添加其他相关内容及代码。最后祝我考试顺利。
小结:
补充:
逻辑结构:
存储结构:简单来说:逻辑结构在存储器中的映像(表示) 。书本上定义:数据对象在计算机中的存储表示称为数据的存储结构,也称为物理结构。把数据对象存储到计算机时,通常要求既要存储各数据元素的数据,又要存储数据元素之间的逻辑关系,数据元素在计算机内用一个结点来表示。
- 数据:所有能被输入到计算机中且被计算机处理的符号的集合,是计算机操作对象的总称,也是计算机处理的信息的某种特定的符号表示形式。
- 数据项:数据结构讨论的最小单位
- 数据元素:数据中的一个“个体”,数据结构讨论中的基本单位,数据元素是数据项的集合
- 数据结构:带结构的数据元素的集合
- 若采用顺序存储,则各个数据元素在物理上必须是连续的;若采用非顺序存储,则各个数据元素在物理上是离散的
- 数据的存储结构会影响存储空间分配的方便程度
- 数据的存储结构会影响对数据运算的速度
- 数据的运算:施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤
线性表的概念
由n(n>=O)个数据特性相同的元素构成的有限序列称为线性表。
线性表中元素的个数n(n>=O)定义为线性表的长度,n=O时称为空表。
对于非空的线性表或线性结构,其特点是:
- 存在唯一的一个被称作“第一个"的数据元素;
- 存在唯一的一个被称作“最后一个"的数据元素;
- 除第一个之外,结构中的每个数据元素均只有一个前驱;
- 除最后一个之外,结构中的每个数据元素均只有一个后继。
线性表的顺序存储结构及实现
线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素, 这种表示也称作线性表的顺序存储结构或顺序映像。通常,称这种存储结构的线性表为顺序表(SequentialList)。其特点是,逻辑上相邻的数据元素,其物理次序也是相邻的。
#include <stdlib.h>
#include <stdio.h>
#define Size 10//宏定义,表示线性表的长度
typedef struct{
int *head;//定义一个长度不确定的数组,‘动态数组’
int length;//定义线性表的长度
int size;//定义线性表的存储容量
}sequenceTable;
void initTable(sequenceTable *t){//初始化顺序表
t->head=(int *)malloc(Size*sizeof(int));//动态申请线性表存储空间
if(!t->head){//申请空间失败,初始化失败,提示后退出程序
printf("初始化失败\n");
exit(0);
}
//申请内存空间成功,将表长度初始为0,存储容量初始为Size
t->length=0;
t->size=Size;
}
//插入fn-ele插入元素,i-插入位置
void insertTable(sequenceTable *t,int ele, int i){
if(i>t->length+1||i<1){
printf("插入地址超出线性表地址范围\n");
return;
}
if(t->length==t->size){//如果内存空间不足,需要额外申请空间
t->head=(int *)realloc(t->head,(t->length+1)*sizeof(int));
if(!t->head){
printf("线性表内存申请失败");
return;
}
t->size++;
}
/*插入操作,所有元素先后移一位在插入
用户输入的位置比实际位置大1,因此需要j>=i-1*/
for(int j=t->length;j>=i-1;j--){
t->head[j+1]=t->head[j];
}
t->head[i-1]=ele;
t->length++;
}
//删除fn
void delTable(sequenceTable *t,int i){
if(i>t->length||i<1){
printf("删除失败!");
return;
}
//删除
for(int j=i;j<t->length;j++){
t->head[j-1]=t->head[j];
}
t->length--;
}
//查询fn
int searchTable(sequenceTable t,int ele){
for(int i=0;i<t.length;i++){
if(t.head[i]==ele){
return i+1;
}
}
return -1;
}
//修改fn
void updateTable(sequenceTable *t,int ele,int replace){
int searchResult=searchTable(*t,ele);
if(searchResult>0){
t->head[searchResult-1]=replace;
}else{
printf("被替换的数据不存在\n");
}
}
void showData(sequenceTable t1){
printf("输出线性表元素:");
for(int i=0;i<t1.length;i++){
printf("%d ",t1.head[i]);
}
printf("\n");
}
int main()
{
sequenceTable t1={NULL,0,0};//初始化动态指针,t1={NULL,0,0}
initTable(&t1);
for(int i=0;i<Size;i++){//给线性表添加元素
t1.head[i]=i+1;
t1.length++;
}
showData(t1);
//插入元素
insertTable(&t1,11,2);
showData(t1);
//删除元素
delTable(&t1,2);
showData(t1);
//修改元素
updateTable(&t1,3,33);
showData(t1);
//释放申请内存
free(t1.head);
return(0);
}
线性表的链接存储结构及实现
线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素,与其直接后继数据元素
之间的逻辑关系,对数据元素
来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素
的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点(
,(1<=i<=n)的存储映像)链结成一个链表,即为线性表(
,
,··,
)的链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表。
根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表用千实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
单链表
用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。换句话说,指针为数据元素之间的逻辑关系的映像,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻,由此,这种存储结构为非顺序映像或链式映像。通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。在使用链表时,我们关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。
#include <stdlib.h>
#include <stdio.h>
typedef struct link{
int ele;//数据域
struct link *next;//指针域,指向后继元素。因常用next,又叫Next域
}Link;
Link* initLink(int num){
//创建头指针
Link* p=NULL;
//创建临时变量temp暂时当作首元结点
Link* temp=(Link*)malloc(sizeof(Link));
temp->ele=0;//有头给首元结点赋值
//temp->ele=1;//无头给首元结点赋值
temp->next=NULL;//首元结点后继节点暂时置为NULL
p=temp;//头指针指向首元结点
//创建节点
for(int i=1;i<num;i++){//有头
//for(int i=2;i<num;i++){//无头解开注释
Link* a=(Link*)malloc(sizeof(Link));
a->ele=i;//给创建节点数据域赋值
a->next=NULL;//给创建节点指针域赋值
temp->next=a;//每次temp的后继节点指向当前的a
temp=temp->next;//给每次temp赋值为当前的a;此处temp=a;也可
}
return p;//返回链表
}
Link* insertLink(Link* p,int ele,int index){
// if(index==1){//无头解开注释
// Link* new=(Link*)malloc(sizeof(Link));
// new->ele=ele;
// new->next=p;
// p=new;
// return p;
// }
Link* new=NULL;
Link* temp=p;
for(int i=1;i<index;i++){
//for(int i=1;i<index-1;i++){//无头解开注释
temp=temp->next;
if(temp==NULL){
printf("插入元素失败!!!\n");
return p;
}
}
//创建节点
new=(Link*)malloc(sizeof(Link));
new->ele=ele;
new->next=temp->next;//将temp的后继节点地址赋值给插入元素的后继节点
temp->next=new;//temp的后继节点指向new
return p;
}
void updateLink(Link* p,int oldEle,int newEle){
p=p->next;//有头加这句,无头注释这句
while(p){
if(p->ele==oldEle){
p->ele=newEle;
return;
}
p=p->next;
}
printf("更新元素失败\n");
}
int delLink(Link* p,int ele){
//int delLink(Link* *p,int ele){//无头解开注释
Link* del=NULL;
Link* temp=p;//有头
// Link* temp=*p;//无头解开注释
// if(temp->ele==ele){//无头解开注释
// (*p)=(*p)->next;
// free(temp);
// return 1;
// }
int find=-1;
while(temp->next){
if(temp->next->ele==ele){
find=1;
break;
}
temp=temp->next;
}
if(find==-1){
printf("删除失败,该元素不在链表中\n");
return 0;
}
del=temp->next;
temp->next=temp->next->next;
free(del);
return 1;
}
int selectLink(Link* p,int ele){
int i=1;
p=p->next;//有头节点加这句,无头结点注释这句
while(p){
if(p->ele==ele){
printf("查找成功,%d元素在链表中第%d位\n",ele,i);
return i;
}
p=p->next;
i++;
}
printf("查找失败,%d元素不在链表中\n",ele);
return -1;
}
void displayLink(Link* p){//循环输出链表数据
p=p->next;//有头节点加这句,无头结点注释这句
while(p){
printf("%d ",p->ele);
p=p->next;
}
printf("\n");
}
void LinkFree(Link* p){//释放链表
//解决free(): double free detected in tcache 2
Link* fr=NULL;
while(p->next){
fr=p->next;
p->next=p->next->next;
free(fr);
}
free(p);
}
int main()
{
int num=5;
Link* p=NULL;
printf("初始化链表:");
p=initLink(num);
displayLink(p);
printf("在链表第1位插入9:");
p=insertLink(p,9,1);
displayLink(p);
printf("删除链表元素9:");
//delLink(&p,9);//无头结点删除
delLink(p,9);//有头结点删除
displayLink(p);
selectLink(p,4);
printf("更新链表元素4为6:");
updateLink(p,4,6);
displayLink(p);
LinkFree(p);
return(0);
}
/* 标准输出:
初始化链表:1 2 3 4
在链表第1位插入9:9 1 2 3 4
删除链表元素9:1 2 3 4
查找成功,4元素在链表中第4位
更新链表元素4为6:1 2 3 6
*/
循环链表
循环链表(CircularLinked List)是另一种形式的链式存储结构。其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他结点,图2.17所示为单链的循环链表。类似地,还可以有多重链的循环链表。
循环单链表的操作和单链表基本一致,差别仅在于:当链表遍历时,判别当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL或p->next!=NULL,而循环单链表的判别条件为p!=L或p->next!=L。在某些情况下,若在循环链表中设立尾指针而不设头指针(见图2.18(a)),可使一些操作简化。例如,将两个线性表合并成一个表时,仅需将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点。当线性表以图2.18(a)的循环链表作存储结构时,这个操作仅需改变两个指针值即可,主要语句段如下:
p=B->next->next;
B->next=A->next;
A->next=p;
上述操作的时间复杂度为0(1),合并后的表如图2.18(b)所示。
双向链表
单链表和循环链表的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继结点的执行时间为0(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表(DoubleLinked List)。顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。结点结构如图2.19(a)所示,在C语言中可描述如下:
//双向链表的存储结构
typedef struct DuLNode{
ElemType data; //数据域
struct DuLNode *prior;//直接前驱
struct DuLNode *next;//直接后继
}DuLNode,*DuLinkList;
和单链的循环表类似,双向链表也可以有循环表,如图2.19(c)所示,链表中存有两个环,图2.19(b)所示为只有一个表头结点的空表。
在双向链表中,若d为指向表中某一结点的指针(即d为DuLinkList型变量),则显然有
d->next->prior = d->prior->next = d
这个表示方式恰当地反映了这种结构的特性。
顺序表和链表的比较
-
空间性能的比较
-
存储空间的分配顺序表的存储空间必须预先分配,元素个数扩充受一定限制,易造成存储空间浪费或空间溢出现象;而链表不需要为其预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。基于此,当线性表的长度变化较大,难以预估存储规模时,宜采用链表作为存储结构。
-
存储密度的大小链表的每个结点除了设置数据域用来存储数据元素外,还要额外设置指针域,用来存储指示元素之间逻辑关系的指针,从存储密度上来讲,这是不经济的。所谓存储密度是指数据元素本身所占用的存储量和整个结点结构所占用的存储量之比,即
存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为1而链表的存储密度小于1。如果每个元素数据域占据的空间较小,则指针的结构性开销就占用了整个结点的大部分空间,这样存储密度较小。例如,若单链表的结点数据均为整数,指针所占用的空间和整型量相同,则单链表的存储密度为0.5。因此,如果不考虑顺序表中的空闲区,则顺序表的存储空间利用率为100%,而单链表的存储空间利用率仅为50%。基于此,当线性表的长度变化不大,易于事先确定其大小时,为了节约存储空间,宜采用顺序表作为存储结构。
- 时间性能的比较
- 存取元素的效率顺序表是由数组实现的,它是一种随机存取结构,指定任意一个位置序号i都可以在0(1)时间内直接存取该位置上的元素,即取值操作的效率高;而链表是一种顺序存取结构,按位置访问链表中第i个元素时,只能从表头开始依次向后遍历链表,直到找到第i个位置上的元素,时间复杂度为O(n),即取值操作的效率低。基于此,若线性表的主要操作是和元素位置紧密相关的这类取值操作,很少做插入或删除时,宜采用顺序表作为存储结构。
- 插入和删除操作的效率对千链表,在确定插入或删除的位置后,插入或删除操作无需移动数据,只需要修改指针,时间复杂度为0(1)。而对千顺序表,进行插入或删除时,平均要移动表中近一半的结点,时间复杂度为O(n)。尤其是当每个结点的信息量较大时,移动结点的时间开销就相当可观。基于此,对于频繁进行插入或删除操作的线性表,宜采用链表作为存储结构。
参考书目:严蔚敏-《数据结构(C语言版)》