考研数据结构与算法线性表
Posted MangataTS
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了考研数据结构与算法线性表相关的知识,希望对你有一定的参考价值。
文章目录
一、线性表定义
线性表是最常用且最简单的一种数据结构,简言之,一个线性表是
n
n
n 个数据元素的有限序列,当
n
n
n 为
0
0
0 的时候线性表是一个空表,用
L
L
L 命名线性表,则一般表示为:
L
=
(
a
1
,
a
2
,
a
3
…
…
,
a
i
,
a
i
+
1
…
…
,
a
n
)
L=(a_1,a_2,a_3……,a_i,a_i+1……,a_n)
L=(a1,a2,a3……,ai,ai+1……,an)
其中,
a
1
a_1
a1 表示表首元素,
a
n
a_n
an 表示表尾元素,除了表首元素外其余元素皆有直接后继节点,除了表尾元素其余元素皆有直接前驱节点。
线性表是一个相当灵活的数据结构,他的长度可根据需要增长或缩短,即对线性表的元素不仅可以访问,还可以进行插入和删除等
于是我们能得出线性表的特点如下:
- 表中元素的个数 有限 ,但是可以扩张和删减
- 表中元素具有逻辑上的顺序性 表中元素有其先后次序。
- 表中元素都是数据元素,每个元素都是单个元素。
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- 表中元素具有抽象性, 即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
二、线性表基本操作
一个数据结构的基本操作是指其最核心、最基本的操作。其他较复杂的操作可通过调用其 本操作来实现。线性表的主要操作如下
name | operation |
---|---|
InitList(&L) | 初始化表,构造一个空的线性表 |
Length(L) | 求表长,返回线性表 L L L 的长度,即 L L L 中数据元素的个数。 |
LocateElem(L,e) | 按值查找操作,在表 L L L 中查找具有给定关键宇值的元素。 |
GetElem(L,i) | 按位查找操作,获取表 L L L 中第 i i i 个位置的元素的值 |
Listinsert(&L,i,e) | 插入操作,在表 L L L 中的第 i i i 个位置上插入指定元素 |
ListDelete(&L,i,&e) | 删除操作,删除表 L L L 中第 i i i 个位置的元素,并用 e e e 返回删除元素的值。 |
PrintList(L) | 输出操作 ,按前后顺序输出线性表 L L L 的所有元素值 |
Empty(L) | 判空操作,若 L L L 为空表, 则返回 t r u e true true ,否则返回 f a l s e false false |
DestroyList(&L) | 销毁操作 ,销毁线性表,井释放线性表 L L L 所占用的内存空间。 |
三、线性表的顺序表示
这个顺序表示是从物理上定义的,线性表的顺序存储 称顺序表 它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得 逻辑上相邻的两个元素在物理位置上也相邻 。这个可以简单理解为就是数组。
由于顺序表在逻辑和物理上都是连续的特点,我们很容易通过物理位置的间隔长度和单个元素的大小推断出这个间隔的元素个数,或者反过来计算出一定元素占用的空间,假设每一个元素占用
l
l
l 的物理空间,
L
O
C
(
a
i
)
LOC(a_i)
LOC(ai) 表示
a
i
a_i
ai 元素的物理位置,那么可以得到:
L
O
C
(
a
i
+
1
)
=
L
O
C
(
a
i
)
+
l
LOC(a_i+1) = LOC(a_i) + l
LOC(ai+1)=LOC(ai)+l
以及从第一个元素物理位置推出第
i
i
i 个元素的物理位置:
L
O
C
(
a
i
)
=
L
O
C
(
a
1
)
+
(
i
−
1
)
×
l
LOC(a_i) = LOC(a_1) + (i-1)\\times l
LOC(ai)=LOC(a1)+(i−1)×l
简单的一张图来表示:
这里需要注意的是,数组下表是从 0 0 0 开始的,而顺序表下表是从 1 1 1 开始的
一维数组可以是静态分配的,比如一开始就设定好数组的大小,当然也可以是动态分配的,例如C语言中使用 malloc
分配内存,注意的是这里的动态分配并不是链式存储,仍然是顺序存储,物理结构并未发生改变,只不过分配的空间大小是由程序运行的时候决定。
3.1 顺序表特点
由此我们能得到顺序表的一些特点:
- 顺序表最主要的特点是 随机访问 ,即通过首地址和元素序号可在时间 0 ( 1 ) 0(1) 0(1)内找到指定的元素。
- 顺序表的 存储密度高 ,结点只存储 数据元素 。
- 顺序表逻辑上相邻的元素物理上也相邻,所以 插入和删除 操作需要 移动大量元素
3.2 操作实现
这里提前先说一下,假设表的元素下标是从 1 1 1 开始的,下面展示的是一些基础或者说常用的操作函数
3.2.1 插入操作
在顺序表
L
L
L 的第
i
i
i 个位置(假设表的元素从1开始)插入新元素
e
e
e 。若插入失败返回 false
,否则将顺序表中第
i
i
i 个元素以及后面所有元素往后移动一个位置,然后将新元素放在第
i
i
i 个位置上
bool Listinsert(List &L,int i,ElemType e)
int len = L.length;
if(i < 1 || i > len + 1) return false;//判断插入的位置是否合法
if(len == L.MaxSize) return false;//判断表中的空间是否占满
for(int j = len;j >= i ; --j)//将i以及后面的元素全部往后移动一位
L.data[j + 1] = L.data[j];
L[i] = e;//将元素e放到第i个位置
L.length++;
return true;
3.2.2 删除操作
删除操作和插入操作类似,先判断删除的条件是否合法,然后直接第 i i i 个元素之后的元素往前覆盖即可
bool ListDelete(List &L,int i,ElemType &e)
int len = L.length;
if(i < 1 || i > len) return false;//判断删除的位置是否合法
e = L.data[i];
for(int j = i;j < len; ++j)
L.data[j] = L.data[j+1];
L.length++;
return true;
3.2.3 查找操作
这里就实现一下按值查找吧,我们从前往后,或者从后往前不断的比对顺序表中的元素和我们查找的元素的值是否相等,如果相等的话,那么我们就返回当前的位置,如果没查找到,那么就返回 -1
int LocateElem(List &L,ElemType e)
int len = L.length;
for(int i = 1;i <= len; ++i)
if(L.data[i] == e)
return i;
return -1;
这里最好的情况就是查找的元素在第一个位置,复杂度就为 O ( 1 ) O(1) O(1) ,最坏的情况在最后一个位置,复杂度为 O ( n ) O(n) O(n) ,平均复杂度就是 O ( n ) + O ( 1 ) 2 = O ( n ) \\fracO(n) + O(1)2= O(n) 2O(n)+O(1)=O(n)
3.3 顺序表合并
问题:给定两个非递减的顺序表,然后将这两个表合并成一个新的表,同样希望按照非递减排列
其实思路很简单,我们同时遍历两个顺序表,然后将两个表的当前元素进行比较,直到其中一个表遍历完成,然后我们再将另一个表剩下的元素一次加入即可。不难得到如下代码
void MergeList(List &a,List &b,List &c)
int lena = a.length,lenb = b.length;
c.length = lena + lenb;
c.data = (ElemType*)malloc(c.length * sizeof(ElemType));//给c表申请空间
int i = 1,j = 1,k = 1;
while(i <= lena && j <= lenb) //不断的比对两表中当前位置的元素的大小
if(a.data[i] < b.data[j])//如果a表中元素较小,则插入a表当前的元素
c.data[k++] = a.data[i++];
else //如果b表当前元素较小,则插入b表当前的元素
c.data[k++] = b.data[j++];
while(i <= lena) c.data[k++] = a.data[i++];//插入a顺序表剩下的元素
while(j <= lenb) c.data[k++] = b.data[j++];//插入b顺序表剩下的元素
四、线性表的链式表示
我们上面谈了关于顺序存储的优点和缺点,其中最明显的就是如果发生插入和删除的操作,需要移动大量的元素,导致了效率非常的低效,那么链式存储就帮我们解决了这一问题,让我们从 O ( n ) O(n) O(n) 的复杂度降低到了 O ( 1 ) O(1) O(1) 的复杂度
在链式存储中最为简单的结构便是 单链表 ,它是通过每一个结点(数据元素)存储的一个后继节点指针串联起来,形成了一个在逻辑上连续,而空间 不一定 连续的链式结构,对于每一个数据元素我们称其为 结点 ,一个结点包括两个域。其中存储数据元素信息的域称为 数据域 ,存储直接 后继 存储位置(一般用指针存储)的域称为指针域,多个结点连接在一起就构成了一个链表,即线性表的链式存储
一个常见的链表的结构如下:
typedef struct Node
ElemType data;//数据域
struct Node *next;//指针域
Node;
关于头指针部分
:
头指针的数据与可以不存放数据,只需要指针域指向第一个结点元素即可,但是也是可以存放数据的,即头指针就是整个链表的第一个元素结点,这里头指针不存放数据是为了方便 统一化管理 ,加入我们的链表删除所有的元素,当前链表为空了,这时我们如果需要添加元素,在头指针不为空的版本的话那么就需要特判一下,而如果头指针置空那么则直接往后加入元素即可,换句话说, 头指针数据域为空是为了空链表和非空链表的统一化操作
关于尾结点部分
:
尾部的结点其实就是最后一个结点元素,我们只需要将其指针域置为 NULL
即可
4.1 链式表特点
由此我们能得到关于链式表的一些特点:
- 在插入和删除操作的时候我们只需要修改两个结点的指向复杂度为 O ( 1 ) O(1) O(1)
- 因为存储空间不连续,不能随机访问,每次查找的效率只能遍历链表,效率非常低下
- 能够大大提高空间的利用率
4.2 操作实现
4.2.1 下标找值
我们只需要遍历这个链表,找到第
i
i
i 个下标(从0开始)的元素,如果链表长度不够则返回 NULL
否则找到元素则返回元素的指针
Node *GetElem(Node *Lhead,int i)
Node p = Lhead->next;
int j = 0;
while(p && j < i)
p = p->next;
j++;
if(p) return p;
return NULL;
4.2.2 按值查找
与按照下标查找相对应的就是按值查找了,我们同样的遍历这个链表,然后如果发现值相同,然后返回当前的指针,如果找不到则返回 NULL
Node *LocateElem(Node *Lhead,ElemType e)
Node p = Lhead->next;
while(p && p->data != e)
p = p->next;
return p;
4.2.3 头部插入
我们每次只需要操作头指针和第一个结点的指针,我们让当前新的元素的后继指针指向头指针后继结点指向的元素,然后再让头指针的后继指针指向当前结点,这样就完成了结点的头部插入
void List_Head_Insert(Node *Lhead,ElemType e)
Node *t = (Node *)malloc(sizeof(Node));
t->data = e;
t->next = Lhead->next;
Lhead->next = t;
4.2.4 尾部插入
尾部插入实际上是和头部插入相对应的一个方法,就是每次插入到链表的末尾,我们只需要先遍历链表,然后找到最末尾的位置,最后直接将最末尾的结点指向新的结点即可
void List_Tail_Insert(Node *Lhead,ElemType e)
Node *t = (Node *)malloc(sizeof(Node));
t->data = e;
t->next = NULL;
Node *p = Lhead;
while(p->next考研笔记之数据结构之线性表(严蔚敏版)