Day26-【13003】短文,什么是顺序表?顺序表和数组、内存地址的关系?顺序表的插入、删除操作如何实现?操作的时间复杂度是多少?

第二节,线性表的顺序存储及实现概览

在这里插入图片描述

什么是顺序表和链表?顺序存储的叫顺序表

可以使用顺序存储方式和链式存储方式保存线性表,从而分别得到线性表的顺序存储结构和链式存储结构。

顺序存储结构使用数组保存线性表中的各元素

  • 原来数组,不是线性结构的一种,而是线性表的一种实现方式:

来自AI:

线性结构是基础概念

  • 线性结构是一种数据结构,它具有线性的逻辑关系,即数据元素之间存在着一对一的线性关系。

线性表是典型的线性结构

  • 线性表是最基本、最常用的一种线性结构。它是具有相同数据类型的 n(n≥0)个数据元素的有限序列,通常表示为 (a1, a2, …, an)。

数组是线性表的一种实现方式

  • 从逻辑结构上看,数组可以被看作是一种线性表。例如,一维数组可以看作是一个线性表,数组中的每个元素对应线性表中的一个数据元素,它们在内存中是连续存储的,元素之间的逻辑顺序与物理存储顺序一致,很好地体现了线性表的线性关系。

    对于二维数组,可以将其视为线性表的推广,即线性表中的元素又是一个线性表。

    ;就是线性表形成一个元素,再组成一个线性表

  • 从存储结构上看,数组具有随机访问的特性,通过下标可以快速地访问数组中的元素,时间复杂度为 O (1)。这使得数组在很多需要快速定位和访问元素的场景中非常高效,这也是数组作为线性表实现方式的一个重要优势。

1、相应的线性表称为顺序表

使用顺序存储结构保存线性表非常方便,因为可以通过下标来访问数组元素,所以可以实现对表中元素的随机访问。

这是顺序存储结构的优势。

在顺序表中实现插入和删除时可能需要移动元素,

  • 如果插入和删除的位置靠近表头位置,则移动的元素个数偏多。

  • 当有频繁的插入、删除操作时,元素的移动也会很频繁,操作的效率较低。

  • 另外,由于数组的大小是相对固定的,因此,当表的长度有很大变化时,数组空间的利用率也不好控制。

    1. 有可能会因为表中元素个数过多而导致数组空间不足
    2. 也可能会因为表中元素个数较少,使得数组中的很多位置是空置的

这些都是顺序存储结构的不足之处。

针对顺序表的这些问题,提出了线性表的另一种存储方式,即链式存储方式。

链式存储结构使用链表保存线性表中的各元素,

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保存LA再内存中的示意图如图21所示A占据内存从位置M起的连续空间元素存储字节范围a012字节a134字节a256字节a378字节a4910字节a51112字节线性表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)i0)表示元素a的存储首地址,每个元素需要占用d个存储单元,则有:(21):LOC(ai)=LOC(ai1)+d进一步地有:(22):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];  //保存元素的数组,最大容量为 maxSize
    int 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,否则返回0
    if (L->n == 0)
        return TRUE;
    else
        return FALSE;
}

int isFull(SeqList *L) {	// 如果表L已满,则返回1,否则返回0
    if (L->n == maxSize)
        return TRUE;
    else
        return 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处插入元素x
    int 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;  								// 放置x
    L->n++;  											// 表长增1
    return 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+1k=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--;  											// 表长减1
    return 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=nk=0n1k=2n1

3、赋值和查找操作怎样达成?

因为能通过数组下标直接定位到元素,从而可以直接访问到元素本身,所以,很容易实现给顺序表中某位置的元素赋值、获取表中某位置处的元素值。

在表中查找某个值时,需要从前向后依次判定元素是不是要查找的目标,使用一个循环完成查找过程。

当然,也可以从后向前进行依次判别查找。

假设,顺序表中一定能找到查找目标,则最优情况下,在数组下标0处即找到查找目标。

在最坏情况下,需要查找到数组最后一个元素。

  • 所以平均来讲,也需要查找顺序表中约一半的元素。

如果查找失败,则需要查找到数组最后一个元素,与查找成功时的最坏情况类似。

在C语言中,函数参数的传递方式有值传递和地址传递。

  • 这就是讲过的传值和传址

见,Day13-【软考】雄文!一口气看懂程序设计语言所有内容!有限自动机如何求解?正规式如何解析(核心!)?传值和传址原理是什么?(重点!),中:传址原理是什么?(重点!)传值原理是什么?(重点!)

如果在函数体内修改了实参值,且操作结果需要传递到函数外,即要对相应的实参起作用,则相应的形参选择为指针形式

也就是说,x是形参

见,Day13-【软考】雄文!一口气看懂程序设计语言所有内容!有限自动机如何求解?正规式如何解析(核心!)?传值和传址原理是什么?(重点!),中:什么是形参?什么是实参?

此外,为了各函数参数表的形式一致及调用时的高效率,形参中的顺序表均使用指针形式

调用时需要传递实参顺序表的地址

以初始化操作initList为例,

定义的形参是SeqList*L。

在main函数中,调用initList函数时使用的实参是顺序表的地址&listtest。

  • 就没看到main函数
这些的操作时间复杂是多少?

假设顺序表长度为n,则上述系列方法中,插入操作、删除操作与操作的位置有关,

  • 插入,删除,这两个方法的时间复杂度均为O(n)。;线性关系,难怪叫线性表,不会随规模增大变得过于复杂

  • 查找,赋值(也就改值)等其他操作的时间复杂度均为O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值