一、绪论
程序=数据结构+算法
(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都是非空的单循环链表
p=Ta->next; //p存表头结点
Ta->next=Tb->next-next; //Tb表头连结Ta表尾
delete Tb->next; //释放Tb表头结点
Tb->next=p; //修改指针
return Tb;
}
(10)双向链表
在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,形成两个方向不同的链
-
双向循环链表
头结点的前驱指针指向链表的最后一个结点
最后一个结点的后继指针指向头结点
-
双向链表的对称性
p->prior->next = p = p->next->prior
-
双链表结构定义:
typedef struct DuLNode{ Elemtype data; struct DuLNode *prior,*next; }DuLNode,*DuLinkList;
-
双链表的插入
void ListInsert_DuL(DuLinkList &L,Int i,ElemType e){ //在带头结点的双向循环链表L中第i个位置之前插入元素e if(!(p=GetElemP_Dul(L,i))) return ERROR; s=new DulNode; s->data=e; s->prior=p->prior; p->prior->next=s; s->next=p; p->prior=s; return OK; }//ListInsert_DuL
-
双向链表的删除
void ListDelete_DuL(DuLink &L,Int i,ElemType &e){ if(!(p=GetElemP_DuL(L,i))) return ERROR; e=p->data; p->prior->next=p->next; p->next->prior=p->prior; free(p); return OK; }//ListDelete_DuL
(11)顺序表和链表的比较
- 链式存储结构
-
优点
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动元素
-
缺点
-
存储密度小,每个结点的指针域需额外占用存储空间
(存储密度=结点数据本身占用的空间/结点占用的空间总量)
存储密度越大,存储空间的利用率就越高。顺序表存储密度为1(100%),链表的存储密度小于1
-
链式存储结构是非随机存取结构,对任一结点的操作都要从头指针依指针链查找到该结点,增加了算法的复杂度
-
顺序表 | 链表 | |
---|---|---|
存储空间 | 预先分配,会导致空间闲置或溢出现象 | 动态分配 |
存储密度 | 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度=1 | 需要借助指针来体现元素间的逻辑关系,存储密度小于1 |
存取元素 | 随机存取,按位置访问元素的时间复杂度为O(1) | 顺序存取,按位置访问元素的时间复杂度为O(n) |
插入、删除 | 时间复杂度为O(n) | 不需移动元素,时间复杂度为O(1) |
适用情况 | 1. 表变化不大,且能事先确定变化的范围2. 很少进行插入或删除操作,经常按元素位置序号访问数据元素 | 1. 长度变化较大2. 频繁进行插入或删除操作 |
(12)线性表的合并
void union(List &La,List Lb){
La_len=ListLength(La);
Lb_len=ListLength(Lb);
for(i=1;i<=Lb_len;i++){
GetElem(Lb,i,e);
if(!LocateElem(La,e))
ListInsert(&La,++La_len,e);
}
}
(13)有序表的合并
- 顺序表
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
pa=LA.elem;
pb=LB.elem; //指针pa和pb的处置分别指向两个表的第一个元素
LC.length=LA.length+LB.length; //新表长度为待合并两表的长度之和
LC.elem=new ElemType[LC.length];//为合并后的表分配一个数组空间
pc=LC.elem; //指针pc指向新表的第一个元素
pa_last=LA.elem+LA.length-1; //指针pa_last指向LA表的最后一个元素
pb_last=LB.elem+LB.length-1; //指针pb_last指向LB表的最后一个元素
while(pa<=pa_last && pb<=pb_last){ //两个表都非空
if(*pa<=*pb) *pc++=*pa++; //依次“摘要”两表中值较小的结点
else *pc++=*pb++;
}
while(pa<=pa_last) *pc++=*pa++;//LB表已到达表尾,将LA中剩余元素加入LC
while(pb<=pb_last) *pc++=*pb++;//LA表已到达表尾,将LB中剩余元素加入LC
}//MergeList_Sq
- 链表
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
pa=La->next;
pb=Lb->next;
pc=Lc=La; //用La的头结点作为Lc的头结点
while(pa&&pb){
if(pa->data <= pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}
else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb; //插入剩余段
delete Lb; //释放Lb的头结点
}
(14)多项式的实现
void CreatePolyn(Polynomial &P,int n){
//输入m项的系数和指数,建立表示多项式的有序链表p
P=new PNode;
p->next=NULL; //先建立一个带头结点的单链表
for(i=2;i<=n;++i){ //依次输入n个非零项
s=new PNode; //生成新结点
cin>>s->code>>s->expn; //输入系数和指数
pre=P; //pre用于保存q的前驱,值为头结点
q=P->next; //q初始化,指向首元结点
while(q&&q->expn<s->expn){ //找到第一个大于输入项指数的项*q
pre=q;
q=q->next;
}
s->next=q;
pre->next=s;
}
}
(15)图书信息管理
struct Book{
char id[20];
char name[50];
int price;
};
typedef struct{
Book *elem;
int length;
}SqList;
typedef struct LNode{
Book data;
struct LNode *next;
}LNode,*LinkList;
三、栈和队列
栈和队列是限定插入和删除只能在表的“端点“进行的线性表(操作受限)
(1)栈
1.定义
- 栈(stack),一种特殊的线性表,是限定表尾进行插入和删除操作的线性表
- 又称为后进先出(Last In First Out)的线性表,简称LIFO结构
- 表尾称为栈顶Top;表头称为栈底Base
- 插入元素到栈顶(即表尾)的操作,称为入栈
- 从栈顶(即表尾)删除最后一个元素的操作,称为出栈
- “入”=压入=PUST(x) “出”=弹出=POP(y)
2.表示
操作 | 描述 |
---|---|
InitStack(&S) | 初始化操作 |
DestoryStack(&S) | 销毁栈操作 |
StackEmpty(S) | 判定S是否为空栈 |
StackLength(S) | 求栈的长度 |
GetTop(S,&e) | 取栈顶元素 |
ClearStack(&S) | 栈置空操作 |
Push(&S,e) | 入栈操作 |
Pop(&S,&e) | 出栈操作 |
3.实现
A. 顺序栈
- top指针,指示栈顶元素之上
- base指针,指示栈底元素
- stacksize表示栈可使用的最大容量
- 空栈:base==top是栈空标志
- 栈满;top-base==stacksize
- 栈满时的处理方法:
- 1.报错,返回操作系统
- 2.分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈
- 使用数组作为顺序栈存储方式的特点:简单、方便、但易产生溢出(数组大小固定)
- 上溢(overflow)、下溢(underflow)
#define MAXSIZE 100
typedef struct{
SElemType *base; //栈底指针
SElemType *top; //栈顶指针
int stacksize; //栈可用最大容量
}SqStack;
- 顺序栈的初始化
Status InitStack(SqStack &S){ //构造一个空栈
S.base=new SElemType[MAXSIZE];
//S.base=(SElemType*)malloc(MAXSIZE*sizeof(SElemType));
if(!S.base)exit (OVERFLOW); //存储分配失败
S.top=S.base; //栈顶指针等于栈底指针
S.stacksize=MAXSIZE;
return OK;
}
- 判断栈是否为空
Status StackEmpty(SqStack S){
if(S.top==S.base)
return TRUE;
else
return FALSE;
}
- 求栈的长度
int StackLength(SqStack S){
return S.top-S.base;
}
- 清空栈
Status ClearStack(SqStack S){
if(S.base)S.top=S.base;
return OK;
}
- 销毁顺序栈
Status DestoryStack(SqStack &S){
if(S.base){
delete S.base;
S.stacksize=0;
S.base=S.top=NULL;
}
return OK;
}
- 入栈
Status Push(SqStack &S,SElemType e){
if(S.top-S.base==Stacksize)//栈满
return ERROR;
*S.top++=e;
return OK;
}
- 出栈
Status Pop(SqStack &S,SElemType &e){
if(S.top==S.base) return ERROR;
e=*--S.top;
return OK;
}
B. 链栈
- 表示
typedef struct StackNode{
SElemType data;
struct StackNode *next;
}StackNode,*LinkStack;
LinkStack S;
- 初始化
void InitStack(LinkStack &S){
//构造一个空栈,栈顶指针置为空
S=NULL;
return OK;
}
- 判断栈是否为空
Status StackEmpty(LinkStack S){
if(S==NULL)return TRUE;
else return FALSE;
}
- 入栈
Status Push(LinkStack &S,SElemType e){
p=new StackNode;
p->data=e;
p->next=S;
S=p;
return OK;
}
- 出栈
Status Pop(LinkStack &S,SElemType &e){
if(S==NULL)return ERROR;
e=S->data;
p=S;
S=S->next;
delete p;
return OK;
}
- 取栈顶元素
SElemType GetTop(LinkStack S){
if(S!=NULL)
return S-data;
}
4. 栈与递归
-
若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的;
-
若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。
-
函数调用过程
- 调用前,系统完成
- 将实参,返回地址等传递给被调用函数
- 为被调用函数的局部变量分配存储区
- 将控制转移到被调用函数的入口
- 调用后,系统完成:
- 保存被调用函数的计算结果
- 释放被调用函数的数据区
- 依照被调用函数保存的返回地址将控制转移到调用函数
- 调用前,系统完成
-
嵌套调用:遵循后调用的先返回
-
递归工作栈——递归程序运行期间使用的数据存储区
long Fact(long n){
if(n==0)return 1;
else return n*Fact(n-1);
}
//两者等同
long Fact(long n){
t=1;
for(i=1;i<=n;i++)t=t*i;
return t;
}
(2)队列
1.定义
-
队列(queue)是一种先进先出(First In First Out----FIFO)的线性表。
-
在表一端插入(表尾),在另一端(表头)删除
-
入队、出队
-
表达式的组成
操作数(operand):常数、变量
运算符(operator):算术运算符、关系运算符、逻辑运算符
界限符(delimiter):左右括弧和表达式结束符
2.表示
操作 | 描述 |
---|---|
InitQueue(&Q) | 构造空队列Q |
DestoyQueue(&Q) | 销毁队列 |
ClearQueue(&Q) | 清空队列 |
QueueLength(Q) | 求队长 |
GetHead(Q,&e) | 返回对头元素 |
EnQueue(&Q,e) | 插入队尾元素 |
DeQueue(&Q,&e) | 删除对头元素 |
3.实现
A. 顺序队列
- 循环队列的类型定义
#define MAXSIZE 100
Typedef struct{
QElemType *base; //初始化的动态分配存储空间
int front; //头指针
int rear; //尾指针
}SqQueue;
- 队列的初始化
Status InitQueue(SqQueue &Q){
Q.base=new QElemType[MAXQSIZE]; //分配数组空间
//Q.base=(QElemType*)malloc(MAXQSIZE*sizeof(QElemType));
if(!Q.base)exit(OVERFLOW); //存储分配失败
Q.front=Q.rear=0; //头指针尾指针置为0,队列为空
return OK;
}
- 求队列的长度
int QueueLength(SqQueue Q){
return((Q.rear-Q.front+MAXQSIZE)%MAXQSIZE);
}
- 循环队列的入队
Status EnQueue(SqQueue &Q,QElemType e){
if((Q.rear+1)%MAXQSIZE==Q.front)return ERROR; //队满
Q.base[Q.rear]=e; //新元素加入队尾
Q.rear=(Q.rear+1)%MAXQSIZE; //队尾指针+1
return OK;
}
- 循环队列的出队
Status DeQueue(SqQueue &Q,QElemType &e){
if(Q.front==Q.rear)return ERROR; //队空
e=Q.base[Q.front]; //保存队头元素
Q.front=(Q.front+1)%MAXQSIZE; //队头指针+1
return OK;
}
- 取队头元素
QElemType GetHead(SqQueue Q){
if(Q.front!=Q.rear) //队列不为空
return Q.base[Q.front]; //返回队头指针元素的值,队头指针不变
}
B. 链式队列
- 链队列的类型定义
#define MAXQSIZE 100 //最大队列长度
typedef struct Qnode{
QElemType data;
struct Qnode *next;
}QNode,*QueuePrt;
typedef struct{
QueuePrt front; //队头指针
QueuePrt rear; //队尾指针
}LinkQueue;
- 链队列初始化
Status InitQueue(LinkQueue &Q){
Q.front=Q.rear=(QueuePrt)malloc(sizeof(QNode));
if(!Q.front)exit(OVERFLOW);
Q.front->next=NULL;
return OK;
}
- 销毁链队列
Status DestroyQueue(LinkQueue &Q){
while(Q.front){
p=Q.front->next;
free(Q.front);
Q.front=p;
}
return OK;
}
- 链队列的入队
Status EnQueue(LinkQueue &Q,QElemType e){
p=(QueuePtr)malloc(sizeof(QNode));
if(!p)exit(OVERFLOW);
p->data=e;
p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return OK;
}
- 链队列的出队
Status DeQueue(LinkQueue &Q,QElemType &e){
if(Q.front==Q.rear)return ERROR;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p)Q.rear=Q.front;
delete p;
return OK;
}
- 求链队列的队头元素
Status GetHead(LinkQueue Q,QElemType &e){
if(Q.front==Q.rear)return ERROR;
e=Q.front->next->data;
return OK;
}
四、串、数组和广义表
(1) 串
1. 串的定义
-
串(String)——零个或多个任意字符组成的有限序列
-
s=“a1a2…an”(n>=0)
-
空串——n=0
-
子串:串中任意个连续字符组成的子序列称为该串的子串
真子串:指不包含自身的所有子串
-
主串:包含字串的串相应地称为主串
-
字符位置:字符在序列中的序号为该字符在串中的位置
-
字串位置:字串第一个字符在主串中的位置
-
空格串:由一个或多个空格组成的串
-
串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的
所有空串是相等的
2. 串的操作
操作 | 描述 |
---|---|
StrAssign(&T,chars) | 串赋值 |
StrCompare(S,T) | 串比较 |
StrLength(S) | 求串长 |
Concat(&T,S1,S2) | 串连结 |
SubString(&Sub,S,pos,len) | 求子串 |
StrCopy(&T,S) | 串拷贝 |
StrEmpty(S) | 串判空 |
ClearString(&S) | 清空串 |
Index(S,T,pos) | 子串的位置 |
Replace(&S,T,V) | 串替换 |
StrInsert(&S,pos,T) | 子串插入 |
StrDelete(&S,pos,len) | 子串删除 |
DestroyString(&S) | 串销毁 |
3. 串的存储结构
A.顺序存储结构
#define MAXLEN 255
typedef struct{
char ch[MAXLEN+1]; //存储串的一维数组
int length; //串的当前长度
}SString;
B.链式存储结构——块链结构
#define CHUNKSIZE 90 //块的大小
typedef struct Chunk{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{
Chunk *head,*tail; //串的头指针和尾指针
int curlen; //串的当前长度
}LString; //字符串的块链结构
4. 串的运算
A. 串的模式匹配算法
- 算法目的:确定主串中所含子串(模式串)第一次出现的位置(定位)
- 算法应用:搜索引擎、拼写检查、语言翻译、数据压缩
- 算法种类:BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)、KMP算法(特点:速度快)
B. BF算法
亦称简单匹配算法,采用穷举法的思路
- 将主串的第i个字符和模式串的第一个字符比较
- 若相等,继续读个比较后续字符
- 若不等,从主串的下一字符起,重新与模式串的第一个字符比较
- 知道主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。
- 否则,匹配失败,返回值0
int Index_BF(SString S,SString T){
int i=1,j=1;
while(i<=S.length && j<=T.length){
if(s.ch[i]==t.ch[j]){++i;++j;} //主串和子串依次匹配下一个字符
else {i=i-j+2;j=1;} //主串、子串指针回溯重新开始下一次匹配
}
if(j>=T.length)return i-T.length; //返回匹配的第一个字符的下标
else return 0; //模式匹配不成功
}
时间复杂度:O(n*m) (n为主串长度,m为子串长度)
C. KMP算法
int Index_KMP(SString S,SString T,int pos){
i=pos,j=1;
while(i<S.length && j<T.length){
if(j==0 || S.ch[i]==T.ch[j]){i++;j++;}
else
j=next[j]; //i不变,j后退
}
if(j>T.length) return i-T.length; //匹配成功
else return 0; //匹配失败
}
void get_next(SString T,int &next[]){
i=1;next[1]=0;j=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
++i;++j;
next[i]=j;
}
else
j=next[j];
}
}
//改进
void get_nextval(SString T,int &nextval[]){
i=1,j=0;
nextval[1]=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
++i;++j;
if(T.ch[i]!=T.ch[j])nextval[i]=j;
else nextval[i]=nextval[j];
}
else j=nextval[j];
}
}
时间复杂度:O(n+m) (n为主串长度,m为子串长度)
(2) 数组
1. 基本要点
-
数组:按一定格式排列起来的,具有相同类型的数据元素的集合
-
一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组
-
一维数组的逻辑结构:线性结构。定长的线性表。
-
声明格式:数据类型 变量名称[长度];
-
在C语言中,一个二维数组类型也可以定义为一维数组类型(其分量类型为一维数组类型)即:
typedef elemtype array2[m] [n];
等价于:
typedef elemtype array1[n];
typedef array1 arrray2[m];
-
线性表结构是数组结构的一个特例,而数组结构又是线性表结构的扩展。
-
数组特点:结构固定——定义后,维数和维界不再改变。
-
基本操作:
操作 | 描述 |
---|---|
InitArray(&A,n,bound1,…boundn) | 构造数组A |
DestroyArray(&A) | 销毁数组A |
Value(A,&e,index1,…,indexn) | 取数组元素值 |
Assign(A,&e,index1,…,indexn) | 给数组元素赋值 |
2. 数组的顺序存储
LOC(i)=LOC(i-1)+L=a+i*L(LOC(0)=a)
LOC(i,j)=LOC(0,0)+(n*i+j)*L
两种顺序存储方式:
- 以行序为主序(低下标优先) BASIC、COBOL、PASCAL、C、JAVA
- 以列序为主序(高下标优先) FORTRAN
3. 特殊矩阵的压缩存储
压缩存储:若多个数据元素的值都相同,则只分配一个元素值的存储空间,且零元素不占存储空间。
对称矩阵、三角矩阵、对角矩阵
稀疏矩阵:
三元组顺序表 i j v
十字链表法
(3)广义表
-
又称列表Lists
-
LS=(a1,a2,…,an)
-
表头head(LS)=a1
-
表尾tail(LS)=(a2,…,an),表尾一定是一个表
- A=() 空表,长度为0
- B=() 长度为1,表头、表尾均为()
- C=(a,(b,c)) 长度为2,由原子a和子表(b,c)构成。表头为a,表尾为((b,c))
- D=(x,y,z) 长度为3,每一项都是原子。表头为x,表尾为(y,z)
-
广义表的长度定义为最外层所包含元素的个数
-
广义表的深度定义为该广义表展开后所含括号的重数
-
原子的深度为0,空表的深度为1
-
广义表可以是一个递归表,例如F=(a,F)=(a,(a,(a,…))),递归表的深度是无穷值,长度是有限值
-
广义表可以看成是线性表的推广,线性表是广义表的特例
-
广义表的结构灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构
-
基本运算:
功能 | 描述 |
---|---|
GetHead(L) | 求表头 |
GetTail(L) | 求表尾 |
五、树、二叉树
(1) 树
- 树形结构(非线性结构):节点之间有分支、具有层次关系
- 树(Tree)是n(n>=0)个结点的有限集。
- n=0,称为空树
- n>0,则它满足两个条件:
- 有且仅有一个特定的称为根(Root)的结点
- 其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并成为根的子树(SubTree)
- 树的定义是一个递归的定义
- 树的表示方式:层次结构、嵌套集合、广义表、凹入表示
- 基本术语:
- 根结点:非空树中无前驱结点的结点
- 结点的度:结点拥有的子树数
- 树的度:树内各结点的度的最大值
- 叶子:终端节点,度=0
- 分支节点:度!=0,非终端节点,根节点以外的分支节点称为内部结点
- 树的深度:树中结点的最大层次(高度)
- 有序树:树中结点的各子树从左至右有次序
- 无序树:树中结点的各子树无次序
- 森林:是m(m>=0)棵互不相交的树的集合
- 树一定是森林,森林不一定是树
- 树的表示法:双亲表示法、孩子表示法、孩子兄弟表示法
(2) 二叉树
1. 基本概念
- 二叉树的结构最简单,规律性最强
- 所有树都能转为唯一对应的二叉树
- 由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成
- 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)
- 子树有左右之分,其次序不能颠倒
- 二叉树可以是空集合,根可以有空的左子树或空的右子树
- 二叉树不是树的情况:
- 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分
- 树当结点只有一个孩子时,就无需区分它是左还是右的次序。
- 二叉树的五种基本形态:
- 空二叉树
- 根和空的左右子树
- 根和左子树
- 根和右子树
- 根和左右子树
- 基本操作:
功能 | 描述 |
---|---|
CreateBiTree(&T,definition) | 按definition构造二叉树T |
PreOrderTraverse(T) | 先序遍历T |
InOrderTraverse(T) | 中序遍历T |
PostOrderTraverse(T) | 后序遍历T |
- 满二叉树:一棵深度为k且有2^k-1个结点的二叉树称为满二叉树
- 每一层上的结点数都是最大结点数(即每层都满)
- 叶子结点全部在最底层
- 编号规则:自上而下,自左而右
- 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。(Complete binary tree)
- 在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树
- 叶子只可能分布在层次最大的两层上。
- 对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
- 满二叉树一定是完全二叉树
2. 二叉树的性质
-
性质1:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
第i层上至少有1个结点
-
性质2:深度为k的二叉树至多有2^k-1个结点(k>=1)
深度为k时至少有k个结点
-
性质3:对任何一棵二叉树T,如果其叶子树为n0,度为2的结点树为n2,则n0=n2+1
总边数B=结点数n - 1
B=n2 * 2 + n1 * 1
n=B+1=n22 + n11 + 1
n=n2+n1+n0
-
性质4:具有n个结点的完全二叉树的深度为|log2n|+1
|x|:称作x的低,表示不大于x的最大整数
表明了完全二叉树结点数n与完全二叉树深度k之间的关系 k=|log2n|+1
-
性质5:如果对一棵有n个结点的完全二叉树(深度为|log2n|+1)的结点按层序编号(从第一层到第|log2n|+1层,每层从左到右),则对任一结点i(1<=i<=n),有:
a. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点|i/2|
b. 如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i
c. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
3. 二叉树的顺序存储结构
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素
#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXSTIZE]
SqBiTree bt;
缺点:最坏情况,深度为k的且只有k个结点的单支树需要长度2^k-1的一维数组
特点:结点间关系蕴含在其存储位置中
浪费空间,适于存满二叉树和完全二叉树
4. 二叉树的链式存储结构
A. 二叉链表
指向两个孩子
typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild; //左右孩子指针
}BiNode,*BiTree;
在n个结点的二叉链表中,有_________个空指针域:
必有2n个链域。除根结点外,每个结点有且仅有一个双亲,所以只会有n-1个结点的链域存放指针,指向非空子女结点。
空指针数目=2n-(n-1)=n+1
B. 三叉链表
指向两个孩子和老父亲
typedef struct TriTNode{
TelemType data;
struct TriTNode *lchild,*parent,*rchild;
}TriTNode,*TriTree;
(3) 遍历二叉树
1. 概念
- 遍历定义:顺着某一条搜索路径寻访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
- 遍历目的:得到树中所有结点的一个线性排列
- 遍历用途:是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心
- L:遍历左子树 D:访问根结点 R:遍历右子树
- 遍历整个二叉树方案共有:DLR、LDR、LRD、DRL、RDL、RLD六种
- 若规定先左后右:DLR(先(根)序遍历)、LDR(中(根)序遍历)、LRD(后(根)序遍历)
- 先序:前缀表示(波兰式)
- 中序:中缀表示
- 后序:后缀表示(逆波兰式)
- 根据遍历序列确定二叉树:
- 若二叉树中各节点的值均不同,则二叉树结点的线序序列、中序序列和后序序列都是唯一的
- 由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一颗二叉树
2. 二叉树先序遍历算法
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
visit(T); //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
void Pre(BiTtee *T){
if(T!=NULL){
printf("%d\t",T->data);
pre(T->lchild);
pre(T->rchild);
}
}
3. 中序遍历算法:
Status InOrderTraverse(BiTree T){
if(T==NULL)return OK; //空二叉树
else{
InOrderTraverse(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
4. 后序遍历算法:
Status PostOrderTraverse(BiTree T){
if(T==NULL)return OK; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
递归遍历算法的时间效率:O(n) //每个结点只访问一次
空间效率:O(n) //栈占用的最大辅助空间
5. 中序非递归算法
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S);
p=T;
while(p||!StackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,q);
printf("%c",q->data);
p=q->rchild;
}
}
return OK;
}
6. 层次遍历算法
typedef struct{
BTNode data[MaxSize]; //存放队中元素
int front,rear; //队头和队尾指针
}SqQueue; //顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *qu;
InitQueue(qu); //初始化队列
enQueue(qu,b); //根结点指针进入队列
while(!QueueEmpty(qu)){ //队不为空,则循环
deQueue(qu,p); //出队结点p
printf("%c",p->data); //访问结点p
if(p->lchild!=NULL)enQueue(qu,p->lchild); //有左孩子是将其进队
if(p->rchild!=NULL)enQueue(qu,p->rchild); //有右孩子是将其进队
}
}
7. 二叉树的建立
按先序遍历序列建立二叉树的二叉链表
Status CreateBiTree(BiTree &T){
scanf(&ch);
if(ch=="#") T=NULL;
else{
if(!(T=(BiTNode *)malloc(sizeof(BiTNode))))
exit(OVERFLOW); //T=new BiTNode;
T->data=ch; //生成根结点
CreateBiTree(T->lchild); //构造左子树
CreateBiTree(T->rchild); //构造右子树
}
return OK;
}//CreateBiTree
8. 复制二叉树
int Copy(BiTree T,BiTree &NewT){
if(T==NULL){
NewT=NULL;
retrun 0;
}
else{
NewT=new BiTNode;
NewT->data=T->data;
Copy(T->lChild,NewT->lchild);
Copy(T->rChild,NewT->rchild);
}
}
9. 计算二叉树的深度
int Depth(BiTree T){
if(T==NULL)return 0;
else{
m=Depth(T->lChild);
n=Depth(T->rChild);
if(m>n)return(m+1);
else return(n+1);
}
}
10. 计算二叉树结点总数
int NodeCount(BiTree T){
if(T==NULL)return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
11. 计算二叉树叶子结点数
int LeadCount(BiTree T){
if(T==NULL) return 0;
if(T->lchild==NULL && T->rchild==NULL) return 1;
else return LeafCount(T->lchild)+LeafCount(T->rchild);
}
(4) 线索二叉树
-
利用某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;
-
如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继;
——这种改变指向的指针称为"线索"
-
加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)
-
对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化
-
为区分lchild和rchild指针到底是指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:
Itag=0 lchild指向该结点的左孩子
ltag=1 lchild指向该结点的前驱
rtag=0 rchild指向该结点的右孩子
rtag=1 rchild指向该结点的后继
typedef struct BiThrNode{
int data;
int ltag,rtag;
struct BiThrNode *lchild,*rchild;
}BiThrNode,*BiThrTree;
-
增设了一个头结点:
ltag=0,lchild指向根结点,rtag=1,rchild指向遍历序列中最后一个结点
遍历序列中第一个结点的lchild域和最后一个结点的rc域都指向头结点
(5) 树和森林
1. 树的存储结构
A.双亲表示法
- 实现:定义结构数组,存放树的结点,每个结点含两个域(数据域、双亲域)
- 特点:找双亲容易,找孩子难
typedef struct PTNode{
TElemType data;
int parent; //双亲位置域
}PTNode;
#define MAX_TREE_SIZE 100
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int r,n; //根结点的位置和结点个数
}PTree;
B. 孩子链表
- 把每个结点的孩子排列起来,看成是一个线性表,用单链表存储。则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
//孩子结点结构
typedef struct CTNode{
int child;
sturct CTNode *next;
}*ChildPtr;
//双亲结点结构
typedef struct{
TElemType data;
ChildPtr firstchild; //孩子链表头指针
}CTBox;
//树结构
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点数和根结点的位置
}CTree;
- 特点:找孩子容易,找双亲难。
C. 孩子兄弟表示法
又称二叉树表示法,二叉链表表示法
- 实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
- 特点:找孩子容易,找双亲难。
2. 树与二叉树的转换
- 将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。
- 由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系。
- 给定一棵树,可以找到唯一的一棵儿茶素与之对应
- 树变二叉树:兄弟相连留长子
- 加线:在兄弟之间加一连线
- 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
- 旋转:以树的根结点为轴心,将整树顺时针转45度
- 二叉树变树:左孩右右连双亲,去掉原来右孩线
- 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的有孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来
- 抹线:抹掉源二叉树中双亲与右孩子之间的连线
- 调整:将结点按层次排列,形成树结构
3. 森林与二叉树的转化
- 森林变二叉树:树变二叉根相连
- 将各课树分别转换成二叉树
- 将每棵树的根结点用线相连
- 以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
- 二叉树变森林:去掉全部右孩线,孤立二叉再还原
- 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成鼓励的二叉树
- 还原:将孤立的二叉树还原成树
4. 树的遍历
-
A. 先根(次序)遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树
-
B. 后根(次序)遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点
-
C. 按层次遍历:若树不空,则自上而下自左至右访问树中每个结
5. 森林的遍历
-
A. 先序遍历
若森林不空,则
-
访问森林中第一棵树的子树森林
-
先序遍历森林中第一棵树的子树森林
-
遍历森林中(除第一棵树之外)其余树构成的森林
-
-
B. 中序遍历
(6) 哈夫曼树
1. 基本概念
- 也称最优二叉树
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
- 结点的路径长度:两结点间路径上的分支数
- 树的路径长度:从树根到每一个结点的路径长度之和,记作TL
- 结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
- 权(weight):将书中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
- 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作WPL(Weighted Path Length)
- 哈夫曼树:最优树 带权路径长度(WPL)最短的树
- 注:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等
- 构造这种树的算法是由哈夫曼教授于1952年提出的,称为哈夫曼树,相应的算法称为哈夫曼算法
- 满二叉树不一定是哈夫曼树
- 哈夫曼树中权越大的叶子离根越近
- 具有相同带权结点的哈夫曼树不唯一
2. 构造算法
-
贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
-
哈夫曼树中权越大的叶子离根越近
-
哈夫曼算法:
- 构造森林全是根
- 选用两小造新树
- 删除两小添新人
- 重复2、3剩单根
-
哈夫曼树的结点的度数为0或2,没有度为1的结点
-
包含n个叶子结点的哈夫曼树中共有2n-1个结点
-
包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点
3. 算法实现
//结构类型定义
typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
哈夫曼树中共有2n-1个结点,不使用0下标,数组大小为2n
void CreateHuffmanTree(HuffmanTree HT,int n){
if(n<=1)return 0;
m=2*n-1; //数组共2n-1个元素
HT=new HTNode[m+1]; //0号单元未用,HT[m]表示根结点
for(i=1;i<=m;++i){ //将2n-1个元素的lch、rch、parent置为0
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for(i=1;i<=n;++i)
ch>>HT[i].weight; //输入前n个元素的weight值
//初始化结束,下面开始建立哈夫曼树
for(i=n+1;i<=m;i++){ //合并产生n-1个结点——构造Huffman树
Select(HT,i-1,s1,s2); //在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回它们在 HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i; //表示从F中删除s1,s2
HT[i].lch=s1;
HT[i].rch=s2; //s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和
}
}
4. 哈夫曼编码思想
-
关键:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀——前缀编码
-
哈夫曼编码
-
统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)
-
利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短
-
在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连结起来,作为该叶子代表的字符的编码
-
-
为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀
-
为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短
性质1:哈夫曼编码是前缀码
性质2:哈夫曼编码是最优前缀码
5. 哈夫曼编码的算法实现
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
HC=new char *[n+1]; //分配n个字符编码的头指针矢量
cd=new char[n]; //分配临时存放编码的动态数组空间
cd[n-1]='\0'; //编码结束符
for(i=1;i<=n;++i){ //逐个字符求哈夫曼编码
start=n-1;
c=i;
f=HT[i].parent;
while(f!=0){ //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if(HT[f].lchild==c) cd[start]='0'; //结点c是f的左孩子,则生成代码0
else cd[start]='1'; //结点c是f的右孩子,则生成代码1
c=f;
f=HT[f].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i]=new char[n-start]; //为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}//CreateHuffanCode
6. 文件的编码和解码
- 编码
- 输入各字符及其权值
- 构造哈夫曼树——HT[i]
- 进行哈夫曼编码——HC[i]
- 查HC[i],得到各字符的哈夫曼编码
- 解码
- 构造哈夫曼树
- 依次读入二进制码
- 读入0,则走向左孩子;读入1,则走向右孩子
- 一旦到达叶子时,即可译出字符
- 然后再从根出发继续译码,直到结束
六、图
(1) 图的定义和术语
-
图:G=(V,E) Graph=(Vertex,Edge)
-
V:顶点(数据元素)的有穷非空集合
-
E:边的有穷集合
-
无向图:每条边都是无方向的
-
有向图:每条边都是有方向的
-
完全图:任意两个点都有一条边相连
-
无向完全图:n个顶点,n(n-1)/2条边
-
有向完全图:n个顶点,n(n-1)条边
-
稀疏图:有很少边或弧的图(e<nlogn)
-
稠密图:有较多边或弧的图
-
网:边/弧带权的图
-
邻接:有边/弧相连的两个顶点之间的关系
存在(vi,vj),则称vi和vj互为邻接点
存在<vi,vj>,则称vi邻接到vj,vj邻接于vi
-
关联(依附):边/弧与顶点之间的关系
-
顶点的度:与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
顶点v的出度是以v为终点的有向边的条数,记作OD(v)
-
当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是一棵树,而且是一棵有向树
-
路径:接续的边构成的顶点序列
-
路径长度:路径上边或弧的数目/权值之和
-
回路(环):第一个顶点和最后一个顶点相同的路径
-
简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径
-
简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径
-
连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)
-
权:图中边或弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或耗费
-
网:带权的图
-
子图
设有两个图G=(V,{E})、G1=(V1,{E1}),若V1属于V,E1属于E,则称G1是G的子图
-
连通分量:无向图G的极大连通子图称为G的连通分量
-
强连通分量:有向图G的极大强连通子图
-
极大连通子图:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通
-
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通
-
生成树:包含无向图G所有顶点的极小连通子图
-
生成森林:对非连通图,由各个连通分量的生成树的集合
(2) 图的基本操作
操作 | 描述 |
---|---|
Create_Graph() | 创建图 |
GetVex(G,v) | 求顶点v的值 |
Create_Graph(&G,V,VR) | V是图的顶点集,VR是图中弧的集合,按V和VR的定义构造图G |
DFSTraverse(G) | 对图进行深度优先遍历 |
BFSTraverse | 对图进行广度优先遍历 |
(3) 图的存储结构
图的逻辑结构:多对多
图没有顺序存储结构,可以借助二维数组来表示元素间的关系——数组表示法(邻接矩阵)
链式存储结构:多重链表(邻接表、邻接多重表、十字链表)
1. 邻接矩阵(数组)表示法
-
建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)
-
设图A=(V,E)有n个顶点,则顶点表Vexs[n]
-
图的邻接矩阵是一个二维数组A.arcs[n] [n],定义为
A.arcs[i] [j]=1 如果<i,j>属于E 或者 (i,j)属于E
A.arcs[i] [j]=0 否则
-
A. 无向图的邻接矩阵表示法
- 无向图的邻接矩阵是对称的
- 顶点i的度=第i行(列)中1的个数
- 特别:完全图的邻接矩阵中,对角元素为0,其余1
B. 有向图的邻接矩阵表示法
- 第i行含义:以结点vi为尾的弧(即出度边)
- 第j列含义:以结点vi为头的弧(即入度边)
- 有向图的邻接矩阵可能是不对称的
- 顶点的出度=第i行元素之和
- 顶点的入度=第i列元素之和
- 顶点的度=第i行元素之和+第i列元素之和
C. 网(即有权图)的邻接矩阵表示法
-
定义为A.arcs[i] [j] =Wij <vi,vj>或(vi,vj) 属于VR
无穷 无边(弧)
D. 邻接矩阵的存储表示
用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 32767 //表示极大值,即无穷
#define MVNum 100 //最大顶点数
typedef char VerTexType; //设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
typedef struct{
VerTexType verxs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph; //Adjacency Matrix Graph
E. 采用邻接矩阵表示法创建无向网
- 输入总顶点数和总边数
- 依次输入点的信息存入顶点表中
- 初始化邻接矩阵,使每个权值初始化为极大值
- 构造邻接矩阵
Status CreateUDN(AMGraph &G){
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i=0;i<G.vexnum;++i)
cin>>G.vexs[i]; //依次输入点的信息
for(i=0;i<G.vexnum;++i) //初始化邻接矩阵
for(j=0;j<G.vexnum;++j)
G.arcs[i][j]=MaxInt;//边的权值均置为极大值
for(k=0;k<G.arcnum;++k){ //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边所依附的顶点及边的权值
i=LocateVex(G,v1);
j=LocateVex(G,v2); //确定v1和v2在G中的位置
G.arcs[i][j]=w; //边<v1,v2>的权值置为w
G.arcs[j][i]=G.arcs[i][j]; //置<v1,v2>的对称边<v2,v1>的权值为w
} //for
return OK;
}//CreateUDN
int LocateVex(AMGraph G,VertexType u){
//图G中查找顶点u,存在则返回顶点表中的下标,否则返回-1
int i;
for(i=0;i<G.vexnum;++i)
if(u==G.vexs[i]) return i;
return -1;
}
F. 邻接矩阵的优点:
-
直观、简单、好理解
-
方便检查任意一对顶点间是否存在边
-
方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
-
方便计算任一顶点的“度”(从该点发出的边数为"出度",指向该点的边数为"入度")
无向图:对应行(或列)非0元素的个数
有向图:对应行非0元素的个数是"出度";对应列非0的个数是"入度"
G. 邻接矩阵的缺点:
- 不便于增加和删除顶点
- 浪费空间——存稀疏图(点很多而边很少)有大量无效元素
- 浪费时间——统计稀疏图中一共有多少边
2. 邻接表(链式)表示法
顶点:按编号顺序将顶点数据存储在一维数组中;
关联同一顶点的边(以顶点为尾的弧):用线性链表存储
头结点、表结点
A. 无向图
- 邻接表不唯一
- 若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适宜存储稀疏图
- 无向图中顶点vi的度为第i个单链表中的结点数
B. 有向图
- 顶点vi的出度为第i个单链表中的结点个数
- 顶点vi的入度为整个单链表中邻接点域值是i-1的结点个数
- 找出度易,找入度难
逆邻接表:找入度易,找出度难
C. 图的邻接表存储表示
typedef struct VNode{
VerTexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum]; //AdjList表示邻接表类型
//AdjList v; 相当于 VNode v[MVNum];
//弧(边)的结点结构
#define MVNum 100 //最大顶点数
typedef struct ArcNode{ //边结点
int adjvex; //该边所指向的顶点的位置
struct ArcNode *nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
typedef struct{
AdjList vertices; //vertices--vertex的复数
int vexnum,arcnum; //图的当前顶点数和弧数
}ALGraph;
//邻接表操作举例说明
ALGraph G; //定义了邻接表表示的图G
G.vexum=5; //图G包含5个顶点,5条边
G.arcnum=5;
G.vertices[1].data='b'; //图G中第2个顶点是b
p=G.vertices[1].firtarc; //指针p指向顶点b的第一条边结点
p->adjvex=4; //p指针所指边结点是到下标为4的结点的边
D. 采用邻接表示法创建无向网
算法思想:
-
输入总顶点数和总边数
-
建立顶点表
依次输入点的信息存入顶点表中
使每个表头结点的指针域初始化为NULL
-
创建邻接表
依次输入每条边依附的两个顶点
确定两个顶点的序号i和j,建立边结点
将此边结点分别插入到vi和vj对应的两个边链表的头部
Status CreateUDG(ALGraph &G){ //采用邻接表表示法,创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数,总边数
for(i=0;i<G.vexnum;++i){ //输入各点,构造表头结点表
cin>>G.vertices[i].data //输入顶点值
G.vertices[i].firstarc=NULL //初始化表头结点的指针域
}//for
for(k=0;k<G.arcnum;++k){ //输入各边,构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
i=LocateVex(G,v1);
j=LocateVex(G,v2);
p1=newArcNode; //生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1; //将新结点*p2插入顶点vi的边表头部
p2=newArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex=i; //邻接点序号为i
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2; //将新结点*p2插入顶点vj的边表头部
}//for
return OK;
}//CreateUDG
E. 邻接表特点
-
方便找任一顶点的所有“邻接点”
-
节约稀疏图的空间
需要N个头指针+2E个结点(每个结点至少2个域)
-
对无向图方便计算任一顶点的“度”
-
对有向图只能计算“出度”;需要构造“逆邻接表”(存指向自己的边)来方便计算“入度”
-
不方便检查任意一对顶点间是否存在边
F. 邻接矩阵与邻接表表示法的关系
- 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数
- 区别:
- 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)
- 邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e)
- 用途:邻接矩阵多用于稠密图,而邻接表多用于稀疏图
3. 十字链表
用于有向图
邻接表——有向图——缺点:求结点的度困难——十字链表
邻接表——无向图——缺点:每条边都要存储两遍——邻接多重表
- 十字链表(Orthogonal List)是有向图的另一种链式存储结构(可以看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表)
- 有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每一个定点在十字链表中对应有一个结点,叫做顶点结点
4. 邻接多重表
(4)图的遍历
遍历的定义:
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,叫做图的遍历,它是图的基本运算。
遍历实质:
找每个顶点的邻接点的过程
图的特点:
图中可能存在回路,且图的任一顶点都可能与其他顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点
怎样避免重复访问?
解决思路:设置辅助数组visited[n]用来标记每个被访问过的顶点
- 初始状态visited[i]为1
- 顶点i被访问,改visited[i]为1,防止被多次访问
1. 深度优先搜索
(Depth_First Search——DFS)
方法:
- 在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w1
- 再从w1出发,访问与w1邻接但还未被访问过的顶点w2
- 然后再从w2出发,进行类似的访问
- 直至到达所有的邻接顶点都被访问过的顶点u为止
- 退回一部,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点
- 依此退退退,退回起始点
- 连通图的深度优先遍历类似于树的先根遍历
- 用邻接矩阵来表示图,时间复杂度为O(n^2),适于稠密图
- 用邻接表来表示图,时间复杂度为O(n+e),适于稀疏图
- 邻接矩阵表示的无向图深度遍历实现:
void DFS(AMGraph G,int v){ //图G为邻接矩阵类型
printf("%d",v);
visited[v]=true; //访问第v个顶点
for(w=0;w<G.vexnum;w++) //依次检查邻接矩阵v所在的行
if((G.arcs[v][w]!=0)&&(visited[w]))
DFS(G,w);
//w是v的邻接点,如果w未访问,则递归调用DFS
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JjhT6NkT-1677386139857)(C:\Users\LQN\AppData\Roaming\Typora\typora-user-images\image-20230223075815966.png)]
2. 广度优先搜索
(Breadth_First Search——BFS)
-
方法:
从图的某一结点出发,首先依次访问该结点的所有邻接顶点Vi1,Vi2,…,Vin,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点,重复此过程,直至所有顶点均被访问为止。
void BFS(Graph G,int v){ //按广度优先非递归遍历连通图G
count<<v;
visited[v]=true; //访问第v个顶点
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q,v); //v进队
while(!QueueEmpty(Q)){ //队列非空
DeQueue(Q,u); //队头元素出队并置为u
for(w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))
if(!visited[w]){ //w为u的尚未访问的邻接顶点
cout<<w;visited[w]=true;
EnQueue(Q,w); //w进队
}//if
}//while
}//BFS
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAmIpNBh-1677386139860)(C:\Users\LQN\AppData\Roaming\Typora\typora-user-images\image-20230223075830186.png)]
DFS与BFS算法效率比较:
- 空间复杂度相同,都是O(n)(借用了堆栈或队列);
- 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关
(5)图的应用
1. 最小生成树
-
生成树:所有顶点均由边连接在一起,但不存在回路的图
-
一个图可以有许多棵不同的生成树
-
所有生成树具有以下共同特点:
- 生成树的顶点个数与图的顶点个数相同
- 生成树是图的极小连通子图,去掉一条边则非连通
- 一个有n个顶点的连通图的生成树有n-1条边
- 在生成树中再加一条边必然形成回路
- 生成树中任一两个顶点间的路径是唯一的
-
无向图的生成树:
- 深度优先生成树
- 广度优先生成树
-
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树
-
构造最小生成树(Minimum Spanning Tree)
-
MST性质:
在生成树的构造过程中,图中n个顶点分属两个集合:
- 已落在生成树上的顶点集:U
- 尚未落在生成树上的顶点集:V-U
接下来则应在所有联通U中顶点和V-U中顶点的边中选取权值最小的边。
普里姆(Prim)算法
算法思想:
- 设N=(V,E)是连通网,TE是N上最小生成树在边的集合
- 初始令U={u0},(u0属于V),TE={}。
- 在所有u属于U,v属于V-U的边(u,v)属于E中,找一条代价最小的边(u0,v0)
- 将(u0,v0)并入集合TE,同时v0并入U
- 重复上述操作直至U=V为止,则T=(V,TE)为N的最小生成树
-
克鲁斯卡尔(Kruskal)算法
算法思想:
- 设连通网N=(V,E),令最小生成树初始状态为只有n个顶点而无边的非连通图T={V,{}},每个顶点自成一个连通分量。
- 在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边
- 以此类推,直至T中所有顶点都在同一连通分量上为止
-
最小生成树可能不唯一
算法名 | 普里姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2)(n为顶点数) | O(eloge)(e为边数) |
适应范围 | 稠密图 | 稀疏图 |
2. 最短路径
-
问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径
-
最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边
-
第一类问题:两点间最短路径——单源最短路径——用Dijkstra(迪杰斯特拉)算法
-
第二类问题:某源点到其他各点最短路径——所有顶点间的最短路径——用Floyd(弗洛伊德)算法
Dijkstra算法
“有效的程序员不应该浪费很多时间用于程序调试,他们应该一开始就不要把故障引入。”
-
初始化:先找出从源点v0到各终点vk的直达路径(v0,vk),即通过一条弧到达的路径。
-
选择:从这些路径中找出一条长度最短的路径(v0,u)。
-
更新:然后对其余各条路径进行适当调整
若在图中存在弧(u,vk),且(v0,u)+(u,vk)<(v0,vk),则以路径(v0,u,vk)代替(v0,vk)
在调整后的各条路径中,再找长度最短的路径,依次类推
按路径长度递增次序产生最短路径
-
把V分成两组
S:已求出最短路径的顶点的集合
T=V-S:尚未确定最短路径的顶点集合
-
将T中顶点按最短路径递增的次序加入到S中
保证:
-
从源点v0到S中各顶点的最短路径长度都不大于从v0到T中任何顶点的最短路径长度
-
每个顶点对应一个距离值:
S中顶点:从v0到此顶点的最短距离长度
T中顶点:从v0到此顶点的只包括S中顶点作中间顶点的最短路径长度
-
Floyd算法
所有顶点间的最短路径:
- 方法一:每次以一个顶点为源点,重复执行Dijkstra算法n次
- 方法二:弗洛伊德(Floyd)算法
算法思想:
- 逐个顶点试探
- 从vi到vj的所有可能存在的路径中,选出一条长度最短的路径
3. 拓扑排序
-
有向无环图:无环的有向图,简称DAG图(Directed Acycline Graph)
常用来描述一个工程或系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程)
一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成。
-
AOV网:(拓扑排序)
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
AOV网中不允许有回路。(因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的)
如何判别AOV网中是否存在回路?拓扑排序。
-
拓扑排序:
在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排列的算法称为拓扑排序。
-
拓扑排序的方法:
- 在有向图中选一个没有前驱的顶点且输出之
- 从图中删除该顶点和所有以它为尾的弧
- 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止
一个AOV网的拓扑序列不是唯一的
-
检测AOV网中是否存在环方法:
对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。
4. 关键路径
-
AOE网:(关键路径)
用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称AOE网(Activity On Edge)。
-
把工程计划表示为边表示活动的网络,即AOV网,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。
- 完成整项工程至少需要多少时间?
- 哪些活动是影响工程进度的关键?
求解关键路径问题。
-
事件表示在它之前的活动已经完成,在它之后的活动可以开始
-
源点——表示整个工程的开始,入度为0的顶点
-
汇点——表示整个工程的结束,出度为0的顶点
-
关键路径——路径长度最长的路径
-
路径长度——路径各活动持续时间之和
-
求解关键路径:
- ve(vj)——表示事件vj的最早发生时间
- vl(vj)——表示事件vj的最迟发生时间
- e(i)——表示活动ai的最早开始时间
- l(i)——表示活动的最迟开始时间
- l(i)-e(i)——表示完成活动ai的时间余量
- 关键活动——关键路径上的活动,即l(i)==e(i)的活动
- 求关键路径步骤:
- 求ve(i)、vl(j)
- 求e(i)、l(i)
- 计算l(i)-e(i)
- 若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动
- 如果一个活动处在所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间
- 处于所有的关键路径上的活动完成时间不能缩短太多,否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。
七、查找
(1) 基本概念
- 查找表是由同一类型的数据元素(或记录)构成的集合。由于“集合”中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。
- 查找——根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)
- 关键字——用来表示一个数据元素(或记录)的某个数据项的值
- 主关键字——可唯一地标识一个记录的关键字是主关键字
- 次关键字——用以识别若干记录的关键字是次关键字
- 若查找表存在这样一个记录,则称"查找成功",查找结果给出整个记录的信息,或指示该记录在查找表中的位置
- “查找不成功”,查找结果给出"空记录"或"空指针"
- 查找的目的:
- 查询某个"特定的"数据元素是否在查找表中;
- 检索某个"特定的"数据元素的各种属性
- 在查找表中插入一个数据元素
- 删除查找表中的某个数据元素
- 查找表:
- 静态查找表:仅作"查询"(检索)操作的查找表
- 动态查找表:作"插入"和"删除"操作的查找表
- 评价查找算法的指标
- 关键字的平均比较次数,也称平均查找长度ASL(Average Search Length)
- 关键字比较次数的期望值
(2) 线性表的查找
1. 顺序查找(线性查找)
-
应用范围:
- 顺序表或线性链表表示的静态查找表
- 表内元素之间无序
-
数据元素类型定义:
typedef struct{ KeyType key; //关键字 ... //其他域 }ElemType;
typedef struct{ //顺序表结构类型定义 ElemType *R; //表基址 int length; //表长 }SSTable; //Sequential Search Table SSTable ST; //定义顺序表ST
int Search_Seq(SSTable ST,KeyType key){ //若成功返回其位置信息,否则返回0 for(i=ST.length;i>=1;--1) if(ST.R[i].key==key)return i; return 0; } //其他形式 int Search_Seq(SSTable ST,KeyType key){ for(i=ST.length;ST.R[i].key!=key;--i) if(i<=0)break; //for(i=ST.length;ST.R[i].key!=key&&i>0;--i); if(i>0)retrun i; else retrun 0; } //改进:把待查关键字key存入表头("哨兵""监视哨") int Search_Seq(SSTable ST,KeyType key){ ST.R[0].key=key; for(i=ST.length;ST.R[i].key!=key;--i); return i; }
-
比较次数
- 查找第i个元素,需要比较n+1-i次
- 查找失败,需比较n+1次
-
时间复杂度:O(n)
- 查找成功时的平均查找长度:ASL(n)=(1+2+…+n)/n=(n+1)/2
-
空间复杂度:一个辅助空间——O(1)
-
顺序查找的特点:
- 优点:算法简单,逻辑次序无要求,且不同存储结构均适用
- 缺点:ASL太长,时间效率太低
-
如何提高查找概率
查找表存储记录原则——按查找概率高低存储
- 查找概率越高,比较次数越少
- 查找概率越低,比较次数较多
2. 折半查找(二分或对分查找)
-
有序表表示静态查找表——折半查找
-
折半查找——每次将待查记录所在区间缩小一半
-
折半查找算法:(非递归算法)
- 设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,key为给定的要查的值
- 初始时,另low=1,high=n,mid=(low+high)/2
- 让k与mid指向的记录比较
- 若key==R[mid].key,查找成功
- 若key<R[mid].key,则high=mid-1
- 若key>R[mid].key,则low=mid+1
- 重复上述操作,直至low>high时,查找失败
int Search_Bin(SSTable ST,KeyType key){ low=1; high=ST.length; //置区间初值 while(low<=high){ mid=(low+higt)/2; if(ST.R[mid].key==key) return mid; //找到待查元素 else if(key<ST.R[mid].key) //缩小查找区间 high=mid-1; //继续在前半区间进行查找 else low=mid+1; //继续在后半区间进行查找 } return 0; //顺序表中不存在待查元素 }//Search_Bin
-
折半查找——递归算法
int Search_Bin(SSTable ST,keyType key,int low,int high){ if(low>high)return 0; //查找不到时返回0 mid=(low+high)/2; if(key==ST.elem[mid].key) return mid; else if(key<ST.elem[mid].key) ...... //递归,在前半区间进行查找 else ...... //递归,在后半区间进行查找 }
-
判定树
- 查找成功比较次数=路径上的结点数=结点的层数
- 比较次数<=树的深度<=log2n+1
-
平均查找长度ASL约等于log2(n+1)-1(n>50)
-
折半查找的特点
- 优点:效率比顺序查找高
- 缺点:只适用于有序表,且限于顺序存储结构(对线性链表无效)
3. 分块查找
- 又称索引顺序查找
- 条件
- 将表分成几块,且表或者有序,或者分块有序
- 建立"索引表"(每个结点含有最大关键字域和指向本快递一个结点的指针,且按关键字有序)
- 查找效率
- ASL=对索引表查找的ASL+对块内查找的ASL
- 分块查找的特点:
- 优点:插入和删除比较容易,无需进行大量移动
- 缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算
- 适用情况:如果线性表既要快速查找有警察动态变化,则可采用分块查找
4. 比较
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
ASL | 最大 | 最小 | 中间 |
表结构 | 有序表、无序表 | 有序表 | 分块有序 |
存储结构 | 顺序表、线性链表 | 顺序表 | 顺序表、线性链表 |
(3) 树表的查找
1. 基本概念
- 动态查找表——几种特殊的树
- 二叉排序树
- 平衡二叉树
- 红黑树
- B-树
- B+树
- 键树
- 表结构在查找过程中动态生成
- 对于给定值key,若表中存在,则成功返回;否则,插入关键字等于key的记录
2. 二叉排序树
-
二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树
-
二叉排序树或是空树,或是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的值均小于根节点的值
- 若其右子树非空,则右子树上所有结点的值均大于等于根结点的值
- 其左右子树本身又各是一棵二叉排序树
-
二叉排序树的性质:中序遍历非空的二叉排序树,所得到的数据元素序列是一个按关键字排列的递增有序序列
-
二叉排序树查找递归算法:
typedef struct{ KeyType key; //关键字项 Info Type otherinfo; //其他数据域 }ElemType;
typedef struct BSTNode{ ElemType data; //数据域 struct BSTNode *lchild,*rchild; //左右孩子指针 }BSTNode,*BSTree; BSTree T; //定义二叉排序树T
BSTree SearchBST(BSTree T,KeyType key){ if((!T) || key==T->data.key)return T; else if(key<T->data.key) return SearchBST(T->lchild,key); //在左子树中继续查找 else return SearchBST(T->rchild,key); //在右子树中继续查找 }//SearchBST
-
二叉排序树效率分析
- 含有n个结点的二叉排序树的平均查找长度和树的形态有关
- 最好情况:域折半查找中的判定树相同,树的深度为log2n+1,时间复杂度O(log2n)
- 最坏情况:查找效率域顺序查找情况相同,时间复杂度O(n)
-
提高形态不均衡的二叉排序树的查找效率:做"平衡化"处理,即尽量让二叉树的形状均衡——平衡二叉树
-
一个无序序列可通过构造二叉排序树而变成一个有序序列
-
构造树的过程就是对无序序列进行排序的过程
-
不同插入次序的序列生成不同形态的二叉树排序树
-
从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变
-
二叉排序树的操作——删除
- 被删除的结点是叶子结点:直接删去该结点,其双亲结点中相应指针域的值改“空”
- 被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换它(结点替换),其双亲结点的相应指针域的值改为"指向被删除节点的左子树或右子树"
- 被删除的结点既有左子树,也有右子树
- 以其中序前驱值替换之(值替换),然后再删除该前趋结点(前趋是左子树中最大的结点)
- 也可以用其后继替换之,然后再删除该后继结点(后继是右子树中最小的结点)
3. 平衡二叉树
-
平衡二叉树(balanced binary tree),又称AVL树(Adelson-Velskii and Landis)
-
一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:
- 左子树与右子树的高度只差的绝对值小于等于1
- 左子树和右子树也是平衡二叉排序树
-
平衡因子(BF)——该结点左子树与右子树的高度差
平衡因子=结点左子树的高度-结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1、0、或1
-
失衡二叉排序树的调整
如果再一棵AVL树中插入一个新结点后造成失衡,则必须重新调整树的结构,使之恢复平衡。
- 平衡调整的四种类型
- LL型:把中间的拉上去作根结点
- LR型:把最下面拉上去做根节点
- RL型:把最下面拉上去做根节点
- RR型:把中间的拉上去作根结点
- 调整原则:1)降低高度 2)保持二叉排序树性质
- 谁大小在中间,谁就调到中间去当根结点
- 平衡调整的四种类型
4. 多路查找树(B树)
1. 概念
- Muitl-Way Search Tree
- 其每一个节点的孩子数可以多于两个,且每一个结点处可以存储多个元素
2. 2-3树
- 每个结点都具有两个孩子或三个孩子
- 一个2结点包含一个元素和两个孩子(或没有孩子)(不能只有一个孩子)
- 一个3结点包含一小一大两个元素和三个孩子(或没有孩子)
- 2-3树中所有的叶子都在同一层次上
3. 2-3-4树
- 一个4结点包含小中大3个元素和4个孩子(或没有孩子)
4. B树
- B树(B-tree)是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶(order)
- 2-3树是3阶B树,2-3-4树是4阶B树
- 一个m阶的B树具有如下属性:
- 如果根节点不是叶子结点,则其至少有两棵子树
- 每一个非根的分支节点都有k-1个元素和k个孩子
- 所有叶子结点都位于同一层次
5. B+树
(4) 哈希表的查找
1. 散列表的基本概念
-
基本思想:记录的存储位置与关键字之间存在对应关系
对应关系——hash函数
Loc(i)=H(keyi)
-
散列方法(杂凑法)
选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素键码进行比,确定查找是否成功
-
散列函数(杂凑函数):散列方法中使用的转换函数
-
散列表(杂凑表)
-
优点:查找效率高
-
缺点:空间效率低
-
冲突:不同的关键码映射到同一个散列地址
key1!=key2,但是H(key1)=H(key2)
在散列查找方法中,冲突是不可能避免的,只能尽可能减少
-
同义词:具有相同函数值的多个关键字
2. 散列函数的构造
-
考虑因素
- 执行速度(即计算散列函数所需时间)
- 关键字的长度
- 散列表的大小
- 关键字的分布情况
- 查找频率
- 地址空间尽量少
-
方法
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
-
直接定址法
Hash(key)=a.key+b (a、b为常数)
优点:以关键码key的某个线性函数值为散列地址,不会产生冲突
缺点:要占用连续地址空间,空间效率低
-
除留余数法
Hash(key)=key mod p (p是一个整数)
3. 处理冲突的方法
A. 开放定址法(开地址法)
- 基本思想:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入
- 常用方法:线性探测法、二次探测法、伪随机探测法
B. 链地址法(拉链法)
- 基本思想:相同散列地址的记录链成一单链表,m个散列地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构
- 建立散列表步骤:
- 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表
- 若不为空,则根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入此链表
- 链地址法的优点:
- 非同义词不会冲突,无"聚集"现象
- 链表上借点空间动态申请,更适合于表长不确定的情况
C. 再散列法(双散列函数法)
D. 建立一个公共溢出区
4. 散列表的查找
使用平均查找长度ASL来衡量查找算法,ASL取决于:
- 散列函数
- 处理冲突的方法
- 散列表的装填因子a
- 链地址法由于开放地址法
- 除留余数法法作散列函数由于其他类型函数
八、排序
(1) 概念
-
排序:将一组杂乱无章的数据按一定规律顺次排列起来,即将无序序列排成一个有序序列(由小到大或由大到小)的运算
-
分类:
-
按数据存储介质:内部排序和外部排序
-
按比较器个数:串行排序和并行排序
-
按主要操作:比较排序和基数排序
-
按辅助空间:原地排序和非原地排序
-
按稳定性:稳定排序和非稳定排序
-
按自然性:自然排序和非自然排序
-
内部排序:数据量不大、数据在内存,无需内外存交换数据
-
外部排序:数据量较大、数据在外存(文件排序)
-
串行排序:单处理机(同一时刻比较一对元素)
-
并行排序:多处理机(同一时刻比较多对元素)
-
比较排序:用比较的方法(插入排序、交换排序、选择排序、归并排序)
-
基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置
-
原地排序:辅助空间用量为O(1)的排序方法(所占的辅助存储空间与参加排序的数据量大小无关)
-
非原地排序:辅助空间用量超过O(1)的排序方法
-
稳定排序:能够使任何数值相等的元素,排序以后相对次序不变
-
非稳定性排序:不是稳定排序的方法
排序的稳定性只对结构类型数据排序有意义
-
自然排序:输入数据越有序,排序的速度越快的排序方法
-
非自然排序:不是自然排序的方法
-
-
-
按排序依据原则
1. 插入排序:直接插入排序、折半插入排序、希尔排序 2. 交换排序:冒泡排序、快速排序 3. 选择排序:简单选择排序、堆排序 4. 归并排序:2-路归并排序 5. 基数排序
-
按排序所需工作量
- 简单的排序方法:T(n)=O(n^2)
- 基数排序:T(n)=O(d. n)
- 先进的排序方法:T(n)=O(nlogn)
-
存储结构——记录序列以顺序表存储
#define MAXSIZE 20 //设记录不超过20个 typedef int KeyType; //设关键字为整型量(int)
Typedef struct{ //定义每个记录(数据元素)的结构 KeyType key; //关键字 InfoType otherinfo; //其它数据项 }RedType; //Record Type
Typedef struct{ //定义顺序表的结构 RedType r[MAXSIZE+1]; //存储顺序表的向量 int length; //顺序表的长度 }SqList;
(2) 插入排序
-
基本思想:每一步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,知道对象全部插入为止
-
即边插入边排序,保证子序列中随时都是排好序的
-
在插入a[i]前,数组a的前半段(a[0]a[i-1])是有序段,后半段(a[i]a[n-1])是停留于输入次序的"无序段"
1. 直接插入排序
顺序法定位插入位置
1. 复制插入元素
x=a[i]
2. 记录后移,查找插入位置
for(j=i-1;j>=0&&x<a[j];j--)
a[j+1]=a[j];
3. 插入到正确位置
a[j+1]=x;
1. 复制为哨兵
L.r[0]=L.r[i];
2. 记录后移,查找插入位置
for(j=i-1;L.r[0].key<L.r[j].key;--j)
L.r[j+1]=L.r[j];
3. 插入到正确位置
L.r[j+1]=L.r[0]
void InsertSort(SqList &L){
int i,j;
for(i=2;i<-L.length;++i){
if(L.r[i].key<L.r[i-1].key){ //若"<",需将L.r[i]插入有序子表
L.r[0]=L.r[i]; //复制为哨兵
for(j=i-1;L.r[0].key<L.r[j].key;--j){
L.r[j+1]=L.r[j]; //记录后移
}
L.r[j+1]=L.r[0]; //插入到正确位置
}
}
}
时间复杂度结论:
1. 原始数据越接近有序,排序速度越快
2. 最坏情况下(输入数据是逆有序的),Tw(n)=O(n^2)
3. 平均情况下,耗时差不多是最坏情况的一半,Te(n)=O(n^2)
4. 要提高查找速度:减少元素的比较次数、减少元素的移动次数
2. 二分插入排序
二分法定位插入位置
void BInsertSort(SqList &L){
for(i=2;i<=L.length;++i){ //依次插入第2~第n个元素
L.r[0]=L.r[i]; //当前插入元素存到"哨兵"位置
low=1; //采用二分法查找插入位置
high=i-1;
while(low<=high){
mid=(low+high)/2;
if(L.r[0].key<L.r[mid].key)high=mid-1;
else low=mid+1;
}//循环结束,high+1则为插入位置
for(j=i-1;j>=high+1;--j)L.r[j+1]=L.r[j]; //移动元素
L.r[high+1]=L.r[0]; //插入到正确位置
}
}//BInsertSort
- 折半哈如排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
- 减少了比较次数,但没有减少移动次数
- 平均性能优于直接插入排序
3. 希尔排序
(Donald.L.Shell) 缩小增量多变插入排序
-
基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,带整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序
-
特点:
- 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
- 最后一次只需要少量移动
- 增量序列必须是递减的,最后一个必须是1
- 增量序列应该是互质的
void ShellSort(Sqlist &L,int dlta[],int t){ //按增量序列dlta[0..t-1]对顺序表L作希尔排序 for(k=0;k<t;++k) ShellInsert(L,dlta[k]); //一趟增量为dlta[k]的插入排序 }//ShellSort
void ShellInsert(SqList &L,int dk){ //对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子 for(i=dk+1;i<=L.length;++i) if(r[i].key<r[i-dk].key){ r[0]=r[i]; for(j=i-dk;j>0&&(r[0].key<r[j].key);j=j-dk) r[j+dk]=r[j]; r[j+dk]=r[0]; } }
- 希尔排序法是一种不稳定的排序方法
- 最后一个增量值必须为1,无除了1之外的公因子
- 不宜在链式存储结构上实现
(3) 交换排序
1. 冒泡排序
- 基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换
void bubble_sort(SqList &L){ //冒泡排序算法
int m,i,j;
RedType x; //交换时临时存储
for(m=1;m<=n-1;m++){ //总共需m趟
for(j=1;j<=n-m;j++)
if(L.r[j].key>L.r[j+1].key){//发生逆序
x=L.r[j];
L.r[j]=L.r[j+1];
L.r[j+1]=x; //交换
}//endif
}//for
}
//改进
void bubble_sort(SqList &L){//赶紧的冒泡排序算法
int m,i,j,flag=1; //flag作为是否有交换的标记
RedType x;
for(m=1;m<=n&&flag==1;m++){
flag=0;
for(j-1;j<=m;j++)
if(L.r[j].key>L.r[j+1].key){//发生逆序
flag=1; //发生交换,flag置位1
x=L.r[j]; //交换
L.r[j]=L.r[j+1];
L.r[j+1]=x;
}//endfi
}//for
}
2. 快速排序
- 基本思想
- 任取一个元素为中心(pivot:枢轴、中心点)
- 所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表
- 对各子表重新选择中心元素并依此规则调整
- 直到每个子表的元素只剩一个
void main(){
QSort(L,1,L.length);
}
void Qsort(SqList &L,int low,int high){//对顺序表L快速排序
if(low<high){ //长度大于1
pivotloc=Partition(L,low,high);
//将L.r[low...high]一分为二,pivotloc为枢轴元素排好序的位置
QSort(L,low,pivotloc-1); //对低子表递归排序
QSort(L,pivotloc+1,high); //对高子表递归排序
}//endif
}//QSort
int Partition(SqList &L,int low,int high){
L.r[0]=L.r[low];
pivotkey=L.r[low].key;
while(low<high){
while(low<high&&L.r[high].key>=pivotkey) --high;
L.r[row]=L.r[high];
while(low<high&&L.r[low].key<=pivotkey) ++low;
L.r[high]=L.r[low];
}
L.r[low]=L.r[0];
return low;
}
- 快速排序不适于对原本有序或基本有序的记录序列进行排序
(4) 选择排序
1. 简单选择排序
- 基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置
void SelectSort(SqList &K){
for(i=1;i<L.length;++i){
k=i;
for(j=i+1;j<=L.length;j++)
if(L.r[j].key<L.r[k].key)k=j; //记录最小值位置
if(k!=i)L.r[i]=L.r[k]; //交换
}
}
2. 堆排序
- 若n个元素的序列(a1,a2…an)满足ai<=a2i且ai<=a2i+1,或ai>=a2i且ai=a2i+1,则分别称该序列{a1,a2…an}为小根堆和大根堆
- 堆实质是满足如下性质的完全二叉树:
- 二叉树中任一非叶子结点均小于(大于)它的孩子结点
- 堆排序——若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值)…如此反复,便能得到一个有序序列,这个过程称之为堆排序
- 堆调整
- 输出堆顶元素之后,以堆中最后一个元素替代之
- 然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
- 重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为"筛选"
void HeapAdjust(elem R[],int s,int m){
rc=R[s];
for(j=2*s;j<=m;j*=2){ //沿key较大的孩子结点向下筛选
if(j<m&&R[j]<R[j+1])++j;//j为key较大的记录的下标
if(rc>=R[j])break;
R[s]=R[j]; //rc应插入在位置s上
s=j;
}//for
R[s]=rc; //插入
}//HeapAdjust
- 堆排序算法:
void HeapSort(elem R[]){ //对R[1]到R[n]进行堆排序
int i;
for(i=n/2;i>=1;i--)
HeapAdjust(R,i,n); //建初始堆
for(i=n;i>1;i--){
Swap(R[1],R[i]); //根与最后一个元素交换
HeapAdjust(R,1,i-1); //对R[1]到R[i-1]重新建堆
}
}//HeapSort
(5) 归并排序
- 基本思想:将两个或两个以上的有序子序列"归并"为一个有序序列
- 在内部排序中,通常采用的是2-路归并排序
void merge(node r[], int low, int mid, int high) {
node tmp[MAXSIZE]; // tmp是汇总2个有序区间的临时区域。
int i = low; // 第一个有序区的索引
int j = mid + 1; // 第二个有序区的索引
int k = 0; // 临时区域的索引
while (i <= mid && j <= high) {
if (r[i].key <= r[j].key) {
tmp[k++].key = r[i++].key;
}
else {
tmp[k++].key = r[j++].key;
}
}
while (i <= mid) {
tmp[k++].key = r[i++].key;
}
while (j <= high) {
tmp[k++].key = r[j++].key; // 将两个有序区间合并
}
// 排序后的元素,全部都整合到数组a中
for (i = 0; i < k; i++) {
r[low + i].key = tmp[i].key;
}
}
void Msort(node r[MAXSIZE],int low,int high){
if (low >= high)
{
return ;
}
int mid = (low + high) / 2;
Msort(r, low, mid); // 递归排序r[low..mid]
Msort(r, mid + 1, high); // 递归排序r[mid..high]
// r[low..mid]和r[mid..high]是两个有序空间
merge(r, low, mid,high); // 将它们排序成一个有序空间r[low..high]
}
(6) 基数排序
- 基本思想:分配+收集
- 也叫桶排序或箱排序
(7) 外部排序
(8) 排序的比较
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|---|
最好情况 | 最坏情况 | 平均情况 | 辅助存储 | |||
插入排序 | 直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n) | O(n^2) | O(n^1.3) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(nlogn) | 不稳定 | |
选择排序 | 直接选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n^2) | 不稳定 | |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | |
基数排序 | k:待排元素的维数m:基数的个数 | O(n+m) | O(k*(n+m)) | O(k*(n+m)) | O(n+m) | 稳定 |
未完待续。。。