文章目录
三、线性表
1、定义
线性表 ( L i s t List List ):零个或多个数据元素的有限序列。
- 序列:元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。
- 有限:线性表强调是有限的,元素个数当然也是有限的。
- 数学定义:若将线性表记为
(
a
1
,
.
.
.
,
a
i
−
1
,
a
i
,
a
i
+
i
,
.
.
.
,
a
n
)
(a_1, ...,a_{i-1}, a_i, a_{i+i}, ..., a_n)
(a1,...,ai−1,ai,ai+i,...,an), 则表中
a
i
−
1
a_{i-1}
ai−1 领先于
a
i
a_{i}
ai,
a
i
a_{i}
ai 领先于
a
i
+
1
a_{i+1}
ai+1,称
a
i
−
1
a_{i-1}
ai−1 是
a
i
a_{i}
ai 的直接前驱元素,
a
i
+
1
a_{i+1}
ai+1 是
a
i
a_{i}
ai 的直接后继元素。当$i=1,2,…,n-1
时,
时,
时,a_{i}$ 有且仅有一个直接后继,当 $i=2, 3,…,n $ 时,
a
i
a_{i}
ai 有且仅有一个直接前驱。
- 线性表元素的个数 n ( n > 0 ) n (n>0) n(n>0) 定义为线性表的长度,当 n = 0 n=0 n=0 时,称为空表。
- 在较复杂的线性表中,一个数据元素可以由若干个数据项组成。譬如“刘华强”这一数据元素,包含有“喜爱的食物:西瓜”、“坐骑:摩托车”等数据项。
2、线性表的抽象数据类型
线性表的基本操作:
InitList(*L)
: 初始化操作,建立一个空的线性表 LListEmpty (L)
: 若线性表为空,返回 true, 否则返回 falseClearList(*L)
: 将线性表清空GetElem ( L,i,*e)
:将线性表 L 中的第 i 个位置元素值返回给 eLocateElem ( L,e)
:在线性表 L 中查找与给定值 e 相等的元素,如果查找成功,返回该元素在表中序号表示成功;否则,返回 0 表示失败Listinsert ( *L,i,e )
: 在线性表 L 中的第 i 个位置插入新元素 eListDelete(*L,i,*e)
:删除线性表 L 中第 i 个位五元素,并用 e 返回其值ListLength (L)
: 返回线性表 L 的元素个数
3、线性表的顺序存储结构
(1)顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
(2)顺序存储方式
- C中的一维数组可以来实现顺序存储结构
- 线性表的顺序存储结构,就是在内存中找了块地儿,通过占位的形式,把一定内存空间给占了,然后把相同数据类型的数据元素依次存放在这块空地中。
- 线性表中,我们估算这个线性表的最大存储容量,建立一个数组,数组的长度就是这个最大存储容量。
- 有了起始的位置,也有了最大的容量,于是我们可以在里面增加数据了。随着数据的插入,我们线性表的长度开始变大,不过线性表的当前长度不能超过存储容量,即数组的长度。
据此与上述对应的,顺序存储结构需要三个属性:
- 存储空间的起始位置:数组 d a t a data data, 它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度 M a x S i z e MaxSize MaxSize。
- 线性表的当前长度: l e n g t h length length。
(3)数据长度与线性表长度区别
数组的长度是存放线性表的存储空间的长度,存储分配后这个量是一般是不变的;
线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
(4)地址计算方法
用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度。
存储器中的每个存储单元都有自己的编号,这个编号称为地址。
由于每个数据元素,不管它是整型、实型还是字符型,它都是需要占用一定的存储单元空间的。假设占用的是
c
c
c 个存储单元,那么线性表中第
i
+
1
i+1
i+1 个数据元素的存储位置和第
i
i
i 个数据元素的存储位置满足下列关系(
L
O
C
LOC
LOC 表示获得存储位置的函数)。
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
i
)
+
c
LOC(a_{i+1})= LOC(a_i)+c
LOC(ai+1)=LOC(ai)+c
L
O
C
(
a
i
)
=
L
O
C
(
a
1
)
+
(
i
−
l
)
∗
c
LOC(a_i)= LOC(a_1)+(i-l)*c
LOC(ai)=LOC(a1)+(i−l)∗c
(5)存入或者取出数据
通过这个公式,你可以随时算出线性表中任意位置的地址,不管它是第一个还是最后一个,都是相同的时间。
那么我们对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用我们算法中学到的时间复杂度的概念来说,它的存取时间性能为
O
(
1
)
O(1)
O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。
4、顺序存储结构的插入与删除
(1)元素操作
- 获得元素操作
- 要实现 G e t E l e m GetElem GetElem 操作,即将线性表 L L L中的第 i i i 个位置元素值返回,只要 i i i 的数值在数组下标范围内,就是把数组第 i − 1 i-1 i−1下标的值返回即可。
- 插入操作
- 要实现
Listinsert (*L,i,e)
, 即在线性表 L L L 中的第 i i i 个位置插入新元素 e e e - 插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第 i i i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置 i i i 处;
- 表长加 1 1 1。
- 要实现
- 删除操作
- 删除算法的思路:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减 1 1 1。
- 删除算法的思路:
(2)操作的时间复杂度
- 如果元素要插入到最后一个位置,或者删除最后一个元素,此时时间复杂度为 O ( 1 ) O(1) O(1), 因为不需要移动元素的。
- 最坏的情况,如果元素要插入到第一个位置或者删除第一个元素,此时时间复杂度是 O ( n ) O(n) O(n)。
- 至于平均的情况,由于元素插入到第 i i i 个位置,或删除第 i i i 个元素,需要移动 n − i n-i n−i个元素。
- 根据概率原理,每个位置插入或删除元素的可能性是相同的,也就说位置靠前,移动元素多,位置靠后,移动元素少。最终平均移动次数和最中间的那个元素的移动次数相等,为 n − 1 2 \frac{n-1}{2} 2n−1。推导可以得出,平均时间复杂度还是 O ( n ) O(n) O(n)。
- 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是 O ( 1 ) O(1) O(1);而插入或删除时,时间复杂度都是 O ( n ) O(n) O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。
(3)优缺点
- 优点
- 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存取表中任一位置的元素
- 缺点
- 插入和删除操作需要移动大量元素
- 当线性表长度变化较大时,难以确定存储空间的容量
- 造成存储空间的 “碎片”
5、线性表的链式存储结构
(1)线性表的链式存储结构——通俗解释
干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
(2)线性表的链式存储结构——特点
用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
(3)线性表的链式存储结构——区别
以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
- 为了表示每个数据元素 a i a_i ai 与其直接后继数据元素 a i + 1 a_{i+1} ai+1 之间的逻辑关系,对数据元素 a i a_i ai 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。
(4)线性表的链式存储结构——术语名词
-
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域,指针域中存储的信息称做指针或链。这两部分信息组成数据元素 a i a_i ai 的存储映像,称为结点(Node)。
-
结点由存放数据元素的数据域存放后继结点地址的指针域组成
-
n 个结点(即的存储映像)链结成一个链表,即为线性表( a 1 , a 2 , … , a n a_1,a_2, …,a_n a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域,将线性表的数据元素按其逻辑次序链接在一起。
-
我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。
-
最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为 “空”(通常用 NULL 或 "^"符号表示)
-
为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
-
-
-
带有头结点的单链表
-
空链表
-
头指针与头结点的异同
- 头指针:指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
- 头指针具有标识作用,所以常用头指针冠以链表的名字。
- 无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
- 头结点:为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义( 也可存放链表的长度 )
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
- 头结点不一定是链表必须要素
- 头指针:指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针。
6、单链表的读取
不同于线性表的顺序存储结构,单链表获取第 i i i 个元素的数据比较困难, 获得链表第 i i i 个数据的算法思路:
- 声明一个结点 p p p 指向链表第一个结点,初始化 j j j 从1开始;
- 当 j < i j<i j<i 时,就遍历链表,让 $p 的指针向后移动,不断指向下一结点, 的指针向后移动,不断指向下一结点, 的指针向后移动,不断指向下一结点,j$ 累加1;
- 若到链表末尾 p p p 为空,则说明第 i i i 个元素不存在;
- 否则查找成功,返回结点 p p p 的数据
缺点:
- 说白了,就是从头开始找,直到第 i i i 个元素为止。由于这个算法的时间复杂度取决于 i i i 的位置,当 i = 1 i=1 i=1 时,则不需遍历,第一个就取出数据了,而当 i = n i=n i=n 时则遍历 n − i n-i n−i 次才可以。因此最坏情况的时间复杂度是 O ( n ) O(n) O(n)。
- 由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用 for 来控制循环。其主要核心思想就是“工作指针后移”,这其实也是很多算法的常用技术。
7、单链表的插入与删除
(1)单链表的插入
-
假设存储元素 e e e 的结点为 s s s, 要实现结点 p p p、 p − > n e x t p->next p−>next 和 s s s 之间逻辑关系的变化,只需将结点 s s s 插入到结点 p p p 和 p − > n e x t p->next p−>next 之间即可。
-
让 p p p 的后继结点改成 s s s 的后继结点,再把结点 s s s 变成 p p p 的后继结点。
s->next=p->next; p->next=s;
-
单链表的表头和表尾的特殊情况
(2)单链表的删除
- 设存储元素 a i a_i ai 的结点为 q q q, 要实现将结点 q q q 删除单链表的操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可。
p->next=p->next->next
, 用 q q q 来取代 p − > n e x t p->next p−>next。q=p->next; p->next=q->next;
(3)分析
- 单链表插入和删除算法的时间复杂度都是 O ( n ) O(n) O(n)。如果在我们不知道第 i i i 个元素的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第 i i i 个位置,插入10 个元素,对于顺序存储结构意味着,每一次插入都需要移动 n − i n-i n−i 个元素,每次都是 O ( n ) O(n) O(n)。而单链表,我们只需要在第一次时,找到第 i i i 个位置的指针,此时为 O ( n ) O(n) O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是 O ( 1 ) O(1) O(1) 。
- 显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显。
8、单链表的整表创建
单链表和顺序存储结构不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
- 声明一结点 p p p 和计数器变量 i i i ;
- 初始化一空链表 L L L;
- 让 L L L 的头结点的指针指向 N U L L NULL NULL, 即建立一个带头结点的单链表;
- 循环:
- 生成一新结点赋值给 p p p;
- 随机生成一数字赋值给 p p p 的数据域 p − > d a t a p->data p−>data;
- 将 p p p 插入到头结点与前一新结点之间。
(1)头插法
本质是插队的办法,就是始终让新结点在第一的位置。
/*随机产生n 个元素的值,建立带表头结点的单链线性表L(头插法)*/
void CreateListHead ( LinkList *L, int n)
{
LinkList p;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL; /*先定立一个带头结点的单链表*/
for ( i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node));/*生成新结点*/
p->data = rand()%100+l;/*随机生成 100 以内的数字*/
p->next = (*L)->next;
(*L)->next = p; /*插入到表头*/
}
}
(2)尾插法
把每次新结点都插在最后,所谓的先来后到。
/* 随机产生 n 个元素的值,建立带表头结点的单链线性表L(尾插法)*/
void CreateListTail( LinkList *L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /*初始化随机数种子*/
*L = (LinkList)malloc(sizeof(Node)); /*为整个线性表*/
r=*L; /*r 为指向尾部的结点*/
for ( i=0; i<n; i++)
{
P = (Node *)malloc(sizeof(Node)); /* 生成新结点*/
p->data = rand()%100+l; /*随机生成 100 以内的敷字*/
r->next=p; /*将表尾终端结点的指针指向新结点*/
r = P; /*将当前的新结点定义为表尾终端结点*/
}
r->next = NULL; /*表示当前链表结束*/
}
L L L 是指整个单链表,而 r r r 是指向尾结点的变量, r r r 会随着循环不断地变化结点,而 L L L 则是随着循环增长为一个多结点的链表。