线性表的顺序表示和实现
前文我们提到过线性表是逻辑结构,只说明了数据元素之间的相互关系,想要使用线性表,我们还需要在计算机上表示出这些数据元素以及元素之间的关系。而对于同一种逻辑结构,可以有多种存储结构来实现它。线性表作为一种基础的数据结构,可以用顺序存储和链式存储两种不同方式来实现,本节主要说明线性表的顺序表示。
1 线性表的顺序表示
1.1 顺序表的定义
线性表的顺序存储又称为顺序表, 它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理地址上也相邻。顺序表的特点是表中元素的逻辑顺序与其物理顺序相同。顺序表是一种存储结构,关注的是如何在计算机中存储数据元素及其关系。
1.2 顺序表的特点
假设线性表的每个元素需占用
l
l
l 个存储单元,并以所占的第一个单元的存储地址作为数据元素的存储位置。则线性表中第
i
+
1
i+1
i+1 个数据元素的存储位置
L
O
C
(
a
i
+
1
)
LOC(a_i+1)
LOC(ai+1) 和第
i
i
i 个数据元素的存储位置
L
O
C
(
a
i
)
LOC(a_i)
LOC(ai) 之间满足下列关系:
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
i
)
+
l
LOC(a_i+1) = LOC(a_i) +l
LOC(ai+1)=LOC(ai)+l
一般来说,线性表的第
i
i
i 个数据元素
a
i
a_i
ai 的存储位置为:
L
O
C
(
a
i
)
=
L
O
C
(
a
1
)
+
(
i
−
1
)
∗
l
LOC(a_i) = LOC(a_1) +(i-1)*l
LOC(ai)=LOC(a1)+(i−1)∗l
其中
L
O
C
(
a
1
)
LOC(a_1)
LOC(a1) 是线性表的第一个数据元素
a
1
a_1
a1 的存储位置,也被称为线性表的起始位置或基地址。
观察上图我们可以发现,顺序表的每一个数据元素的存储位置都和线性表的起始位置相差一个和数据元素在线性表中的位序成正比的常数。由此,只要确定了存储线性表的起始位置,表中任一数据元素都可以随机存取(也称为直接访问),所以线性表的顺序存储结构是一种随机存取的存储结构。更广泛地说,顺序存储一般对应随机存取。
总结
顺序表有以下主要特点:
- 随机存取,读取(或查找)操作时间复杂度为 O ( 1 ) O(1) O(1) 。
- 存储密度高,每个节点只存储数据元素。
- 逻辑上相邻的元素物理上也相邻,因此插入和删除操作需要移动大量元素。
2 顺序表的实现
由于高级程序语言中的数组类型也有随机存取的特性,因此通常用数组来描述数据结构中的顺序存储结构。
下文的默认实现语言为C++,因此一切未注明的语法都以C++为准。
2.1 定义
C++中的一维数组可以是静态分配的,也可以是动态分配的。详情见C++动态数组。
-
静态分配时,由于数组的大小和空间已经事先决定了,一但空间占满,再加入新的数据就会产生溢出,导致程序崩溃。
/*** 静态数组实现顺序表的类型定义 ***/ #define MaxSize 10 typedef int elemType; // 定义新的类型elemType typedef struct{ elemType data[MaxSize]; // ElemType为指定元素类型 int length; } SqList;
-
动态分配就不存在这样的问题,一旦空间占满,就另外开辟一块更大的存储空间,用以替换原来的空间。但要注意的是,动态分配并不是链式存储,它同样属于顺序存储结构,依然是随机存取的方式。但是动态数组需要用户显式地释放内存。
/*** 动态数组实现顺序表的类型定义 ***/ #define InitSize 50 // 线性表存储空间的初始分配量 #define Increment 10 // 线性表存储空间的分配增量 typedef int elemType; // 定义新的类型elemType typedef struct{ elemType *data; // 存储空间基地址 int capacity, length // 当前分配的最大容量和数组长度 } SqList
2.2 主要操作的实现
在顺序存储下,容易实现线性表的某些操作,如随机存取第 i i i 个元素等,因此这里只讨论顺序表较复杂的插入、删除和按值查找操作。
因为静态数组的局限性较大,后文正文内皆以动态数组实现为例展示代码,静态数组实现将放于文末附录。
2.2.1 插入操作
在顺序表 L L L 的第 i ( 1 ≤ i ≤ L . l e n g t h + 1 ) i (1 \le i \le L.\mathrm{length+1}) i(1≤i≤L.length+1) 个位置插入新元素 e e e。若 i i i 的输入不合法,则返回 false,表示插入失败;否则,将第 i i i 个元素及其后的所有元素依次往后移动一个位置,顺序表长度增加1,返回 true 。
/*** 动态数组顺序表实现插入操作 ***/
// 扩容操作
void increment(SqList &L){
elemType *data = new elemType[L.capacity+Increment]; // 重新申请内存
for (int i=0;i<L.capacity;i++){ // 将原来的元素移动到新的顺序表中
data[i] = L.data[i];
}
delete[] L.data; // 释放内存
L.data = data;
L.capacity += Increment;
printf("Finish Increment.\n");
}
// 插入操作
bool listInsert(SqList &L, int i, elemType e){
if (i<1 || i>L.length+1){ // 判断i的范围是否有效
printf("Index is illegal!\n");
return false;
}
if (L.length >= L.capacity){ // 判断存储空间是否还有剩余,
printf("Need Increment.\n");
increment(L); // 不够则扩容
}
for (int j=L.length;j>=i;j--){ // 将位置i及之后的元素向后移动一位
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
return true;
}
时间复杂度分析
对插入操作来说,插入元素只需要一步,而移动元素可能重复很多次,所以基本操作为移动元素。我们只需要关心每一次插入操作发生时,总共移动了多少个元素,而移动元素的个数取决于插入新元素的位置。
- 最优情况:在表尾插入(即 i = n + 1 i = n+1 i=n+1),元素不用后移,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最差情况:
- 静态数组:在表头插入(即 i = 1 i = 1 i=1),现存所有元素(共 n n n 个)都需后移,时间复杂度为 O ( n ) O(n) O(n)。
- 动态数组:在表头插入且空间不够,需先执行扩容操作,移动表内现存的所有元素,共 n n n 次。然后再插入新的元素,将表内的旧元素全部后移一位,共 n n n 次。总共需要 2 n 2n 2n 次移动,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:令
p
i
p_i
pi 为在第
i
i
i 个位置上插入一个元素的概率,假设任意一个位置上发生插入元素的概率是相等的,那么
p
i
=
1
(
n
+
1
)
p_i = \frac{1}{(n+1)}
pi=(n+1)1。在长度为
n
n
n 的线性表中插入一个元素时,所需移动元素的平均次数为
∑ i = 1 n + 1 p i ( n − i + 1 ) = n 2 \sum_{i=1}^{n+1} p_i(n-i+1) = \frac{n}{2} i=1∑n+1pi(n−i+1)=2n
因此,平均情况的时间复杂度为 O ( n ) O(n) O(n)。
2.2.2 删除操作
删除顺序表 L L L 中第 i ( 1 ≤ i ≤ L . l e n g t h + 1 ) i(1 \le i \le L.\mathrm{length+1}) i(1≤i≤L.length+1) 个位置的元素,用引用变量 e e e 返回。若 i i i 的值不合法,则返回 false ;否则,将被删除元素赋给引用变量 e e e ,并将第 i = 1 i=1 i=1 个元素及其后的所有元素依次往前移动一个位置,返回 true 。
/*** 动态数组顺序表实现删除操作 ***/
// 删除操作
bool listDelete(SqList &L, int i, elemType &e){
if (i<1 || i>L.length){ // 判断i的范围是否有效
printf("Index is illegal!\n");
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--;
return true;
}
时间复杂度分析
分析和插入操作类似,删除操作的基本操作也是对元素的移动。
- 最优情况:在表尾删除(即 i = n i = n i=n),元素不用后移,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最差情况:在表头删除(即 i = 1 i = 1 i=1),现存所有元素(共 n n n 个)都需前移,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:令
p
i
p_i
pi 为在第
i
i
i 个位置上删除一个元素的概率,假设任意一个位置上发生插入元素的概率是相等的,那么
p
i
=
1
n
p_i = \frac{1}{n}
pi=n1。在长度为
n
n
n 的线性表中插入一个元素时,所需移动元素的平均次数为
∑ i = 1 n p i ( n − i ) = n − 1 2 \sum_{i=1}^{n} p_i(n-i) = \frac{n-1}{2} i=1∑npi(n−i)=2n−1
因此,平均情况的时间复杂度为 O ( n ) O(n) O(n)。
2.2.3 按值查找操作
在顺序表 L L L 中查找第一个元素值等于 e e e 的元素,并返回其位序。
/*** 动态数组顺序表实现按位查找操作 ***/
// 按值查找操作
int locateElem(SqList L, elemType e){
for (int i=0;i<L.length;i++){
if (L.data[i] == e){
return i+1;
}
}
return 0;
}
时间复杂度分析
查找操作的基本操作是比较元素,因此我们只关注元素的比较次数。
- 最优情况:查找元素就在表头(即 i = 1 i = 1 i=1),只用比较一次,时间复杂度为 O ( 1 ) O(1) O(1)。
- 最差情况:查找元素在表尾(即 i = n i = n i=n)或不存在时,需要比较表内的所有元素,共 n n n 次,时间复杂度为 O ( n ) O(n) O(n)。
- 平均情况:令
p
i
p_i
pi 为查找元素出现在第
i
i
i 个位置上的概率,假设任意一个位置上发生插入元素的概率是相等的,那么
p
i
=
1
n
p_i = \frac{1}{n}
pi=n1。在长度为
n
n
n 的线性表中插入一个元素时,所需移动元素的平均次数为
∑ 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 \times i = \sum_{i=1}^{n}\frac{1}{n}\times i = \frac{1}{n} \frac{n(n+1)}{2} = \frac{n+1}{2} i=1∑npi×i=i=1∑nn1×i=n12n(n+1)=2n+1
因此,平均情况的时间复杂度为 O ( n ) O(n) O(n)。
参考资料:
【C++】细说C++中的数组之“静态”数组
【C++】细说C++中的数组之动态数组
相关章节
第一节 【绪论】数据结构的基本概念
第二节 【绪论】算法和算法评价
第三节 【线性表】线性表概述
第四节 【线性表】线性表的顺序表示和实现
第五节 【线性表】线性表的链式表示和实现
第六节 【线性表】双向链表、循环链表和静态链表
第七节 【栈和队列】栈
第八节 【栈和队列】栈的应用
第九节 【栈和队列】栈和递归
第十节 【栈和队列】队列
附录
静态数组实现顺序表的完整代码
/*
* File: SqList.h
* -------------------------
* Using struct to implement SqList
*/
/********** 使用静态数组实现顺序表 **********/
#ifndef _STATIC_SEQUENTIAL_LIST_h_
#define _STATIC_SEQUENTIAL_LIST_h_
#include <stdio.h>
#include <iostream>
using namespace std;
/*** 静态数组顺序表的定义 ***/
#define MaxSize 10 // 默认静态数组最大容量为10
typedef int elemType; // 定义新的类型elemType
typedef struct {
elemType data[MaxSize];
int length; // 记录静态数组的有效长度
} SqList;
/*** 基本操作的实现 ***/
// 初始化操作
void initList(SqList &L){
L.length = 0; // 将有效长度归零
}
// 销毁操作
void destroyList(SqList &L){}
// 判空操作
bool listEmpty(SqList L){
return not L.length;
}
// 求表长操作
int listLength(SqList L){
return L.length;
}
// 按位查找操作,如果存在返回1,否则返回0
int getElem(SqList L, int i, elemType &e){
if (i<1 || i>L.length) {
printf("Index is illegal!\n");
return 0;
} else {
e = L.data[i-1];
return 1;
}
}
// 按值查找操作,返回第一个符合查找要求的元素的位置,如果不存在则返回0
int locateElem(SqList L, elemType e){
for (int i=0;i<L.length;i++){
if (L.data[i] == e){
return i+1;
}
}
return 0;
}
// 插入操作,在位值i处插入元素e
bool listInsert(SqList &L, int i, elemType e){
if (i<1 || i>L.length+1){ // 判断i的范围是否有效
printf("Index is illegal!\n");
return false;
}
if (L.length >= MaxSize){ // 判断存储空间是否还有剩余
printf("Out of space!\n");
return false;
}
for (int j=L.length;j>=i;j--){ // 将位置i及之后的元素向后移动一位
L.data[j] = L.data[j-1];
}
L.data[i-1] = e;
L.length++;
return true;
}
// 删除操作,删除第i个位置的元素,并用e返回删除元素的值
bool listDelete(SqList &L, int i, elemType &e){
if (i<1 || i>L.length){ // 判断i的范围是否有效
printf("Index is illegal!\n");
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--;
return true;
}
// 输出操作
void listPrint(SqList L){
for (int i=0;i<L.length;i++){
printf("%d ", L.data[i]);
}
printf("\n");
}
#endif // _STATIC_SEQUENTIAL_LIST_h_
动态数组实现顺序表的完整代码
/*
* File: SqList.h
* -------------------------
* Using struct to implement SqList
*/
/********** 使用动态数组实现顺序表 **********/
#ifndef _DYNAMIC_SEQUENTIAL_LIST_h_
#define _DYNAMIC_SEQUENTIAL_LIST_h_
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
/***** 动态数组顺序表的定义 *****/
#define InitSize 10 // 动态数组初始容量为10
#define Increment 10 // 线性表存储空间的分配增量
typedef int elemType; // 定义新的类型elemType
typedef struct {
elemType *data;
int capacity, length; // 动态数组当前最大容量和有效长度
} SqList;
/***** 基本操作的实现 *****/
// 初始化操作
void initList(SqList &L){
L.data = new elemType[InitSize]; // 申请内存
L.length = 0;
L.capacity = InitSize;
}
// 销毁操作
void destroyList(SqList &L){
if (L.data != NULL){
delete[] L.data;
L.length = 0;
L.capacity = 0;
}
}
// 判空操作
bool listEmpty(SqList L){
return not L.length;
}
// 求表长操作
int listLength(SqList L){
return L.length;
}
// 按位查找操作
int getElem(SqList L, int i, elemType &e){
if (i<1 || i>L.length) {
printf("Index is illegal!\n");
return 0;
} else {
e = L.data[i-1];
return 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 increment(SqList &L){
elemType *data = new elemType[L.capacity+Increment]; // 重新申请内存
for (int i=0;i<L.capacity;i++){ // 将原来的元素移动到新的顺序表中
data[i] = L.data[i];
}
delete[] L.data; // 释放内存
L.data = data;
L.capacity += Increment;
printf("Finish Increment.\n");
}
// 插入操作
bool listInsert(SqList &L, int i, elemType e){
if (i<1 || i>L.length+1){ // 判断i的范围是否有效
printf("Index is illegal!\n");
return false;
}
if (L.length >= L.capacity){ // 判断存储空间是否还有剩余,
printf("Need Increment.\n");
increment(L); // 不够则扩容
}
for (int j=L.length;j>=i;j--){ // 将位置i及之后的元素向后移动一位
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){ // 判断i的范围是否有效
printf("Index is illegal!\n");
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--;
return true;
}
// 输出操作
void listPrint(SqList L){
for (int i=0;i<L.length;i++){
printf("%d ", L.data[i]);
}
printf("\n");
}
#endif // _DYNAMIC_SEQUENTIAL_LIST_h_
顺序表检测程序
/*
* File name: SqList.cpp
* -----------------------
* Using struct to implement SqList
*/
#include "SqList.h"
int main(){
SqList L;
initList(L);
int n, i, res;
elemType e;
char helpInfo[] = "*****************************\n" \
"Sequential list check: \n"
"\t1-Insert element\n" \
"\t2-Delete element\n" \
"\t3-Print\n" \
"\t4-Empty check\n" \
"\t5-Get Length\n" \
"\t6-Search by value\n" \
"\t7-Search by location\n" \
"\t8-Initialization\n" \
"\t9-Quit\n" \
"*****************************\n";
while (n!= 9){
printf(helpInfo);
scanf("%d", &n);
switch(n){
case 1:
printf("Please enter the location: ");
scanf("%d", &i);
printf("Please enter the element: ");
scanf("%d", &e);
listInsert(L, i, e);
break;
case 2:
printf("Please enter the location: ");
scanf("%d", &i);
listDelete(L, i, e);
printf("Element in location %d with value %d has been deleted.\n", i, e);
break;
case 3:
printf("List: ");
listPrint(L);
break;
case 4:
if (listEmpty(L)) {
printf("This list is empty!\n");
}else {
printf("This list is not empty!\n");
}
break;
case 5:
printf("Length of list is: %d\n", listLength(L));
break;
case 6:
printf("Please enter the target value: ");
scanf("%d", &e);
res = locateElem(L, e);
if (res) {
printf("The index of target is %d\n", res);
} else {
printf("We didn't find the value.\n");
}
break;
case 7:
printf("Please enter the target index: ");
scanf("%d", &i);
res = getElem(L, i, e);
if (res) {
printf("The value of target is %d\n", e);
}
break;
case 8:
initList(L);
printf("List has been initialized.\n");
break;
}
}
destroyList(L);
return 0;
}