目录
数据结构 DATA STRUCTURE
二、线性表
2.1 线性表的定义和基本操作概述
2.2 线性表的顺序表示
顺序表:线性表的顺序存储。(顺序存储≠顺序存取)
用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的两个元素在物理位置上也相邻。
特点:
- 逻辑相邻,物理也相邻。
- 任一数据元素可 随机存取。
通常用数组来描述线性表的顺序存储结构。
注意:线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的。
2.2.1 顺序表存储结构描述和特点
1. 静态存储方式
#define MaxSize 50 // 定义线性表的最大长度的值
// 静态分配数组顺序表的类型定义
typedef struct{
ElemType data[MaxSize]; // 顺序表的元素类型、长度
int length; // 顺序表当前长度
}SqList;
2. 动态存储方式
#define InitSize 100
// 动态分配数组顺序表的类型定义
typedef struct{
ElemType *data; // 指示动态分配数组的指针
int MaxSize, length; // 数组的最大容量和当前个数
}SeqList;
// C 的初始动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
// C++的初始动态分配语句
L.data = new ElemType[InitSize];
一维数组是可以静态分配,也可以是动态分配的。(不管是哪种分配方式,都属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时动态决定。)
- 静态分配当空间占满的时候,容易产生溢出;
- 动态分配中存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,空间占满,还可以开辟一块更大的存储空间替换原来的存储空间,不需要一次性划分所有空间。
静态动态的区别也就是用不用指针,指针就是为了间接访问,方便动态操作而设计的。用指针来指向数据域并且用指针进行动态分配即可实现“动态存储”。
关于malloc、free函数:
malloc会在内存中申请一整片连续的存储空间,并且返回指针。
动态分配基本操作实现:
初始化:
// 动态分配顺序表的初始化
void InitList(SqList &L) {
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}
对 L 的三个成员变量进行赋值。
增加动态数组的长度:
void IncreaseSize(SqList &L, int len) {
int *p = L.data;
L.data = (int *)malloc((L.MaxSize+len)*sizeof(int));
for (int i=0; i<L.length; i++) {
L.data[i] = p[i]; // 将数据复制到新区域
}
L.MaxSize = L.MaxSize+len; // 顺序表最大长度增加len
free(p); // 释放原来的内存空间
}
重新找到另外一块更大的区域,并且把之前的空间拿来,而且删除原来的空间。
- 时间开销大❗❗
- realloc 函数也可实现,malloc和free更底层。
3. 顺序表的优缺点
总是和表长length以及数组打交道(毕竟人家存储结构就是这俩🤦♂️)
优点:
- 随机存取:通过 首地址 和 元素序号 可在时间 O(1) 内找到指定的元素。
- 存储密度高:每个结点只存储数据元素。
缺点:
- 静态分配数组拓展容量不方便,动态分配数组方式时间复杂度高。
- 插入和删除操作需要移动大量元素。
- 需要使用一整块地址连续的存储单元。(逻辑相邻的物理也相邻)
2.2.2 顺序表基本操作实现
基于静态分配数组的顺序表。
1. 插入操作
在表 L 的第 i 个位置插入 e。
Steps:
判断i的范围和空间是否已满
将 i 元素及之后的元素后移
在 i 插入 e
表长 +1
返回 true
从后往前,先挪开后插入
bool ListInsert(SqList &L, int i, ElemType e) {
if (i<1 || i>L.length+1) // 判断i的范围是否有效(此处判断理论上位置 i 是否和法,不是数组下标是否合法,所以是和1比较而不是和0; L.length+1表示最后一个元素的后一个位置)
return false;
if (L.length >= MaxSize) // 判断当前存储空间是否已满(这就是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(位置i对应L.data[i-1])
L.length++; // 表长+1
return true;
}
不对 i 和 length 进行合法性检查的话,其他人用的时候,可能插到不合法位置,比如隔了n个插入到后面的位置,这样是不好的。(所以添加了if进行合法性判断,并且将返回数据类型改为bool,提高代码的健壮性。也因此注意关注代码的时候要关注最核心语句!!)
函数代码一般包括:
- 合法性判断
- 核心逻辑算法
- 良好的return
Question:for 循环为什么从L.length
开始,不应该像 if 的条件表达式一样是L.length+1
吗?
因为数组是从
0
开始,data[L.length]
就是线性表的L.length+1
的位置。
线性表插入平均时间复杂度:O(n)
计算过程:(Avg = 在每个位置 i 插入的概率 * 其对应的主语句执行次数)
∑ i = 1 n + 1 p i ( n − i + 1 ) = ∑ i = 1 n + 1 1 n + 1 ( n − i + 1 ) = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 n ( n + 1 ) 2 = n 2 \sum\limits_{i=1}^{n+1} p_i(n-i+1) = \sum\limits_{i=1}^{n+1}\frac{1}{n+1}(n-i+1) = \frac{1}{n+1} \sum\limits_{i=1}^{n+1}(n-i+1) = \frac{1}{n+1}\frac{n(n+1)}{2} = \frac{n}{2} i=1∑n+1pi(n−i+1)=i=1∑n+1n+11(n−i+1)=n+11i=1∑n+1(n−i+1)=n+112n(n+1)=2n
最好情况:在表尾插入(i=n+1),不需要后移,O(1)。
最坏情况:在表头插入(i=1),元素后移语句执行 n 次,O(n)。
2. 删除操作
删除 L 第 i 个位置的元素,并用 e 返回删除元素的值。
Steps:
- 判断 i 的范围
- 将被删的元素赋值给 e
- 将第 i 个位置后的元素前移(删除其实就是复制给 e,然后重新赋值为后继元素)
- 线性表长度-1
先拿出然后让后面的向前覆盖上
bool ListDelete(SqList &L, int i, ElemType &e){
if (i<1 || i>L.length) // (此处判断理论上位置 i 是否和法,不是数组下标是否合法,所以是和1比较)
return false;
e = L.data[i-1]; // 数组下标=真实位序-1
for (int j = i; j < L.length; j++) // 从i开始是没错的,因为下面是L.data[j-1] = L.data[j],对应着数组的正确位置(=位序-1)
L.data[j-1] = L.data[j];
L.length--;
return true;
}
线性表删除平均时间复杂度:O(n)
和插入相同,表头删除是O(n),表尾删除是O(1),平均的话O(n)。
计算过程略。
3. 按位查找
在 L 中找第 i 个元素并返回。
Steps:
注意数组下标和位序 i 的区别,返回 data[i-1]。
// 按位查找
ElemType GetElem(SqList L, int i) {
if (i<1 || i>L.length)
return false;
return L.data[i-1]; // 注意数组下标和位序i的区别;动态分配数组顺序表也可以这样写,只不过含义变为:指针+[]用法,相当于指针的运算。
}
4. 按值查找(顺序查找)
在 L 中找第一个元素值等于 e 的元素,返回位置。
Steps:
- 循环比较并返回 i+1
- 循环外返回 0
// 查找(按值查找)
int LocateElem(SqList L, ElemType e) {
int i;
for (i = 0; i < L.length; i++) // 顺序查找(数组从0开始)
if (L.data[i] == e) // int\char\double\float等可以直接用运算符== 进行比较;结构体不可以用==直接比较,可以用C++的运算符重载,或者成员变量一一比较
return i+1; // 返回和e相等的元素的逻辑位序 i+1(数组下标为i,那位置就是i+1)
return 0; // 退出循环说明查找失败,返回0
}
线性表按值查找平均时间复杂度:O(n)
查找的元素在表头O(1),查找的元素在表尾O(n),平均的话O(n)。
计算过程略。
5. 初始化
// 初始化为空表
bool ListInit(SqList &L) {
for (int i = 0; i < MaxSize; i++) // 清除脏数据,不清除的话防君子不防小人(可以不写)
L.data[i] = 0;
L.length = 0; // 初始长度为0
return true;
}
/*
* 防君子不防小人:
* 君子都是用基本操作来访问顺序表的,小人可能直接去printf数据。
*/
// 初始化(赋值为数组的值)
void ListAssign(SqList &L, ElemType Arr[], int len) { // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能额外添加数组长度参数
int i;
for (i = 0; i < len; i++)
L.data[i] = Arr[i];
L.length = len;
}
6. 判空
判断是不是为空
// 判空
bool Empty(SqList L) {
if (L.length == 0)
return true;
else
return false;
}
// 或者这样写
bool Empty(SqList L) {
return (L.length == 0);
}
7. 打印
- 合法性判断
- 循环遍历输出data[i]
// 打印
void ListPrint(SqList L) {
if (L.length > 0) {
int i;
for (i = 0; i < L.length; i++)
printf("%d ", L.data[i]);
printf("\n");
}
else
printf("List is empty.\n");
}
2.2.3 顺序表总结
一、顺序表
- 可根据序号随机存取;(按位置查找 O(1))
- 表尾插入删除不用移动任何元素,很快 O(1);表头/除表尾外其他位置插入删除需要移动大量元素,很慢 O(n)。
- 按值查找 O(n)
二、为什么顺序表的插入、删除函数设计的时候都要有位置 i 这个参数呢?
考虑到基础操作的话,插入,肯定是插入到某个位置;删除,也是删除某个位置的元素;
像“删除表中某元素”这样的操作,可以规划到“不常用/非基础操作”。
而且顺序表的优势,就是“顺序”,可根据位置 i 随机存取。
三、算法设计注意:
- 数组下标i 和 位序i 的区别
- 注意参数是否引用
- 算法要有健壮性,要有 i 的合法性判断
- 核心逻辑算法
- 良好的return
全部代码(复制可直接运行)
/*
* 顺序表存储结构及基本操作实现
* 纯享版
* 可直接运行
*/
#include <stdio.h>
#define MaxSize 50
typedef int ElemType;
// 顺序表的类型定义(静态分配数组方式)
typedef struct{
ElemType data[MaxSize];
int length;
}SqList;
// 初始化为空表
bool ListInit(SqList &L) {
for (int i = 0; i < MaxSize; i++)
L.data[i] = 0;
L.length = 0;
return true;
}
// 判空
bool Empty(SqList L) {
if (L.length == 0)
return true;
else
return false;
}
// 赋值(将数据域赋值为数组)
bool ListAssign(SqList &L, ElemType Arr[], int len) { // 由于形参数组只是个指针,无法传送求数组长度信息,所以只能添加数组长度参数
if (len<1 || len>MaxSize)
return false;
for (int i = 0; i < len; i++)
L.data[i] = Arr[i];
L.length = len;
return true;
}
// 插入
bool ListInsert(SqList &L, int i, ElemType e) {
if (i<1 || i>L.length+1)
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-1] = e;
L.length++;
return true;
}
// 删除
bool ListDelete(SqList &L, int i, ElemType &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;
}
// 按位查找
ElemType GetElem(SqList L, int i) {
if (i<1 || i>L.length)
return false;
return L.data[i-1];
}
// 查找(按值查找)
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++)
if (L.data[i] == e)
return i+1;
return 0;
}
// 打印
void ListPrint(SqList L) {
if (L.length > 0) {
for (int i = 0; i < L.length; i++)
printf("%d ", L.data[i]);
printf("\n");
}
else
printf("List is empty.\n");
}
/* 测试——主函数 */
int main()
{
SqList list1;
int a[10] = {1,2,3,4,5,6,7,8,9,10};
int e = 11;
int temp;
ListInit(list1);
if (Empty(list1))
printf("list1 is empty.\n");
else
printf("list1 is not empty.\n");
ListAssign(list1, a, 10);
ListPrint(list1);
ListInsert(list1, 2, e);
ListPrint(list1);
ListDelete(list1, 2, e);
ListPrint(list1);
printf("list1 第二个位置上的元素是:%d\n", GetElem(list1, 2));
temp = LocateElem(list1, 2);
if(temp)
printf("2 在list1中的位置是:%d\n");
else
printf("2 不在list1中。\n");
printf("测试结束。");
return 0;
}
运行结果:
不难看出,其实顺序表就是数组,外加一个表示当前表长的length,没错,就是这么简单。😎