数据结构学习笔记(二:线性表)
线性表的定义和基本操作
1. 线性表的定义
-
线性表是具有相同数据结构类型的n(n≥0)个数据元素的有限序列,其中表长为n,当n=0时线性表是一个空表。
-
线性表的特点:
- 表中的元素有限
- 表中的元素具有有逻辑上的顺序性,表中的元素有其先后次序。
- 表中的元素都是数据元素,每个元素都是单个元素。
- 表中元素的数据类型都相同,这意味着每个元素都占有相同大小的存储空间。
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
-
注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构。
2.线性表的基本操作
- InitList(&L):
- Length(L):
- LocateElem(L,e):
- GetElem(L,i):
- ListInsert(&L,i,e):
- ListDelete(&L,i,&e):
- PrintList(L):
- Empty(L):
- DestroyList(&L):
线性表的顺序表示
1. 顺序表的定义
- 顺序表是用一组地址连续的存储单元依次存储线性表中的数据元素,从而是的逻辑相邻的两个元素在物理位置上也相邻。
- 称 i 为元素 ai 在线性表中的位序。
- 顺序表特点:
- 逻辑顺序与其物理顺序相同,插入和删除操作需要移动大量元素
- 随机访问,即通过首地址和元素序号可在时间O(1)内找到指定元素
- 存储密度高,每个节点只存储数据元素
- 除第一个元素外,每个元素有且仅有一个直接前驱。
- 除最后一个元素外,每个元素有且仅有一个直接后继。
- 注意:线性表中元素的位序是从1开始的,而数组中元素的下标是从0开始的。
- 静态顺序表存储类型表述:
#define MaxSize 50 //定义线性表的最大长度
typedef struct {
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
} SqList; //顺序表的类型定义
- 动态顺序表存储类型表述:
#define InitSize 50 //定义线性表的初始长度
typedef struct {
ElemType *data; //指示动态分配数组的指针
int MaxSize, length; //数组的最大容量和当前个数
} SqList; //动态分配数组顺序表的类型定义
C的初始动态分配语句:
L.data = (ElemType*)malloc(sizeof(ElemType) * InitSize);
2. 顺序表的基本操作
- 算法思路:
- 1.判断i的值是否正确
- 2.判断表长是否超过数组长度
- 3.从后向前到第i个位置,分别将这些元素都向后移动一位
- 4.将该元素插入位置i 并修改表长
- 代码
- 分析:
- 最好情况:在表尾插入(即i=n+1),元素后移语句将不执行,时间复杂度为O(1)。
- 最坏情况:在表头插入(即i=1),元素后移语句将执行n次,时间复杂度为O(n)。
- 平均情况:假设pi(pi=1/(n+1) )是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时所需移动结点的平均次数为 n/2,时间复杂度为O(n)
- 算法思路:
- 1.判断i的值是否正确
- 2.取删除的元素
- 3.将被删元素后面的所有元素都依次向前移动一位
- 4.修改表长
- 代码
- 分析
- 最好情况:删除表尾元素(即i=n),无须移动元素,时间复杂度为O(1)。
- 最坏情况:删除表头元素(即i=1),需要移动除第一个元素外的所有元素,时间复杂度为O(n)。
- 平均情况:假设pi(pi=1/n)是删除第i个位置上结点的概率,则在长度为n的线性表中删除一个结点时所需移动结点的平均次数为 (n-1)/2,时间复杂度为O(n)
3.顺序表的优缺点
- 优点:存取速度快
- 缺点:插入和删除操作效率低
线性表的链式存储
1.单链表的定义
- 单链表是指通过一组任意的存储单元来存储线性表中的数据元素。
- 单链表每个结点由数据与和指针域两部分构成。
- 单链表节点类型的描述:
typedef struct LNode { //定义单链表节点类型
ElemType data; //数据域
struct LNode *next; //指针域
}LNode, *LinkList; //LNode是struct LNode的别名,*LinkList是该结构体的指针类型
- 单链表是非随机存取的存储结构,即不能直接找到表中的某个特定的结点。
- 通常用头指针来表示一个单链表,头指针为NULL时表示一个空表。
- 为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点,头结点的数据域可为空,也可记录表长等信息,指针域指向线性表的第一个元素结点。
- 头指针和头结点的区分:
- 不管带不带头结点,头指针始终指向链表的第一个结点,而头结点是带头结点链表中的第一个结点,结点内通常不存储信息。
- 引入头结点的优点:
- 第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都是指向头结点的非空指针,因此空表和飞控表的处理也就得到了统一。
2.单链表的操作
- 访问后继结点的时间复杂度为O(1),访问前驱结点的时间复杂度为O(n)。
- 1.头插法建立单链表:
- 建立新的结点分配内存空间,将新结点插入到当前链表的表头。
- 读入数据的顺序与生成的链表中的元素的顺序是相反的
- 每个结点插入的时间为O(1),设单链表长度为n,则总时间复杂度为O(n)
- 代码
- 2.尾插法建立单链表:
- 建立新的结点分配内存空间,将新结点插入到当前链表的表尾
- 读入数据的顺序和生成的链表中节点的次序一致
- 时间复杂的与头插法相同
- 代码
- 3.按序号查找结点
- 在单链表中从第一个结点出发,顺指针next域逐个往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL。
- 按序号查找操作的时间复杂度为O(n)
- 代码
- 4.按值查找结点
- 从单链表第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL。
- 按值查找操作的时间复杂度为O(n)
- 代码
- 5.插入
- 插入操作是将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i−1个结点,再在其后插入新结点。
- 算法思路:
1.取指向插入位置的前驱结点的指针
① p=GetElem(L,i-1);
2.令新结点s的指针域指向p的后继结点
② s->next=p->next;
3.令结点p的指针域指向新插入的结点s
③ p->next=s; - 本算法主要的时间开销在于查找第i-1个元素,时间复杂度为O(n),之后插入新节点的时间复杂度仅为O(1)
- 对某一结点的前插操作:
1. 找到第i-1个结点,转化为后插操作,时间复杂度为O(n) 2. 将待插入的结点*s插入到*p前面,我们仍然将*s插入到*p的后面,然后交换它们的数据域,时间复杂度为O(1)
//将*s结点前插到*p的主要代码片段
s->next = p->next;
p->next = s;
temp = p->data;
p->data = s->data;
s->data = temp;
- 删除操作是将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i−1个结点,即被删结点的前驱结点,再将其删除。
- 算法思路:
1.取指向删除位置的前驱结点的指针 p=GetElem(L,i-1);
2.取指向删除位置的指针 q=p->next;
3.p指向结点的后继指向被删除结点的后继 p->next=q->next
4.释放删除结点 free(q); - 时间复杂度为O(n)
- 删除给定结点*p:
删除结点*p的操作可以用删除*p的后继结点操作来实现,实质就是将其后继结点的值赋予自身,然后删除后继结点,也能使得时间复杂度为O(1)
//主要代码片段
q = p->next;
p->data = q->data;
p->next = q->next;
free(q);
3.双链表
- 为克服单链表访问前驱结点只能从头开始遍历的缺点,引入了双链表
- 双链表有两个指针prior和next,分别指向其前驱结点和后继结点
- 双链表中结点类型的描述如下:
typedef struct DNode {
ElemType data;
struct DNode *prior, *next;
} DNode, *DLinklist;
-
- 插入:(方法不唯一)
① s->next = p->next;
② p->next->prior = s;
③ s->prior = p;
④ p->next = s;
- 插入:(方法不唯一)
-
- 删除:
① p->next=q->next;
② q->next->prior=p;
③ free(q);
- 删除:
4.循环链表
- 循环单链表
- 表中的最后一个结点的指针不是NULL,而是改为指向头结点。从而整个链表形成一个环
- 循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针
- 循环单链表可以从表中的任意一个节点开始遍历整个链表。
- 循环单链表的插入、删除操作无需判断是否为表尾。
- 循环单链表不设头指针而仅设尾指针,从而使得操作效率更高。因为若设尾指针r,则r->next即为头指针,对于表头与表尾进行操作都只需要O(1)的时间复杂度。
- 表中的最后一个结点的指针不是NULL,而是改为指向头结点。从而整个链表形成一个环
- 循环双链表
- 表中最后一个结点的next指针指向头结点,头结点的prior指针指向尾结点
- 当循环双链表为空表时,其头结点的prior和next都等于头指针L
- 表中最后一个结点的next指针指向头结点,头结点的prior指针指向尾结点
5,静态链表
- 借助数组来表述线性表的链式存储结构,与顺序表一样须要预先分配一块连续的内存空间。
- 指针域存放数组下标
- 以next==-1作为结束标志
- 没有单链表好用,主要应用于不支持指针的高级语言
顺序表和链表的比较
1. 存取(读写)方式
顺序表可以顺序存储,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置上执行存或取的操作,顺序表仅需一次访问,而链表则需要从表头开始依次访问i次。
2. 逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系时通过指针链接来表示的。
3. 查找、插入和删除操作
- 对于按值查找,顺序表无序时,两者的时间复杂度均为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n).
- 对于按序查找,顺序表支持随机访问,时间复杂度为O(1);链表的平均时间复杂度为O(n)。
- 对于插入和删除操作,顺序表平均需要移动半个表长的元素,而链表只需修改相关结点的指针域即可。
- 由于链表的每个结点都带有指针域,故存储密度不够大
4. 空间分配
- 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入元素则会内存溢出,因此需要预先分配足够大的存储空间。分配过大会造成浪费,分配过小会溢出。
- 顺序存储再动态存储分配情形下,虽春初空间可以扩充,但需要移动大量元素,导致操作效率降低。而若内存中没有更大块的连续存储空间,则会导致分配失败。
- 链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。