线性表
线性表是一种基础的数据结构,用于表示元素集合,这些元素按照一对一的线性关系排列。在线性表中,除了第一个和最后一个元素,每个元素都只有一个直接前驱和一个直接后继。
线性表可以分为两类:顺序表(线性表的顺序表示)和链表(线性表的链式表示)
1.1、线性表的顺序存储表示
//顺序存储的存储结构
#define MAXSIZE 100 //线性表的可能达到的最大长度
typedef struct
{
ElemType *elem; //存储空间的基地址
int length; //当前长度
}Sqlist; //顺序表的结构体类型为Sqlist
typedef关键字用于为已存在的数据类型创建一个新的名字,如上面的代码中为新建的结构体起名为Sqlist。
Elemtype 是为了描述统一而制定的,实际运用时使用typedef 数据类型 Elemtype为Elemtype确定数据类型。(后面的status也是如此)
typedef 数据类型 Elemtype //eg.typedef int Elemtype
顺序存储中结构体也可以作为元素对象,即elem。举个例子:
//定义一个结构体,包含学生姓名和学号
typedef struct
{
string name; //姓名
string ID; //学号
}Student;
//定义顺序表
typedef struct
{
Student *elem;
int length;
}Sqlist;
说白了就是创建一个名为elem的动态数组,其中elem[i]中存储的都是学生这个结构体,其中length表明顺序表中的元素个数,最后一个元素为elem[length-1].
1.2、顺序表中的基本操作
(1)初始化
define OK 1
define OVERFLOW -2
Status InitList(Sqlist &L)
{//构造一个空的顺序表
L.elem=new ElemType[MAXSIZE]; //为顺序表分配一个大小为MAXSIZE的空间
if(!L.elem)exit(OVERFLOW);
L.length=0;
return OK;
}
这里有个代码小技巧,退出程序使用exit(OVERFLOW)而不直接使用exit(-2),而OVERFLOW也是我们进行预定义的 这是为什么呢.
虽然两种写法都可以,且都不会影响代码的运行结果,但是使用OVERFLOW就很容易知道是溢出错误。
此外,这里使用到了new
关键字动态分配内存,即在程序运行时(而非编译时)分配的内存,它允许程序在需要时请求更多的内存,并在不再需要时释放内存。
new
关键字的使用通常遵循以下步骤:
- 分配内存:
new
会在堆(heap)上为对象分配足够的内存。 - 构造对象:如果分配的是类类型的对象,
new
会调用该类的构造函数来初始化对象。 - 返回指针:
new
操作完成后,会返回一个指向分配内存的指针。
(2)取值
Status GetElem(Sqlist L,int i,Elemtype &e)
{
if(i<1||i>L.length) return ERROR
e=L.elem[i-1]
return OK
}
(3)查找
int LcateElem(Sqlist L,Elemtype e)
{
for(int i=0;i<L.length;i++)
if(L.elem[i]==e)
return i+1;
return 0;
}
(4)插入
Status ListInsert(Sqlist &L,int i,Elemtype e) //i:插入位置 e:插入的元素
{
if(L.length==MAXSIZE) return ERROR;
if(i<1||i>L.length+1) return ERROR;
for(int j=L.length-1; j>=i-1;j--)
L.elem[j+1]=L.elem[j];
L.elem[i-1]=e;
++L.length;
return OK;
}
(5)删除(删除第i个元素)
Status ListDelete(Sqlist &L,int i)
{
if(i<1||i>L.length) return ERROR;
for(int j=i-1;j<L.length;j++)
L.elem[j-1]=L.elem[j];
--L.length;
return OK;
}
2.1、线性表的链式表示
链表是一种线性数据结构,由一系列节点组成,每个节点包含数据和一个或多个指向其他节点的引用(或指针)。链表的特点是节点在内存中不必连续存储,它们通过指针连接,这样可以更有效地利用内存空间,并且可以快速地进行插入和删除操作。
线性表的线性表示可以称之为链表,链表又分为单链表、循环链表、双向链表。
-
单向链表(Single Linked List):每个节点只包含数据和指向下一个节点的指针。最后一个节点的指针指向null,表示链表的末尾。
-
双向链表(Double Linked List):每个节点除了数据和指向下一个节点的指针外,还有一个指向前一个节点的指针。这使得双向链表可以从两个方向遍历。
-
循环链表(Circular Linked List):在循环链表中,最后一个节点的指针不是指向null,而是指向第一个节点,形成一个环。这种链表可以无限循环遍历。
2.1.1 单链表的定义和表示
在单链表中,每个元素都是一个独立的节点,每个节点由两部分组成:数据域和指针域。数据域用于存储节点的值,指针域(通常称为“下一个指针”或“后继指针”)用于指向下一个节点。
//单链表的存储结构
typedef struct LNode
{
Elemtype data; //结点的数据域
Struct LNode *next; //结点的指针域
}LNode,*LinkList; //LinkList为指向结构体LNode的指针类型
ps.对于初学者而言,可能有点不理解结构体中定义结构体指针这句代码,原因在于对c语言的编译过程还不是太了解。
在C语言中,定义一个结构体时,可以在定义内部使用结构体类型名来声明指向该结构体的指针。这是因为结构体类型名在结构体定义的开始就已经可见了。因此,即使在结构体定义完成之前,也可以使用 struct LNode *next;
来声明一个指向 struct LNode
的指针。
另一方面,LNode
是通过 typedef
关键字定义的 struct LNode
的一个别名。这个别名在 typedef
语句之后才有效。因此,在结构体定义内部,不能使用 LNode *next;
,因为此时 LNode
别名还没有被定义。
2.1.2、单链表中的基本操作
(1)初始化
Status InitList(LinkList &L)
{
L=new LNode;
L->next=NULL;
return OK;
}
这里new出来的L就是图2.8中的L,它只是一个头指针,并没有为其数据域赋值。
(2)取值
Status GetElem(LinkLIst &,int i,Elemtype &e)
{
LNode *p=L->next;
int j=1;
while(p&&j<i)
{
p=p->next;
++j;
}
if(!p||j>i)return ERROR;
e=p->data;
return OK;
}
(3)查找(按值查找)
LNode* LocateElem(LinkList L,Elemtype e)
{
LNode *p=L->next;
while(p&&p->data!=e)
{
p=p->next;
}
return p;
}
在函数定义位置可以知道我们返回的是一个指针类型的数据,但是教材上的定义为LNode *Locate()
* 在LocateElem的右上角可能让很多小伙伴产生疑问?但其实在c++编译的过程中,*默认是属于前面LNode数据返回值类型的,因为函数名左上角带星号没什么意义。所以两种写法都是一样的。但是在定义指针变量的的时候*应该紧挨变量名 eg.LNode *p
(4)插入
Status ListInsert(Sqlist &L,inti,Elemtype e)
{
LNode *p=L;
int j=0;
while(p && j<i-1)
{
p=p->next;
++j;
}
if(!p || j>i+1) //这个地方个人认为j>i+1有点多余,根据上面代码,j不可能大于i+1,欢迎思考评论
reruen ERROR
s=new LNode; //区别LNode *s
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
LNode *p
和p = new LNode
是指针声明的两种不同用法,它们在内存分配和对象构造方面有显著差异。
LNode *p
: 这行代码声明了一个指向LNode
类型对象的指针变量p
。这个声明并没有分配任何实际的内存,也没有构造任何LNode
对象。它只是告诉编译器p
是一个可以指向LNode
类型对象的指针。此时,指针p
的值是未定义的,如果尝试访问它所指向的内存,将会导致未定义行为,可能引发程序错误或崩溃。
p = new LNode
: 这行代码不仅声明了一个指针变量p
,还使用了new
运算符来动态分配内存,并构造了一个LNode
类型的对象。new LNode
会分配足够大小的内存来存储一个LNode
对象,并调用LNode
的构造函数来初始化这块内存。分配的内存的地址被赋值给指针p
,此时p
指向一个新的LNode
对象。使用new
分配的内存储需要使用delete
来释放,以避免内存泄漏。简而言之,
LNode *p
只是声明了一个指针,而p = new LNode
声明了一个指针并分配了内存来存储一个LNode
对象,同时构造了该对象。