一、绪论
程序=数据结构+算法
(1)基本的数据结构
-
线性结构
- 线性表
- 栈和队列
- 串
- 数组和广义表
-
非线性结构
- 树
- 图
用计算机解题一个问题的步骤
- 具体问题抽象为数学模型
- 设计算法
- 编程、调试、运行
数据结构是一门研究非数值计算的程序设计中计算机的操作对象以及它们之间的关系和操作的学科
(2)基本概念和术语
-
数据(Data)
是能输入计算机且能被计算机处理的各种符号的集合
- 信息的载体
- 对客观事物符号化的表示
- 能够被计算机识别、存储和加工
数值型的数据:整数、实数等
非数值型的数据:文字、图像、图形、声音等
-
数据元素(Data Element)
- 是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理
- 也简称为元素,或称为记录、结点和顶点
-
数据项(Data Item)
- 构成数据元素的不可分割的最小单位
-
数据对象(Data Object)
- 是性质相同的数据元素的集合,是数据的一个子集
-
数据结构(Data Structure)
- 数据元素相互之间的关系称为结构
- 是指相互之间存在一种或多种特定关系的数据元素集合
- 数据结构是带结构的数据元素的集合
a. 数据元素之间的逻辑关系,称为逻辑结构
b. 数据元素及其关系在计算机内存中的表示(映像),称为物理结构或存储结构
(3)数据结构的两个层次
- 逻辑结构
- 描述数据元素之间的逻辑关系
- 与数据的存储无关,独立于计算机
- 是从具体问题抽象出来的数学模型
- 物理结构(存储结构)
- 数据元素及其关系在计算机存储器中的结构(存储方式)
- 是数据结构在计算机中的表示
关系:
- 存储结构是逻辑关系的映像与元素本身的映像
- 逻辑结构是数据结构的抽象,存储结构是数据结构的实现
(4)逻辑结构的种类
- 线性结构
有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后继。
例如:线性表、栈、队列、串
- 非线性结构
一个结点可能有多个直接前驱和直接后继
例如:树、图
(5)四种基本逻辑结构
- 集合结构
- 线性结构
- 树形结构
- 图形结构或网状结构
(6)四种基本的存储结构
- 顺序存储结构
- C语言中用数组来实现顺序存储结构
- 链式存储结构
- C语言中用指针来实现链式存储结构
- 索引存储结构
- 在存储结点信息的同时,还建立附件的索引表
- 散列存储结构
(7)数据类型
(Data Type)
定义:数据类型是一组性质相同的值的集合以及定义于这个值集合上的一组操作的总称
- C语言中,提供int,char,float,double等基本数据类型
- 数组、结构、共用体、枚举等构造数据类型
- 指针、空(void)类型
- 自定义数据类型(typedef)
(8)抽象数据类型
(Abstract Data Type,ADT)
是指一个数学模型以及定义在此数学模型上的一组操作。
- 由用户定义,从问题抽象出数据模型(逻辑结构)
- 还包括定义在数据模型上的一组抽象运算(相关操作)
- 不考虑计算机内的具体存储结构与运算的具体实现算法
抽象数据类型可用(D,S,P)三元组表示:D是数据对象,S是D上的关系集,P是对D的基本操作集
一个抽象数据类型的定义格式如下:
ADT 抽象数据类型名{
数据对象:<数据对象的定义> //伪代码
数据关系:<数据关系的定义> //伪代码
基本操作:<基本操作的定义> //基本操作名(参数表)、初始条件、操作结果
}ADT 抽象数据类型名
参数表:赋值参数,只为操作提供输入值
引用参数,以&打头,除可提供输入值外,还将返回操作结果
初始条件:操作执行之前数据结构和参数应满足的条件
操作结果:操作正常完成之后,数据结构的变化状况和应返回的结果
例如:Circle的定义
ADT Cirle{
数据对象:D={r,x,y|r,x,y均为实数}
数据关系:R={<r,x,y>|r是半径,<x,y>是圆心坐标}
基本操作:
Circle(&C,r,x,y)
操作结果:构造一个圆
double Area(C)
初始条件:圆已存在。
操作结果:计算面积。
double Circumference(C)
初始条件:圆已存在。
操作结果:计算周长。
}ADT Circle
复数的定义
ADT Complex{
D={r1,r2|r1,r2都是实数}
S={<r1,r2>|r1是实部,r2是虚部}
assign(&C,v1,v2)
初始条件:空的复数C已存在
操作结果:构造复数C,r1,r2分别被赋以参数v1,v2的值
destroy(&C)
初始条件:复数C已存在
操作结果:复数C被销毁
}ADT Complex
(9)用C语言实现抽象数据类型
复数的实现
typedef struct{
float realpart; /*实部*/
float imagpart; /*虚部*/
}Complex /*定义复数抽象类型*/
//函数声明
void assign(Complex *A,float real,float imag); /*赋值*/
void add(Complex *A,flaot real,float imag); /*A+B*/
void minus(Complex *A,flaot real,float imag); /*A-B*/
void multiply(Complex *A,flaot real,float imag); /*A*B*/
void divide(Complex *A,flaot real,float imag); /*A/B*/
//函数定义
Void assign(Complex *A,float real,float imag){
A->realpart=real; /*实部赋值*/
A->imagpart=imag; /*虚部赋值*/
}
void add(Complex *c,Complex A,Complex B){ /*c=A+B*/
c->relpart=A.realpart+B.realpart; /*实部相加*/
c->imagpart=A.imagpart+B.imagpart; /*虚部相加*/
}
(10)算法和算法分析
-
算法的定义
对特定问题求解方法和步骤的一种描述,它是指令的有限序列。
-
算法的描述
自然语言:英语、中文
流程图:传统流程图、NS流程图
伪代码:类语言:类C语言
程序代码:C语言程序、JAVA语言程序
-
算法与程序
算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法。
程序是用某种程序设计语言对算法的具体实现。
程序=数据结构+算法
-
算法特性
有穷性、确定性、可行性、输入、输出
-
算法设计的要求
正确性(Correctness)、可读性(Readability)、健壮性(Robustness)、高效性(Efficiency)
-
算法的效率
- 时间效率:算法所耗费的时间
- 空间效率:算法执行过程中所耗费的存储空间
时间效率和空间效率有时候是矛盾的。
-
算法时间效率的度量
- 事后统计:将算法实现,测算其时间和空间开销
- 事前分析:对算法所消耗资源的一种估算方法
算法运行时间=一个简单操作所需的时间*简单操作次数
for(i=1;i<=n;i++) //n+1次 for(j=1;j<=n;j++) //n(n+1)次 c[i][j]=0; //n*n次 for(k=0;k<n;k++) //n*n*(n+1)次 c[i][j]=c[i][j]+a[i][k]*b[k][j]; //n*n*n次
-
算法的渐进时间复杂度
T(n)=O(f(n)) O是数量级的符号
简称时间复杂度。
方法:
忽略所有低次幂项和最高次幂系数
-
找出语句频度最大的那条语句最为基本语句
-
计算基本语句的频度得到问题规模n的某个函数f(n)
-
取其数量级用符号“O”表示
//分析以下程序段的时间复杂度 i=1; while(i<=n) i=i*2; //若循环执行1次:i=1*2=2 //若循环执行2次:i=2*2 //若循环执行3次:i=2*2*2 //若循环执行x次:i=2^x //因为i<=n,所以2^x<=n,所以x<=log2n,所以T(n)=O(log2n)=O(lgh)
-
最坏时间复杂度、平均时间复杂度、最好时间复杂度
-
时间复杂度T(n)按数量级递增顺序为:
常数阶 对数阶 线性阶 线性对数阶 平方阶 立方阶 … K次方阶 指数阶 O(1) O(log2n) O(n) O(nlog2n) O(n^2) O(n^3) O(n^k) O(2^n)
-
-
渐进空间复杂度
算法所需存储空间的度量
记作:S(n)=O(f(n)) n为问题的规模(或大小)
二、线性表
(1)线性表的定义和特点
- 线性表(Linear List)是具有相同特性的数据元素的一个有限序列。
- 由n(n>=0)个数据元素(结点)a1,a2,…,an组成的有限序列
- 数据元素的个数n定义为表的长度
- 当n=0时称为空表
顺序存储结构存在的问题:
- 存储空间分配不灵活
- 运算的空间复杂度高
(2)线性表的类型定义
基本操作 | 功能 | 操作结果 |
---|---|---|
InitList(&L) | 初始化 | 构建一个空的线性表L |
DestroyList(&L) | 销毁 | 销毁线性表L |
ClearList(&L) | 清除 | 将线性表L重置为空表 |
ListEmpty(L) | 判断是否为空 | 若线性表L为空表,则返回TRUE,否则返回FALSE |
ListLength(L) | 求长度 | 返回线性表L中的数据元素个数 |
GetElem(L,i,&e) | 获取元素 | 用e返回线性表L中第i个数据元素的值(1<=i<=ListLength(L)) |
LocateElem(L,e,compare()) | 查找搜索 | 返回L中第一个与e满足compare()的数据元素的位序。元素不存在则返回值为0 |
PriorElem(L,cur_e,&pre_e) | 求前驱 | cur_e不是第一个数据元素 |
NextElem(L,cur_e,&next_e) | 求后继 | cur_e不是第最后个数据元素 |
ListInsert(&L,i,e) | 插入 | 在L的第i个位置之前插入新的数据元素e,L的长度加一(1<=i<=ListLength(L)+1) |
ListDelete(&L,i,&e) | 删除 | 删除L的第i个数据元素,并用e返回其值,L的长度减一(1<=i<=ListLength(L)) |
ListTraverse(&L,visited()) | 遍历 | 依次对线性表中每个元素调用visited() |
(3)线性表的顺序表示
线性表的顺序表示又称为顺序存储结构或顺序映像
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构
- 顺序存储结构:
-
依次存储,地址连续——中间没有空出存储单元,是一个典型的线性表顺序存储结构。
-
地址不连续——中间存在空的存储单元,不是一个线性表顺序存储结构。
LOC(ai+1)=LOC(ai)+l
所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
LOC(ai)=LOC(a1)+(i-1)*l
- 优点:
- 以物理位置相邻表示逻辑关系
- 任一元素均可随机存取
-
表示
顺序表(元素)>地址连续、依次存放、随机存取、类型相同
数组(元素)>用一维数组表示顺序表线性表长可变,数组长度不可动态定义
#define LIST_INIT SIZE 100 //线性表存储空间的初始分配量 typedef int ElemType; typedef struct{ ElemType elem[LIST INIT SIZE]; int length; //当前长度 }SqList;
多项式的顺序存储结构类型定义:
```c++
#define MAXSIZE 1000 //多项式可能达到的最大长度
typedef struct{
float p; //系数
int w; //指数
}Ploynomial;
typedef struct{
Polynomial *elem //存储空间的基地址
int length; //多项式中当前项的个数
}SqList; //多项式的顺序存储结构类型为SqList
图书表的顺序存储结构类型定义:
#define MAXSIZE 10000 //图书表可能达到的最大长度
typedef struct{ //图书信息定义
char no[20]; //图书ISBN
char name[50]; //图书名字
float price; //图书价格
}Book;
typedef struct{
Book *elem; //存储空间的基地址
int length; //图书表中当前图书个数
}SqList; //图书表的顺序存储结构类型为SqList
顺序表(Sequence List)
-
逻辑位序(0开始)和物理位序相差1
//数组动态分配 typedef struct{ ElemType *elem; int length; }SqList; L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE); //malloc(m)函数,开辟m字节长度的地址空间,并返回这段空间的首地址 //sizeof(x)运算,计算变量x的长度 //free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量 //需要加载头文件:<stdlib.h>
```c++
//数组静态分配
#define MAXSIZE 100
typedef struct{
ElemType elem [MAXSIZE];
int length;
}SqList; //定义顺序表类型
SqList L; //定义变量L,L是SqList这种类型的,L是个顺序表
//引用成员L.elem、L.length
SqList *L //引用成员L->elem、L->length
typedef char ElemType;
typedef int ElemType;
预定义常量和类型:
//函数结果状态代码
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
//Status 是函数的类型,其值是函数结果状态代码
typedef int Status;
typedef char ElemType;
(4)线性表的顺序实现
-
线性表L的初始化
Status InitList_Sq(SqList &L){ //构造一个空的顺序表L L.elem=new ElemType[MAXSIZE]; //为顺序表分配空间c++ if(!L.elem) exit(OVERFLOW); //存储分配失败 L.length=0; //空表长度为0 return OK; }
-
销毁线性表
void DestroyList(SqList &L){ if(L.elem)delete L.elem; //释放存储空间c++ }
-
清空线性表L
void ClearList(SqList &L){ L.length=0; //将线性表的长度置为0 }
-
求线性表L的长度
int GetLength(SqList){ return (L.length); }
-
判断线性表L是否为空
int IsEmpty(SqList L){ if(L.length==0) return 1; else return 0; }
-
顺序表的取值
根据位置i获取相应位置数据元素的内容
int GetElem(SqList L,int i,ElemType &e){ if(i<1||i>L.length)return ERROR; //判断i值是否合理,若不合理返回error e=L.elem[i-1]; //第i-1的单元存储着第i个数据 return OK; }
-
顺序表的查找
int LocateElem(SqList L,ElemType e){ //在线性表L中查找值为e的数据元素,返回其序号(是第几个元素) for(i=0;i<L.length;i++) if(L.elem[i]==e) return i+1; //查找成功,返回序号 return 0; //查找失败,返回0 }
平均查找长度ASL(Average Search Length)
-
顺序表的插入
Status ListInsert_Sq(SqList &L,int i,ElemType e){ if(i<1||i>L.length+1) return ERROR; //i值不合法 if(L.length==MAXSIZE) retrun ERROR; //当前存储空间已满 for(j=L.length-1;j>=i-1;j--) L.elem[j+1]=L.elem[j]; //插入位置及之后的元素后移 L.elem[i-1]=e; //将新元素e放入第i个位置 L.length++; //表长增1 return OK; }
-
顺序表的删除
Status ListDelete_Sq(SqList &L,int i){ if((i<1)||(i>L.length)) return ERROR; for(j=i;j<=L.length-1;j++) L.elem[j-1]=L.elem[j]; L.length--; return OK; }
-
顺序表的操作算法分析
-
时间复杂度
查找、插入、删除算法的平均时间复杂度为O(n)
-
空间复杂度
S(n)=O(1) (没有占用辅助空间)
-
-
优点
(1)存储密度大
(2)可以随机存取表中任一元素
-
缺点
(1)在插入、删除某一元素时,需要移动大量元素
(2)存储空间不灵活,浪费存储空间
(3)属于静态存储形式,数据元素的个数不能自由扩充
(5)线性表的链式表示
-
链式存储结构:结点在存储器中的位置时任意的,即逻辑上相邻的数据元素在物理上不一定相邻
-
线性表的链式表示又称为非顺序映像或链式映像
-
用一组物理位置任意的存储单元来存放线性表的数据元素
-
这组存储单元既可以是连续的,也可以是不连续的
-
链表中元素的逻辑次序和物理次序不一定相同
-
单链表是由头指针唯一确定,因此单链表可以用头指针的名字来命名
-
各结点有两个域组成:
数据域:存储袁术数值数据
指针域:存储直接后继结点的存储位置
-
相关术语:
1.结点:数据元素的存储映像。由数据域和指针域两部分组成
2.链表:n个结点由指针链组成一个链表,它是线性表的链式存储映像,称为线性表的链式存储结构
-
分类:
1.单链表:结点只有一个指针域的链表,称为单链表或线性链表
2.双链表:结点有两个指针域的链表
3.循环链表:首尾相接的链表
-
空表:
无头结点是,头指针为空时表示空表
有头结点时,当头结点的指针域为空时表示空表
-
特点:
1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
2.访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等(顺序存取法)
(6)单链表的定义
typedef struct Lnode{ //生命结点的类型和指向结点的指针类型
ElemType data; //结点的数据域
struct Lnode *next; //结点的指针域
}Lnode,*LinkList; //LinkList为指向结构体Lnode的指针类型
LinkList L; //定义链表L:
Lnode *p;
LinkList p; //定义结点指针p:
typedef Struct student{
char num[8];
char name[8];
int score;
struct student *next;
}Lnode,*LinkList;
typedef Struct{
char num[8];
char name[8];
int score;
}ElemType;
typedef struct Lnode{
ElemType data;
struct Lnode *next;
}Lnode,*LinkList;
(7)单链表基本操作
-
单链表的初始化
即构造一个空表
Status InitList L(LinkList &L){ L=new LNode; //或L=(LinkList)malloc(sizeof(LNode)); L->next=NULL; return OK; }
-
判断链表是否为空
int ListEmpty(LinkList){ //若L为空表,则返回1,否则返回0 if(L->next) //非空 return 0; else return 1; }
-
单链表的销毁
Status DestroyList_L(LinkList &L){ Lnode *p; //或LinkList p; while(L){ p=L, L=L->next; delete p; } return OK; }
-
清空单链表
Status ClearList(LinkList &L){ //将L重置为空表 Lnode *p,*q; //或LinkList p,q; p=L->next; while(p){ //没到表尾 q=p->next; delete p; p=q; } L->next=NULL; //头结点指针域为空 return OK; }
-
求单链表的表长
int ListLengt_L(LinkList L){ //返回L中数据元素个数 LinkList p; p=L->next; //p指向第一个结点 i=0; while(p){ //遍历单链表,统计结点数 i++; p=p->next; } return i; }
-
取值
//获取线性表中的某个数据元素的内容,通过变量e返回 Status GetElem_L(LinkList L,int i,ElemType &e){ p=L->next; j=1; //初始化 while(p&&j<i){ //向后扫描,知道p指向第i个元素或p为空 p=p-next;++j; } if(!p||j>i)return ERROR; //第i个元素不存在 e=p->data; return OK; }//GetElem_L
-
查找
//1. Lnode *LocateElem_L(LinkList L,Elemtype e){ //在线性表L中查找值为e的数据元素 //找到,则返回L中值为e的数据元素的地址,查找失败返回NULL p=L->next; while(p&&p->data!=e) p=p->next; return p; } //2. int LocateElem_L(LinkList L,Elemtype e){ //在线性表L中查找值为e的数据元素的位置序号 //返回L中值为e的数据元素的位置序号,查找失败返回0 p=L-next; j=1; while(p&&p->data!=e) { p=p->next;j++; } if(p) return j; else return 0; }
-
插入
Status ListInsert_L(LinkList &L,int i,ElemType e){ p=L; j=0; while(p&&j<i-1){ p=p->next;++j; //寻找第i-1个结点,p指向i-1结点 } if(!p||j>i-1)return ERROR; //i大于表长+1或者小于1,插入位置非法 s=new LNode; //生成新结点s,将结点s的数据域置为e s->data=e; //将结点s插入L中 s-next=p-next; p-next=s; return OK; }//ListInsert_L
-
删除
//将线性表L中第i个数据元素删除 Status ListDelete_L(LinkList &L,int i,ElemType &e){ p=L; j=0; while(p-next&&j<i-1){ p=p-next;++j; //寻找第i个结点,并令p指向其前驱 } if(!(p->next)||j>i-1)return ERROR; //删除位置不合理 q=p->next; //临时保存被删结点的地址以备释放 p->next=q->next; //改变删除结点前驱结点的指针域 e=q->data; //保存删除结点的数据域 delete q; //释放删除结点的空间 return OK; }//ListDelete_L
-
单链表的建立
- 头插法——元素插入在链表头部
void CreateList_H(LinkList &L,int n){ L=new LNnode; L->next=NULL; //先建立一个带头结点的单链表 for(i=n;i>0;--i){ p=new LNode; //生产新结点 //p=(LNode*)malloc(sizeof(LNode)); scanf(&p->data); //输入元素值 p->next=L-next; //插入到表头 L->next=p; } }//CreateList_H //算法的时间复杂度是O(n)
- 尾插法——元素插入在链表尾部
void CreateList_R(LinkList &L,int n){ L=new LNode; L->next=NULL; r=L; //尾指针r指向头结点 for(i=0;i<n;++i){ p=(LNode*)malloc(sizeof(LNode));; scanf(&p->data); p->next=NULL; r->next=p; r=p; } }//CreateList_R //算法的时间复杂度是O(n)
(8)单链表算法时间效率分析
-
查找
因线性链表只能顺序存取,即在查找时要从头指针找起,查找时间复杂度为O(n)
-
插入和删除
因线性表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)
(9)循环链表
头尾相接的链表——表中最后一个结点的指针域指向头结点,整个链表形成一个环
优点:从表中任一结点出发均可找到表中其他结点
循环条件:
p != L
p->next != L
带尾指针循环链表的合并:
LinkList Connect(LinkList Ta,LinkList Tb){ //假设Ta、Tb都