线性表
知识框架
线性表
- 顺序存储——顺序表
- 链式存储
- 单链表
- 双链表
- 循环链表 1,2,3都是指针实现
- 静态链表(借助数组实现)
线性表的定义和基本操作
线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列。
线性表的特点:
- 表中元素的个数有限。
- 表中元素具有逻辑上的顺序性,表中元素尤其先后次序。
- 表中元素都是数据元素,每个元素都是单个元素。
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间。
注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不用层面的概念,因此不要混淆。
1. 顺序存储
1.1 线性表的顺序表示的基本操作—初始化
#include <iostream>
#define MaxSize 10 //定义最大长度
using namespace std;
typedef struct{
int data[MaxSize]; //静态分配
int length;
}SqList;
void InitList(SqList &L){ //基本操作——初始化一个顺序表
for(int i = 0; i < MaxSize; i++){
L.data[i]=0;
}
L.length=0;
}
int main()
{
SqList L;
InitList(L);
for(int i = 0; i < MaxSize; i++)
cout <<L.data[i]<< endl;
return 0;
}
MaxSize是固定的,当表满了之后就不可更改,(存储空间是静态的)
所以来啦顺序表的实现–动态分配
#define InitSize 10 //顺序表的初始长度
Typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int Length; //顺序表的当前容量
} SeqList; //顺序表的类型定义(动态分配方式)
Key:动态申请和释放内存空间
C语言中—— malloc、free函数
L.date=(ElemType *)malloc(sizeof(ElemType)*InitSize);
malloc函数返回一个指针,需要强制转换为定义的数据元素类型的指针。 malloc函数的参数,指明要分配多大的连续内存空间
C++可使用new,delete关键字。
#include <stdlib.h> //包含malloc和free函数
#define InitSize 10 //默认的最大长度
Typedef struct{
int * data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int Length; //顺序表的当前容量
}SeqList;
void InitList(SeqList &L){
//用malloc函数申请一片连续的存续空间
L.data = (int *)malloc(sizeof(int) * InitSize);
L.MaxSize = InitSize;
L.Length = 0;
}
//增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
int *p = L.date;
L.date = (int *)malloc(sizeof(int)*(len+L.MaxSize));
for(int i = 0; i < L.length; i++){
L.data[i] = p[i]; //将数据复制到新的区域。
}
L.MaxSize += len; //长度增加了len
free(p); //释放原来的内存空间
}
int main() {
//....
}
//注意realloc函数也可实现,但建议初学者使用malloc和free更能理解背后的过程。
1.2 顺序表的实现
顺序表的特点:
- 随机访问即在O(1)时间内找到第i个元素。
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便
- 插入、删除操作不方便,需要移动大量元素
知识回顾
1.3 顺序表—插入、删除
1.3.1 插入操作
代码实现
ListInsert(&L,i,e):插入操作。在表L中的第i个位置插入指定的元素e。
#define MaxSize 10 //定义最大长度
typedef struct{
int data[MaxSize]; //本节用静态分配方式实现顺序表,动态也相同
int length; //顺序表当前的长度
}SqList; //顺序表的类型定义
void ListInsert(SqList &L, int i, int e){
for(int j = L.length; j >= i; j--)
L.data[j] = L.data[j-1];
L.data[i-1] = e; //注意数组从0开始!
L.length += 1;
}
int main() {
SqList L;
InitList(L);
//....
ListInsert(L, 3, 3);
return 0;
}
可以使插入操作更加健壮 可进行下面改进。
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--)
L.data[j] = L.data[j-1];
L.data[j-1] = e;
L.length += 1;
return true;
}
好的算法,应该具有“健壮性”
插入操作的时间复杂度
关注最深层循环语句的执行次数与问题规模n的关系
问题的规模n=L.length(表长)
最好是表尾=O(1);
最坏是表头=O(n);
平均:n/2 所以平均时间复制度为=O(n)
1.3.2 删除操作
代码实现
bool ListDelete(SqList &L, int i, int &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;
}
int main() {
Sqlist L;
InitList(L);
//..此处插入一些元素
int e = -1; //可以把删除的数据带回来
if(ListDelete(L, 3, e))
printf("删除元素为%d",e);
else
printf("位置不合法");
return 0;
}
事件复杂度
最好情况:删除表尾O(1)
最坏情况:删除表头O(n)
平均情况,删除任何一个元素的概率都是相同的,故等差数列求0+1+2+…+(n-1) 故为O(n);
1.4 顺序表的查找
- 按位查找
GetElem(L,i):获得第i个位置的元素
ElemType GetElem(SqList L, i) {
return L.data[i-1];
}
时间复杂度=O(1);
- 按值查找
int LocateElem(SqList L, ElemType e) {
for(int i = 0; i < L.length; i++)
if(L.data[i] == e)
return i+1; //数组的下标为i,位序i+1
return 0; //退出循环
}
当为结构体不可以用 ==来判断是否相等,编译都不能通过———考试如果是数据结构的话 可能会有,但是是C语言与程序设计不行。
时间复杂度
((1+n)* n/2)*(1/n)
2. 链式存储
2.1 单链表
单链表的定义
- 什么是单链表
每个结点除了存放数据元素外,还要存储指向下一个节点的指针
优点:不要求大片连续空间,改造容量方便
缺点:不可随机存取,要消耗一定空间存放指针
- 用代码定义一个单链表
typedef struct LNode { //定义单链表的结构类型
ElemType data; //每个结点存放一个数据元素
struct LNode *next; //指针指向下一个节点
}LNode, *LinkList;
LNode *p = (LNode *)malloc(sizeof(LNode)); //增加新节点:在内存中申请一个节点所需的空间,并用指针p指向这个结点
//LNode *L 可写成LinkList L 声明一个指向单链表第一个结点的指针
LNode * GetElem(LinkList L, int i) {
int j = 1;
LNode *p = L -> next;
if(i==0)
return L;
if(i < 1)
return NULL;
while(p!=NULL && j<i) {
p = p -> next;
j++;
}
return p; //强调是一个单链表 --使用LinkList
} //强调这是一个结点 --使用LNode *
要表示一个单链表时,只需要声明一个头指针L,指向单链表的第一个结点
- 两种实现
- 不带头节点
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据结构
struct LNode *next; //指针指向下一个节点
}LNode, *LinkList;
//初始化一个单链表
bool InitList(LinkList &L) {
L = NULL; //空表,暂时还没有任何节点
return true;
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L==NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
InitList(L); //初始化一个空表
//...后续代码...
}
- 带头结点
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;
}
//判断单链表是否为空(带头结点)
bool Empty(LinkList L) {
return (L->next == NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
InitList(L); //初始化一个空表
//...后续代码...
}
->表示左边是指针,现在要提取右边的成员
.表示左边是实体,现在要提取右边的成员
头结点不存数据只是为了操作方便。
2.2 单链表的基本操作
2.2.1 单链表的插入和删除
- 按位序插入(带头结点)
ListInsert(&L,i,e):插入操作。表示L中的第i个位置上插入指定元素e。(找到i-1位置,插入其后)
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//在第i个位置插入元素e
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) { //j是从零开始循环的不要看不懂,循环到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。(找到i-1位置,插入其后)
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListInsert(LinkList &L, int i, ElemType e) {
if(i<1)
return false;
if(i == 1) { //插入第1个结点的操作与其他结点操作不同
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指向第一个结点 (注意不是头结点)
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;
return true;
}
除特别声明外,之后的代码默认带头结点
不带头结点写代码更不方便,推荐用带头结点的
2.2.2 指定结点的后插操作
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
//后插操作:在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->next = p->next;
p->next = s;
return true;
}
2.2.3 指定结点的前插操作
前插操作需要传入头指针,否则没法往前遍历,但是可以往后插一个,然后把p 和插入的元素互换位置就解决了问题
//前插操作:在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->data = p->data;
p->data = e;
return true;
}
//时间复杂度O(1)
2.2.4 按位序删除(带头结点的)
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除的元素。
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i<1)
return false;
LNode *p; //指针p指向当前扫描到的结点
int j = 0; //当前p指向的第几个结点
p = L; //L指向头结点,头结点是第零个结点(不存数据)
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;
}
2.2.5 指定结点的删除
//删除指定的结点 p,就是删除p后面的结点 然后把p后面的值放在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是最后一个元素的时候,就只能用土办法,通过头结点找到前驱结点
单链表的局限性:无法逆向检索
2.3 单链表的查找
只讨论带头结点的情况
- 按位查找
GetElem(L,i):按位查找操作。获取表L中的第i个位置的元素的值。
//按位查找,返回第i个元素(带头结点)
LNode * GetElem(LinkList L, 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;
}
- 按值查找
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值得元素。