3.1 开场白
各位同学,大家好。
今天我们要开始学习数据结构中最常用和最简单的一种结构,在介绍它之前先讲个例子。
我经常下午去幼儿园接送儿子,每次都能在门口看到老师带着小朋友们, 一个拉着另一个的衣服,依次从教室出来 。而且我发现很有规律的是,每次他们的次序都是一样。比如我儿子排在第 5 个,每次他都是在第 5 个,前面同样是那个小女孩,后面一直是那个小男孩。这点让我很奇怪,为什么一定要这样?
有一天我就问老师原因。她告诉我,为了保障小朋友的安全,避免漏掉小朋友,所以给他们安排了出门的次序,率先规定好了,谁在谁的前面,谁在谁的后面。这样养成习惯后,如果有谁没有到位,他前面和后面的小朋友就会主动报告老师 ,某人不在 。 即使以后如果要外出到公园或博物馆等情况下,老师也可以很快地清点人数,万一有人走丢,也能在最快时间知道,及时去寻找。
我一想,还真是这样。小朋友们始终按照次序排队做事,出意外的情况就可能会少很多 。毕竟,遵守秩序是文明的标志,应该从娃娃抓起。而且,真要有人丢失,小孩子反而是最认真负责的监督员 。
再看看门外的这帮家长们,都挤在大门口,哪个分得清他们谁是谁呀。与小孩子们的井然有序形成了鲜明的对比。哎,有时大人的所作所为,其实还不如孩子。
这种排好队的组织方式,其实就是今天我们要介绍的数据结构:线性表。
3.2 线性表的定义
线性表,从名字上你就能感觉到,是具有像线一样的性质的表。在广场上,有很多人分散在各处,当中有些是小朋友,可也有很多大人,甚至还有不少宠物,这些小朋友的数据对于整个广场人群来说,不能算是线性表的结构。但像刚才提到的那样,一个班级的小朋友,一个跟着一个排着队,有一个打头,有一个收尾,当中的小朋友每一个都知道他前面一个是谁,他后面一个是谁,这样如同有一根线把他们串联起来了。就可以称之为线性表 。
线性表(List):零个或多个数据元素的有限序列。
这里需要强调几个关键的地方 。
首先它是一个序列 。 也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其它每个元素都有且只有一个前驱和后继 。 如果一个小朋友去拉两个小朋友后面的衣服,那就不可以排成一队了;同样,如果一个小朋友后面的衣服,被两个甚至多个小朋友拉址,这其实是在打架,而不是有序排队。
然后,线性表强调是有限的,小朋友班级人数是有限的,元素个数当然也是有限的。事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念申。
如果用数学语言来进行定义。可如下 :
若将线性表记为 (a1 ,…, ai-1 , ai, ai+1,…, an) ,则表中 ai-1 领先于ai, ai领先于ai+1,称 ai-1是ai的直接前驱元素, ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当 i=2,3 ,… , n 时, ai有且仅有一个直接前驱。如图 3-2-1 所示。
所以线性表元素的个数n(n>=0) 定义为线性表的长度,当n=0时,称为空表。
在非空表中的每个数据元素都有一个确定的位置,如 al 是第一个数据元素, an 是最后一个数据元素,ai是第 i 个数据元素,称i为数据元素 ai在线性表中的位序。
我现在说一些数据集,大家来判断一下是否是线性表。
先来一个大家最感兴趣的, 一年里的星座列表 ,是不是线性表呢?如图 3-2-2 所示。
ps:我大天蝎怎么没了!?(补上)
当然是,星座通常都是用白羊座打头,双鱼座收尾,当中的星座都有前驱和后继,而且一共也只有十二个,所以它完全符合线性表的定义。
公司的组织架构,总经理管理几个总监,每个总监管理几个经理,每个经理都有各自的下属和员工。这样的组织架构是不是线性关系呢?
不是,为什么不是呢?哦,因为每一个元素,都有不只一个后继,所以它不是线性表。那种让一个总经理只管一个总监, 一个总监只管一个经理, 一个经理只管一个员工的公司 , 俗称皮包公司,岗位设置等于就是在忽悠外人。
班级同学之间的友谊关系,是不是线性关系?哈哈,不是,因为每个人都可以和多个同学建立友谊,不满足线性的定义。嗯?有人说爱情关系就是了。胡扯,难道每个人都要有一个爱的人和一个被爱的人,而且他们还都不可以重复爱同一个人这样的情况出现,最终形成一个班级情感人物串联?这怎么可能,也许网络小说里可能出现,但现实中是不可能的 。
班级同学的点名册,是不是线性表?是,这和刚才的友谊关系是完全不同了,因为它是有限序列,也满足类型相同的特点。这个点名册 (如表 3-2-1 所示)中,每一个元素除学生的学号外,还可以有同学的姓名、性别、出生年月什么的,这其实就是我们之前讲的数据项。在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
一群同学排队买演唱会门票,每人限购一张, 此时排队的人群是不是线性表?是,对的。此时来了三个同学要插当中一个同学A的队,说同学A之前拿着的三个书包就是用来占位的,书包也算是在排队。如果你是后面早已来排队的同学,你们愿不愿意?肯定不愿意,书包怎么能算排队的人呢,如果这也算,我浑身上下的衣服裤子都在排队了。于是不让这三个人进来 。
这里用线性表的定义来说,是什么理由?嗯,因为要相同类型的数据,书包根本不算是人,当然排队无效,三个人想不劳而获,自然遭到大家的谴责。看来大家的线性表学得都不错。
3.3 线性表的抽象数据类型
前面我们已经给了线性表的定义,现在我们来分析一下 , 线性表应该有一些什么样的操作呢?
还是回到刚才幼儿园小朋友的例子,老师为了让小朋友有秩序地出入,所以就考虑给他们排一个队,并且是长期使用的顺序,这个考虑和安排的过程其实就是一个线性表的创建和初始化过程。
一开始没经验,把小朋友排好队后,发现有的高有的矮,队伍很难看,于是就让小朋友解散重新排--这是一个线性表重置为空表的操作。
排好了队,我们随时可以叫出队伍某一位置的小朋友名字及他的具体情况。比如有家长问,队伍里第五个孩子,怎么这么调皮,他叫什么名字呀,老师可以很快告诉这位家长,这就是封清扬的儿子,叫封云卞 。 我在旁就非常扭捏,看来是我给儿子的名字没取好,儿子让班级"风云突变'了。这种可以根据位序得到数据元素也是一种很重要的线性表操作.
还有什么呢,有时我们想知道,某个小朋友,比如麦兜是否是班里的小朋友,老师会告诉我说,不是,麦兜在春田花花幼儿园里,不在我们幼儿园。这种查找某个元素是否存在的操作很常用。
而后有家长问老师,班里现在到底有多少个小朋友呀,这种获得线性表长度的问题也很普遍。
显然,对于一个幼儿园来说,加入一个新的小朋友到队列中,或因某个小朋友生病,需要移除某个位置,都是很正常的情况。对于一个线性表来说,插入数据和删除数据都是必须的操作。
所以,线性表的抽象数据类型定义如下 :
ADT 线性表(List)
Data
线性表的数据对象集合为 {a1,a2, ...... ,an } .每个元素的类型均为 DataType。其中,除了第一个元素外 。每一个元素有且只有一个直接前驱元素,除了最后一个元素an外 ,每一个元素,有且只有一个直接后继元索。数据元素之间的关系是一对一的关系。
Operation
InitList ( *L) : 初始化操作,建立一个空的线性表L。
ListEmpty ( L) : 若线性表为空,返回true . 否则返回 false。
ClearList (*L) : 将线性表清空。
GetElem (L, i , *e ) :将线性表L 中的第 i 个元素的值返回给 e。
LocateElem ( L, e) :在线性表 L 中查找与给定值 e 相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则.返回 0表示失败。
Listlnsert (*L,i , e) : 在线性表 L 中的第 i个位置插入新元素e。
ListDelete (*L,i,*e) : 删除线性L中第i个位置元素,并用e返回其值。
ListLength (L) : 返回线性表 L 的元素个数。
endADT
对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现.
比如,要实现两个线性表集合 A 和 B 的并集操作。即要使得集合 A=A U B 。 说白了,就是把存在集合 B 中但并不存在 A 中的数据元素插入到 A 中即可。
仔细分析一下这个操作,发现我们只要循环集合 B 中的每个元素,判断当前元素是否存在A中,若不存在,则插入到 A 中即可。 思路应该是很容易想到的 。
我们假设La表示集合 A ,Lb表示集合B,则实现的代码如下:
这里,我们对于union 操作,用到了前面线性表基本操作 ListLength、GetElem、LocateElem、Listlnsert 等,可见,对于复杂的个性化的操作,其实就是把基本操作组合起来实现的。
Java代码实现:
static void union(List a,List b){
System.out.println("合并前A:"+a.toString());
System.out.println("合并前B:"+b.toString());
int a_len,b_len;
String elem;/*声明与a和b相同的数据元素*/
a_len = a.size();/*求a,b的线性表长度*/
b_len = b.size();
for(int i = 0; i < b_len; i++){
elem = b.get(i);/*取b中的第i个元素给elem*/
if(!a.contains(elem)){
a.add(elem);
}
}
System.out.println("合并后A:"+a.toString());
}
3.4 线性表的顺序存储结构
3.4.1 顺序存储定义
说这么多的线性表,我们来看看线性表的两种物理结构的第一种--顺序存储结构。
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表(a1,a2,......,an) 的顺序存储示意图如下:
我们在第一课时已经讲过顺序存储结构。今天我再举一个例子。
记得大学时,我们同宿舍有一个同学,人特别老实、热心,我们时常会让他帮我们去图书馆占座,他总是答应,你想想,我们一个宿舍连他共有九个人,这其实明摆着是欺负人的事。他每次一吃完早饭就冲去图书馆 ,挑一个好地儿,把他书包里的书,一本一本地按座位放好,若书包里的书不够,他会把他的饭盒、水杯等能用上的物品都用上,长长一排,九个座硬是被他占了,后来有一次因占座的事弄得差点都要打架。
3.4.2 顺序存储方式
线性表的顺序存储结构,说白了,和刚才的例子一样,就是在内存中找了块地儿,通过占位的形式,把一定内存空间给占了, 然后把相同数据类型的数据元素依次存放在这块空地中。既然线性表的每个数据元素的类型都相同,所以可以用C语言、Java语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为 0 的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。
我那同学占座时,如果图书馆里空座很多,他当然不必一定要选择第一排第一个位子 , 而是可以选择风水不错、美女较多的地儿。找到后,放一个书包在第一个位置,就表示从这开始,这地方暂时归我了 。为了建立一个线性表,要在内存中找一块地,于是这块地的第一个位置就非常关键,它是存储空间的起始位置。
接着,因为我们一共九个人,所以他需要占九个座。 线性表中,我们估算这个线性表的最大存储容量,建立一个数组,数组的长度就是这个最大存储容量 。
可现实中,我们宿舍总有那么几个不是很好学的人,为了游戏,为了恋爱,就不去图书馆自习了。假设我们九个人,去了六个,真正被使用的座位也就只是六个,另三个是空的。同样的,我们已经有了起始的位置,也有了最大的容量, 于是我们可以在里面增加数据了。随着数据的插入,我们线性表的长度开始变大,不过线性表的当前长度不能超过存储容量,即数组的长度。 想想也是,如果我们有十个人,只占了九个座,自然是坐不下的。
来看线性表的顺序存储的结构代码。
这里,我们就发现描述顺序存储结构需要三个属性:
• 存储空间的起始位置:数组 data,它的存储位置就是存储空间的存储位置。
• 线性表的最大存储容量:数组长度 MaxSize 。
• 线性表的当前长度:Length。
3.4.3 数据长度与线性表长度区别
注意哦,这里有两个概念"数组的长度"和"续性表的长度"需要区分一下。
数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般是不变的。有个别同学可能会问,数组的大小一定不可以变吗?我怎么看到有书中谈到可以动态分配的一维数组。是的,一般高级语言 ,比如C、VB、 C++都可以用编程手段实现动态分配数组,不过这会带来性能上的损耗。
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。
3.4.4地址计算方法
由于我们数数都是从1开始数的,线性表的定义也同样如此,起始也是1,可C、Java语言中的数组却是从0开始第一个下标的,于是线性表的第 i 个元素是要存储在数组下标为 i-1 的位置,即数据元素的序号和存放它的数组下标之间存在对应关系(如图3-4-3所示)。
用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。
其实,内存中的地址,就和图书馆或电影院里的座位一样,都是有编号的。存储器中的每个存储单元都有自己的编号,这个编号称为地址。当我们占座后,占座的第一个位置确定后,后面的位置都是可以计算的。试想一下,我是班级成绩第五名,我后面的10名同学成绩名次是多少呢?当然是6,7 ,…、 15 ,因为 5+1,5+2,…, 5+10。由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是c个存储单元,那么线性表中第 i+l 个数据元素的存储位置和第i个数据元素的存储位置满足下列关系 ( LOC 表示获得存储位置的函数)。
LOC(a(i+1)) = LOC(ai )+ c
所以对于第 i 个数据元素 ai,的存储位置可以由 a1 推算得出:
LOC(ai)= LOC(a1) + (i-l)*c
从图3-4-4来理解:
通过这个公式,你可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间 。 那么我们对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用我们算法中学到的时间复杂度的概念来说,它的存取时间性能为 O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。
3.5 顺序存储结构的插入与删除
3.5.1 获得元素操作
对于线性表的顺序存储结构来说,如果我们要实现 GetElem 操作,即将线性表 L中的第 i 个位置元素值返回,其实是非常简单的。就程序而言,只要 i 的数值在数组下标范围内,就是把数组第i-l下标的值返回即可。 来看C代码 :
#define OK 1;
#define ERROR 0;
#define TRUE 1;
#define FALSE 0;
typedef int Status;
/*status是函数的类型,其值是函数结果状态代码,如OK等*/
/*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/
/*操作结果:用e返回L中的第i个数据元素的值*/
Status GetElem(SqList L, int i , ElemType *e)
{
if(L.length==0 || i < 1 || i > L.length)
return ERROR;
*e = L.data[i-1];
return OK;
}
注意这里返回值类型Status是一个整型,返回OK代表1 ,ERROR代表0。之后代码中出现就不再详述。
Java代码:
static int getElem(int[] array,int i, int e){
int elem;
if(array.length==0 || i < 1 || i > array.length)
return 0;
elem = array[i-1];
return elem;
}
3.5.2 插入操作
刚才我们也谈到,这里的时间复杂度为O(1) 。我们现在来考虑,如果我们要实现ListInsert(*L,i,e) ,即在线性表 L 中的第 i 个位置插入新元素 e ,应该如何操作?
举个例子,本来我们在春运时去买火车票,大家都排队排的好好的。这时来了一个美女,对着队伍中排在第三位的你说,"大哥,求求你帮帮忙,我家母亲有病,我得急着回去看她,这队伍这么长,你可否让我排在你的前面? "你心一软,就同意了 。这时,你必须得退后一步,否则她是没法进到队伍来的。这可不得了,后面的人像蠕虫一样,全部都得退一步。骂起四声。但后面的人也不清楚这加塞是怎么回事,没什么办法。
这个例子其实已经说明了线性表的顺序存储结构,在插入数据时的实现过程(如图 3-5-1 所示)。
插入算法的思路;
• 如果插入位置不合理,抛出异常;
• 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
• 从最后一个元素开始向前遍历到第 i 个位置,分别将它们都向后移动一个位置;
• 将要插入元素填入位置 i 处;
• 表长加 1 。
C实现代码如下:
应该说这代码不难理解。如果是以前学习其他语言的同学,可以考虑把它转换成你熟悉的语言再实现一遍,只要思路相同就可以了 。
Java实现如下:
static final int MAXSIZE = 10;
static final int ERROR = 0;
static final int OK = 1;
int[] L = new int[MAXSIZE];
static int ListInsert(int[] L, int i, int e){
int k;
if(L.length == MAXSIZE)/*顺序线性表已经满*/
return ERROR;
if(i < 1 || i > L.length)/*当i不在范围内时*/
return ERROR;
if(i < L.length){/*若插入数据位置不在表尾*/
for(k = L.length; k >= i-1; k--)/*将要插入位置后面的数据元素往后移动一位*/
L[k+1] = L[k];
}
L[i-1] = e;/*将新元素插入*/
return OK;
}
3.5.3 删除操作
接着刚才的例子。此时后面排队的人群意见都很大,都说怎么可以这样,不管什么原因,插队就是不行,有本事,找火车站开后门去。就在这时,远处跑来一胖子,对着这美女喊,可找到你了,你这骗子,还我钱。只见这女子二话不说,突然就冲出了队伍,胖子迫在其后,消失在人群中。哦,原来她是倒卖火车票的黄牛,刚才还装可怜。于是排队的人群,又像蠕虫一样,均向前移动了一步,骂声渐息,队伍又恢复了平静。
这就是线性表的顺序存储结构删除元素的过程(如图 3-5-2 所示)。
删除算法的思路:
• 如果删除位置不合理,抛出异常;
• 取出删除元素;
• 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
• 表长减 1 。
C实现代码如下:
/*初始条件:顺序线性表L已存在 , l<=i<=ListLength(L)*/
/*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
Status ListDelete (SqList *L,int i, ElemType *e)
{
int k;
if (L->length==0) /*线性表为空*/
return ERROR;
if(i < 1 || i > L->length)/*删除位置不正确*/
return ERROR;
*e = L->data[i-1];
if(i < L->length)/*如果删除不是最后元素*/
{
for(k=i; k < L->length; k++)/*将删除位置后继元素前移*/
L->data[k-1] = L->data[k];
}
L->length--;
return OK;
}
Java实现代码如下:
static final int MAXSIZE = 10;
static final int ERROR = 0;
static final int OK = 1;
int[] L = new int[MAXSIZE];
static int ListDelete(int[] L, int i, int e){
int k;
if(L.length >= 0)/*线性表为空*/
return ERROR;
if(i < 1 || i > L.length)/*删除位置不正确*/
return ERROR;
if(i < L.length){/*如果删除不是最后元素*/
for(k = i; k <= L.length; k++)/*将删除位置后继元素前移*/
L[k-1] = L[k];
}
return OK;
}
现在我们来分析一下,插入和删除的时间复杂度。
先来看最好的情况,如果元素要插入到最后一个位置,或者删除最后一个元素,此时时间复杂度为O(1) ,因为不需要移动元素的,就如同来了一个新人要正常排队,当然是排在最后,如果此时他又不想排了,那么他一个人离开就好了 ,不影响任何人。
最坏的情况呢,如果元素要插入到第一个位置或者删除第一个元素,此时时间复杂度是多少呢?那就意味着要移动所有的元素向后或者向前,所以这个时间复杂庭为O(n) 。
至于平均的情况,由于元素插入到第i个位置,或删除第i个元素,需要移动 n -i个元素。根据概率原理 ,每个位置插入或删除元素的可能性是相同的,也就说位置靠前,移动元素多,位置靠后,移动元素少。最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2。
我们前面讨论过时间复杂度的推导,可以得出,平均时间复杂度还是 O(n) 。
这说明什么?线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是 0(1); 而插入或删除时,时间复杂度都是 O(n)。这就说明,它比较适合元素个数不太变化,更多是存取数据的应用。当然,它的优缺点还不只这些......
3.5.4 线性表顺序存储结构的优缺点
线性表的顺序存储结构的优缺点如图 3-5-3 所示。
好了,大家休息一下,我们等会儿接着讲另一个存储结构。
引用《大话数据结构》作者:程杰