一、线性表概述与基本操作
1.1线性表概述
线性表概述:线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线性表是一个空表。线性表的定义是:逻辑结构;线性表,顾名思义,逻辑上就是一条线;基于逻辑的操作为数据的运算。
按存储/物理结构又划分为顺序存储(顺序表)和链式存储(链表)。
1.2线性表基础操作
**增删改查:**改查本质都是定位找到元素所在的位置
- InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
- DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
- ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
- ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
- LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
- GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
其他常用操作:
- Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
- PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
- Empty(L):判空操作。若L为空表,则返回true,否则返回false。
二、顺序表
2.1顺序表特点概述
**顺序表:**用顺序存储的方式实现线性表的顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
顺序表的特点:
- 随机访问,即可以在 O(1) 时间内找到第 i 个元素。
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
以下数据域都用int类型了,按需改为想要的ElemType即可
2.2静态分配方式增删改查等代码实现与解析
//静态分配,一旦确定静态顺序表的大小,无法改变
#define MaxSize 10
typedef struct{
int data[MaxSize]; //数据域
int length; //顺序表当前的长度
}SqList; //sequence 顺序,序列
//初始化,先假设数据域存int类型吧
void InitList(SqList &L){
for(int i = 0; i < MaxSize; i ++ ) //初始化,覆盖可能存在的垃圾数据
L.data[i] = 0;
L.length = 0; //也将长度初始化,防止 垃圾数据
}
//插入,在位序为i处插入元素e,注意:位序是指第i个数,其下标为i-1,
//最好O(1), 最差、平均O(n)
bool ListInsert(SqList &L, int i, int e){
/*
如果插入的是第0个数,下标是-1,显然不成立;如果插入的是第length + 2个数,那么下标是length + 1,而有 效值的下标所到达的是length-1, 因此下标为length位置的内存就会空出来,中间就会有缝隙,在物理上就不是连续存储的有效数据了
*/
if(i < 1 || i > L.length + 1)
return false;
//如果长度达到上限,没办法在插入了,这里长度与下标差1,所以当长度=MaxSize时,就已经满了
if(L.length >= MaxSize)
return false;
//将i之后的所有数(包括i)往后挪一个位置,从下标i-1到有效数据的最后往后挪一个位置
for(int j = L.length; j >= i; j -- )
L.data[j] = L.data[j - 1]; //先挪后面的,然后用前面的往后直接覆盖就ok
L.data[i - 1] = e; //将e放入下标为i-1的位置
L.length ++;
return true;
}
//删除第i个插入的元素(位序),并返回删除元素的值e
//最好O(1), 最差、平均O(n)
bool ListDelete(SqList &L, int i, int &e){ //通过引用e得到删除的值
if(i < 1 || i > L.length) //注意是位序,不是下标
return false;
e = L.data[i - 1];
//删除只需要将被删除后面的元素从前到后向前移动即可
for(int j = i; j < L.length; j ++ )
L.data[j - 1] = L.data[j];
L.length --;
return true;
}
//按位序查找,无脑操作了O(1)
int GetElem(SqList L, int i){
return L.data[i - 1];
}
/*
按值查找最好O(1), 最差、平均O(n),只能基本数据类型进行==比较, 若是结构体类型要成员变量都进行==比较,但考研初试手写是可以直接写==的,重要的是思维
*/
int LocateElem(SqList L, int e){
for(int i = 0; i < L.length; i ++ )
if(L.data[i] == e)
return i + 1; //返回的是位序
return 0; //返回0表示没找到
}
2.3动态分配方式增删改查等代码实现与解析
//动态分配
#define InitSize 10 //初始时的最大长度(可扩展)
typedef struct{
int *data; //动态分配数组的指针,指向分配空间的首地址
int MaxSize; //顺序表的最大容量
int length; //顺序表当前的长度
}SqList; //sequence 顺序,序列
//初始化
void InitList(SqList &L){
//malloc()申请一片连续存储空间
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
//初始化之后就可以直接使用L.data[]的方式增删数据了,这里与静态分配一样
//如何增加数组的长度?
/*
如果长度不够了,那么就要重新分配一块更大的连续内存,然后将原来的已有的数据copy到新分配的内存中,再将原来较小的内存释放掉,因此会比较耗时
*/
//增加动态数组的长度
void IncreaseSize(SqList &L, int len){ //len为需要增加的长度
int *p = L.data; //让p指针也指向原来的内存首地址
/*将data指向新分配的内存地址,此时L.data为一块崭新的内存与之前的无关,但之前的那块内存并没有丢P所指向 的就是那块内存,结构体变量成员信息只是记录开辟的存放有效信息的空间的信息物理存储结构没有什么关联
*/
L.data = (int *)malloc((L.MaxSize + len) * sizeof(int));
//将旧的那块内存中的信息挪到新的内存块中
for(int i = 0; i < L.length; i ++ )
L.data[i] = p[i];
free(p); //释放旧的内存
}
//按位序查找与静态一致
//按值查找与按位查找与静态一致
三、链表
3.1单链表
**优点:**不要求大片连续空间,改变容量方便
**缺点:**不可随机存取,要耗费一定空间存放指针
3.1.1单链表的初始化
创建带有头节点的单链表:
typedef struct LNode{
ElemType data;
struct LNode * next; //可以指向该数据类型的指针
}LNode, *LinkList;
//创建并初始化带有头节点的单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL) return false; //说明内存不足,创建节点失败
L->next = NULL;
return true;
}
创建没有头节点的单链表与有头节点的单链表相似,在初始化的时候直接将L指向空就ok了
具有虚拟头节点的链表对后续的操作会更加方便,虚拟头节点是为了操作方便,并不存放有效数据
LinkList 与 LNode *是等价的,只不过前者强调这是个链表,后者强调指向的某个节点
3.1.2单链表的插入
带有头节点的插入: O(n)
在第i个节点(位序)处加入节点e
bool ListInsert(LinkList &L, int i, ElemType e){
if(i < 1) return false;
LNode * p = L;
int j = 0;
//j表示当前p指针指向的是下标是几的节点,因为有虚拟头节点的存在,所以当下标为i-1时就停下来,在后面插入
while(p != NULL && j < i - 1){
p = p->next;
j ++;
}
//退出循环有两种可能,一种是正常退出即j<i-1, 一种是p==NULL,此时i太大了,i-1节点不存在,导致p为空
if(p == NULL) return false;
LNode * s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
不带头节点的插入:
会更加麻烦一点,要在对第一个插入的数进行特判
bool ListInsert(LinkList &L, int i, ElemType e){
if(i < 1) return false;
if(i == 1){
LNode * s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = L; //将s指向L
L = s; //更新链表头
return true;
}
int j = 1; //当前p指向的是第几个节点
LNode * p = L;
while(p != NULL && j < i - 1){
p = p->next;
j ++;
}
if(p == NULL) return false;
LNode * s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
在指定的节点后插入: O(1)
给定节点p,在节点p的后面插入元素e
bool InsertNextNode(LNode * p, ElemType e){
if(p == NULL) return false;
LNode * s = (LNode *)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
在指定的节点前插入: O(1)
如果不传入头节点,单链表是找不到某个节点的前驱节点的。
换个思路,那么就将该插入的节点插入指定节点的后面,然后交换插入节点与其前驱节点的数据
bool InsertPriorNode(LNode * p, ElemType e){
if(p == NULL) return false;
LNode * s = (LNode *)malloc(sizeof(LNode));
//将该节点连上
s->next = p->next;
p->next = s;
s->data = p->data; //将p指向的数据copy到s中
p->data = e; //用e将旧数据覆盖
return true;
}
3.1.3单链表的删除
按位序删除: O(n)
这里只讨论带头结点的
因为单链表无法找到某个节点的前驱节点,只能遍历整个链表,找到要删除节点的上一个节点
删除第i个节点,并返回删除的值
bool ListDelete(LinkList &L,int i, ElemType &e){
if(i < 1) return false;
LNode * p = L; //用p去寻找下标为i-1的节点
int j = 0;
while(p != NULL && j < i - 1){
p = p->next;
j ++;
}
if(p == NULL) return false; //i的值不合法
LNode * q = p->next;
e = q->data;
p->next = q->next;
free(q); //删除后,一定要释放内存
return true;
}
指定节点的删除: O(1)
同样的,无法进行直接删除。思路与上述在某节点前插入类似
将指定节点的后继节点的数据copy到当前节点,将该节点的指针域指向后继节点的下一个节点,然后将后继节点释放,ok
bool DeleteNode(LNode * p){
if(p == NULL) return false;
LNode * q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
return true;
}
这段代码是有bug的,当p->next为NULL的时候,再去访问q的数据域就会出现空指针异常的错误
3.1.4单链表的查找
按位查找:
按位查找,返回第i个元素(带头结点)
LNode * GetElem(LinkList L, int i){
if(i < 0) return NULL;
//如果i等于0,那也正常返回,虚拟头节点这里正常处理,当作
LNode * p = L;
int j = 0;
while(p != NULL && j < i){ //因为是查找,所以正常d的,j=i时返回第i个节点就ok
p = p->next;
j ++;
}
return p;
}
按值查找:
LNode * LocateElem(LinkList L, ElemType e){
LNode * p = L->next; //这里有虚拟头节点
while(p != NULL && p->data != e) //这么比较,这里的ElemType只能是基本数据类型
p = p->next;
return p;
}
3.1.5单链表的长度
遍历一遍链表即可,带有头节点
int Length(LinkList L){
LNode * p = L;
int len = 0;
while(p->next != NULL){
p = p-next;
len ++;
}
return len;
}
3.1.5单链表的创建
单链表的尾插创建:
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
int data;
struct LNode * next;
}LNode, *LinkList;
LinkList List_TailInsert(LinkList &L){
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; //r始终指向最后的节点, s为临时指针变量,指向新开辟的节点
int x;
scanf("%d", &x);
while(x != 666) {
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
return L;
}
int main()
{
LinkList L;
LNode *p = List_TailInsert(L);
while(p->next != NULL){
printf("%d\t", p->next->data);
p = p->next;
}
return 0;
}
单链表的头插创建:
重要应用:链表的逆置
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //初始为空链表
scanf("%d",&x);
while(x != 666){
s = (LNode *)malloc(sizeof(LNode)); //创建新结点
s->data = x;
s->next = L->next;
L->next = s;
//将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
3.2双链表
~~