数据结构和算法-线性表
定义(Linear List)
线性表是具有相同数据类型
的n(n>=0)个数据元素的有序序列
.
顺序存储: 顺序表
链式存储: 单链表,双链表,循环链表,静态链表(数组实现)
Notes:
(1) 表中元素有限
(2) 逻辑上的顺序性
(3) 表中数据元素的类型都相同,每个元素都战友相同大小的存储空间.
(4)线性表是一种逻辑结构;顺序表和链表是值存储结构.
线性表的基本操作
InitList(&L); //构造一个空的线性表。成功返回0出错返回-1
Length(L); //返回L中数据元素个数
DestroyList(&L); //销毁线性表L
ListEmpty(L); //若L为空表,则返回TRUE,否则返回FALSE
GetElem(L,i); //返回L中第i个数据元素的值
LocatElem(L,e); //按值查找操作
ListInsert(&L,i,e); //在L中第i个位置上插入新的数据元素e
ListDelete(&L,i,&e) //删除L的第i个数据元素,并用e返回其值
顺序表的定义
线性表的顺序存储
也叫作顺序表.
顺序存储: 把逻辑上相邻的元素存储在物理位置上也相邻
的存储单元中.
顺序表的实现–静态分配
#define MaxSize 10 //定义最大长度
typedef struct
{
ElemType data[MaxSize]; // 用静态数组存放数据元素
int length; // 顺序表当前长度
}SqList // 静态表的类型定义(静态分配方式)
//Sq: sequence - 顺序,序列
void InitList(Sqlist &L){
for (int i = 0; i< MaxSize; i++)
{
L.data[i] = 0;
}
L.length=0; //顺序表的初始长度为0
}
顺序表的实现–动态分配
#define InitSize 10
typedef struct
{
ElemType * data; //指向动态分配的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
} SeqList;
顺序表的特点
随机访问
,可以在O(1)
时间内找到第i个元素,代码data[i-1]- 存储密度高,每个节点只存储数据元素
- 容量扩展不方便,时间复杂度高
- 插入删除数据不方便,需要移动大量元素,效率低.
顺序表的插入/删除
插入
如果是下标i位置插入元素e,则代码为:
void ListInsert(SqList &L,int i,int e)
{
if (i < 0 || i >L.length)
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[i] = e;
L.length++;
return true;
}
- 时间复杂度:
最好:O(1)
; 最坏:O(n)
; 平均:O(n)
删除
如果是下标i位置删除元素,则代码为:
void ListDelete(SqList &L,int i,int &e)
{
if (i < 0 || i > L.length-1)
return false;
e = L.data[i];
for (int j = i; j < L.length -1; j++)
L.data[j] = L.data[j+1];
L.length--;
return true;
}
- 时间复杂度:
最好:O(1)
; 最坏:O(n)
; 平均:O(n)
顺序表的查找
按位置查找
- 静态分配顺序表
#define MaxSize 10 //定义最大长度
typedef struct{
ElemType data[MaxSize]; //用静态的“数组”存放数据元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义(静态分配方式)
ElemType GetElem(SqList L, int i){
return L.data[i-1];
}
- 动态分配顺序表
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
} SeqList; //顺序表的类型定义(动态分配方式)
//指针依然可以使用数组一样下标的方式来访问,系统会自动按照指针指向的数据类型的大小来向后移动
ElemType GetElem(SqList L, int i){
return L.data[i-1];
}
- 时间复杂度:
O(1)
因为顺序表的各个数据元素在内存中连续存放,因此可以更具起始地址和数据元素的大小立刻找到第i个元素。即“随机存取”
特性
按值查找
#define InitSize 10 //顺序表的初始长度
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize; //顺序表的最大容量
int length; //顺序表的当前长度
} SeqList; //顺序表的类型定义(动态分配方式)
//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L,ElemType e){
for(int i=0;i<L.length;i++)
{
if(L.data[i]==e)
return i+1; //数组下标为i的元素值等于e,返回其位序i+1
}
return 0; //退出循环,说明查找失败
}
- 时间复杂度:
最好:O(1)
; 最坏:O(n)
; 平均:O(n)
单链表的定义
优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针
- 代码实现单链表节点
- 申请一个新的结点
struct LNode * p = (struct LNode *) malloc(sizeof(struct LNode));
- 使用typedef关键字
//typedef <数据类型> <别名>
int x = 1;
int* p;
//等价于
zhengshu x = 1;
zhengshuzhizhen p;
//同理
typedef struct LNode LNode;
typedef struct LNode *LinkList;
LNode * p = (LNode *) malloc(sizeof(LNode));
要表示一个单链表时,只需声明一个头指针 L ,指向单链表的第一个结
点
或:
代码可读性更强
单链表的实现-不带头结点的单链表
单链表的实现-带头结点的单链表(使用更广泛)
单链表的插入/删除
按位置插入(带头结点)
Tips:
(1) 最后两句代码一定不能颠倒!
(2) 要在第i个位置插入节点。需要找到的是第i-1个节点,也就是从头结点开始往后next的次数为i-1次。
- 平均时间复杂度:
O(n)
按位置插入(不带头结点)
Tips:
(1) 不带头结点的代码书写不方便,推荐用带头结点的。
(2) 注意审题!
指定节点后插入操作
- 平均时间复杂度:
O(1)
指定节点前插入操作
-
核心思想:
构造一个新的节点s插入到节点p的后面,然后把节点p和节点s的值交换,就可以实现把新节点s插入到p之前的操作。 -
若给定的时元素值e
-
若给定的时元素节点s
-
时间复杂度
O(1)
按位置删除
- 平均时间复杂度:
O(n)
指定节点的删除
- 核心思想:
(1) 找到要删除节点p的后续节点q
(2) 把q节点的数据域赋给p
(3) 让p直接指向q的后继节点
(4) 释放q节点内存 - 时间复杂度
O(1)
Tips:
(1) 如果要删除的节点是最后一个节点,此时这种巧办法不可行!还得使用笨办法。
单链表的查找
按位查找(带头结点)
- 平均时间复杂度:
O(n)
按值查找
单链表的建立(带头节点)
尾插法
LinkList List_TailInsert(LinkList &L){ //正向建立单链表
int x; //设ElemType为整型
L=(LinkList)malloc(sizeof(LNode)); //建立头结点
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL; //尾结点指针置空
return L;
}
头插法
LinkList List_HeadInsert(LinkList &L){ //逆向建立单链表
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode)); //创建头结点
L->next=NULL; //初始为空链表
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode*)malloc(sizeof(LNode)); //创建新结点
s->data=x;
s->next=L->next;
L->next=s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
Tips:
(1) 重要应用:链表的实际顺序和输入的顺序是相反的,链表的逆置!
双链表
双链表的定义
初始化(带头指针)![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/3fd6d5e49a22941cdf660c4982fb2741.png)
判断双链表是否为空.等价于判断(L.next == NULL)?,即头节点后是否有下一个节点.
双链表的插入
注意顺序!!!
双链表的删除
别忘记判断p是否为最后一个节点,即其next节点是否为NULL!
循环列表
循环单链表
-
定义
-
初始化
循环双链表
-
表头节点的prior指向标为节点; 表尾节点的next指向头结点
-
初始化
-
判空和表尾节点的判断同循环单链表
-
插入
与双链表的区别在于: 不再需要额外判断p节点是不是最后一个节点了.
静态链表
定义
-
核心思想: 使用数组下标来指明下一个元素的位置.
两种写法都可以. -
使用:
或:
静态链表的操作
-
查找:
从头结点出发挨个往后遍历结点 -
插入位序为 i 的结点: 点是否为空?
(1) 找到一个空的结点,存入数据元素
(2) 从头结点出发找到位序为 i-1 的结点
(3) 修改新结点的 next
(4) 修改 i-1 号结点的 next -
删除某个结点:
(1) 从头结点出发找到前驱结点
(2) 修改前驱结点的游标
(3) 被删除结点 next 设为 -2(初始化时亦应该设置各节点为此值)
顺序表VS链表比较
逻辑结构:
都属于线性表,都是线性结构存储结构:
<顺序表>
优点:支持随机存取、存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
<链表>
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低基本操作
<创建>
顺序表: 需要预分配大片连续空间。
顺序表-静态数组: 容量不可改变
顺序表-动态数组: 容量可改变,但需要移动大量元素,时间代价高
链表: 只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便拓展
<销毁>
顺序表: 修改 Length = 0
顺序表-静态数组: 系统自动回收空间
顺序表-动态数组: 需要手动 free
链表: 依次删除各个结点(free)
<增删>
顺序表: 插入/删除元素要将后续元素都后移/前移;时间复杂度 O(n),时间开销主要来自移动元素
链表: 插入/删除元素只需修改指针即可;时间复杂度 O(n),时间开销主要来自查找目标元素
<查找>
顺序表: 按位查找:O(1);按值查找:O(n),若表内元素有序,可在O(log 2 n) 时间内找到
链表: 按位查找:O(n),按值查找:O(n)总结:
表长难以预估、经常要增加/删除元素 ——链表
表长可预估、查询(搜索)操作较多 ——顺序表