文章目录
- 第二节,线性表的顺序存储及实现概览
- 什么是顺序表和链表?顺序存储的叫顺序表
- 顺序表和数组还有内存地址的关系?
- 顺序表的基本操作如何实现?
- 1、插入操作如何实现?
- 2、删除操作如何实现?
- 3、赋值和查找操作怎样达成?
- 这些的操作时间复杂是多少?
第二节,线性表的顺序存储及实现概览
什么是顺序表和链表?顺序存储的叫顺序表
可以使用顺序存储方式和链式存储方式保存线性表,从而分别得到线性表的顺序存储结构和链式存储结构。
顺序存储结构使用数组保存线性表中的各元素
- 原来数组,不是线性结构的一种,而是线性表的一种实现方式:
来自AI:
线性结构是基础概念
- 线性结构是一种数据结构,它具有线性的逻辑关系,即数据元素之间存在着一对一的线性关系。
线性表是典型的线性结构
- 线性表是最基本、最常用的一种线性结构。它是具有相同数据类型的 n(n≥0)个数据元素的有限序列,通常表示为 (a1, a2, …, an)。
数组是线性表的一种实现方式
从逻辑结构上看,数组可以被看作是一种线性表。例如,一维数组可以看作是一个线性表,数组中的每个元素对应线性表中的一个数据元素,它们在内存中是连续存储的,元素之间的逻辑顺序与物理存储顺序一致,很好地体现了线性表的线性关系。
对于二维数组,可以将其视为线性表的推广,即线性表中的元素又是一个线性表。
;就是线性表形成一个元素,再组成一个线性表
从存储结构上看,数组具有随机访问的特性,通过下标可以快速地访问数组中的元素,时间复杂度为 O (1)。这使得数组在很多需要快速定位和访问元素的场景中非常高效,这也是数组作为线性表实现方式的一个重要优势。
1、相应的线性表称为顺序表。
使用顺序存储结构保存线性表非常方便,因为可以通过下标来访问数组元素,所以可以实现对表中元素的随机访问。
这是顺序存储结构的优势。
在顺序表中实现插入和删除时可能需要移动元素,
-
如果插入和删除的位置靠近表头位置,则移动的元素个数偏多。
-
当有频繁的插入、删除操作时,元素的移动也会很频繁,操作的效率较低。
-
另外,由于数组的大小是相对固定的,因此,当表的长度有很大变化时,数组空间的利用率也不好控制。
- 有可能会因为表中元素个数过多而导致数组空间不足,
- 也可能会因为表中元素个数较少,使得数组中的很多位置是空置的。
这些都是顺序存储结构的不足之处。
针对顺序表的这些问题,提出了线性表的另一种存储方式,即链式存储方式。
链式存储结构使用链表保存线性表中的各元素,
2、相应的线性表称为链表。
顺序表和数组还有内存地址的关系?
在C语言中,一维数组是顺序存储的连续存储空间,所以线性表的顺序存储结构就是将线性表中的各数据元素,按照其逻辑次序,依次保存到数组中。
线性表中的一个元素保存在数组的一个单元中。线性表中逻辑上相邻的两个元素,保存在数组内相邻的两个单元中。
为了保存一个线性表,需要分配一个多大的数组呢?
线性表中的元素个数可以是变化的,这意味着数组的单元数也要变化。
而一旦数组分配完毕,它的个数就不会改变。一般地,需要分配一个足够大的数组以供线性表使用,这样既保证能够保存线性表中当前的全部元素,又为后续的插入操作预留了空间。
在分配数组时,预留的数组空间越大,数组空间占满的可能性越小,空间利用率越低,即存储效率越低。
应该根据线性表中可能包含的元素的最大个数来分配数组。
为了表示数组中保存的实际元素个数,通常还需要使用一个整型变量来记录顺序表的当前长度。
在分配数组空间后,将线性表中的n个元素依次保存在数组中,从表头至表尾的各个元素分别对应从下标0到下标n-1的位置。
数组是内存中一片连续的空间,相邻的两个单元在内存中的实际地址也是相邻的
这表明,线性表中逻辑上相邻的两个元素,其存储地址也是相邻的。
这是顺序表的一个显著特点。
线性表中的元素可以是有定义的任何类型。
在内存中,保存不同类型的元素时会需要数目不等的存储单元。
要正确理解“数组中相邻单元的存储地址相邻”这句话的含义。
假设有线性表 L = ( a 0 , a 1 , a 2 , a 3 , a 4 , a 5 ) , 每个元素需占用 2 字节,也就是一共占 12 字节 分配一个含 8 个元素的数组 A 保存 L 则 A 再内存中的示意图如图 2 − 1 所示 A 占据内存从位置 M 起的连续空间 ∣ 元素 ∣ 存储字节范围 ∣ ∣ a 0 ∣ 第 1 − 2 字节 ∣ ∣ a 1 ∣ 第 3 − 4 字节 ∣ ∣ a 2 ∣ 第 5 − 6 字节 ∣ ∣ a 3 ∣ 第 7 − 8 字节 ∣ ∣ a 4 ∣ 第 9 − 10 字节 ∣ ∣ a 5 ∣ 第 11 − 12 字节 ∣ 线性表 L 共占 12 字节。 一般每个字节对应内存中的一个存储单元 计算机编址方式有字编址、字节编址等,编址方式可能不完全一致 所以保存 a 0 的首地址与保存 a 1 的首地址未必连续, 但这两地址编号间不会再保存其他元素的地址编号。 假设有线性表 L=(a_0, a_1, a_2, a_3, a_4, a_5),\\ 每个元素需占用 2 字节,也就是一共占12字节\\ 分配一个含8个元素的数组 A保存 L\\ 则A再内存中的示意图如图2-1所示\\ A占据内存从位置M起的连续空间\\ |元素|存储字节范围|\\ |a_0|第1 - 2字节|\\ |a_1|第3 - 4字节|\\ |a_2|第5 - 6字节|\\ |a_3|第7 - 8字节|\\ |a_4|第9 - 10字节|\\ |a_5|第11 - 12字节|\\ 线性表L共占 12 字节。\\ 一般每个字节对应内存中的一个存储单元\\ 计算机编址方式有字编址、字节编址等,编址方式可能不完全一致\\ 所以保存a_0 的首地址与保存 a_1的首地址未必连续,\\ 但这两地址编号间不会再保存其他元素的地址编号。 假设有线性表L=(a0,a1,a2,a3,a4,a5),每个元素需占用2字节,也就是一共占12字节分配一个含8个元素的数组A保存L则A再内存中的示意图如图2−1所示A占据内存从位置M起的连续空间∣元素∣存储字节范围∣∣a0∣第1−2字节∣∣a1∣第3−4字节∣∣a2∣第5−6字节∣∣a3∣第7−8字节∣∣a4∣第9−10字节∣∣a5∣第11−12字节∣线性表L共占12字节。一般每个字节对应内存中的一个存储单元计算机编址方式有字编址、字节编址等,编址方式可能不完全一致所以保存a0的首地址与保存a1的首地址未必连续,但这两地址编号间不会再保存其他元素的地址编号。
数组下标与线性表元素的位置相对应。
线性表元素依次存放的特性,决定了表中位置 i ( i≥0 ) 的元素存储在数组的下标 i 处。
表头元素保存在位置0处,;也就是数组的下标0处
这个位置也称为数组的首地址。
有了这个约定,对表中任意一个元素的访问将变得非常容易。
只要给出表中元素的序号,就可以根据下标地址计算公式很容易地计算出元素所在的内存位置(实际上是相对于数组首地址的偏移量),因此可以直接访问该元素。
顺序表中的访问方式称为随机访问方式,;这是从存储结构的角度来看,上面AI部分有介绍
其含义是,只要给定数组下标,就能立即计算出相应元素的存储地址,并据此访问该元素。
1、下标地址计算公式如下:
设LOC ( a i ) ( i ≥ 0 )表示元素 a 的存储首地址,每个元素需要占用 d 个存储单元,则有: ( 2 − 1 ) : LOC ( a i ) = LOC ( a i − 1 ) + d 进一步地有: ( 2 − 2 ) : LOC ( a i ) = LOC ( a 0 ) + i × d LOC ( a 0 ) 即数组的首地址。 设 \text{LOC}(a_i)(i \geq 0)表示元素 a的存储首地址,每个元素需要占用 d 个存储单元,则有:\\ (2-1):\text{LOC}(a_i)=\text{LOC}(a_{i - 1})+d \quad\\ 进一步地有:\\ (2-2):\text{LOC}(a_i)=\text{LOC}(a_0)+i\times d\quad\\ \text{LOC}(a_0) 即数组的首地址。\\ 设LOC(ai)(i≥0)表示元素a的存储首地址,每个元素需要占用d个存储单元,则有:(2−1):LOC(ai)=LOC(ai−1)+d进一步地有:(2−2):LOC(ai)=LOC(a0)+i×dLOC(a0)即数组的首地址。
- 用这个公式是怎么算的
2、也可以使用另一种求解方法。
第6个元素占用的最后一个存储单元,实际上是第7个元素占用的第1个存储单元的前一个单元。
可以先计算第7个元素的首地址,得到148,再减1,
得到相同的答案。
线性表的插入和删除是两个基本操作。
顺序表要求表中的相邻元素存储在数组的相邻单元中,
所以当在某个位置插入新元素时,必须先为这个元素找到相应的存储空间,同时要保证数组中所有元素依然依次相邻存放。、
在删除元素时,被删除元素所占用的位置要由其他元素来填补。
- 总的来说,在当前位置插入元素或删除当前位置的元素时,看都会涉及从当前位置开始,一直到表尾的所有元素,即这些元素都需要移动。
当在表尾后插入元素或删除表尾元素时,操作是容易实现的,因为操作不会引起其他元素的移动。
当插入或删除操作的位置是其他位置时,移动元素的个数依赖于操作的位置。
例如,当要在表头插入新元素时,表中当前所有元素都必须向表尾方向移动一个位置以腾出空间。
当要在表中(合理的)位置i插入一个新元素时,这个位置及其到表尾的所有元素都必须向表尾方向移动一个位置。
删除操作与此类似,只是元素的移动是向表头方向进行的。
- 平均来说,插入和删除操作要移动表中约一半的元素。
设给定一个顺序表,初始时含有5个元素:11、5、23、19和6。
在位置2插入元素27,然后删除位置3的元素,
每步操作后的顺序表如图2-2所示。
注意,这里是删除初始顺序表中,位置3的元素,而不是插入之后的位置3
注意,删除位置3,其实是删除第4个位置的元素,也就是说,确实是删除插入之后的表中的,位置3的元素!
因为位置0,才是第1个元素!
- 这两个步骤,实际上就是一个修改操作,把位置3的元素给替换了
1、为了执行“在位置2插入元素27”,需要依次将元素6、19和23向后移动一个位置,注意移动的次序。
先移动6,最后移动23。;也就是先从最后一个开始移
此时,位置3的空间是可用的,将元素27保存在这个位置。、
这个过程如图2-3所示。
2、在执行“删除位置3的元素”时,需要将23后面的元素(19和6)依次前移一个位置。
移动的次序是,先移动19,再移动6。;注意这里,是从最靠近删除元素位置的这个19,开始移动
这个过程如图2-4所示。
顺序表的基本操作如何实现?
实现基本操作的程序中会用到一些常量,其定义如下。
#define TRUE 1
#define FALSE 0
#define ERROR -1
#ifndef maxSize
#define maxSize 100
#endif
表中每个元素的类型是ELEMType,顺序表的定义如下,
typedef int ELEMTYPE;
typedef struct {ELEMTYPE element[maxSize]; //保存元素的数组,最大容量为 maxSizeint n; //顺序表中的元素个数
} SeqList;
typedef SeqList LinearList;
typedef int Position;
新构造的顺序表为空表。
空表中所含的元素个数为零,将表清空也意味着表中元素个数为零。
int initList(SeqList *L) // 初始化顺序表,创建一个空表L
{L->n = 0;return TRUE;
}int clear(SeqList *L) // 将表L置空
{L->n = 0;return TRUE;
}
根据顺序表中n的值,可以判断顺序表是否为空、是否已满,
值n也直接代表顺序表的长度。
int isEmpty(SeqList *L) { // 如果表L为空,则返回1,否则返回0if (L->n == 0)return TRUE;elsereturn FALSE;
}int isFull(SeqList *L) { // 如果表L已满,则返回1,否则返回0if (L->n == maxSize)return TRUE;elsereturn FALSE;
}int length(SeqList *L) { // 返回表L的当前长度return L->n;
}
1、插入操作如何实现?
当在不满的顺序表中插入一个元素x时,除了要指明元素的值以外,还要指出插入的位置。
insertList函数带有3个参数,分别是
顺序表、
插入位置
及要插入的值。
位置值pos必须是一个合理的整数值,即pos值介于**0和“L->n”**之间。
合理的位置值有n+1个。
- 为何加1个,就是空表也能插入
插入在位置0处,意味着插入在表头位置。
插入在“L->n”处。意味着添加在原表尾的后一个位置。
当确认表不满且插入位置有效后,从表尾元素开始,到插入位置的元素为止,依次将各元素后移一个位置。
移动完毕,将元素x放到移动后出现的空闲位置中。
之后将元素个数增1,即表长增1。也就是n增加1
插入操作的实现如下。
int insertList(SeqList *L, Position pos, ELEMTYPE x) { // 在表L的位置pos处插入元素xint i;if (isFull(L) == TRUE) return FALSE; // 表已满if (pos < 0 || pos > L->n) return ERROR; // 位置错误,与表满区分开for (i = L->n; i > pos; i--) {L->element[i] = L->element[i - 1]; // 移动元素}L->element[i] = x; // 放置xL->n++; // 表长增1return TRUE;
}
对于长度为n的顺序表,当在表尾的后一个位置插入元素时,不需要移动任何元素。
如果插入在倒数第一个位置,则需要移动1个元素;
如果插入在倒数第二个位置,则需要移动两个元素。
依此类推,插入在第一个位置,需要移动n个元素。
总之,当在位置i插入元素时,需要移动n-i个元素。
如果在任何位置进行插入的概率都相等,则插入操作中,移动元素的平均次数N为:
N = ∑ k = 0 n k n + 1 = n 2 N = \frac{\sum_{k = 0}^{n} k}{n + 1} = \frac{n}{2} N=n+1∑k=0nk=2n
2、删除操作如何实现?
删除操作是类似的,函数removeList中需要指明顺序表及要删除的元素所在的位置。
通常,删除的元素值需要通过一个变量返回给操作调用者,所以removeList函数也带有三个参数,前两个参数分别是
顺序表
和删除位置,
删除的元素值将放到第三个参数中。;这和插入是差不多的三个参数呀
也可以让removeList函数只带前两个参数,
而删除的元素值通过函数的返回值带回。;这个才是和插入的区别
- 也就是形参x是指针元素
在删除时,需要确认表不为空且位置值pos有效,
这里pos应介于0和“L->n-1”之间。
合理的位置值有n个。
-
合理的位置值少一个,为什么是0和n-1之间内,为何n要减一个1,n不减行不行
是不是因为插入,可以从空表开始插入,但是删除,空表没法删
注意,删除的合理位置值比插入的合理位置值少1。
移动表元素时,从被删除元素的直接后继开始,一直到表尾结束,各元素依次前移一个位置。
同时,表长减1。也就是n减去1
删除操作的实现如下。
int removeList(SeqList *L, Position pos, ELEMTYPE *x) { // 删除表L中位置pos处的元素并通过x返回 //这个形参x,是指针元素int i;if (isEmpty(L) == TRUE) return FALSE; // 表空if (pos < 0 || pos > L->n - 1) return ERROR; // 位置错误,与表空区分开*x = L->element[pos]; // 记下被删除的元素值for (i = pos; i < L->n - 1; i++) {L->element[i] = L->element[i + 1]; // 前移元素}L->n--; // 表长减1return TRUE;
}
来自AI:
在形参列表中,
ELEMTYPE *x
这种写法表明x
是一个指针。这里
ELEMTYPE
是自定义的数据类型(在前面代码中被定义为int
类型 ),
*
是指针声明符,它表示x
是一个指向ELEMTYPE
类型数据的指针。
- 在函数体中,有语句
*x = L->element[pos];
。- 这里使用了
*
操作符(解引用操作符),它的作用是访问指针x
所指向的内存空间。- 将
L->element[pos]
的值赋值给*x
,意味着把数组中指定位置的元素值存储到x
所指向的内存位置,- 这进一步证明了
x
是一个指针。
对于长度为n的顺序表,当删除表尾元素时,不需要移动任何元素。
当删除倒数第二个元素时,需要向前移动1个元素,依此类推,当删除表头元素时,需要前移n-1个元素。
如果在任何位置进行删除的概率都相等,则删除操作中,移动元素的平均次数N为
N = ∑ k = 0 n − 1 k n = n − 1 2 N = \frac{\sum_{k = 0}^{n - 1} k}{n} = \frac{n - 1}{2} N=n∑k=0n−1k=2n−1
3、赋值和查找操作怎样达成?
因为能通过数组下标直接定位到元素,从而可以直接访问到元素本身,所以,很容易实现给顺序表中某位置的元素赋值、获取表中某位置处的元素值。
在表中查找某个值时,需要从前向后依次判定元素是不是要查找的目标,使用一个循环完成查找过程。
当然,也可以从后向前进行依次判别查找。
假设,顺序表中一定能找到查找目标,则最优情况下,在数组下标0处即找到查找目标。
在最坏情况下,需要查找到数组最后一个元素。
- 所以平均来讲,也需要查找顺序表中约一半的元素。
如果查找失败,则需要查找到数组最后一个元素,与查找成功时的最坏情况类似。
在C语言中,函数参数的传递方式有值传递和地址传递。
- 这就是讲过的传值和传址
见,Day13-【软考】雄文!一口气看懂程序设计语言所有内容!有限自动机如何求解?正规式如何解析(核心!)?传值和传址原理是什么?(重点!),中:传址原理是什么?(重点!)传值原理是什么?(重点!)
如果在函数体内修改了实参值,且操作结果需要传递到函数外,即要对相应的实参起作用,则相应的形参选择为指针形式。
也就是说,x是形参
见,Day13-【软考】雄文!一口气看懂程序设计语言所有内容!有限自动机如何求解?正规式如何解析(核心!)?传值和传址原理是什么?(重点!),中:什么是形参?什么是实参?
此外,为了各函数参数表的形式一致及调用时的高效率,形参中的顺序表均使用指针形式。
调用时需要传递实参顺序表的地址。
以初始化操作initList为例,
定义的形参是SeqList*L。
在main函数中,调用initList函数时使用的实参是顺序表的地址&listtest。
- 就没看到main函数
这些的操作时间复杂是多少?
假设顺序表长度为n,则上述系列方法中,插入操作、删除操作与操作的位置有关,
-
插入,删除,这两个方法的时间复杂度均为O(n)。;线性关系,难怪叫线性表,不会随规模增大变得过于复杂
-
查找,赋值(也就改值)等其他操作的时间复杂度均为O(1)。