数据结构与算法2:线性表补充
文章目录
- 2.4.4.5、补充:C++中的参数传递
- 2.4.4.5.1、值传递:
- 2.4.4.5.2、传地址:
- 2.4.4.5.2.1、传地址方式——指针变量作为参数
- 2.4.4.5.2.2、传地址方式——数组名作为参数
- 2.4.4.5.2.2、传地址方式——引用类型作为参数(C++)
- 2.4.4.5.2.3、引用类型做形参的三点说明
- 2.4、线性表的顺序表示和实现
- 2.4.1、线性表的顺序存储表示
- 2.4.1.1、预备内容
- 2.4.1.2、顺序表示意图
- 2.4.2、顺序表基本操作的实现3
- 2.4.2.1、线性表的基本操作
- 2.4.2.2、操作算法中用到的预定义常量和类型
- 2.4.2.3、【算法2.1 】线性表L的初始化(参数用引用)
- 2.4.2.4、线性表L的销毁和清空。
- 2.4.2.5、求线性表L的长度和判断其是否为空。
- 2.4.2.6、【算法2.2 】顺序表中的取值(根据位置i获取相应数据元素的内容)
- 2.4.3、线性表的顺序表示和实现4
- 2.4.3.1、顺序表上的查找操作
- 2.4.3.2、【算法2.3 】顺序表中的查找
- 2.4.3.3、【算法2.3 】顺序表中的查找算法分析
- 2.4.3.4、平均查找长度ALS(Average Search Length):
- 2.4.4、线性表的顺序表示和实现5
- 2.4.4.1、顺序表的插入思路
- 插入位置在最后
- 插入位置在中间
- 插入位置在最前面
- 插入异常的两种情况:
- **总结:**
- 2.4.4.2、顺序表的插入的算法实现
- 2.4.4.3、顺序表的插入的算法分析(主要看时间效率)
- 2.4.5、线性表的顺序表示和实现6
- 2.4.5.1、顺序表的插删除思路
- 删除位置在最后
- 删除位置在中间
- 删除位置在最前面
- 总结
- 2.4.5.2、顺序表的算法实现
- 2.4.5.2、顺序表的算法分析
- 2.4.5、小结(2.4内容总结)
2.4.4.5、补充:C++中的参数传递
函数调用时传送给形参表的实参必须与形参三个一致(类型、个数、顺序)
参数传递有两种方式
值传递(参数为整形、实型、字符型等)
传地址
● 参数为指针变量
● 参数为引用类型(重点了解这个)
● 参数为数组名
2.4.4.5.1、值传递:
把实参的值传送给函数局部工作区相应的副本中,函数使用这个副本执行必要的功能。函数修改的时副本的值,实参的值不变。
注解:float a,b;定义了两个值,作为实参,在swap(a,b)中调用过程中,将a传递给m,b传递给n,然后在被调用的函数中【也就是左边声明下面的那一块】m和n调换了各自的值,被调用完后,m和n的值就被释放了,然后返回到调用的地方继续执行,a和b的值没有发生任何变化。传递的是值,形参和实参各用各的空间。比如a=3,b=5,在swap(a,b)传递过程中,实参a的值传递给了形参m,m=3,实参b的值传递给了形参n,n=5,然后在void swap(float m,float n)中m和n发生值发生了交换,m=5,n=3,但当被调用函数void swap(float m,float n)结束了,返回到释放得地方的时候,被调用函数释放掉了,a和b没有任何变化。这就是传值,形参发生改变,实参值不会改变。
2.4.4.5.2、传地址:
2.4.4.5.2.1、传地址方式——指针变量作为参数
1、形参变化影响实参
注解:定义了两个指针p1和p2,p1指向a,p2指向b作为实参传递。假设a里面存放3,b里面存放5。p1存放的是的a的地址,p2存放的是b的地址,用箭头表示指向它。现在调用函数swap(p1,p2),void swap(float *m,float *n),指针m指向的也是a,指针n指向的也是b。*m表示指针变量的内容,这个函数中变量a和变量b交换了值,交换完毕了,形参m,n释放没有了,返回到调用的地方,a,b输出的时候有无变化呢?我们说是变化了。
2、形参变化不影响实参?
注解:相比形参变化影响实参的,不同点在哪里呢?void swap(float *m,float *n){}里面多了一个临时变量float t。给t赋值的不是m,而是m本身,m存放的是什么呢?m存放的是a的地址,所以t存放的时候的时候也是a的地址,这样过来就对a,b没什么影响。
2.4.4.5.2.2、传地址方式——数组名作为参数
传递的是数组的首地址
对形参数组所作的任何改变都将反应到实参数组中
注解:char a[10] = “hello”;主函数中定义了一个字符数组,存放了5个字符串,调用子函数sub(a),将数组名作为参数传递实际上就是传递了数组的首地址,那么就可以写成void sub(char b[]){}的形式,注意char []里面不能有大小,或者用*b表示,都可以,对形参b[]= “world”;操作也就是对a[10]操作,释放完后就返回到调用出,此时输出的a就发生了变化。
例:用数组做函数的参数,求10个整数的最大数
2.4.4.5.2.2、传地址方式——引用类型作为参数(C++)
什么是应用???
引用:它用来给一个对象提供一个替代的名字。
int i =5;定义了一个i,但是int &j=i,j引用了i,所以操作j就和操作i是一样的,它就是一个i的代名词,i改变时j也跟着改变,i一开始是5,后来是7,所以j也跟着i变成7啦。从另一个角度理解,&j是地址,它和i的地址是一样的,所以它们公用的是同一个空间。
注解:m,a公用要给空间,n,b公用一个空间,所以对m,n的操作实际上就是对a,b的操作,这种操作比之前的操作数组的容易,公用同一个空间,所以形参不比产生新的空间。
2.4.4.5.2.3、引用类型做形参的三点说明
1、传递引用给函数与传递指针的效果是一样的,形参变化实参也发生变化。
2、引用类型作形参,在内存中并没有产生实参的副本,它直接对实参操作;而一般变量作为参数,形参与实参就占用不同的存储单元,所以形参变量的值是实参变量的副本。因此,当参数传递的数据较大时,引用比用一般变量传递参数的时间和空间效率都好。
3、指针参数虽然也能达到与使用引用的效果,但在被调函数中需要重复是使用“*指针变量名”的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。
2.4、线性表的顺序表示和实现
2.4.1、线性表的顺序存储表示
2.4.1.1、预备内容
注解:将逻辑上相邻的元素存储在物理位置上也相邻的元素,这种直接映射过去的方式就是顺序表,在高级语言里实用数组实现的。线性表中的元素不是固定的,经常要插入元素个数增加,删除元素个数减小,怎么定义数组的大小呢?解决办法就是定义一个比较大的数组,不用的就空着,但这样的话怎么知道元素的个数呢?就需要额外需要一个变量length来记录这个表中元素的个数。也就是需要表达线性表的存储就需要2个部分,一部分呢是一个数组,另一部分是一个整数,两部分放到一起表示一个顺序表。(Sequence List)。逻辑位序和物理位序是相差1的,逻辑上存储是从1开始的,而在物理存储上元素位数是从0开始的,所以逻辑位序比物理位序大1。
2.4.1.2、顺序表示意图
注:理解好这个示意图之后接下来的算法就比较简单了。我们定义了一个顺序表类型,定义类型之后才会对这顺序表分配空间,就和int a;一样,定义了a的类型之后,内存就会给这个变量开辟一个变量名字为a的空间。Sqlist L;这个定义了之后就可以对顺序表进行操作了。定义了这样一个类型的变量L,就会被分配空间。这样的空间是怎么样子的呢?有俩部分组成,首先是一个数组ElemType *elem;有100个元素,就可以往里面存储了,第二个是一个整数,用来记录这个数组中元素的个数的。具体来操作该怎么做呢?整个绿色+蓝色的就是顺序表L,那我们要操作绿色即数组这一块,可以用下标来表示它,L.elem[0]就是下标为0的第一个元素,L.elem[99]下标为99的第100个元素。那我们就可以操作它。比如增加一个元素,就要修改线性表L的成员L.ength。这便是操作线性表L的方法。如果是指针型的,那可以用L->elem L->length。
2.4.2、顺序表基本操作的实现3
2.4.2.1、线性表的基本操作
2.4.2.2、操作算法中用到的预定义常量和类型
注解:操作算法中有一些预定常量和类型,比如OK返回的是什么?如果没有这些上机就会报错,也不知道怎么回事?,TRUE, FALSE这些到底返回的是什么,需要去看看。还有返回值是那种类型int or char 需要定义一下。
2.4.2.3、【算法2.1 】线性表L的初始化(参数用引用)
注解:第一个算法是线性表的初始化,我们这个参数用得是应用型的,也就是我们调用这个函数完成线性表的初始化,那么这个主调函数也就自动获得了咱们这个形参,我们对形参进行操作也就是对实参进行操作。操作名字是inistList初始化线性表,这个不是普通的线性表,是顺序表也就是Sq,它的操作对象是什么呢?你给我一个线性表L,我构造好线性表L,通过引用型&L就返回了,最前面是返回的状态值Status,如果构造好了就返回OK,return OK;那么怎么构造的呢?给L动态分配空间,new ElemType[MAXSZIE];获取元素中的地址L.elm = ,刚分配好空间线性表里面一个元素也没有,所以L.ength = 0。一般情况下有数组分配空间和元素长度这两部分就够了,那么中间if(!L.elem)=exit(OVERFLOW);是干什么的,这是一种异常处理,分配内存的时候,空间分配太小了,那存储就会发生错误,死机了或者是不出结果了,或是其他的错误,我们要避免这种情况,如果里面没有内容,也就是分配失败,退出的时候就返回OVERFLOW这个值,OVERFLOW返回的是-2,成功了就返回 Ok-1,我们根据返回的结果进行后续的操作。这样一个空的顺序便构造好,可以往里面写内容了。
2.4.2.4、线性表L的销毁和清空。
注解:所谓销毁线性表,就是线性表所占的空间释放掉,让空间回归内存。直接用C++语法删除掉,但前提是得有,所以加了一个if判断语句。
注解:清空的意思是线性表仍在,但是我要告诉计算机我这里面没有元素了,以将L.length = 0的形式告诉计算机。
2.4.2.5、求线性表L的长度和判断其是否为空。
注解:就是我们需要知道线性表中到底有多少个元素。GetLength获取长度,然后返回线性表的L.length成员。
注解:只需判断一下L.length是否为0,如果是0那就是没有元素,也就是空的。
2.4.2.6、【算法2.2 】顺序表中的取值(根据位置i获取相应数据元素的内容)
注解:这个算法是取i的值,我们十多a1存在下标为0的位置,a2 存在下标为1的位置,ai存在下标为i-1的位置,所以我们直接把下标为[i-1]的取出来放到e里面就行了。(e = Lelem[i-1]);痛过引用型返回,但是不能直接返回,需要判断一下其位置是否合理性,如果你给了我一个负数或者超过n-1的,那么就说这些没有元素,所以要判断一下,给个返回值告诉一下。这个的算法复杂度是多少呀?这些都执行一次,执行10000次,只要不发生变化,那就是常量阶,即O(1)。
2.4.3、线性表的顺序表示和实现4
2.4.3.1、顺序表上的查找操作
按值查找:
例如:在图书表中,按照给定书号进行查找,确定是否存在该图书。如果存在:输出的第几个元素。如果不存在,输出0。
2.4.3.2、【算法2.3 】顺序表中的查找
在线性表L中查找与指定值e相同的数据元素位置。
从表的一端开始,逐个进行记录的关键字和给定值的比较。找到,返回该元素的位置序号,未找到,返回0。
注解:给定线性表L,(SqList L),给定元素e(ElemType e),从这个线性表中查找,怎么查找呢?从存储位置下标为0的元素开始查找(i=0),看是否与e一样,一样就返回逻辑序号(存储序号是从0开始的,逻辑序号是从1开始的,所以逻辑序号=存储序号+1),如果不一样,那么i++,进行下一次循环,如果一样返回e的位置,如果在存储序号(0,L.length)范围里面仍旧没有找到,就return 0不再执行了。接下来就分析一下这个算法的复杂度,算法的复杂度要分析时间效率和空间效率,重点分析时间效率,时间效率是用渐进复杂度分析的,它是怎么计算的呢?分三个步骤,(1)找到执行次数最多的语句,(2)看它到底执行多少次(3)表示成O(x)的形式。见2.4.3.3
还有另一种算法,这里就不介绍了。
2.4.3.3、【算法2.3 】顺序表中的查找算法分析
因为查找算法的基本操作为:将记录的关键字同给定值进行比较。
基本操作: L.elem[i] == e
注解:这个线性表中存储了7个元素,所以L.length=7,比如查找元素是a,则e=‘a’,那么第一次查找用得是L.elem[0]的值和查找的值a进行比较,结果只需第一查找就找到了,那么就返回i+1(此时i=0,i+1=1),代表是这个表中的第一个元素,这个时候就比较了1次。假设查找值是b,则e=‘b’,那么进行第一次查找用的L.elem[0]的值和查找的值b进行比较,第一次没有找到,那么i++进行下一次循环,第二次用的是L.elem[1]与查找值b进行比较,这次找到了,就返回i+1(此时i=1,i+1=2),代表是这个表中的第二个元素,c,d,e,f,g亦是如此。那平均查找几次呢?那每个元素都找一遍。比较次数:e=a,1次;e=b,2次;e=c,3次,…,e=g,7次;平均下来(1+2+3+…+7)/7=4。那么这个顺序查找衡量需要查找多少次呢,就需要引入一个描述词——平均比较次数(即平均查找长度)来衡量。
总结:L.elem[i] == e,此处e就是查找值,要查找之前使其具体化,L.elem[i]指针执向的内存中的值,当找到查找值e时,i也就随之确定了,此时i是存储序号,但我们要看的是表L中的序号,也就是逻辑序号,此时根据逻辑序号与存储序号的关系,返回i+1就得到了逻辑序号了。
2.4.3.4、平均查找长度ALS(Average Search Length):
为确定记录在表中的位置,需要与给定值进行比较的关键字的个数的期望值叫做查找算法的平均查找长度。
对含有n个记录的表,查找成功时:
ALS=∑i=0nPiCiALS=\sum_{i=0}^n {P_iC_i}ALS=∑i=0nPiCi
注解:从平均查找长度角度看上一个问题就是(1+2+3+…+7)/7=4,换一种角度来表示11/7+21/7+…+7*1/7=4。这里是n是7个的时候。
注解:假设Pi=1/n,则ASL=1P1 +2P2+…+(n-1)P(n-1)+nP(n)=1/n*(1+2+…+(n-1)+n)=1/n*[n(n-1)]/2=(n+1)/2
2.4.4、线性表的顺序表示和实现5
2.4.4.1、顺序表的插入思路
顺序表的插入:
插入不同的位置的算法演示:(L.length下面三种情况都会先赋值7,只介绍L.elem[]的变化)
插入位置在最后
注解:比如插入元素g,那么就为L.elem[6]的位置赋值。
插入位置在中间
注解:比如把元素放到L.elem[4]的位置,那么就把f搬到L.elem[6],把a搬到L.elem[5],此时把L.elem[4]的位置腾出来了,然后给L.elem[4]赋值即可。
插入位置在最前面
注解:比如把g插入到L.elem[0],就需要把圈出来的元素往后面移动,具体移动规则,从圈出的元素最后一个f开始,从L.elem[5]移动到L.elem[6],然后是a从L.elem[4]移动到L.elem[5],依此如是,就会把L.elem[0]的位置腾出来,然后给L.elem[0]赋值完成。
插入异常的两种情况:
第一、比如插入L.elem[0]位置前面。
第二、比如插入上面数组已经存满,但要插入到L.elem[100]位置,此种情况为溢出。
总结:
插入表的插入运算是指在表的第i(1<=i<=n+1)个位置,插入新结点e,使长度为n的线性表(a1,a2,…,ai-1,ai,…,an)变成长度为n+1的线性表(a1,…,ai-1,e,ai,…,an)
算法思想:
(1)、判断插入位置i是否合法。
(2)、判断顺序表的存储空间是否已满,若已满返回ERROR。
(3)、将n至第i位的元素依此向后移动一个位置,空出第i个位置。
(4)、将要插入的新元素e放到第i个位置。
(5)、表长加1,插入成功返回OK。
2.4.4.2、顺序表的插入的算法实现
2.4.4.3、顺序表的插入的算法分析(主要看时间效率)
算法时间主要耗费在移动元素上
● 若插入在尾结点之后,则根本无需移动(特别快);
● 若插入在首接结点之前,则表种元素全部后移(特别慢);
● 若要考率在各种位置插入(n+1可能)的平均移动次数,该如:
● 顺序表插入算法的平均时间复杂度O(n)。
注解:自己理解
2.4.5、线性表的顺序表示和实现6
2.4.5.1、顺序表的插删除思路
顺序表的删除:
删除不同的位置的算法演示:(L.length下面三种情况都会赋值5,只介绍主要的变化——L.elem[]的变化)
删除位置在最后
注解:比如删除最末的f,直接将L.elem[5]删除,L.length-1。
删除位置在中间
注解:比如将L.elem[3]上的e删除,则后面从a开始移动,接着f,从前到后的顺序。
删除位置在最前面
总结
线性表的删除运算是将表的第i(1<=i<=n)个结点删除使得长度为n的线性表(a1,…,ai-1,ai,ai+1,…,an),变长度为n-1的线性表a1,…,ai-1,ai+1,…,an
算法思想:
(1)判断删除位置i是否合法(合法值为1<=i<=n)。
(2)将欲删除的元素保留在e中。
(3)将第i+1至第n位的元素依此向前移动一个位置。
(4)表长减一,删除成功返回OK。
2.4.5.2、顺序表的算法实现
2.4.5.2、顺序表的算法分析
算法时间主要消耗在移动元素的操作上
● 若删除尾结点,则根本无需移动(特别快);
● 若删除首结点,则表中n-1个元素全部前移(特别慢);
● 若考虑在各种位置的删除(共n中可能)的平均次数,该如何计算?
注解:
● 顺序表删除算法的平均时间复杂度O(n)。
2.4.5、小结(2.4内容总结)
顺序表(线性表的顺序存储结构)的特点
(1)利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致。
(2)在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等。
——这种存取元素的方法被称为随机存取法。
顺序表的基本操作
顺序表的操作算法分析:
● 时间复杂度
○ 查找、插入、删除算法的平均复杂度为O(n)
● 空间复杂度
○ 显然,顺序表操作算法的复杂度S(n)=O(1)(没有占用辅助空间)
顺序表的优缺点:
优点
● 存储密度大(结点本身所占存储量/结点结构所占存储量)
● 可以随机存取表中任一元素
缺点——>解决方法:链表
● 在插入、删除某一元素时,需要移动大量元素
● 浪费存储空间
● 属于静态存储形式[数组],数据元素的个数不能自由扩充。
——————————引入链表