线性表
1.1 线性表的定义和特点
线性表(List):零个或多个具有相同特性的数据元素的有限序列
又或者定义为:由n(n>=0)个数据元素(结点)a1,a2,…,an组成的有限序列
关键词:有限 零个或多个 相同特性 数据元素
上图中标注的各个部分都是线性表不可缺少的重要元素。并且,每个数据元素都最多有一个直接前驱和直接后继,这也是判断某数据结构是否为线性表的重要条件。
线性表分为两种:顺序表和链表,而链表有单链表,循环链表,双链表多种
顺序表采用的是顺序存储结构和随机存取,链表采用的是链式存储结构和顺序存取,关于随机存取和顺序存取,后面会说到。
1.2 案例引入
1.2.1 一元多项式的运算
对于简单的一元多项式运算(即最高次数较低的),我们可以用数组存储各项的指数与系数,注意:此时的指数是将其作为下标存在数组内,因此,数组内会含有零项,因此,如果指数过大(比如几千几万,而中间的指数也并不是从1一直连续加到这个很大的数,数组中就会存在很多零项(系数为0)),那么就会造成不必要的内存浪费。
1.2.2 稀疏多项式的运算
为了解决1.2.1中可能出现的内存浪费的情况,我们将指数单独存放,并且只存放非零项(见下图)通过这种方式创建出来的线性表即为顺序表,其物理结构为顺序存储结构在此基础上,我们就可以很方便的表示出稀疏多项式,并通过创建新的数组来存放两个多项式运算后的结果。
但是问题又来了,我们在运算之前,该怎么确定数组的大小,才能避免不必要的内存空间的浪费呢?这就需要一种链式存储结构来解决这种问题。从下图中也可以清楚地看到链式存储结构对于存储空间的分配更加灵活(有多少就分配多少)。
有了链表,我们就可以在不浪费存储空间的前提下更加方便地表示出多项式的相加了。
1.2.3 图书信息管理系统
图为“图书管理系统”的逻辑结构,图书表可以抽象为一个线性表(想一想线性表的特点),表中的每条记录都是线性表中的数据元素。
图为“图书管理系统”的顺序表和链表的两种表示
以下是图书表的顺序存储结构的类型定义
#define MAXSIZE 10000 //图书表可能达到的最大长度
//图书信息定义
typedef struct
{
char no[20];//图书ISBN
char name[50];//图书名字
float price;//图书价格
}Book;
typedef struct
{
Book *elem;//存储空间基地址
int length;//图书表中当前的图书个数
}SqList;
小tips: SqList的全称为sequence list,即为顺序表
1.2.4 总结
通过以上几个案例的分析,可以做出相应总结:
- 线性表中数据元素的类型可以为简单类型,也可以为复杂类型。
- 许多实际应用问题所涉及的基本操作有很大的相似性,不应为每个具体应用单独编写一个程序。
- 从具体应用中抽象出共性的逻辑结构和基本操作(抽象数据类型),然后实现其存储结构和基本操作。
1.2.5 案例补充说明
补充一:类型说明
下面用到了typedef定义数据类型,如果你不太熟悉,可以去CSDN里直接搜索typedef的用法就行了,网上总结的很全面。
typedef char ElemType
typedef struct
{
ElemType data[];
int length;
}SqList;
typedef struct
{
float p;
int e;
}Polynomial;
typedef struct
{
Polynomial *elem;
int length;
}SqList;
补充二:数组定义
首先是数组的静态分配
typedef struct
{
ElemType data[MAXSIZE];
int length;
}SqList;
然后就是数组的动态分配
typedef struct
{
ElemType *data;
int length;
}SqList;
SqList L;
L.data = (ElemType*)malloc(sizeof(ElemType)*MAXSIZE);
如果你不太懂数组的动态分配中malloc是什么东西,没关系,往下看,我们来复习一下。
补充三:C语言的动态内存分配
补充四:C++的动态内存分配
从上面可以看出,C++的动态内存分配的代码还是很简洁的。
1.4线性表的类型定义(抽象数据类型)
1.5 线性表的顺序表示和实现
1.线性表的顺序表示又称为顺序存储结构或顺序映像
下面举一个简单的小栗子
再来看看顺序表中存储位置的计算
2.顺序表的实现,即基本操作
先来看几个简单操作
//线性表L的初始化
Status InitList_Sq(SqList &L)
{
L.elem = new ElemType[MAXSIZE];
if(!L.elem)
exit(OVERFLOW);//存储分配失败
L.length=0;
return OK;
}
//销毁线性表L
void DestoryList(SqList &L)
{
if(L.elem)
delete L.elem;
}
//清空线性表L
void ClearList(SqList &L)
{
L.length=0;
}
再来看看使用频率比较高的算法
a.顺序表的查找
相应的算法分析:
b.顺序表的插入
相应的算法分析:
c.顺序表的删除
相应的算法分析:
1.6 线性表的链式实现和表示
1.6.1 链表的相关介绍
先来看一下链表的相关说明:
- 用一组物理位置任意的存储单元来存放线性表的数据元素
- 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的
- 链表中元素的逻辑次序和物理次序不一定相同
与链式存储的相关术语:
头指针是链表的必要元素,但头结点不是
看到这你可能会问:既然头结点不是必要的,在链表中设置头结点有什么好处呢?
那么,头结点的数据域内装的是什么呢?
搞清楚以上的一些术语后,让我们来看看链表的存储结构示意图,一般有以下两种形式:
链表有很多种形式,这里我们重点分析其中的三种:单链表,循环链表,双链表
1.6.2 单链表
这里我们分析带头结点的单链表
单链表的存储结构如下:
单链表的实现(基本操作)
1.单链表的初始化
2.判断链表是否为空
3.单链表的销毁
4.清空单链表
5.求单链表的表长
6.单链表取值
7.删除元素
8.查找之按值查找(获取元素的位置序号)
9.查找之按地址查找(返回元素的地址)
1.6.3循环链表
先来看一下循环链表的定义:
从下图对比的时间复杂度可以看出,在循环链表中添加尾指针比添加头指针更加方便
带尾指针的链表的合并
1.6.4双向链表
双向链表的插入
双向链表的删除
1.6.5三种链表的时间效率的比较
1.7 顺序表和链表的比较
嗯?存储密度是什么玩意儿,别急,往下看:
下面,我们从空间和时间分别对顺序表和链表做出比较