顺序表总结
1.线性表
零个或多个元素的有限序列。或者 线性表是由n个元素组成的有限序列。
我们都知道了线性表元素之间是有序的,一一对应的。下面是线性表的两种实现。
1.2.顺序表
物理储存:用一段地址连续的存储单元依次存储线性表的数据元素。既然是连续内存,我们就可以想到数组。所以我们可以用数组来实现顺序表。
重点注意:既然是用数组,我们都知道数组字定义时,必须声明数组的大小。所以这里引出两个概念。
数组长度: 我们在定义结构体时的数组申明大小
顺序表长度: 定义变量后,变量存放数据的多少。
由此可以看出,顺序表的长度一定小于等于数组长度。
线性表元素的序号和存放它的数组下标之间存在对应关系:由于我们数数都是从1开始数的,线性表的定义也不能避免,起始也是1,可C语言中的数组却是从0开始的第一个下标,于是线性表的第i个元素是要存储在数组下标为i-1的位置。
既然顺序表有长度,是不是需要一个变量来记录这个长度?
这样我们的顺序表的结构也就出来了。
struct list
{
int data[20]; //存放数据
/*--------------------------------------*/
Int len;; // 记录顺序表的长度
};
这里要注意的是
/*------------*/前面:是可以写多个,任意类型的数据(一般多个数据用结构体来包裹起来,所以一般都是结构体类型)。
/*------------*/后面:的是记录顺序表的长度,也就是存储元素的个数。
1.2.1顺序表插入
我们都知道顺序表的存储是用数组实现的。
所以我们在插入的时候,需要数组元素往后移动。空一个位置来进行插入。
1. 从最后一个位置开始向前遍历到第i个位置,也就是我们需要插入的位置。分别将他们向后移动一位。这样就会空出一个位置来。
2. 我们在第i个位置来插入这个元素。
3. 不要忘记---->顺序表长度加1
图示:
图 5-1 插入示意图
在图5-1中,红色方框表示空出来的位置。我们需要在4这个位置,插入
注意增加的元素就是我们要删除的元素。
代码实现:
要注意数组下标和实际位置之间的关系。我们的循环i控制的是数组的下标。
for(i = list.len; i >= x; i--)
{
list.data[i] = list.dat[i-1];
}
List.data[x-1] = ??;//这里赋值,或者scanf输入data如果是结构体,就给其成员变量分别来赋值。
List.len++;
1.2.2 顺序表的删除
在执行删除操作时,我们只需将数组从删除的位置,向前移动就可以了。
1. 从删除的位置开始,向后遍历,直到最后一个元素位置。分别将他们向前移动一个位置。
2. 不要忘记----->顺序表长度减1
图示:
图 5-2 删除示意图
在图5-2中红色数字表示要删除的数字。我们把位置为5的元素删除。
注意删除元素后,顺序表的长度减1
代码实现:
for(i = x-1; i < list.len; i++)
{
list.data[i] = list.dat[i+1];
}
List.len--;
1.2 单链表
物理存储:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
由此我们可以总结出来链表的结构体这也就是所谓的节点:
struct linklist
{
Int data;
struct linklist *next;
};
typedef struct linklist * linklist; // 这里的linklist一般取名为我们实际链表的名称这
在这里我们需要特别注意的是头节点和头指针之间的区别。当然还有最后一个节点的指针指向NULL。
头指针:
//TODO
头结点:
//TODO
1.2.1单链表的插入
图 5 - 3 单链表的插入
如图所示:要将S节点插入。此时我要注意的是不能将p 后面的元素丢失
代码实现:
1. S -> next = P -> next; // 注意p后面的元素不能丢失,所以先让P后的元素挂到s上去。2. P -> next = s; // 然后让P和 s建立关系
1.2.2 单链表的删除
图 5 -4 单链表删除
如图所示:我们要删除的是p后的一个元素。我们只要把p的next域指向他的下下个元素。
代码实现:
p->next = p->next->next;
//TODO...
1.3 顺序表和单链表的比较
1.3.1 顺序表特点:
- 逻辑上相邻的数据元素,物理存储位置也相邻,并且,顺序表的存储空间需要预先分配
- 存储空间连续,即允许元素的随机访问。
- 存储密度大,内存中存储的全部是数据元素。
- 要访问特定元素,可以使用索引访问,时间复杂度为 O(1)。
- 要想在顺序表中插入或删除一个元素,都涉及到之后所有元素的移动,因此时间复杂度为 O(n)。
优点:
ü 存取速度高效,通过下标来直接存储,随机访问的特点。
ü 方法简单,各种高级语言中都有数组,容易实现。
ü 不用为表示节点间的逻辑关系而增加额外的存储开销。
缺点:
- 在顺序表中做插入、删除操作时,平均移动表中的一半元素,因此对n较大的顺序表效率低。
- 需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
1.3.2 单链表特点
长度不固定,可以任意增删。
存储空间不连续,数据元素之间使用指针相连,每个数据元素只能访问周围的一个元素(根据单链表还是双链表有所不同)。
存储密度小,因为每个数据元素,都需要额外存储一个指向下一元素的指针(双链表则需要两个指针)。
要访问特定元素,只能从链表头开始,遍历到该元素,时间复杂度为 O(n)。
在特定的数据元素之后插入或删除元素,不涉及到其他元素的移动,因此时间复杂度为 O(1)。双链表还允许在特定的数据元素之前插入或删除元素。
优点:
ü 插入和删除速度快,保留原有的物理顺序,比如:插入或者删除一个元素时,只需要改变指针指向即可
缺点:
- 要占用额外的存储空间存储元素之间的关系,存储密度降低。存储密度是指一个节点中数据元素所占的存储单元和整个节点所占的存储单元之比。
- 链表不是一种随机存储结构,不能随机存取元素。
三、顺序表与链表的优缺点切好相反,那么在实践应用中怎样选取存储结构呢?通常有以下几点考虑:
(1)顺序表的存储空间是静态分配的,在程序执行之前必须明确规定它的存储规模,也就是说事先对“MaxSize”要有合适的设定,设定过大会造成存储空间的浪费,过小造成溢出。因此,当对线性表的长度或存储规模难以估计时,不宜采用顺序表。然而,链表的动态分配则可以克服这个缺点。链表不需要预留存储空间,也不需要知道表长如何变化,只要内存空间尚有空闲,就可以再程序运行时随时地动态分配空间,不需要时还可以动态回收。因此,当线性表的长度变化较大或者难以估计其存储规模时,宜采用动态链表作为存储结构。
但在链表中,除数据域外海需要在每个节点上附加指针。如果节点的数据占据的空间小,则链表的结构性开销就占去了整个存储空间的大部分。当顺序表被填满时,则没有结构开销。在这种情况下,顺序表的空间效率更高。由于设置指针域额外地开销了一定的存储空间,从存储密度的角度来讲,链表的存储密度小于1.因此,当线性表的长度变化不大而且事先容易确定其大小时,为节省存储空间,则采用顺序表作为存储结构比较适宜。
(2)基于运算的考虑(时间)
顺序存储是一种随机存取的结构,而链表则是一种顺序存取结构,因此它们对各种操作有完全不同的算法和时间复杂度。例如,要查找线性表中的第i个元素,对于顺序表可以直接计算出a(i)的的地址,不用去查找,其时间复杂度为0(1).而链表必须从链表头开始,依次向后查找,平均需要0(n)的时间。所以,如果经常做的运算是按序号访问数据元素,显然顺表优于链表。
反之,在顺序表中做插入,删除时平均移动表中一半的元素,当数据元素的信息量较大而且表比较长时,这一点是不应忽视的;在链表中作插入、删除,虽然要找插入位置,但操作是比较操作,从这个角度考虑显然后者优于前者。
(3)基于环境的考虑(语言)
顺序表容易实现,任何高级语言中都有数组类型;链表的操作是基于指针的。相对来讲前者简单些,也用户考虑的一个因素。
结论:
总之,两种存储结构各有长短,选择哪一种由实际问题中的主要因素决定。通常“较稳定”的线性表,即主要操作是查找操作的线性表,适于选择顺序存储;而频繁做插入删除运算的(即动态性比较强)的线性表适宜选择链式存储。