定义和基本操作
- 定义:相同数据类型的 n ( n ≥ 0 ) n(n \ge 0) n(n≥0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表
- 一般表示: L = ( a 1 , a 2 , ... ... , a i , a i + 1 , a n ) L=(a_1,a_2,......,a_i,a_{i+1},a_n) L=(a1,a2,......,ai,ai+1,an)
- a 1 a_1 a1是表头元素, a n a_n an是表尾元素
- 除第一个元素外,每个元素有且只有一个直接前驱;除最后一个元素外,每个元素有且只有一个直接后继
基本操作
InitList(&L)
;初始化表。构造一个空的线性表L,分配内存空间DestroyList(&L)
:销毁操作。销毁线性表,并释放线性表L所占用的内存空间ListInsert(&L,i,e)
:插入操作。在表L中的第i个位置插入指定元素eListDelete(&L,i,&e)
:删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值LocateElem(L,e)
:按值查找操作。在表L中查找具有给定关键字值的元素GetElem(L,i)
:按位查找操作。获取表L中第i个位置的元素的值
其他常用操作
Length(L)
:求表长。返回线性表L中数据元素的个数PrintList(L)
:输出操作。按前后顺序输出线性表L的所有元素值Empty(L)
:判空操作。若L为空表,返回true,否则返回false
顺序表
定义
- 顺序表:用顺序存储的方式实现线性表的顺序存储。把逻辑上相邻的元素存储在物理位置也相邻的存储单元中
- 顺序表的实现-静态分配
C++
#define MaxSize 50 //线性表的最大长度
typedef struct {
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
- 顺序表的实现-动态分配
- 时间开销大
C++
#define InitSize 50 //顺序表的初始长度
//动态分配
typedef struct {
ElemType* data; //指针动态分配数组的指针
int maxsize; //顺序表的最大容量
int length; //顺序表的当前长度
}SeqList;
-
动态申请和释放内存空间
- C:malloc、free函数
C
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize); //C语言初始动态分配
2. C++:new、delete关键字
```C++
L.data = new int[InitSize];
- 顺序表的特点
- 随机访问,可以在O(1)时间内找到第i个元素
- 存储密度高
- 拓展容量不方便
- 插入、删除操作不方便,需要移动大量元素
插入和删除
插入
ListInsert(&L,i,e)
:插入操作。在表L中第i个位置上插入指定元素
C++
bool ListInsert(SqList& L, int i, int e){
if (i<1 || i>L.length + 1) { //判断i的范围是否有效
return false;
}
if (L.length >= MaxSize) { //当前存储空间已满,不能插入
return false;
}
for (int j = L.length; j >= i; j--) { //将第i个元素及之后的元素后移
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e; //在位置i处放e
L.length++; //长度加1
return true;
}
- 时间复杂度
- 最好情况:在表尾插入(即i=n+1),不需要移动元素,时间复杂度为O(1)
- 最坏情况:在表头插入(即i=1),元素后移语句执行n次,时间复杂度为O(n)
- 平均情况:移动结点的平均次数 n 2 \frac{n}{2} 2n,时间复杂度O(n)
删除
C++
bool ListDelete(SqList& L, int i, int& e){
if (i<1 || i>L.length + 1) { //判断i的范围是否有效
return false;
}
e = L.data[i - 1]; //将被删除的元素赋值给e
for (int j = i; j < L.length; j++) { //将第i个位置后的元素前移
L.data[j - 1] = L.data[j];
}
L.length--; 线性表长度减1
return true;
}
- 时间复杂度
- 最好情况:删除表尾元素(即i=n),时间复杂度为O(1)
- 最坏情况:删除表头元素(即i=1),时间复杂度为O(n)
- 平均情况:移动结点的平均次数 n − 1 2 \frac{n-1}{2} 2n−1,时间复杂度O(n)
查找
按位查找
GetElem(L,i)
:按位查找操作。获取表L中第i个位置的元素的值
C++
int GetElem(SqList L, int i){
return L.data[i - 1];
}
int GetElem(SeqList L, int i){
return L.data[i - 1];
}
- 时间复杂度O(1)
- 随机存取特性
按值查找
LocateElem(L,e)
:按值查找操作。在表L中查找具有给定关键字值的元素
C++
int LoacteElem(SqList L, int e){
int i;
for (i = 0; i < L.length; i++) {
if (L.data[i] == e) { //"=="不可以用于比较两个结构体
return i + 1; //下标为i的元素值等于e,返回其位序i+1
}
}
return 0; //退出循环,说明查找失败
}
- 时间复杂度
- 最好情况:查找的元素在表头,时间复杂度为O(1)
- 最坏情况:查找的元素在表尾(或不存在),时间复杂度为O(n)
- 平均情况:平均比较次数 n + 1 2 \frac{n+1}{2} 2n+1,时间复杂度为O(n)
单链表
定义
C++
typedef struct LNode { //定义单链表结点类型
int data; //每个结点存放一个指针元素
struct LNode* next; //指针指向下一个结点
}LNode, * LinkList;
- 不带头结点的单链表
C++
bool InitList(LinkList& L){
L = NULL; //空表,暂时没有任何结点
return true;
}
- 带头结点的单链表
C++
bool InitList(LinkList& L) {
L = (LNode*)malloc(sizeof(LNode)); //分配一个头结点
if (L == NULL) {
return false;
}
L -> next = NULL; //头结点之后暂时还没有结点
return true;
}
插入和删除
按位序插入(带头结点)
ListInsert(&L,i,e)
:插入操作。在表L中的第i个位置上插入指定元素e
C++
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1){
return false;
}
LNode *p; //指针p指向当前扫描的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点(不存储数据)
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL){ //i值不合法
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p-next=s; //将结点s连到p之后
return true; //插入成功
}
按位序插入(不带头结点)
ListInsert(&L,i,e)
:插入操作。在表L中的第i个位置上插入指定元素e
C++
bool ListInsert(LinkList &L, int i, ElemType e){
if(i<1){
return false;
}
if(i==1){ //插入第i个结点的操作与其他结点不同
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s; //头指针指向新结点
return true; //插入成功
}
LNode *p; //指针p指向当前扫描的结点
int j=1; //当前p指向的是第几个结点
p=L; //p指向第1个结点(不是头结点)
while(p!=NULL && j<i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL){ //i值不合法
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=p->next;
p-next=s; //将结点s连到p之后
return true; //插入成功
}
指定结点的后插操作
C++
//在p结点之后插入元素e
bool InsertNextNode(LNode *p, ElemType e){
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL){ //内存分配失败
return false;
}
s->data=e; //用结点s保存数据元素e
s->next=p->next;
p->next=s; //将结点s连接到p之后
return true;
}
指定元素的前插操作
C++
//在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
if(p==NULL){
return false;
}
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL){ //内存分配失败
return false;
}
s->next=p->next;
p->next=s; //将结点s连接到p之后
s->data=p->data; //将p中的元素复制到s中
p->data=e; //p中元素覆盖为e
return true;
}
按位序删除(带头结点)
ListDelete(&L,i,&e)
;删除操作,删除表L中第i个位置的元素,并用e返回元素的值
C++
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1){
return false;
}
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点 (不存数据)
while (p!=NULL & j<i-1){ //循环找至第 i-1个结点
p=p->next;
j++;
}
if(p==NULL){ //i值不合法
return false;
}
if(p->next == NULL){ //第i-1个结点之后已无其他结点
return false;
}
LNode *q=p->next; //令q指向被删除结点
e = q->data; //用e返回元素的值
p->next=q->next; //将*q结点从链中"断开"
free(q); //释放结点的存储空间
return true; //删除成功
}
指定结点的删除
C++
//删除指定结点p
bool DeleteNode (LNode *p){
if (p==NULL){
return false;
}
LNode *q=p->next; //令q指向*p的后继结点
p->data=p->next->data; //和后继结点交换数据域
p->next=q->next; //将*q结点从链中"断开"
free(q); //释放后继结点的存储空间
return true;
}
如果p是最后一个结点,只能从表头开始寻找p的前驱,时间复杂度O(n)
查找
按位查找
GetElem(L,i)
:按位查找操作。获取表L中第i个位置的元素的值
C++
//按位查找,返回第i个元素(带头结点)
LNode *GetElem(LinkList, int i){
if(i<0){
return NULL;
}
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头结点,头结点是第0个结点(不存储数据)
while (p!=NULL && j<i){ //循环找到第 i 个结点
p=p->next;
j++;
}
return p;
}
- 平均时间复杂度O(n)
按值查找
LocateElem(L,i)
:按值查找操作。在表L中查找具有给定关键字值的元素
C++
//按值查找,找到数据域==e 的结点
LNode * LocateElem(LinkList L,ElemType e){
LNode *p = L->next;
//从第1个结点开始查找数据域为e的结点
while (p != NULL && p->data != e)
p = p->next;
return p; //找到后返回该结点指针,否则返回NULL
}
- 时间复杂度O(n)
求表的长度
C++
//求表的长度
int Length(LinkList L){
int len = 0; //统计表长
LNode *p = L;
while (p->next != NULL){
p = p->next;
len++;
}
return len;
- 时间复杂度O(n)
建立
- 尾插法
- 头插法
双链表
- 初始化
- 插入
- 删除
- 遍历
循环链表
- 循环单链表:表尾结点的next指针指向头结点
- 从一个结点出发可以找到其他任何一个结点
- 循环双链表
静态链表
- 概念:分配一整连续的内存空间,各个结点集中安置
顺序表和链表的比较
- 逻辑结构
- 都属于线性表,都是线性结构
- 物理结构/存储结构
- 顺序表
- 优点:支持随机存取、存取密度高
- 缺点:大片连续空间分配不方便,改变容量不方便
- 链表
- 优点:离散的小空间分配方便、改变容量方便
- 缺点:不可随机存取、存取密度低
- 顺序表
- 数据的运算/基本操作
- 初始化
- 顺序表:需要预分配大片连续空间若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源
- 静态分配:静态数组,容量不可改变
- 动态分配:动态数组(malloc、free),容量可改变,但需要移动大量元素,时间代价高
- 链表:只需分配一个头结点 (也可以不要头结点,只声明一个头指针) ,之后方便拓展
- 顺序表:需要预分配大片连续空间若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源
- 销毁
- 顺序表:修改Length=0
- 静态分配:系统自动回收空间
- 动态分配:需要手动free
- 链表:依次删除各个结点(free)
- 顺序表:修改Length=0
- 插入和删除
- 顺序表:需要把后续元素后移/前移,若数据元素很大,则移动的时间代价很大
- 链表:修改指针
- 查找
- 顺序表
- 按位查找:O(1)
- 按值查找:O(n)
- 若表内元素有序,可在 O ( l o g 2 n ) O(log_2n) O(log2n)时间内找到
- 链表
- 按位查找:O(n)
- 按值查找:O(n)
- 顺序表
- 初始化
开放式问题回答思路
问题: 请描述顺序表和链表的 bla bla bla... 实现线性表时,用顺序表还是链表好?
顺序表和链表的逻辑结构 都是线性结构,都属于线性表但是二者的存储结构 不同,顺序表采用顺序存储...(特点,带来的优点缺点): 链表采用链式存储...(特点、导致的优缺点)。由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时...;当插入一个数据元素时...;当删除一个数据元素时...; 当查找一个数据元素时...