408答疑
文章目录
二、线性表的顺序表示
顺序表的定义
- 线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。第 1 个元素存储在顺序表的起始位置,第 i i i 个元素的存储位置后面紧接着存储的是第 i + 1 i+1 i+1 个元素,称 i i i 为元素 a i a_i ai 在顺序表中的位序。因此,顺序表的特点是表中元素的逻辑顺序与其存储的物理顺序相同。
存储位置计算
- 假设顺序表 L 存储的起始位置为 LOC(A),sizeof(ElemType) 是每个数据元素所占用存储空间的大小,则表 L 所对应的顺序存储如下图所示。
- 每个数据元素的存储位置都和顺序表的起始位置相差一个和该数据元素的位序成正比的常数,因此,顺序表中的任意一个数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构。通常用高级程序设计语言中的数组来描述线性表的顺序存储结构。
注意事项
- 顺序表是线性表的一种顺序存储实现方式,通过数组来存储数据。数组应由结构体进行管理,避免直接操作数组。
- 使用作用域操作符时,可以理解为“中国.西安”这样的结构。
- 在对顺序表进行增加或删除操作时,必须更新
length
属性以反映顺序表的新长度。
线性表与数组的区别
- 线性表中元素的位序是从 1 开始的,而数组中元素的下标是从 0 开始的。
动态分配与静态分配
- 一维数组可以是静态分配的,也可以是动态分配的。对数组进行静态分配时,因为数组的大小和空间事先已经固定,所以一旦空间占满,再加入新数据就会产生溢出,进而导致程序崩溃。
- 而在动态分配时,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,将原表中的元素全部拷贝到新空间,从而达到扩充数组存储空间的目的,而不需要为线性表一次性地划分所有空间。
动态分配的特点
动态分配并不是链式存储,它同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,只是分配的空间大小可以在运行时动态决定。
顺序表的优点与缺点
优点
- 可进行随机访问:即可通过首地址和元素序号可以在 O ( 1 ) O(1) O(1) 时间内找到指定的元素。
- 存储密度高:每个结点只存储数据元素。
缺点
- 元素的插入和删除需要移动大量的元素:插入操作平均需要移动 n / 2 n/2 n/2 个元素,删除操作平均需要移动 ( n − 1 ) / 2 (n-1)/2 (n−1)/2 个元素。
- 顺序存储分配需要一段连续的存储空间:不够灵活。
顺序表上基本操作的实现
顺序表的初始化
- 静态分配:在声明一个顺序表时,就已为其分配了数组空间,初始化时只需将顺序表的当前长度设为 0。
- 动态分配:为顺序表分配一个预定义大小的数组空间,并将顺序表的当前长度设为 0。
MaxSize
指示顺序表当前分配的存储空间大小,一旦因插入元素而空间不足,就进行再分配。
动态分配的顺序表存储结构
- 定义了一个结构体 SeqList,包含一个指向元素类型的指针 data,以及两个整数 MaxSize 和 length 分别表示数组的最大容量和当前长度。
#define InitSize 100 // 表长度的初始定义
typedef struct {
ElemType *data; // 指示动态分配数组的指针
int MaxSize, length; // 数组的最大容量和当前个数
} SeqList;
C 语言的初始动态分配语句
- 使用 malloc 函数为顺序表分配初始大小为 InitSize 的存储空间。
L.data = (ElemType *)malloc(sizeof(ElemType) * InitSize);
C++ 语言的初始动态分配语句
- 使用 new 操作符为顺序表分配初始大小为 InitSize 的存储空间。
L.data = new ElemType[InitSize];
初始化顺序表函数
- 该函数用于初始化顺序表,首先通过 malloc 分配存储空间。
- 将顺序表的当前长度 length 初始化为 0。
- 将顺序表的最大容量 MaxSize 初始化为 InitSize,即预定义的初始大小。
void InitList(SeqList &L) {
L.data = (ElemType *)malloc(MaxSize * sizeof(ElemType)); // 分配存储空间
L.length = 0; // 顺序表初始长度为 0
L.MaxSize = InitSize; // 初始存储容量
}
插入操作
在顺序表 L 的第 i (1<=i<=L.length+1) 个位置插入新元素 e。若 i 的输入不合法,则返回 false,表示插入失败;否则,将第 i 个元素及其后的所有元素依次往后移动一个位置,腾出一个空位置插入新元素 e,顺序表长度增加 1,插入成功,返回 true。
- 最好情况:在表尾插入(即 i = n + 1),元素后移语句将不执行,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最坏情况:在表头插入(即 i = 1),元素后移语句将执行 n 次,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:假设 p i p_i pi ( p i = 1 / ( n + 1 ) p_i = 1/(n + 1) pi=1/(n+1)) 是在第 i 个位置上插入一个结点的概率,则在长度为 n 的线性表中插入一个结点时,所需移动结点的平均次数为 ∑ i = 1 n + 1 p i ( n − i + 1 ) = 1 n + 1 ∑ i = 1 n + 1 ( n − i + 1 ) = 1 n + 1 n ( n + 1 ) 2 = n 2 \sum_{i=1}^{n+1} p_i(n - i + 1) = \frac{1}{n + 1} \sum_{i=1}^{n+1} (n - i + 1) = \frac{1}{n+1}\frac{n(n + 1)}{2}=\frac{n}{2} ∑i=1n+1pi(n−i+1)=n+11∑i=1n+1(n−i+1)=n+112n(n+1)=2n。
因此,顺序表插入算法的平均时间复杂度为 O ( n ) O(n) O(n)。
删除操作
删除顺序表 L 中第 i (1<=i<=L.length) 个位置的元素,用引用变量 e 返回。若 i 的输入不合法,则返回 false;否则,将被删元素赋给引用变量 e,并将第 i+1 个元素及其后的所有元素依次往前移动一个位置,返回 true。
- 最好情况:删除表尾元素(即 i = n),无须移动元素,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最坏情况:删除表头元素(即 i = 1),需移动除表头元素外的所有元素,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:假设 p i p_i pi ( p i = 1 / n p_i = 1/n pi=1/n) 是删除第 i 个位置上结点的概率,则在长度为 n 的线性表中删除一个结点时,所需移动结点的平均次数为 ∑ i = 1 n p i ( n − i ) = 1 n ∑ i = 1 n ( n − i ) = n − 1 2 \sum_{i=1}^{n} p_i(n - i) = \frac{1}{n} \sum_{i=1}^{n} (n - i) = \frac{n - 1}{2} ∑i=1npi(n−i)=n1∑i=1n(n−i)=2n−1。
因此,顺序表删除算法的平均时间复杂度为 O ( n ) O(n) O(n)。
可见,顺序表中插入和删除操作的时间主要耗费在移动元素上,而移动元素的个数取决于插入和删除元素的位置。下图所示为一个顺序表在进行插入和删除操作前、后的状态,以及其数据元素在存储空间中的位置变化和表长变化。在下图(1)中,将第 4 个至第 7 个元素从后往前依次后移一个位置,在下图(2)中,将第 5 个至第 7 个元素从前往后依次前移一个位置。
按值查找(顺序查找)
在顺序表 L 中查找第一个元素值等于 e 的元素,并返回其位序。
- 最好情况:查找的元素就在表头,仅需比较一次,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最坏情况:查找的元素在表尾(或不存在)时,需要比较 n 次,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:假设 p i p_i pi ( p i = 1 / n p_i = 1/n pi=1/n) 是查找的元素在第 i (1<=i<=L.length) 个位置上的概率,则在长度为 n 的线性表中查找值为 e 的元素所需比较的平均次数为 ∑ i = 1 n p i ⋅ i = ∑ i = 1 n 1 n ⋅ i = 1 n n ( n + 1 ) 2 = n + 1 2 \sum_{i=1}^{n} p_i \cdot i = \sum_{i=1}^{n} \frac{1}{n} \cdot i = \frac{1}{n} \frac{n(n + 1)}{2} = \frac{n + 1}{2} ∑i=1npi⋅i=∑i=1nn1⋅i=n12n(n+1)=2n+1。
因此,顺序表按值查找算法的平均时间复杂度为 O ( n ) O(n) O(n)。
- 顺序表的按序号查找非常简单,即直接根据数组下标访问数组元素,其时间复杂度为 O ( 1 ) O(1) O(1)。
顺序表的代码实操
定义顺序表(静态)
- 定义了一个结构体 SeqList,包含一个整型数组 data 和一个整型变量 length 用于存储顺序表的数据和长度。
typedef struct SeqList {
ElemType data[MAX_SIZE]; // 数据空间
int length; // 长度
} SeqList;
初始化顺序表
- 通过给定的数组 ar 和长度 n 初始化顺序表 L。
void initSeqList(SeqList *L, ElemType ar[], int n) {
for (ElemType i = 0; i < n; ++i)
L->data[i] = ar[i];
L->length = n;
}
打印顺序表
- 打印顺序表中的所有元素。
void printSeqList(SeqList *L) {
for (int i = 0; i < L->length; ++i)
printf("%d ", L->data[i]);
printf("\n");
}
尾部插入数据
- 在顺序表的尾部插入一个新元素 x。
void insertSeqListTail(SeqList *L, ElemType x) {
// 判断容量
if (L->length >= MAX_SIZE)
return;
L->data[L->length] = x;
L->length++;
}
头部插入数据
- 在顺序表的头部插入一个新元素 x,需要移动已有元素。
void insertSeqListFront(SeqList *L, ElemType x) {
// 判断容量
if (L->length >= MAX_SIZE)
return;
// 移动数据
for (int i = L->length; i > 0; --i)
L->data[i] = L->data[i - 1];
// 插入数据
L->data[0] = x;
L->length++;
}
特定位置插入数据
- 在顺序表的指定位置 pos 插入一个新元素 x,需要移动后续元素。
void insertSeqListByPos(SeqList *L, int pos, ElemType x) {
// 判断容量
if (L->length >= MAX_SIZE)
return;
// 移动数据
for (int i = L->length; i > pos; --i)
L->data[i] = L->data[i - 1];
// 插入数据
L->data[pos] = x;
L->length++;
}
删除数据
- 删除顺序表中第一个值为 key 的元素,删除成功返回 1,否则返回 0。
// 删除数据:自己规定
// 1 删除成功
// 0 删除失败
int deleteElem(SeqList *L, ElemType key) {
// 查找数据
int pos = 0;
while (pos < L->length && L->data[pos] != key)
pos++;
if (pos >= L->length)
return 0; // 删除的数据不存在
// 删除数据
for (int i = pos; i < L->length - 1; ++i)
L->data[i] = L->data[i + 1];
// 更新长度
L->length--;
return 1;
}
反转顺序表
- 反转顺序表中的元素顺序。
void reverseSeqList(SeqList *L) {
int temp;
int left = 0, right = L->length - 1;
while (left < right) {
// 交换两数
temp = L->data[left];
L->data[left] = L->data[right];
L->data[right] = temp;
// 更新区间
left++;
right--;
}
}
排序顺序表
- 文章链接: 点击跳转
查找元素
顺序查找元素
- 顺序查找顺序表中值为 key 的元素,找到返回其位置,否则返回 -1。
int findElem(SeqList *L, ElemType key) {
for (int i = 0; i < L->length; ++i) {
if (L->data[i] == key)
return i;
}
return -1;
}
二分查找
- 对已排序的顺序表进行二分查找,找到返回其位置,否则返回 -1。
int binarySearch(SeqList *L, ElemType key) {
int left = 0, right = L->length - 1;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if (L->data[mid] == key)
return mid;
else if (L->data[mid] > key)
right = mid - 1;
else
left = mid + 1;
}
return -1;
}
合并两个有序的顺序表
- 合并两个已排序的顺序表 L1 和 L2 到顺序表 L 中。
void mergeSeqList(SeqList *L, SeqList *L1, SeqList *L2) {
// L1 L2 L
int tail1 = 0, tail2 = 0;
int tail = 0;
while (tail1 < L1->length && tail2 < L2->length) // 没有合并完成
{
if (L1->data[tail1] > L2->data[tail2])
L->data[tail++] = L2->data[tail2++]; // 合并L2
else
L->data[tail++] = L1->data[tail1++]; // 合并L1
}
while (tail2 < L2->length)
L->data[tail++] = L2->data[tail2++]; // 合并剩余的L2
while (tail1 < L1->length)
L->data[tail++] = L1->data[tail1++]; // 合并剩余的L1
// 更新长度
L->length = tail;
}
五、参考资料
鲍鱼科技课件
b站免费王道课后题讲解:
网课全程班: