菜鸟的复习资料
绪论
1.1什么是数据结构
是研究非数值运算程序设计中的操作对象,操作对象间的关系,以及操作等相关的一门学科
数据结构三要素
数据的逻辑结构
线性结构
非线性结构
数据的存储结构
顺序存储
非顺序存储
运算集合
1.2基本概念和术语
数据
1.能被输入到计算机当中
2.能被计算机程序处理
数据元素
数据的基本单位,可由多个数据项组成
数据项
不可被进一步分割的最小单位
数据对象
数据的子集,性质相同的数据元素组成的集合
数据类型
不仅包括元素值的集合,还包括元素关系的集合
1.3抽象数据类型的表示与实现
抽象数据类型的表示与实现
ADT <抽象数据类型名>{
数据对象D:
数据关系R:
基本操作P:
初始条件:...
操作结果:...
}ADT <抽象数据类型名>;
抽象数据类型的表示与实现
结构定义
typedef struct{
...
}<结构类型名>;
基本操作实现
用C语言中的函数实现
1.4算法与算法分析
什么是算法,与程序的区别
算法的五大特性
0至多个输入
1至多个输出
有穷性
可行性
确定性
算法的设计要求
正确性
可读性
健壮性
高效率与低存储
算法效率的度量
语句频度:语句的执行次数
渐近时间复杂度T(n)=O(f(n))
7个常见的f(n):
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n^2)<O(n^3)<O(2^n)
算法的存储空间需求
空间复杂度:S(n)
仅仅考虑过程中的辅助空间,不考虑程序本身所占的空间
线性表
2.0线性表的特点
同一性:同类数据元素
有穷性:由有限个数据元素组成
有序性:相邻元素之间存在着有序关系
2.1线性表的类型定义
ADT LinearList{
数据对象D:
数据关系R:
基本操作P:
}ADT LinearList;
2.2线性表的顺序存储表示和实现
线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素,通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系,通常称为顺序表。
元素地址计算
Loc(ai)=Loc(al)+(i-l)*k
1.结构定义
#define MAXSIZE 100 // 数组长度
typedef int ElemType
typedef struct{
ElemType a[MAXSIZE+1];
int n; //线性表长度
}SqList;
2.基本操作
1初始化
2查找操作
按序查找
要求查找线性表L中第i个数据元素,其结果是L.elem[i-1]或L->elem[i-1]。
按值查找
要求查找线性表L中与给定值e相等的数据元素,其结果是:若在表L中找到与e相等的元素,则返回该元素在表中的序号;若找不到,则返回一个“空序号”,如-1。 线性表的查找运算算法描述为:
int LocateElem(SeqList L,ElemType e) { i=0 ; /*i为扫描计数器,初值为0,即从第一个元素开始比较*/ while ((i<=L.last)&&(L.elem[i]!=e) ) i++; /*顺序扫描表,直到找到值为key的元素,或扫描到表尾而没找到*/ if (i<=L.last) return(i); /*若找到值为e的元素,则返回其序号*/ else return(-1); /*若没找到,则返回空序号*/ }
3插入操作
算法功能:
在表的第i (1≤i≤n+1)个位置,插入一个新元素e,使长度为n的线性表 (e1,…,ei-1,ei,…,en) 变成长度为n+1的线性表(e1,…,ei-1,e,ei,…,en)。
算法思想
i)
入口判断插入位置正确吗?(i≥1&&i≤L->last+1)
存储容量够吗?(L->last<MAXSIZE-1)
if( (i<1) || (i>L->last+2) ) { printf(“插入位置i值不合法”);return(ERROR); } if(L->last>=maxsize-1) { printf(“表已满无法插入”); return(ERROR); }
ii)元素ai~an后移
for(k=L->Last; k>=i-1; k--) L->elem[k+1]=L->elem[k]
iii)在线性表的第i处插入新元素
L->elem[i-1]=e;
eiv)表长加1
L->last++;
算法实现
#define MAXSIZE 100 typedef struct{ ElemType elem[MAXSIZE]; // 线性表容量 int last; // 线性表长度-1 }SeqList; int InsList(SeqList *L,int i,ElemType e) { int k; if( (i<1) || (i>L->last+2) ) { printf(“插入位置i值不合法”);return(ERROR); } if(L->last>=maxsize-1) { printf(“表已满无法插入”); return(ERROR); } for(k=L->last;k>=i-1;k--) /*为插入元素而移动位置*/ L->elem[k+1]=L->elem[k]; L->elem[i-1]=e; /*在C语言中数组第i个元素的下标为i-1*/ L->last++; return(OK); }
4删除操作
算法功能
将表的第i(1≤i≤n)个元素删去,使长度为n的线性表 (e1,…,ei-1,ei,ei+1,…,en),变成长度为n-1的线性表(e1,…,ei-1, ei+1,…,en)。
算法思想
i)
入口判断线性表是空的吗?
删除位置正确吗?
if (L->last == -1) { printf(“表空无法删除!”); return(ERROR); } if((i<1)||(i>L->last+1)) { printf(“删除位置不合法!”); return(ERROR); }
ii)
将第i个元素的值放入
e = L->elem[i-1];
xiii)
元素ai+1~an前移
iv)
表长减1
L->last--;
算法实现
#define MAXSIZE 100 typedef struct{ ElemType elem[MAXSIZE]; // 线性表容量 int last; // 线性表长度-1 }SeqList; int DelList(SeqList *L,int i,ElemType *e){ int k; if (L->last == -1) { printf(“表空无法删除!”); return(ERROR)} if((i<1)||(i>L->last+1)) { printf(“删除位置不合法!”) return(ERROR) } *e= L->elem[i-1]; for(k=i;i<=L->last;k++) L->elem[k-1]= L->elem[k]; L->last--; return(OK); }
5顺序表合并算法
算法思想
设表LC是一个空表,为使LC也是非递减有序排列,可设两个指针i、j分别指向表LA和LB中的元素,若LA.elem[i]>LB.elem[j],则当前先将LB.elem[j]插入到表LC中,若LA.elem[i]≤LB.elem[j] ,当前先将LA.elem[i]插入到表LC中,如此进行下去,直到其中一个表被扫描完毕,然后再将未扫描完的表中剩余的所有元素放到表LC中。
算法实现
void merge(SeqList *LA, SeqList *LB, SeqList *LC){ i=0;j=0;k=0; while(i<=LA->last&&j<=LB->last) if(LA->elem[i]<=LB->elem[j]) { LC->elem[k]= LA->elem[i]; i++; k++; } else { LC->elem[k]=LB->elem[j]; j++; k++; } while(i<=LA->last) /*当表LA长则将表LA余下的元素赋给表LC*/ { LC->elem[k]= LA->elem[i]; i++; k++; } while(j<=LB->last) /*当表LB长则将表LB余下的元素赋给表LC*/ { LC->elem[k]= LB->elem[j]; j++; k++; } LC->last=LA->last+LB->last; }
3.顺序存储结构的优缺点
优点
1、无需为表示结点间的逻辑关系而增加额外的存储空间;
2、可方便地随机存取表中的任一元素。
缺点
1、插入或删除运算不方便,除表尾的位置外,在表的其它位置上进行插入或删除操作都必须移动大量的结点,其效率较低; 2、由于顺序表要求占用连续的存储空间,存储分配只能预先进行静态分配。因此当表长变化较大时,难以确定合适的存储规模
2.3线性表的链式表示和实现
typedef int ElemType
typedef struct{
ElemType data; // 数据域
struct Node *next; // 指针域
}Node, *LinkList
2.3.1单链表
链表的定义
采用链式存储结构的线性表称为线性链表
1、从实现角度看,链表可分为动态链表和静态链表;
2、从链接方式的角度看,链表可分为单链表、循环链表和双链表
结点
为了正确地表示结点间的逻辑关系,必须在存储线性表的每个数据元素值的同时,存储指示其后继结点的地址(或位置)信息,这两部分信息组成的存储映象叫做结点(Node)。
单链表
链表中的每个结点只有一个指针域,我们将这种链表称为单链表。
单链表包括两个域:数据域用来存储结点的值;
指针域用来存储数据元素的直接后继的地址(或位置)。
头指针 :指向链表头结点的指针。
2.3.2单链表的基本运算
1.建立单链表
头插法建表
算法描述:
从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头结点之后,直至读入结束标志为止
算法实现
Linklist CreateFromHead(){ LinkList L; Node *s; int flag=1; /* 设置一个标志,初值为1,当输入“$”时,flag为0,建表结束*/ L=(Linklist)malloc(sizeof(Node));/*为头结点分配存储空间*/ L->next=NULL; while(flag){ c=getchar(); if(c!=’$’) { /*为读入的字符分配存储空间*/ s=(Node*)malloc(sizeof(Node)); s->data=c; s->next=L->next; L->next=s; } else flag=0; } }
尾插法建表
算法代码:
Linklist CreateFromTail() /*将新增的字符追加到链表的末尾*/{ LinkList L; Node *r, *s; int flag =1; L=(Node * )malloc(sizeof(Node));/*为头结点分配存储空间*/ L->next=NULL; r=L; /*r指针始终动态指向链表的当前表尾*/ while(flag)/*标志,初值为1。输入“$”时flag为0,建表结束*/ { c=getchar(); if(c!=’$’){ s=(Node*)malloc(sizeof(Node)); s->data=c; r->next=s; r=s; } else { flag=0; r->next=NULL; } } }
2.单链表查找
按序号查找
算法描述:
设带头结点的单链表的长度为n,要查找表中第i个结点,则需要从单链表的头指针L出发,从头结点(L->next)开始顺着链域扫描,用指针p指向当前扫描到的结点,初值指向头结点(pL->next),用j做记数器,累计当前扫描过的结点数(初值为0),当j = i 时,指针p所指的结点就是要找的第i个结点 。
算法实现:
/ * 在带头结点的单链表L中查找第i个结点,若找到(1≤i≤n),则返回该结点的存储位置; 否则返回NULL * / Node * Get(LinkList L, int i) { Node *p; p=L;j=0; / * 从头结点开始扫描 * / while ((p->next!=NULL)&&(j<i) { p=p->next; j++; / * 扫描下一结点 * / } / * 已扫描结点计数器 * / if(i= =j)return p; / * 找到了第i个结点 * / else return NULL; / * 找不到,i≤0或i>n * / }
按值查找
算法描述:
按值查找是指在单链表中查找是否有结点值等于e的结点,若有的话,则返回首次找到的其值为e的结点的存储位置,否则返回NULL。查找过程从单链表的头指针指向的头结点出发,顺着链逐个将结点的值和给定值e作比较。
算法实现:
/ * 在带头结点的单链表L中查找其结点值等于key的结点,若找到则返回该结点的位置p,否则返回NULL * / Node *Locate( LinkList L,ElemType key) { Node *p; p=L->next; / * 从表中第一个结点比较 * / while (p!=NULL) if (p->data!=key) p=p->next; else break; / * 找到结点key,退出循环 * / return p; }
3.单链表插入操作
算法功能:
要在带头结点的单链表L中第i个数据元素之前插入一个数据元素e,需要首先在单链表中找到第i-1个结点并由指针pre指示,然后申请一个新的结点并由指针s指s示,其数据域的值为e,并修改第i-1个结点的指针使其指向s,然后使s结点的指针域指向第i个结点。
算法思想
(i) 找到第i-1个结点(p指向第i-1个结点)。
node *loc(node *head, int i){ // head:带头节点的单链表的头指针,该算法定位于链表中的第i个结点 node *p = head; // 指针初始化,p指向头节点 int j =0; // j为计数器,初值为0 while(p!=NULL && j<i){ p = p->next; j++; // p后移动,j计数,p移至第i个结点止 } return (p); // p指向第i个结点(返回第i个结点的地址) }
(ii) 在p结点之后插入新结点q。
void ins(linklist head, int i, datatype x){ // head:带头节点的单链表的头指针 // 该算法在第i个结点后面插入值为x的新结点q node *p = loc(head, i-1); // 令p指向第i-1个结点 if(p!=NULL){ q = (linklist)malloc(sizeof(node)); q->data = x; // 生成新节点,其值为x q->next = p->next; p->next = q; // 完成插入 } }
算法实现
void InsList(LinkList L,int i,ElemType e) { /*在带头结点的单链表L中第i个结点之前插入值为e的新结点。 */ Node *pre,*s; pre=L; int k=0; while(pre!=NULL&&k<i-1) /*先找到第i-1个数据元素的存储位置,使指针Pre指向它*/ { pre=pre->next; k=k+1; } if(k!=i-1) { printf(“插入位置不合理!”); return; } s=(Node*)malloc(sizeof(Node)); /*为e申请一个新的结点*/ s->data=e; /*将待插入结点的值e赋给s的数据域*/ s->next=pre->next; pre->next=s; }
4.单链表删除操作
算法功能
欲在带头结点的单链表L中删除第i个结点,则首先要通过计数方式找到第i-1个结点并使p指向第i-1个结点,而后删除第i个结点并释放结点空间。
算法思想
(i) 找到第i-1个结点(p指向第i-1个结点)
(ii) 删除p结点之的下一个结点
算法实现
void del(node *head, int i, datatype &e){ // head:带头节点的单链表的头指针 // 该算法删除第i个结点 node *p = loc(head, i-1); // 令p指向第i-1个结点 if(p->next!=NULL && j==i-1){ q = p->next; // q指向p的下一个结点,即第i个结点 p->next = q->next; // 删除第i个结点 e = q->data; // 保留第i个结点的值 free(q); // 释放q } }
完整版:
void DelList(LinkList L,int i,ElemType *e) /*在带头结点的单链表L中删除第i个元素,并保存其值到变量*e中*/ { Node *p,*r; p=L; int k =0; while(p->next!=NULL&&k<i-1) /*寻找被删除结点i的前驱结点*/ { p=p->next; k=k+1; } if(k!=i-1) /* 即while循环是因为p->next=NULL而跳出的*/ { printf(“删除结点的位置i不合理!”); return ERROR; } r=p->next; p->next=p->next->next /*删除结点r*/ free(r); /*释放被删除的结点所占的内存空间*/ }
5.求单链表的长度
算法描述
可以采用“数”结点的方法来求出单链表的长度,用指针p依次指向各个结点,从第一个元素开始“数”,一直“数”到最后一个结点(p->next=NULL)
算法实现
int ListLength(LinkList L) /*L为带头结点的单链表*/ { Node *p; p=L->next; j=0; /*用来存放单链表的长度*/ while(p!=NULL) { p=p->next; j ++; } return j; }
应用:两个有序表的合并
算法描述
要求利用现有的表LA和LB中的结点空间来建立新表LC,可通过更改结点的next域来重建新的元素之间的线性关系,为保证新表仍然递增有序,可以利用尾插入法建立单链表的方法,只是新建表中的结点不用malloc,而只需要从表LA和LB中选择合适的点插入到新表LC中即可。
算法实现
LinkList MergeLinkList(LinkList LA, LinkList LB) /*将递增有序的单链表LA和LB合并成一个递增有序的单链表LC*/ { Node *pa,*pb; LinkList LC; /*将LC初始置空表。pa和pb分别指向两个单链表LA和LB中的第一个结点,r初值为LC*/ pa=LA->next; pb=LB->next; LC=LA; LC->next=NULL;r=LC; /*初始化操作*/ /*当两个表中均未处理完时,比较选择将较小值结点插入到新表LC中。*/ while(pa!=NULL&&pb!=NULL){ if(pa->data<=pb->data){ r->next=pa;r=pa;pa=pa->next;} else{ r->next=pb;r=pb;pb=pb->next;} } if(pa) /*若表LA未完,将表LA中后续元素链到新表LC表尾*/ r->next=pa; else /*否则将表LB中后续元素链到新表LC表尾*/ r->next=pb; free(LB); return(LC); } /* MergeLinkList */
2.3.3循环链表
循环链表是一个首尾相接的链表
特点:将单链表最后一个结点的指针域由NULL改为指向头节点或线性表中的第一个结点,就得到了单链形式的循环链表,并称为循环单链表。在循环单链表中,表中所有结点被链在一个环上
1.初始化循环单链表
InitCLinkList (LinkList *CL){ /*CL用来接受待初始化的循环单链表的头指针变量的地址*/ CL = (LinkList)malloc(sizeof(Node)); /*建立头结点*/ (*CL)->next = *CL; /*建立空的循环单链表CL*/ }
2.建立循环单链表
void CreateCLinkList(LinkList CL){ Node *rear, *s; char c; rear = CL; c=getchar(); while(c!=’$’) { s=(Node*)malloc(sizeof(Node)); s->data=c; rear->next=s; rear=s; } rear->next=CL; }
两个循环单链表合并为一个循环单链表
算法思想
先找到两个链表的尾,并分别由指针p、q指向它们,然后将第一个链表的尾与第二个表的第一个结点链接起来,并修改第二个表的尾q,使它的链域指向第一个表的头结点。
算法实现
LinkList merge_1(LinkList LA,LinkList LB){ /*此算法将两个链表的首尾连接起来*/ Node *p, *q; p=LA; q=LB; while (p->next!=LA) p=p->next; /*找到表LA的表尾*/ while (q->next!=LB) q=q->next; /*找到表LB的表尾*/ q->next=LA;/*修改表LB 的尾指针,使之指向表LA 的头结点*/ p->next=LB->next;/*修改表LA的尾指针,使之指向表LB 中的第一个结点*/ free(LB); return(LA); } LinkList merge_2(LinkList RA,LinkList RB){ /*此算法将两个采用尾指针的循环链表的首尾连接起来*/ Node *p; p=RA->next; /*保存RA的头结点地址*/ RA->next=RB->next->next; /*链表RB的开始结点链到链表RA的终端结点之后*/ free(RB->next); /*释放表RB的头结点*/ RB->next=p; /*链表RA的头结点链到链表RB的终端结点之后*/ return RB; /*返回新循环链表的尾指针*/ }
2.3.4双向链表
双向链表:在单链表的每个结点里再增加一个指向其前驱的指针域。这样子形成的链表中就有两条方向不同的链,我们称之为双向链表
双向链表的结构定义
typedef struct Dnode{ ElemType data; struct DNode *prior,*next; } DNode, * DoubleList;
1.双向链表的前插操作
算法描述
1. s->next = p; 2. s->prior = p->prior 3. s->prior->next = s; 4. p->prior = s;
算法实现
void DlinkIns(DoubleList L,int i,ElemType e) { DNode *s,*p; … /*首先检查待插入的位置i是否合法*/ … /*若位置i合法,则让指针p指向它*/ s=(DNode*)malloc(sizeof(DNode)); if (s){ s->data=e; s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; return TRUE; } else return FALSE; }
2.双向链表的删除操作
算法描述
算法功能:删除第i个结点(结点p)
p->prior->next=p->next; p->next->prior=p->prior; free(p);
算法实现
int DlinkDel(DoubleList L,int i,ElemType *e) { DNode *p; … /*首先检查待插入的位置i是否合法*/ … /*若位置i合法,则让指针p指向它*/ *e=p->data; p->prior->next=p->next; p->next->prior=p->prior; free(p); return TRUE; }
2.3.5静态链表
基本概念
游标实现链表的方法:定义一个较大的结构数组作为备用结点空间(即存储池)。当申请结点时,每个结点应含有两个域:data域和next域。data域存放结点的数据信息,next域为游标指示器,指示后继结点在结构数组中的相对位置(即数组下标)。数组的第0个分量可以设计成表的头结点,头结点的next域指示了表中第一个结点的位置,为0表示静态单链表的结束。我们把这种用游标指示器实现的单链表叫做静态单链表(Static Linked List)。静态链表的结构定义,静态链表的插入和删除操作示例。
基本操作:
初始化、分配结点与结点回收、前插操作、删除。
静态链表的结构定义
#define Maxsize= 链表可能达到的最大长度 typedef struct { ElemType data; int cursor; }Component, StaticList[Maxsize];
2.4顺序表与链表的综合比较
2.4.1顺序表与链表的比较
基于空间的考虑
基于时间的考虑
基于语言的考虑
2.4.2线性表链式存储方式的比较
操作名称**链表名称** | 找表头结点 | 找表尾结点 | 找**P结点前驱结点** |
---|---|---|---|
带头结点单链表**L** | L->next**时间耗费O(1)** | 一重循环**时间耗费O(n)** | 顺**P结点的next域无法找到P结点的前驱** |
带头结点循环单链表(头指针)**L** | L->next**时间耗费O(1)** | 一重循环**时间耗费O(n)** | 顺**P结点的next域可以找到P结点的前驱时间耗费O(n)** |
带尾指针的循环单链表**R** | R->next O(1) | R**时间耗费O(1)** | 顺**P结点的next域可以找到P结点的前驱时间耗费O(n)** |
带头结点双向循环链表**L** | L->next O(1) | L->prior**时间耗费O(1)** | P->prior**时间耗费O(1)** |