线性表
(本文以及接下来的每一篇都是这样的结构,先说逻辑结构,然后初步认识物理结构,然后实现代码,最后结合代码重新认识物理结构的优缺点,本章的代码与习题代码都打包成了文件: 线性表及习题代码)
1.逻辑结构
- 线性表:零个或多个相同数据类型的数据元素的有限序列。
(元素之间是有顺序的。第一个元素无前驱,最后一个元素无后继,其他元素都有且只有一个前驱和一个后继。)
- 空表:线性表元素的个数n定义为线性表的长度,当n=0时,称为空表
- 在较为复杂的线性表中,数据元素可以由多个数据项组成。
(线性表是一种逻辑结构,它的物理结构可以用顺序表或链表实现,不要混淆线性表和顺序表)
2.存储结构
(存储结构结合第三节代码实现来看)
2.1顺序存储
- 线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。
(线性表顺序存储时逻辑顺序和物理顺序相同)
- 顺序表存储结构是一种随机存取的存储结构,因为每一种元素所占的字节是相同的,很容易对任意一个位置的元素进行操作。
- 基本结构:类似于数组。
2.2链式存储
-
链式存储:用一组任意的存储单元存储线性表的数据元素,对于数据元素ai除了存储了数据元素信息,还存储了后继位置(也就是指针)。
-
基本结构:链表由n个节点组成,每一个节点有一个数据域存储元素的值,还有一个指针域,存储该元素后继的地址也就是指针。指向第一个节点位置的指针我们称为头指针,最后一个节点的指针指向NULL。(初级的单链表结构)
- 头节点结构:在列表的第一个节点前我们增加一个头节点,头节点的数据域存储链表的元素个数或者不存储任何信息,指针域存储第一个节点的位置,此时的头指针指向头节点。这样的结构更加规范,至于为什么大家在代码中体会,以后的学习中链表通常都会有头节点。
3.线性表的实现
3.1 顺序存储结构实现
3.1.1 静态分配
#define MaxSize 50
typedef int ElemType;
//ElemType可以是任何数据类型,为举例方便此处设为int
typedef struct {
ElemType data[MaxSize];//存储元素的数组
int length;//记录元素个数
}SqList;
静态分配方便大家了解线性表的顺序存储结构实现,但是当我们的元素个数超过MaxSize时会发生数据溢出。
3.1.2 动态分配
#define InitSize 50
#define IncreaseSize 10 //容量不足时一次增加的容量
typedef int ElemType;
typedef struct {
ElemType *data;
int MaxSize, length;//length记录元素个数,MaxSize是当前线性表容量。length<=MaxSize
}SqList;
动态分配在容量不足时会扩容,所以接下来的线性表操作都是以动态分配的顺序存储结构为基础。
3.2顺序存储操作实现
(前言:关于SqList &L,表示引用的意思,L的类型是SqList, 如果在函数里L的值被改变,主函数里L的值也会变的)
(SqList L,传参,L的类型是SqList, L的值的改变并不会影响主函数中的L,但是L->data的值改变了,原函数L->data的值也会变的)
(SqList *L,这里的L是一个地址,改变L的值并不会影响主函数的值,但是L->data的值改变了,原函数L->data的值也会变的,其实就是对参数的改变不会影响主函数,但是对一些地址存储的值的改变是会影响主函数的。大家自己实验理解好方便我们实现操作)
#define InitSize 50
#define IncreaseSize 10
#define OK 1 //为了方便函数编写,表示操作成功
typedef int Status; //Status是函数返回值类型,可以是任何类型,此处用int
typedef int ElemType;
typedef struct {
ElemType *data;
int MaxSize, length;
}SqList;
3.2.1 初始化线性表
/*
获得一个线性表
*/
Status InitSqList(SqList& L) {
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize); //分配初始内存
if (!L.data) //若分配失败则结束程序
return ERROR;
L.MaxSize = InitSize; //容量赋值
L.length = 0; //元素赋值
return OK;
}
int main() {
SqList L ;
int x = InitSqList(L);
cout << x << endl; //打印结果为1
}
3.2.2 插入元素
Status ListInsert(SqList& L, int i, ElemType e) {
if (i < 1 || i > L.length + 1)
return ERROR; //插入元素的位置要合法,i=1时插入在最前面,i=length+1时插入在末尾
if (L.length >= L.MaxSize) { //容量不足
//重新分配内存
L.data = (ElemType*)realloc(L.data, sizeof(ElemType) * (L.MaxSize + IncreaseSize));
if (!L.data) return ERROR;
L.MaxSize = L.MaxSize + IncreaseSize;
}
for (int j = L.length; j >= i; j++) {//第i个位置及之后的位置的元素都向后挪动一个元素的位置
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e; //第i个位置的索引是i-1
L.length++;
return OK;
}
为了验证我们写的对不对,我们写一个打印线性表的函数
Status ListPrint(SqList L) {
for (int i = 0; i < L.length; i++) {
cout << L.data[i] << "";
cout << "\t" << "";
}
cout << "" << endl;
return OK;
}
然后调用函数
int main() {
SqList L ;
int x = InitSqList(L);
for (int i = 0; i < 55; i++) {
ListInsert(L, i + 1, i + 1);
}
ListPrint(L);//打印出1 2 3...55 说明函数写对了
}
3.2.2删除操作
/*
删除i位置元素
*/
Status ListDelete(SqList& L, int i) {
if (i < 1 || i > L.length) //位置不合法
return ERROR;
for (int j = i; j < L.length; j++) {//第i+1位置和之后的都向前挪动一个元素的位置
L.data[j - 1] = L.data[j];
}
L.length--;
return OK;
}
同时验证一下
int main() {
SqList L ;
int x = InitSqList(L);
for (int i = 0; i < 55; i++) {
ListInsert(L, i + 1, i + 1);
}
ListPrint(L);
ListDelete(L,30);//删除第30个位置的元素
ListPrint(L);//打印结果1 2 ...29 31 ... 55
}
3.3.2 按值查找
int GetElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i;
}
}
return -1;//表示没有找到
}
检查一下
int main() {
SqList L ;
int x = InitSqList(L);
for (int i = 0; i < 55; i++) {
ListInsert(L, i + 1, i + 1);
}
ListPrint(L);
ListDelete(L,30);
ListPrint(L);
cout << GetElem(L, 30) << endl; //打印-1
cout << GetElem(L, 29) << endl; //打印28 说明写对了
}
顺序存储的其他操作都很简单了,接下来大家可以自行试着实现能想到的所有操作。
3.3链式存储结构实现(单链表)
(前言:本小节LinkList L是一个LNode* 类型,是地址,建立链表函数中我们需要改变L的值,所以应该是LinkList &L)
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode,* LinkList; //LNode是节点,LinkList是头指针。
//相当于 typedef int* LinkList 此时LinkList是指向int数据的指针,上面只不过把int换成LNode
3.4链式存储操作实现(单链表)
3.4.1 头插法建立单链表:就是添加元素时添加在链表头。
/*
头插法建立单链表
*/
Status List_HeadInsert(LinkList &L){
LNode* s=NULL;
int e=9999;
L = (LNode*)malloc(sizeof(LNode));//创建头节点,头指针L指向头节点。
if (!L) return ERROR;
L->next = NULL;
cin >> e ;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));//s是新的节点
if (!s) return ERROR;
s->data = e;
s->next = L->next;//s节点指向之前的节点
L->next = s; //s始终插入到L后面
cin >> e;
}
return OK;
}
同样的,为了验证我们写一段打印线性表的代码
/*
打印链表
*/
Status ListPrint(LinkList L) {
LinkList p = L->next; //获得头节点
while (p!= NULL) {
cout << p->data ;
cout << "\t";
p = p->next;
}
cout << "\t" << endl;
return OK;
}
int main() {
LinkList L;
List_HeadInsert(L);
ListPrint(L);
}
打印结果如下图:
3.4.2 尾插法建立单链表:添加元素时添加在链表尾
/*
尾插法建立单链表
*/
Status List_TailInsert(LinkList& L) {
LNode* s = NULL;
LNode* r = NULL;
int e = 9999;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->data = NULL;
r = L; //尾插法需要用到r来辅助
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));//s为新的节点
s->data = e;
s->next = NULL;
r->next = s;
r = s; //r始终为链表最后一个节点
cin >> e;
}
return OK;
}
int main() {
LinkList L;
//List_HeadInsert(L);
List_TailInsert(L);
ListPrint(L);
}
打印结果如下:
3.4.3 按位置查找节点(按位置查找结点同理,大家可以自己试一下)
/*
获得第i个位置节点的地址
*/
LNode* GetElme(LinkList L, int i) {
LNode *p = L->next;
if (i < 1) return ERROR;
i--;
while (i > 0 && p!= NULL) {
p = p->next;
i--;
}
return p;
}
int main() {
LinkList L;
//List_HeadInsert(L);
List_TailInsert(L);
ListPrint(L);
if(GetElme(L, 1)!=NULL)
cout << GetElme(L, 1)->data << endl;
if(GetElme(L, 5)==NULL)
cout << ERROR << endl;
}
打印结果:
3.4.4 在第i个位置插入元素
/*
在第i个位置插入元素e
*/
Status LinkListInsert(LinkList L, int i, ElemType e) {
LNode* r = NULL;
LNode* s = NULL;
if (i == 1) r = L;
else r = GetElme(L, i-1); //获得第i个位置的前一个节点
if (r == NULL)
return ERROR; //若前一个节点为NULL则i不合法
s = (LNode*)malloc(sizeof(LNode));
if (!s) return ERROR;
/*在节点i-1后插入节点*/
s->data = e;
s->next = r->next;
r->next = s;
return OK;
}
int main() {
LinkList L;
//List_HeadInsert(L);
List_TailInsert(L);
ListPrint(L);
ElemType e = 9999;
cin >> e;
LinkListInsert(L, 3, e);
ListPrint(L);
}
打印结果如下:其他比如在某个节点前插入等方法大家可以自己试一下。
3.4.5 删除节点
/*
删除节点i
*/
Status LinkListDelete(LinkList L, int i) {
LNode* r = NULL;
LNode* s = NULL;
if (i == 1)
r = L;
else r = GetElme(L, i - 1);
if (r == NULL) return ERROR;
if (r->next == NULL) return ERROR;
s = r->next;
r->next = s->next;
free(s);
return OK;
}
int main() {
LinkList L;
//List_HeadInsert(L);
List_TailInsert(L);
ListPrint(L);
LinkListDelete(L, 3);
ListPrint(L);
}
打印结果:
3.5 线性表实现-静态链表
3.5.1 静态链表的物理结构
typedef struct{
ElemType data;
int cur;
}SLinkList[MaxSize];
使用数组来实现链表的功能,数组元素有一个数据域和一个指针域,指针域的值并不是地址,而是下一个数组元素的索引。
实际上并没有链表方便,但是有一些高级语言并不支持指针,这是一种实现链表的巧妙的方法,大家学习这种思想就好了。大家如果学习过操作系统,也会很熟悉这种思想。关于它的操作代码就略过了,不重要。
3.6线性表实现-循环链表
3.6.1循环链表的物理结构:我们将链表最后一个节点的next由NULL改为头节点,那么整个链表就变成了一个圈。
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, * LinkList;
发现了吧,和普通单链表一模一样。其实就是在创建链表的时候有区别,我们把头插法和尾插法代码写一下如下:
3.7.2 头插法建立循环链表:和普通单链表只差了注释的那一行,其他部分一模一样
Status RLinkList_HeadInsert(RLinkList& L) {
int e = 9999;
LNode* s = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next =L; //没有元素时头节点自己指向自己而非NULL
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
if (!s) return ERROR;
s->data = e;
s->next = L->next;
L->next = s;
cin >> e;
}
return OK;
}
3.7.3 尾插法建立循环链表:同样只差一行
Status RLinkList_TailInsert(RLinkList& L) {
int e = 9999;
LNode* s = NULL;
LNode* r = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = L; //没有元素时头节点自己指向自己而非NULL
r = L;
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
if (!s) return ERROR;
s->data = e;
s->next = r->next;
r->next = s;
r = s;
cin >> e;
}
return OK;
}
为了验证同样要写个打印双链表的。
Status RLinkListPrint(RLinkList L) {
LNode* p = L->next;
while (p != L) { //循环结束条件变了
cout << p->data;
cout << "\t";
p = p->next;
}
cout << "\t" << endl;
return OK;
}
我们来试一下:
int main() {
RLinkList L1 = NULL, L2 = NULL;
cout << "头插法:" << endl;
RLinkList_HeadInsert(L1);
RLinkListPrint(L1);
cout << "尾插法:" << endl;
RLinkList_TailInsert(L2);
RLinkListPrint(L2);
}
打印结果如下:
其他的插入删除等方法和单链表一模一样,甚至把LinkList改为RLinkList就可以直接用了,此处略过。
3.7线性表实现-双链表
3.7.1 双链表的物理结构:单链表是指单向,我们只能从链表头查单链表尾,如果我们想从链表尾查到链表头那再加一条链子吧!
typedef struct LNode {
ElemType data;
struct LNode* next;//后继指针
struct LNode* prior;//前驱指针
}LNode, * DLinkList;
3.7.2 头插法 尾插法 建立双链表:
Status DLinkList_HeadInsert(DLinkList &L){
int e = 9999;
LNode* s = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = NULL;
L->prior = NULL; //多一个“链子” 比单链表多的步骤
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = e;
L->next->prior = s; //后继节点的前驱指向s节点 比单链表多的步骤
s->next = L->next; //s的后继指向其后继节点
s->prior = L; //s的前驱指向L节点 比单链表多的步骤
L->next = s; //L的后继指向s节点
cin >> e;
}
return OK;
}
Status DLinkList_TailInser(DLinkList& L) {
int e = 9999;
LNode* s = NULL;
LNode* r = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = NULL;
L->prior = NULL; //多一个“链子” 比单链表多的步骤
r = L;
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = e;
L->next->prior = s; //后继节点的前驱指向s节点 比单链表多的步骤
s->next = L->next; //s的后继指向其后继节点
s->prior = L; //s的前驱指向L节点 比单链表多的步骤
L->next = s; //L的后继指向s节点
r = s;
cin >> e;
}
return OK;
}
写一个打印函数。这个打印函数可以正反双向打印哦!
Status ListPrint(DLinkList L, int reverse = 0) {
DLinkList p = L->next; //获得头节点
if (reverse == 1) { //反向打印
while (p->next != NULL)
p = p->next;
while (p != L) {
cout << p->data;
cout << "\t";
p = p->prior;
}
}//if
else {
while (p != NULL) {
cout << p->data;
cout << "\t";
p = p->next;
}
}//elese
cout << "\t" << endl;
return OK;
}
然后我们来验证一下:
int main() {
DLinkList L1 = NULL, L2 = NULL;
cout << "头插法建立链表L1" << endl;
DLinkList_HeadInsert(L1);
cout << "正向打印L1" << endl;
ListPrint(L1);//默认是0 正向打印
cout << "逆向打印L1" << endl;
ListPrint(L1, 1);
cout << "尾插法建立链表L2" << endl;
DLinkList_TailInsert(L2);
cout << "正向打印L2" << endl;
ListPrint(L2);//默认是0 正向打印
cout << "逆向打印L2" << endl;
ListPrint(L2, 1);
}
打印结果如下:
3.7.3 按位置取值操作
LNode* GetElem(DLinkList L, int i, int reverse = 0) {
if (i < 1) return NULL;
LNode* p = L->next;
i--;
if (reverse != 1) {
while (i > 0 && p != NULL) {
p = p->next;
i--;
}//while
}//if
else {
while (p->next != NULL && p != NULL)//寻找最后一个节点
p = p->next;
while (i > 0 && p != L) {
p = p->prior;
i--;
}
}
return p;
}
int main() {
DLinkList L1 = NULL;
DLinkList_TailInsert(L1);
cout << GetElem(L1, 3)->data << endl;
cout << GetElem(L1, 3,1)->data << endl;
}
打印结果如图:
3.7.4 插入操作
单链表在插入节点时都要先找到该节点的前驱,然后向后插入。双链表直接找到i插入到它前面就好了。
Status DLinkListInser(DLinkList L, int i, ElemType e) {
LNode* l = GetElem(L, i);
if (!l) return ERROR;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = l;
s->prior = l->prior;
l->prior->next = s;
l->prior = s;
return OK;
}
然后我们验证一下:
int main() {
DLinkList L1 = NULL, L2 = NULL;
DLinkList_TailInsert(L1);
ListPrint(L1);
DLinkListInser(L1, 3, 520);
ListPrint(L1);
}
打印结果如下:
3.7.5 删除元素
Status DLinkListDelete(DLinkList L, int i) {
LNode* l = GetElem(L, i);
if (!l) return ERROR;
l->prior->next = l->next;
if (l->next != NULL)
l->next->prior = l;
return OK;
}
我们验证一下:
int main() {
DLinkList L1 = NULL, L2 = NULL;
DLinkList_TailInsert(L1);
DLinkListDelete(L1, 3);
ListPrint(L1);
}
打印结果:
3.8循环双链表
3.8.1 物理结构 头节点的前驱指向尾节点,尾节点的后继指向头节点。
typedef struct LNode {
ElemType data;
struct LNode* next;
struct LNode* prior;
}LNode, * RDLinkList;
代码是一样的,不过它叫RDLinkList,至于为什么不是DRLinkList,因为我喜欢呀!而头插法和尾插法也和双链表只差了两行:
Status RDLinkList_HeadInsert(RDLinkList& L) {
int e = 9999;
LNode* s = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = L; //与双向链表只差这一行
L->prior = L; //与双向链表只差这一行
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = e;
if (L->next != NULL) //插入第一个节点时没有后继节点
L->next->prior = s;
s->next = L->next; //s的后继指向其后继节点
s->prior = L; //s的前驱指向L节点 比单链表多的步骤
L->next = s; //L的后继指向s节点
cin >> e;
}
return OK;
}
Status DLinkList_TailInsert(RDLinkList& L) {
int e = 9999;
LNode* s = NULL;
LNode* r = NULL;
L = (LNode*)malloc(sizeof(LNode));
if (!L) return ERROR;
L->next = L;//与双向链表只差这一行
L->prior = L; //与双向链表只差这一行
r = L;
cin >> e;
while (e != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = e;
//尾插法后面无节点
s->next = r->next; //s的后继指向其后继节点
s->prior = r; //s的前驱指向L节点 比单链表多的步骤
r->next = s; //L的后继指向s节点
r = s;
cin >> e;
}
return OK;
}
我们来写一个不一样的打印函数,他会正向打印或逆向打印n个节点。
其他操作与双向链表一模一样。代码实现就到此结束了。
4. 总结与分析
4.1顺序表与链表的比较
存取方式:顺序表可以随机存取,链表只能从头顺序存取元素。
逻辑结构与物理结构:采用顺序存储时逻辑上相邻的元素物理上也是相邻的,采用链式存储时逻辑上相邻的元素物理上不一定相邻。
查找、删除与插入操作:若按位置查找,顺序表时间复杂度=O(1),链表=O(n)。
空间分配:顺序表静态分配时容易发生溢出,分配过大也可能浪费空间,不好掌握分配值大小。顺序表动态分配时,每次重新分配都需要移动元素位置,因为原来连续的空间未必满足重新分配空间的大小,操作效率低。链表的空间分配非常简单、灵活。
4.2 选择哪种物理结构?
基于存储的考虑:难以估计线性表的存储规模的时候,应选择链表。
基于运算的考虑:若查找频繁,顺序表是一个好的选择。若插入、删除频繁,链表是一个好的选择。
基于环境的考虑:有些语音并没有指针,容不得我们选择链表。
总结来说,稳定的线性表选择顺序存储,频繁插入、删除的线性表选择链式存储。
5.习题
(习题答案在:线性表及习题代码)
习题1.
已知一个带有头节点的单链表,假设该链表只给出了头指针list,在不改变链表的前提下。请设计一个尽可能高校的算法,查找链表中导数第k个位置的节点,k为整数。若查找成功算法输出该节点data域的值并返回1,否则只返回0。
习题2.
这个题目如果用普通的选择排序思想很容易但是时间复杂第为O(n^2)。所以要想出一个时间复杂度更小的。提醒:和习题1思想一样。
习题3.
提示:普通方法都要O(n^2).所以想一个O(n)算法吧,可以考虑用空间换时间。
习题4.
链表原地逆置,对于带头结点的链表L,设计一个空间复杂度为O(1),也就是不借助辅助空间,将单链表中元素逆置。
习题5.
线性表L=(a1,a2,…,an),我们设计一个空间复杂度为O(1)的算法,将线性表L变为L=(a1,an,a2,an-1,…)。
我们的答案时间复杂度未O(n),不要超过了。