文章目录
第二章 线性表
2.1 线性表的定义和基本操作
2.1.1 线性表的定义
具有相同数据类型的n个数据元素的有限序列
线性表的特点:
-
表中元素的个数是有限的。
-
表中元素都有逻辑上的顺序性,表中元素有其现有次序。
-
表中元素都是数据元素,每个元素都是单个的元素。
-
表中元素的数据类型都相同,也就是每个元素占有相同大小的存储空间。
-
表中元素具有抽象性,仅讨论元素之间的逻辑关系,不考虑元素表示什么内容。
线性表是一种逻辑结构,表示元素之间的一对一的相邻关系,顺序表和链表是指存储结构,两者属于不同的层面的概念。
2.1.2 线性表的进本操作
i表示位置,e表示元素
结构 | 含义 |
---|---|
InitList(&L) | 初始化表,构造一个空的线性表 |
Length(L) | 求表长,返回线性表的长度,L中数据元素的个数 |
LocateElem(L,e) | 按值查找操作 |
GetElem(L,i) | 按位查找操作 |
ListInsert(&L,i,e) | 插入操作 |
ListDelete(&L,i,&e) | 删除操作,并用e返回删除的元素的值 |
PrintList(L) | 输出操作,按照先后顺序输出线性表L的所有值 |
Empty(L) | 判空操作,L为空的话,返回true,否则返回false |
DestoryList(&L) | 销毁线性表,并释放L所占用的内存空间 |
- 基本操作的实现取决于采用哪种存储结构,存储结构不同,算法的实现也不同
2.2 线性表的线性表示
2.2.1 顺序表的定义
线性表的顺序存储又称为顺序表,采用的是一组地址连续的存储单元一次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。
顺序表的特点是表中元素的逻辑顺序与物理顺序相同。
线性表中元素的位序都可以随机存取的,所以线性表的顺序存储结构是一种随机的存储结构。
线性表中的元素的位序是从1开始的,而数组中元素的下标是从0开始的。
一堆数组可以是静态分配的,也可以是动态分配的
在静态分配时,由于数组的大小和空间已经固定,一旦空间占满,再加入新的数据就会产生数据溢出,从而导致程序崩溃。
在动态分配的时候,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就会另外开辟一块更大的存储空间,用来替换原来的存储空间。
动态分配不是链式分配,同样属于顺序存储结构,物理结构没有变化,依然是随机存取方式,知识分配的空间大小可以在运行时间时动态决定。
顺序表最主要的特点就是随机访问,即通过首地址和元素序号可以在O(1)内找到指定的元素。顺序表的存储密度高,每个结点只存储数据元素。
顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量的元素。
2.2.2 顺序表上基本操作的实现
(1)插入操作
在顺序表L的第i个位置插入新元素e,若i的输入不合法,则返回false,表示插入失败,否则,将第i个位置及其之后的所有元素往后移动一个位置,腾出一个位置用来插入新的元素e,顺序表的长度加1,插入成功,返回true。
bool Listinsert(SqList &L,int i,ElemType e){
if(i < 1 || i > L.legnth + 1){ //判断范围
return false;
}
if(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元素放入
L.length++; //长度加1
return true;
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
(2)删除操作
bool ListDelete(SqList &L, int i , Elemtype &e){
if(i < 1 || i > L.legnth){ //判断范围
return false;
}
e = L.data[i - 1]; //将被删除的元素赋值给e
for(int j = i; j < L.length ; j++){
L.data[j - 1] = L.data[j];
}
return true;
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
(3)按值查找(顺序查找)
int LocateElem(SqList L,ElemType e){
for(int i = 0; L.length; i++){
if(L.data[i] == e){
return i + 1;
}
return 0;
}
}
最好时间复杂度:O(1)
最坏时间复杂度:O(n)
平均时间复杂度:O(n)
2.2.3 综合应用题目
//从顺序表中删除具有最小值的元素并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空,则显示出错信息并退出运行
//算法思想:搜索整个顺序表,查找最小值并记录该值的位置,将最后一个元素填充到删除的元素之上。
bool Del_Min(sqList &L, Elemtype &value) {
if (L.length == 0)
{
return false; //如果数组的长度为0的话则直接返回false
}
value = L.data[0]; //令第一个元素为最小值
int pos = 0; //用来记录位置
for (int i = 1; i < L.length; i++)
{
if (L.data[i] < value)
{
value = L.data[i];
pos = i;
}
}
L.data[pos] = L.data[L.length - 1];
L.length--;
return true;
}
//设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)
//算法思想:设置一个中间变量,将顺序表的前几个元素和后几个元素进行交换。
void Reverse(SqList &L) {
Elemtype = temp; //辅助变量
for (int i = 0; i < L.length / 2; i++)
{
temp = L.data[i];
L.data[i] = L.data[L.length - i - 1];
L.data[L.length - 1 - i] = temp;
}
}
//对长度为n的顺序表L,编写一个时间复杂度为O(n),空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据。
//算法思想:查找对应的元素并删除,后面的元素往前排。
void del_x(SqList &L , Elemtype X) {
int k = 0;
for (int i = 0; i < L.length; i++)
{
if (L.data[i] != x)
{
L.data[k] = L.data[i];
K++
}
}
L.length++;
}
//从<有序>顺序表中删除其值在给定S与t之间的所有元素(s<t),若s或t不合理或者顺序表为空,则显示出错信息
//算法思想:首先判断是否正确,然后再进行循环顺序表,符合条件的筛选出来重新对顺序表赋值
bool Del_2(SqList &L , Elemtype s, Elemtype t) {
int k = 0;
int step = 0;
if (s < 0 || L.length == 0 || s >= t)
{
return false;
}
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == t)
{
k = i;
}
}
for (int i = 0; i < L.length; i++)
{
if (L.data[i] == s)
{
step = k - i;
}
L.data[i] = L.data[i + step];
}
L.length -= step;
}
//从有序顺序表中删除其值在给定S与t之间的所有元素(s<t),若s或t不合理或者顺序表为空,则显示出错信息
//算法思想:首先判断是否正确,然后再进行循环顺序表,符合条件的筛选出来重新对顺序表赋值
bool Del_2(SqList &L , Elemtype s, Elemtype t) {
int k = 0;
if (s < 0 || L.length == 0 || s >= t)
{
return false;
}
for (int i = 0; i < L.length; i++)
{
if (L.data[i] < s || L.data[i] > t)
{
L.data[k] = L.data[i];
k++;
}
}
L.length = k;
}
//从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同,要求时间复杂度为O(n)
//算法思想:
bool Delete_Same(SqList &L) {
if (L.length == 0)
{
return false;
}
int j = 1;
for (int i = 0; i < L.length; j++) {
if (L.data[i] != L.data[j])
{
L.data[++i] = L.data[j];
}
}
L.length = i + 1;
return true;
}
//将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。
bool Merge(SqList A, SqList B, SqList &C) {
if (A.length + B.length > C.maxSize) //大于顺序表的最大长度
{
return false;
}
int i = 0, j = 0, k = 0; //分别对应顺序表A,B,C的下标
while (i < A.length && j < B.length) {
if (A.data[i] <= B.data[j]) {
C.data[K++] = A.data[i++];
}
else {
C.data[K++] = B.data[j++];
}
}
while (i < A.length) {
C.data[K++] = A.data[i++];
}
while (j < B.length) {
C.data[K++] = B.data[j++];
}
C.length = k;
return true;
}
//已知在一堆数组A[m + n]中依次存放两个线性表(a1,......am)和 (b1,........bm),编写一个函数
//将数组中两个顺序表的位置互换,即将(n1,.....bn)放在(a1,.......am)的前面。
//算法思想:首先将数组的全部元素逆置,再对前m个数组和后n个数组进行逆置。
void Reverse(SqList &L, int m, int n, int arraysize) {
if (m + n != L.length || m < 0 || n < 0) {
return;
}
ElemType temp; //用来交换顺序表的连个元素的临时变量
for (int i = 0; i < L.length / 2; i++) {
temp = L.data[i];
L.data[i] = L.data[L.length - i - 1]; //交换顺序表前面的部分
L.data[L.length - i - 1] = temp;
}
for (int i = 0; i < m / 2; i++) {
temp = L.data[i];
L.data[i] = L.data[m - i - 1]; //交换顺序表前面的部分
L.data[m - i - 1] = temp;
}
for (int i = 0; i < n / 2; i++) {
temp = L.data[i];
L.data[i] = L.data[n - i - 1]; //交换顺序表后面的部分
L.data[n - i - 1] = temp;
}
}
2.3 线性表的链式表示
2.3.1 单链表的定义
单链表是非随机存取的,不能直接找到表中某个特定的结点。查找某个特定的结点的时候,需要从表头开始进行遍历。
通常用头指针来标识一个单链表,头指针为NULL时表示一个空表,单链表的第一个结点之前附加一个结点称为头结点。
头节点的优点:
①第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和表的其他位置上的操作一致,无需进行特殊处理。
②无论链表是否为空,头指针都指向头结点的非空指针。
2.3.2 单链表的基本操作的实现
1.头插法建立单链表
算法如下:
LinkList List(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;
}
2.尾插法建立单链表
算法如下:
LinkList List(LinkList &L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L; //r为表尾指针
scanf("%d", &x);
while (x != 9999) {
s = (LNode *)malloc(sizeof(LNode));
s -> data = s;
r -> next = s;
r = s; //指向新的表尾结点
scanf("%d", &x);
}
r -> next = NULL;
return L;
}
3.按序号查找结点值
//查找i结点的值
LNode *GetElem(LinkList L,int i){
int j = 1; //初始值为1
LNode *p = L -> next; //第一个结点指针赋值给p
if(i == 0){
return L; //若i等于0,则返回头结点
}
if(i < 1){
return NULL; //若i值无效,则返回NULL
}
while(p && j < i){ //从第一个结点开始找,查找第i个结点
p = p -> next;
j++;
}
return p;
}
4.按值查找表结点
//按值查找表结点的算法:
LNode *LocateElem(LinkList L,ElemType e){
LNode *p = L -> next;
while(p != NULL && p -> data != e){ //从第一个结点开始查找data域或e的结点。
p = p -> next;
}
return p; //找到后返回该结点指针,否则返回NULL
}
5.插入结点操作
s -> next = p -> next;
p -> next = s;
6.删除结点操作
//删除结点
p = GetElem(L, i - 1); //找到被删除结点的前驱结点
q = p -> next;
p -> next = q -> next; //前驱结点指向被删除结点的下一个结点
free(q);
7.求表长操作
链表长度是不包括头结点的,求表长的思路就是循环整个链表,设置计数器,+1
2.3.3 双链表
双链表有两个指针,分别是prior和next,分别指向该结点的前驱结点和后继结点。
1.双链表的插入操作
//双链表的插入操作,在p结点后插入s结点
s -> next = p -> next;
p -> next -> prior = s;
p -> next = s;
s -> prior =p;
2.双链表的删除操作
//双链表的删除操作,删除p结点后的一个结点q
p -> next = q -> next;
q -> next -> prior = p;
free(q);
2.3.4 循环链表
1.循环单链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而是改为指向头结点,从而整个链表形成一个环。
循环单链表的判空条件是判断它的尾指针是否等于头指针。
2.循环双链表
头节点的指针指向表尾结点。
当循环双链表为空的时,其头结点的prior和next都等于L
2.3.5 静态链表
静态链表借助数组来描述线性的存储结构,结点也有数据域data和指针域next,与前面所讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称为游标。和顺序表一样,静态链表也是要预先分配一块连续的内存空间。
静态链表以next == -1作为结束的标志。
2.3.6 顺序表和链表的比较
1.存取(读取)方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取。
2.逻辑结构与物理结构
采用顺序存取时,逻辑上相邻的元素,物理上也相邻。
链式存储时,逻辑上相邻的元素,物理上不一定相邻。,
3.查找,插入和删除操作
对于按值查找,顺序表无序时,两者的时间复杂度都是O(n),顺序表有序时,可以采用折半查找,此时的时间复杂度是O(log2n)
按序号查找的话顺序表的时间复杂度是O(1),而链表的时间复杂度是O(n),顺序表的插入,删除操作平均需要移动半个表长的元素。链表只需要查找指针即可。
4.空间分配
顺序表在静态分配的情况下,一旦空间存满,便不能进行扩充,否则就会出现溢出的现象。动态存储的话可以扩充但是操作效率比较慢。
链式存储比较快。