算法与数据结构【数据结构】——二、线性表:链表与数组
线性表是最简单的线性结构;
线性结构是一个数据元素的有序集;
一、线性结构与线性表
(1)线性结构的基本特征
- 唯一的“第一元素”
- 唯一的“最后元素”
- 除 最后元素 外,均有 唯一的后继
- 除 第一元素 外,均有 唯一的前驱
(2)线性表的类型定义
数据对象:
D={ ai | ai ∈ElemSet, i=1,2,…,n, n≥0 } {称 n 为线性表的表长; 称 n=0 时的线性表为空表。}
数据关系:
R1={ <ai-1 ,ai >|ai-1 ,ai∈D, i=2,…,n } {设线性表为 (… ai …) 称 i 为 ai 在线性表中的位序。}
基本操作:
更复杂的功能 都可以基于 基本功能 实现
- 结构初始化操作
- InitList( &L ) 创建空表
- 结构销毁操作
- DestroyList( &L ) 销毁表
- 引用型操作
- ListEmpty( L ) 判空
- ListLength( L ) 求表长
- PriorElem( L, cur_e, &pre_e ) 求前驱
- NextElem( L, cur_e, &next_e ) 求后继
- GetElem( L, i, &e ) 索引查找——L[i]
- LocateElem( L, e, compare( ) ) 定位函数/按值查找——if L[i]=cmp(e) return i for i in 1…n
- ListTraverse(L, visit( )) 遍历,依次调用 visit() 函数
- 加工型操作
- ClearList( &L ) 置空表
- PutElem( &L, i, &e ) 按索引修改——L[i]=e
- ListInsert( &L, i, e ) 插入——L[i+1…n+1]=L[i…n],L[i]=e,n++
- ListDelete(&L, i, &e)——e=L[i],L[i…n-1]=L[i+1…n],n–
二、顺序映像与数组
(1)概述
- 逻辑关系:以 x 的存储位置和 y 的存储位置之间某种关系表示逻辑关系<x,y>,如最简单的相邻
- 数据元素:用一组地址连续(存储位置相邻)的存储单元依次存放线性表中的数据元素
- LOC(ai) = LOC(ai-1) + C , LOC(ai) = LOC(a1) + (i-1)×C
- (一个元素占用C,LOC(ai)为基地址(起始地址))
(2)基本操作的代码实现
初始化——O(1)
Status InitList_Sq(SqList &L){
L.elem=(ElemType*) malloc(LIST_INIT_SIZE*sizeof(ElemType));
if (!L.elem) exit(OVERFLOW);
L.length=0;
L.listsize=LIST_INIT_SIZE;
return OK;
}
查找——O(L)
Int LocateElems_Sq(SqList L,ElemType e,Status(*compare)(EIemTyps,ElemType)){
//在顺序表中查询第一个满足判定条件的数据元素,若存在则返回位序,否则返回0
i = 1; // i的初值为第1元素的位序
p = L.elem; // P的初值为第1元素的存储位置
while (i <= L.length && !(*compare)(*p++,e)) ++i;
if (i<=LJength) return i; else return0;
}
插入——O(L)
Status ListInsert_Sq(SqList &L, int i, ElemType e){
// 在L[i]前插入新元素e, i 范围为 1≤i≤L.length+1
q = &(L.elem[i-1]); // q指示插入位置
for (p = &(L.elem(L.length-1]); p>=q; --p) *(p+1) = *p;
*q=e; // 插入e
++L.length; // 表长增I
returnOK;
}
删除——O(L)
Status ListDelete_Sq(SqList &L,int i,ElemType &e){
if ( (i<1) || (i>L.length) ) return ERROR;//删除位置不合法
P = &(L.elem[i-l]); // p为被删除元索的位置
e = *p; // 被删除元素的值被赋给e
q=L.eIem + L.Iength - 1; //表尾元素的位置
for (++p;p<=q;p++) *(p-1)=*p; // 被删除元索之后的元素左移
--L.length; // 表长减1
return OK;
}
(3)数组(以及内容总结)
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
- 线性表?
- 线性表:如数组,链表、队列、栈等。线性表上数据只有 前和后 方向。
- 非线性表:如二叉树、堆、图等。非线性表中数据之间并不是只有前后关系
- 连续的内存空间和相同类型的数据?
- 根据下标的“随机访问”
- 原理——寻址公式:a[i]_address = base_address + i * data_type_size (data_type_size 表示数组中每个元素的大小)
- 数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)
- 低效的插入删除
- 当数据有序:搬迁原有数据——消耗资源
- 当数据无序:插入有如下优化
- 基于JVM的垃圾回收思想——只记录,一起删:记录下已经删除的数据,当数组没有更多空间存储时,再触发真正的删除操作。
- 根据下标的“随机访问”
-
警惕数组的访问越界问题!
-
容器能否完全替代数组?——以 JAVA中的ArrayList 为例
- 优势:封装数组操作细节,包括插入、删除等;支持动态扩容。
- 劣势:1. Java ArrayList 无法存储基本类型,int、long需要封装为 Integer、Long 类,但是包装类本身有性能消耗; 2. 数据大小已知,操作简单,可以直接用数组; 3. 表示多维数组时,数组更直观。—— [应用用容器没事,底层开发最好用数组]
三、链式映像与链表
(1)概述
- 用一组地址任意的存储单元存放线性表中的数据元素。
- 元素(数据元素的映象) + 指针(指示后继元素存储位置) = 结点 (表示数据元素 或 数据元素的映象)。
- “结点的序列”称作链表,即用指针串联分散的内存
- 头指针:
- 第一个数据元素的存储地址作为线性表的地址,称作线性表的头指针。
- 有时有虚拟头结点,以指向头节点的指针为链表的头指针
(2)结点和单链表的 C 语言描述
typedef struct LNode{ // 结点类型
ElemType data; // 数据域
struct Lnode *next; // 指针域
}Lnode, *LinkList
LinkList L; // L为单链表的头指针
(3)单链表操作的实现
GetElem(L, i, e) 取第i个数据
移动指针,比较 j 和 i : O(L)
Status GetEIem_L (LinkList L,int i,ElemType &e){
//L是带头结点的链表的头指针,以e返回第i个元素
p = L->next; j = 1; //p指向第一个结点,j为计数器
while (p && j<i) {p=p->next; ++j; } //顺指针向后查找,直到p指向第i个 或 p为空
If (!p || j>i) return ERROR; //第i个元素不存在
e=p->data; return OK;
}
ListInsert(&L, i, e) 插入
找到第i-1个结点,修改其后继指针 : O(L)
Status Listlnsert_L(LinkList L, int i,ElemType e){
// L为带头结点的单链表的头指针,在链表第i个结点前插入元素e
p =L; j=0;
while (p && j<i-1) { p=p->next; ++j; } //寻找第i-1个结点
If (!p || j>i-l) return ERROR; // i大于表长或者小于1
s = (LinkList) malloc(sizeof(Lnode) ); // 生成新结点
s->data = e; s->next = p->next; p->next=s; // 插入
returnOK;
}
ListDelete(&L, i, e) 删除
找到第i-1个结点,修改其后继指针 : O(L)
Status ListDeIete_L(LinkList L,int i,ElemType e) {
// 删除以 L 为头指针(带头结点)的单链表中第 i 个结点
p = L; j = 0;
while (p->next && j<i-1) {p=p->next; ++j; } // 寻找第个结点,并令p指向其前趋
If (!(p->next) || j>i-1) return ERROR; //删除位置不合理
q = p->next; p -> next = q -> next; // 删除并释放结点
e = q -> data; free(q); returnOK;
}
ClearList(&L) 重置为空表
找到头指针,依次后继置空 : O(L)
void ClearList(&L) { //将单链表重新置为一个空表
while (L->next){
p=L->next; L->next=p->next; free(p);
}
}
CreateList(&L, n) 生成 n元素链表
生成链表是结点“逐个插入” : O(L)
void CreateListL(LinkList &L,int n){ // 逆序输入n个数据元素,建立带头结点的单链表
L=(LinkList)malloc(sizeof (Lnode) );
L->next = NULL; //先建立一个带头结点的单链表
for (i = n; i > 0; --i) {
p=(LinkList)malloc(sizeof(LNode));
scanf(&p->data); // 输入元素值
p->next=L->next; L->next=p; // 插入
}
}
改进链表操作的设置
- 增加“表长”、“表尾指针” 和 “当前位置的指针” 三个数据域(时间性)
- 将基本操作中的“位序 i ”改变为“指针 p ”(淡化位序,强化位置)
(4)特殊链表
双向链表
typedef struct DuLNode{
ElemType data;//数据域
struct DuLNode *prep; //指向前驱的指针域
struct DuLNode *next; //指向后继的指针域
}DuLNode,*DuLinkList;
- 可以支持双向遍历,这样也带来了双向链表操作的灵活性
- 用空间换时间的设计思想
循环链表
- 单链表的尾结点指针指向空地址,而循环链表的尾结点指针是指向链表的头结点。
- 判别链表中最后一个结点的条件:“后继是否为头结点”
- 适合于:数据具有环型结构特点,如约瑟夫问题(猴子分桃问题)
- 升级版:双向循环列表 (查找不变,插入、删除需要双方向修改)
有序表表示集合
- 不允许出现a[i] = a[i+1]
(5)链表与数组的对比
对比项 | 插入删除 | 随机访问 | 易读与预读性 | 内存占用 | 内存消耗 |
---|---|---|---|---|---|
数组 | O(n) | O(1) | 简单 | 占用连续内存 | 只存基础数据 |
列表 | O(1) | O(n) | 复杂,无法预读 | 只需零散内存 | 多存一份next |
和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。
四、应用——多项式合并
(1)算法简述
- 第一步:分别建立线性链表LPA、LPB、LPC的表头。
- 第二步:根据输入构建LPA、LPB,以储存两个多项式。在构建过程中采用插入排序的方式,保证LPA、LPB按照升序排列。
- 第三步:建立两个指针P、Q,分别指向LPA、LPB的表头。
- 第四步:为LPC申请新的结点域,加入链表末尾。当P指向域的exp等于Q指向域的exp时,新结点域的exp等于这一共同值,coef等于两者之和,P、Q指针向后移动一位;当P指向域的exp小于Q指向域的exp时,新结点域等于P指向域,P指针向后移动一位;当P指向域的exp大于Q指向域的exp时,新结点域等于Q指向域,Q指针向后移动一位。
- 第五步:重复第四步,直到P或者Q指向目标链表的结尾。
- 第六步:如果P或者Q指针存在未指向结尾的,则将其位置到结尾的所有结点域加入LPC链表末尾。
- 第七步:对LPC链表检查,当出现coef为零时,删除节点。
- 第八步:从链表头开始,遍历并输出LPC。
(2)源代码
# include <stdio.h>
# include <stdlib.h>
# define LEN(x) sizeof(x)
typedef struct node{
double coef; int exp; struct node *next;
}node;
void insert(node *head,double coef,int exp){
node *point = head;
while ((point->next) != NULL && ((point->next)->exp)<exp) point = point->next;
if ((point->next) == NULL){
node *newer = (node *)malloc(sizeof(LEN(node)));
newer->coef=coef; newer->exp=exp;
newer->next=NULL; point->next=newer;
}
else if (((point->next)->exp) == exp) {
point = point->next; point->coef += coef;
}
else if (((point->next)->exp) > exp) {
node *newer = (node *)malloc(sizeof(LEN(node)));
newer->coef=coef; newer->exp=exp;
newer->next=(point->next); point->next=newer;
}
}
void addition (node *LA, node *LB,node *LC){
node *P=LA->next,*Q=LB->next,*T=LC;
// 开始加法
while ( P != NULL && Q != NULL) {
node *newer = (node *)malloc(sizeof(LEN(node)));
T->next = newer; newer->next = NULL; T=T->next;
if ( (P->exp) == (Q->exp) ){ //当P指向域的exp等于Q指向域的exp时
newer->coef = (P->coef) + (Q->coef); newer->exp = P->exp;
P=P->next; Q=Q->next;
}
else if ( (P->exp) < (Q->exp) ) { //当P指向域的exp小于Q指向域的exp时
newer->coef = (P->coef); newer->exp = P->exp;
P=P->next;
}
else if ( (P->exp) > (Q->exp) ) { //当P指向域的exp大于Q指向域的exp时
newer->coef = (Q->coef); newer->exp = Q->exp;
Q=Q->next;
}
}
// 如果存在未指向结尾的指针,则加入所有余剩结点域
while ( P != NULL) {
node *newer = (node *)malloc(sizeof(LEN(node)));
newer->coef = P->coef; newer->exp = P->exp;
T->next = newer; newer->next = NULL; T=T->next;
P = P->next;
}
while ( Q != NULL) {
node *newer = (node *)malloc(sizeof(LEN(node)));
newer->coef = Q->coef; newer->exp = Q->exp;
T->next = newer; newer->next = NULL; T=T->next;
Q = Q->next;
}
}
void printing(node *LC){
node *T=LC;
int cnt=0;
while ((T->next) != NULL) {
while ((T->next)!=NULL && ((T->next)->coef)==0) T->next = (T->next)->next;
//当出现coef为零时,删除节点。
T = T->next; cnt++;
if (T==NULL) break;
if ((T->exp) == 0) {
if (cnt==1 || (T->coef)<0) printf("%lf",T->coef);
else printf("+%lf",(T->coef));
}
else if ((T->exp) == 1) {
if (cnt==1 || (T->coef)<0) printf("%lf*X",(T->coef));
else printf("+%lf*X",(T->coef));
}
else {
if (cnt==1 || (T->coef)<0) printf("%lf*X^%d",(T->coef),(T->exp));
else printf("+%lf*X^%d",(T->coef),(T->exp));
}
}
printf("\n");
}
void main(){
// 建立表头并初始化
node LPA,LPB,LPC;
LPA.next=NULL; LPB.next=NULL; LPC.next=NULL;
// 根据输入构建LPA、LPB,保证升序
double coefa,coefb;
int expa,expb;
printf("请输入LPA,每行为coef 与 exp:\n");
while (1){
scanf("%lf %d",&coefa,&expa);
if (expa<0) break;
insert(&LPA, coefa, expa);
}
printf("LPA表达式如下:\n"); printing(&LPA);
printf("请输入LPB,每行为coef 与 exp:\n");
while (1){
scanf("%lf %d",&coefb,&expb);
if (expb<0) break;
insert(&LPB, coefb, expb);
}
printf("LPB表达式如下:\n"); printing(&LPB);
// 做加法
addition(&LPA,&LPB,&LPC);
// 输出
printf("LPC = LPA+LPB,因此计算结果如下:\n"); printing(&LPC);
}