据结构导论
第一章 概论
1.概述及引言
1>概述:<<数据结构导论>>主要介绍如何组织各种数据在计算机中的存储,传递和转换;
内容包括:
*1.线性表,栈,队列,数组,树,二叉树,图等基本数据结构及其应用;
*2.排序和查找的原理与方法;数据在外存上的组织方法;
*3.四色定理,只需用4种颜色可使相邻的两个省或国家没有相同的颜色;
*算法+数据结构=程序;
2>数据结构的概念
*1.数据结构(Data structure)是指相互之间存在一种或多种特定关系的数据元素的集合.
*2.包含内容
#1.数据的逻辑结构
#2.数据的存储结构
#3.数据的基本运算
3>计算机解决问题的步骤
1) 建立数学模型
2) 设计算法
3) 编程实现算法
4>数据结构主要研究
*1.数据(计算机加工对象)的逻辑结构
*2.实现各种基本操作的算法;
机外处理==建模==>逻辑结构==求精==>存储结构
处理要求==建模==>基本运算==求精==>算法
(问题) (数据模型) (实现)
5>计算机内存分析
CPU和内存 :CPU负责数据的运算和处理, 内存负责交互数据
当程序或操作者对CPU发出指令后,这些指令和数据暂存在内存中,在CPU空闲时传给CPU
CPU处理后把结果输出到输出设备上,输出设备就是显示器
通俗讲:
电脑是企业,内存是车间,CPU是生产线,硬盘是仓库,主板是地基,
CPU速度快,生产就快; 内存大,一次处理的原材料就多,
所以提高机器速度有两条路,一是CPU升级,一是扩大内存,一次处理更多的信息产品,
但CPU与内存又互相制约,车间再大,CPU慢也快不起来,CPU快,但车间小,一次送来的加工材料没多少,也快不了
*.在编译的时候编译器会把程序中出现的所有变量名都换成相对内存地址,变量名不占内存
2.基本概念和术语
1>数据,数据元素和数据项
*1.数据(Data):所有能被计算机存储、处理的对象;
*2.数据元素(Data Element):简称元素,是数据的基本单位,也是运算的基本单位,通常具有完整确定的实际意义;
*3.数据项(Data Item):数据元素常常还可以分为若干个数据项(字段和域),数据项是数据具有意义的最小单位,;
*4.原始数据:实际问题中的数据称为原始数据
*数据项∈数据元素∈数据
2>数据的逻辑结构(Logical Structure)
1)定义:指数据元素之间的关联方式或"邻接关系"
2)类型
数据的逻辑结构(D,{R})可分为下列几种:D={d1,d2...,dn}.
*1.集合:
形式:数据元素同"属于一个集合" R={};
特点:任意两个结点之间都没有邻接关系,组织形式松散;
*2.线性结构(有序):
形式:R={(d1,d2),(d2,d3),...,(dn-1,dn)},即除起始节点和终端结点d1,dn外,
每个节点有一个前驱和一个后继;
特点:结点按逻辑关系依次排列形成一条"链",结点之间一个一个依次相邻接;
*3.树状结构:
形式:(D,{R})构成树,即每个元素最多有一个前驱,可以有多个后继.(叶子节点);
特点:具有分支,层次特性,上层的结点可以和下层多个结点相邻接,
但下层结点只能和上层的一个结点相邻接
*4.图状结构:
形式:(D,{R})构成一个图;
特点:任意两个结点都可以相邻接
3)逻辑结构注意事项
*1.逻辑结构与数据元素本身形式、内容无关;
*2.逻辑结构与数据元素的相对位置无关;
*3.逻辑结构与所含结点个数无关;
3>数据的存储结构(也称物理结构(Physical Structure))
1)概念:指数据结构在机内的表示,数据的逻辑结构在计算机中的实现;
2)存储结构的主要部分
*1.存储结点(每个存储结点存放一个数据元素);
*2.数据元素之间关联方式的表示
*数据结构的存储包含数据元素的存储及其逻辑关系的存储
3)存储结构分为:
*1.顺序存储结构:
线性表的顺序存储方法:将表中的所有存储结点依次存放在计算机内一组 连续 的存储区里,
借助结点在存储器中的相对存储位置(数组下标)来表示数据元素之间的逻辑关系。
#.实现特点
#1.预选分配好长度,需要预估存储数据需要的存储量
#2.插入和删除需要移动其他的元素(不方便)
#3.存取快捷,是随机存储结构(直接根据下标直接存取.)
*2.链式存储结构:
每个存储结点除了含有一个数据元素外,还包含指针,每个指针指向一个与本结点有逻辑关系的结点,
用指针表示数据元素之间的逻辑关系(附加的指针字段,指出其直接后继或直接前驱节点的位置)
即存储结点的存储单元分为两部分:①数据项 ②.指针项
#.实现特点
#1.动态分配,不需要预先确定内存分配;
#2.插入和删除不需要移动其他元素;
#3.非随机存取结构
*3.索引存储方式:
借助索引表中的索引指示各存储节点的存储位置
*4.散列存储方式:
用散列函数指示各节点的存储位置(除以质数)
注:顺序存储结构和链式存储结构是最基本的存储结构;
4>运算
*1.概念:
#1.运算就是指在某种逻辑结构上施加的操作,即对逻辑结构的加工.
#2.以数据的逻辑结构为对象,一般包括:建立,查找,读取,插入和删除等;
*2.分类
1)加工型运算:其操作改变原逻辑结构的值,如结点个数,结点内容等(建立,插入,删除)
2)引用型运算:其操作不改变原逻辑结构的值(取表元,查找定位和读取长度)
3.算法及描述
1>.概念:
算法规定了求解给定类型问题所需的所有"处理步骤"及执行顺序,使给定类型问题
能在有限时间内被机械的求解;
2>算法所使用的描述语言:
1)程序
2)介于自然语言和程序语言的伪代码
3)非形式算法(自然语言)
4)框图(N-S图)
3>特性
1)又穷性:一个算法总是在执行有穷步后结束.
2)确定性:算法的每一步都必须是明确定义的
3)可行性:算法中的每一步都是可以通过已经实现的操作来完成的;
4)输入:一个算法有零个或者多个输入,这些输入取自于特定的对象集合;
5)输出:一个算法有零个或者多个输出,它们是与输入有特定关系的量;
4>运算符:
1)算术运算符:(+ - * / % +(取正) –(取负) )
2)自增,自减运算符: ++ --
3)关系运算符: (< <= == > >= !=)
4)逻辑运算符: ! && ||
5)位运算符: << >> ~ | &
6)赋值运算符 (= += -= *= /=)
7)条件运算符(?:)
8)逗号运算符(,)
9)数据长度运算符 (sizeof)
10)强制类型转换 (类型)
11)成员运算符:( . ->)
12)下标运算符([ ])
13)指针运算符(* &)
*数据结构导论基于C程序开发
C程序-->文件1(*.c)->main() 函数1 函数2 -->函数说明-->函数体-->变量说明-->执行语句
4.算法分析(评价算法的好坏)
1>算法的设计要求
1)正确性:
对于合法的输入产生符合要求的输出;
2)易读性:
算法应该易读,便于交流,这也是确保算法的正确性的前提,添加注释也是一种可以增加可读性的方法
3)健壮性:
当输入非法时,算法还能适当的反应而不崩溃,如输入错误信息;算法中应该考虑适当的错误处理
4)时空性:
一个算法的时空性是指算法的时间性能(时间效率)和空间性能(空间效率),算法分析主要分析
算法的时间复杂度(算法包含的计算量)和算法的空间复杂度(算法需要的存储量),
目的是提高算法的效率;
2>时间复杂度(大O算法)
1)概念:
大O算法也称渐进表示法
算法输入规模的函数
时间复杂度是算法运行时需要的总步数,包括最坏情况(所有输入下计算量的最大值作为算法的计算量)
时间复杂度(n)和平均时间复杂度(所有输入下计算量的加权平均值作为算法的计算量)(n/2);
*.我们将时间复杂度记为输入数据规模n的函数,若求解问题需要执行n^2次操作,记作O(n^2)
2)计算方法:
循环结构 每多一次嵌套循环都会增加一个指数即 t(n)=O(n^2); 不循环就是 O(1)
等差数列的求和公式
sn=n(a1+an)/2=(d/2)n^2+(a1-d/2)n;
3)时间复杂度按数量级排序: 如下
f(n)=log2^n f(n)=n f(n)=n^2 f(n)=n^3 f(n)=n^4 f(n)=n^10 f(n)=2^n
*对数与指数
对数:
若 a^n=b(a>0 && a!=1),称为a的n次幂等于b,在这里,a叫做底数,n叫做指数,b叫做以a为底的n次幂;
指数:
转换为对数形式
n=loga^b(a>0 && a!=1),a仍叫做底数,b叫做真数,n叫做以a为底b的对数;
a^loga^b=b; 例如:2^3=8; log2^8=3; 8=a^log2^8=a^3
4)常见算法时间复杂度的阶数有:
常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n)
平方阶(O(n^2)),多项式阶O(n^c),指数阶(O(C^n)) (C为大于1的正整数)
通常指数阶量级的算法实际不可计算,而量级低于平方阶的算法是高效率的;
3>空间复杂度
1)概念:算法在执行过程中临时占的存储空间大小的量度.
2)算法在执行期间所需的存储空间量组成:
*1.程序代码所占用的空间
*2.输入数据所占用的空间
*3.辅助变量所占用的空间
*注:估算算法空间复杂度时,一般只分析辅助变量所占用的空间
变量将磁盘(辅存)存放到主存;
3)计算临时的辅助变量所占用的存储空间 temp=1 O(1);temp=int b[n] O(n);
第二章 线性表
1.线性表的基本概念
1>概念:线性表是由n(n>=0)个数据元素(结点)a1,a2,...,an组成的有穷序列;
2>特点:
1)元素个数n定义为表的长度,n=0时表示空表,记作()或∅
2)将非空的线性表(n>0)记作:L=(a1,a2,...,an);
3)a1为起始结点,an为终端结点,对任意一对相邻的结点ai和ai+1(1<=i线性表是逻辑结构,实现它的存储结构是顺序存储结构和链式存储结构
2.线性表的基本运算
1>初始化 Initiate(L) 建立一个空表L=(),L不含数据元素.
2>求表的长度 Length(L) 返回线性表L的长度
3>取表元 Get(L,i) 返回线性表弟i个数据元素,当i不满足1<=i<=Length(L)时,返回特殊值
4>定位 Locate(L,x) 查找线性表中数据元素值定于x的结点序号,若有多个数据元素值与x相等,
运算结果为这些结点中序号的最小值,若找不到该结点,则运算结果为0
5>插入 Insert(L,x,i) 在线性表L的第i数据元素之前,插入一个值为x的新数据元素,参数i的合法取值
范围是1<=i<=n+1,操作结束后线性表L由(a1,a2,...,ai-1,ai,ai+1,...,an)
到(a1,a2,...,ai-1,x,ai,ai+1,...,an),表长度加1;
6>删除 Delete(L,i) 删除线性表L的第i个数据元素ai,参数i的合法取值范围是1<=i<=n+1,
操作结束后线性表L由(a1,a2,...,ai-1,ai,ai+1,...,an)到
(a1,a2,...,ai-1,ai+1,...,an),表长度减1;
3.线性表的顺序存储实现--顺序表
1>实现方法:
1)用顺序表实现:
将表中的结点依次存放在计算机内存中一组连续的存储单元中,数据元素在线性表中的邻接关系决定它们在
存储空间中的存储位置,即逻辑结构中相邻的结点其存储位置也相邻;
用顺序存储实现的线性表称为顺序表,顺序是表用一维数组实现的线性表,数组下标可以看成是元素的相对地址;
2)顺序表存储结构的特点:
*1.线性表的逻辑结构与存储结构一致;
*2.可以对数据元素实现随机读取;
2>线性表顺序存储的类型定义
*1.计算线性表中元素的存储地址:
假设已知a1地址为Loc(a1),每个数据占L个单元则计算ai地址
Loc(ai)=Loc(a1)+L*(i-1);
[e.g]
若线性表采用顺序存储结构,每个元素占用4个存储单元,第1个元素的存储地址为100,
则第12个元素的存储地址是多少?
loc(a12)=100 (Loc(a1)) +4*(12-1);
loc(a12)=100+4*11=144;
*2.顺序表内的数据成员
顺序表数组的大小 MaxSize
顺序表长度 length
所存放数据类型 DataType
位置:下标+1;
3>顺序表的结构体定义
Const int maxsize=100 //预先定义好的的顺序表数组的大小
typedef struct{
DataType data[maxsize];//存放数据的数组
int length; //顺序表的实际长度
}Seqlist; //顺序表的类型
Seqlist L;//线性表的声明
//示例
const int Maxsize=7;
typedef struct{
int num;
char name[8];
char sex[2];
int age;
int score;
}DataType;//定义结点类型
4>线性表的基本运算在顺序表上的实现
1)Insert 插入(增)的实现 (*****算法题*****)
*1.当表空间已满,不可再做插入操作
*2.当插入位置为非法位置,不可做正常的插入操作
*3.data的下标:0~L.length-1,位置是1-length
*4.算法实现代码
//需求:将结点x插入到顺序表L的第i个数据元素之前 {a1,...,ai-1,x,ai,...an}
void InsertSeqList(SeqList L,DataType x,int i){
//1.检查插入的位置是否合法(lengthL.length+1) exit("位置错");
/*
*2.设置x的存储位置
*初始i=L.length 元素最多移动次数为n-i+1; 平均移动次数n/2,时间复杂度为O(n);
*仅当插入位置i=n+1时才无需移动结点,直接将结点x插入表的末尾;
*如果i不等于n+1,就把i这个位置(下标为i-1)空出来,来存放结点x;
*将现有最后位置元素的值赋给后一位元素,从length(n)位置向前实现依次右移直到i的位置
*将下标为j-1的值赋值给下标为j的空间,直到将下标为i-1(位置i)赋值给下标为i(位置i+1)的结点
*这样将下标为i-1(i位置)的结点的数据元素就能再赋值了
*/
for(j=L.length;j>=i;j--){
L.data[j]=L.data[j-1];//依次后移
}
L.data[i-1]=x;//3.元素x置入到下标为i-1的位置(i位置)
L.length++;//4.表长度加1,更新顺序表结构中的length数据成员
}
*5.移动次数n-i+1,平均移动n/2,时间复杂度O(n)
2)Delete 删除的实现(*****算法题*****)
//删除线性表L中的第i个数据结点,删除后结点值仍然相邻,
//元素最多移动n-1次,平均复杂度(n-1)/2,时间复杂度为O(n)
void DeleteSeqList(SeqList L,int i){
if(i<1||i>L.length) exit("非法位置");//检查位置是否合法 (1<=i<=length)
for(j=i;j顺序表的优点
1)无需为表示结点间的逻辑关系而增加额外存储空间(提取快速)
2)可以方便地随机存取表中的任一结点(更改某个结点的值)
6>顺序表的缺点
1)插入和删除运算不方便,必须移动大量的结点
2)顺序表要求占用连续的空间,存储分配只能预先进行,因此当表变化较大时,
难以确定合适的存储规模;
4.线性表的链接存储--链表
1>概念:链式方式存储的线性表简称链表
Link List
链表的具体存储表示为:
1) 用一组任意的存储单元来存放线性表中的数据元素
2) 链表中结点的逻辑次序和物理次序不一定相同,还必须存储指示其 后继结点 的地址信息;
2>单链表中的结点结构:
1) data域:存放结点值的数据域
2) next域:存放结点的直接后继的地址(位置)的指针域(链域)
3) 注意事项:
*1.所有结点通过指针链接而组成单链表
*2.NULL称为 空指针
*3.Head称为 头指针变量,一般不存数据,只存放链表中首结点地址;
增加头结点的目的是方便运算的实现;
3>指针
1) 概念:
一个变量的地址称为该变量的指针,例如地址2000是变量i的指针,
如果有一个变量专门用来存放另一个变量的地址(即指针)的,则称它为 "指针变量";
2)注意事项:
指针变量是存放地址的变量,不要讲一个整数(或其他非地址类型的数据)赋给一个指针变量
3)与指针变量有关的运算符
*1.&(取地址符) :其功能是返回操作数的内存地址,操作对象为一个变量;
*2.*(取内容符) :表示的是地址对应单元中的内容,操作对象为一个变量的地址;
*3.*和&优先级相同,结合方向自右向左;
//指针示例:
#include
void main(){
int a=10,*p1;
p1=&a;//将变量a的地址值赋值给指针p1;//操作的是变量a;
*p1=30;//给指针p1所指向的地址的内容赋值,也就是将a的值改为30;//操作的是a的地址
printf("%d,%d\n",a,*p1);//分别打印整数a和指针p1所指地址中内容的值 都为30;
}
4)头结点指针
struct node *head:内存放头结点的地址;
4>头结点
1)概念:单链表中第一个结点内一般不存数据,称为头结点,利用头指针存放该结点的地址;
2)设置头结点的原因: 方便运算
3)链表为空:
*1.无头结点:head==NULL;
*2.有头结点: head.next==NULL;
5>单链表特点
1) 起始点又称为首结点,无前驱,故设头指针head指向起始结点
2) 链表由头指针唯一确定,单链表可以用头指针的名字来命名,头指针名是head的链表可称为表head
3) 终端结点又称尾结点,无后继,故终端结点的指针域为空,即NULL;
4) 除头结点外的结点为表结点;
5) 为运算操作方便,头结点中不存数据
6>单链表的类型定义
typedef struct node{
DataType data;//结点数据元素 数据域
struct Node * next;//后继结点指针 指针域(存放着后继结点的存储地址)
}Node,*LinkList;
7>线性表的基本运算在单链表上的实现
1) 初始化:
*1.建立一个空的单链表L.InitiateLinkList(L);
*2.一个空的单链表由一个头指针和一个头结点构成
*3.假设已定义指针变量t,令t指向一个头结点并令头结点的next为NULL
*4.malloc函数的使用格式及作用:
作用:产生头结点时有malloc函数产生一个新结点;
动态分配内存函数malloc函数格式如下:
(数据类型*)malloc(sizeof (数据类型))
如:int *p; p=(int*) malloc(sizeof (int))将开辟的空间地址返回
*5.链表插入结点时要malloc(sizeof(DataType))开辟结点空间
链表删除结点时要free(*DataType)释放空间
*5.代码实现
空表由一个头指针和一个头结点组成.算法描述如下:
//建立一个带头结点的单链表
LinkList InitiateLinkList(){
LinkList head; //头指针
head=malloc(sizeof(Node));//开辟空间,动态构建一个结点,它是头结点
head->next=NULL;
return head;
}
//在算法中,变量head是链表的头指针,他指向创建的结点,即头结点,
//一个空单链表仅有一个头结点,它的指针域为NULL;
2)求表长
在单链表存储结构中,线性表的长度等于单链表所含结点的个数(不含头结点);
算法步骤:
*1.令表长计数器j为0;
*2.令指针p指向头结点;
*3.当下一个结点不为空时,j加1,p指向下一个结点;
*4.j的值即为链表中结点个数,即表长度;
代码实现:时间复杂度 O(n);
int lengthLinkList(LinkList head){
Node *p;
p=head;//p是指向头结点的指针
j=0;//表长计数器
while(p->next!=NULL){//如果p所在结点的后继结点地址不为空
p=p->next;//更新当前结点,使其后移一位
j++;//表的长度加1
}
return j;//返回表长
}
3)读表元素
//查找第i个结点(ai)
算法步骤:
*1.令计数器j为0;
*2.令p指向头结点;
*3.当下一个结点不为空时,并且jnext;
int j=0;
while((p!=NULL)&&(jnext;
j++;
}
if(i==j) return(p);
else return(NULL);
}
Node *p=Node*GetLinkList(LinkList head,int i)
结点ai元素域=p->data;
结点ai的后继结点地址=p->next;
4)定位算法
//给定一个结点的值,找出这个结点是单链表的第几个结点,定位运算又称作按值查找.
//在定位运算中,也需要从头至尾访问链表,直到找到需要的结点,返回其序号,若未找到,返回0;
具体算法
//求表head中第一个值等于x的结点的序号, 若不存在这种结点, 返回结果为0
int LocateLinkList(LinkList head,DataType x){
Node *p=head; //p为工作指针,变量名可认为是变量地址
p=p->next;//初始时p指向首结点
int i=0; //i代表结点的序号,这里置初值为0;
while(p!=NULL && p->data!=x){ //访问链表
i++;
p=p->next
}
if(p!=NULL) return i+1;
else return 0;
}
5) 插入算法
//需求.假设p的结点是ai,而q的结点是ai-1,现在在ai前插入一个结点s,并将s的结点值设为x;
*1.算法思路
//先搭上后继结点ai(p)结点,再剪断ai-1(q)结点,使ai-1连接到新的结点s
①.找到i-1元素结点指针q;
②.动态构建一个结点 malloc(sizeof(node))并将新开辟的内存地址赋值给新结点指针*p;
③.将值域x赋给新结点p
④.将结点q的下一结点地址赋给p所指的next;
⑤.将结点p赋值个结点q->next;
核心代码:
s->next=p;先将s结点连接上ai(p)结点;
q->next=s;再将q(ai-1)结点的后继结点指向s;
注:如果先将q的后继结点指向s了,那s后的链接就断了,p结点~an结点这段就丢失了,
也就是说s先连接后面这段,再连接前面这段;
//具体实现:在表head的第i个数据元素结点之前插入一个以x为值的新结点;
void InsertLinkList(LinkList head,DataType x,int i){
Node *p,*q;
//*p是要插入到i位置前的新结点,*q是第i-1个元素结点
if(i==1)q=head;
else q=GetLinklist(head,i-1);//找第i-1个元素结点
if(q==NULL) exit ("找不到插入的位置");//i-1个结点不存在
else{//在q所指结点后插入p所指结点
p=malloc(sizeof(Node));//分配内存地址给新结点p;
p->data=x//新结点p元素域设置为x;
p->next=q->next;//新结点p的链接域指向第i-1位置结点q的后继结点,(新结点p先搭上第i个结点的存储地址)
q->next=p;//修改*q的链接域(将原来第i-1位置结点的连接域改为新结点p的存储位置)
//注意:
链接操作p->next=q->next和q->next=p两条语句顺序不能颠倒,
否则结点*q的链接域值(即指向原表第i个结点的指针)将丢失;
}
}
6)删除算法
*1.算法思路(此算法描述删除第i个结点)
①.找到第i-1个结点;若存在继续,否则结束;
②.删除第i个结点,并释放对应的内存,结束;
③.时间复杂度O(1);
*2.算法步骤
①.找到ai-1的存储位置p;
②.令p->next指向ai的直接后继结点;
③.释放结点ai的空间,将其归还给"存储池";
*3.关键代码
//在单链表中删除第i个结点的基本操作为:找到线性表中第一个i-1个结点,修改其后指向后继的指针;
//*p是第i-1的元素结点,*q是第i个元素结点
p=q->next;(将第i-1的结点q的后继结点地址赋值给第i个结点p)//保存第i个结点p的存储地址;
q->next=p->next;(第i-1个结点q的后继结点指向第i个结点p的后继结点的内存位置(也就是i+1结点的存储地址))
free(p);(释放第i个结点的内存空间);
或者:q->next=q->next->next;
*4.代码实现
/**
* 删除表head的第i个结点
* @param head 链表的头结点
* @param i 要删除结点的位置
*/
void DeleteLinkList(LinkList head,int i){
Node *q,*p;//指针q用来接收待删除结点的直接前驱的存储地址,指针p用来接收待删除结点的存储地址;
if(i==1) q=head; //若i为1就说明要删头结点
else q=GetLinkList(head,i-1); //否则先找到待删结点的直接前驱
if(q!=NULL && q->next!=NULL){ //若直接前驱存在且待删结点存在
p=q->next; //p指向待删结点
q->next=p->next; //移除待删结点
free(p); //释放已经移除结点p的空间,因为p是指针所以释放p就是释放p所指的内存地址的空间了
}else{
exit("找不到要删除的结点"); //结点不存在;
}
//注意free(p)是必不可少的,因为当一个结点从链表中移除后,
如果不释放它的空间.他将变成一个无用的结点,从而造成内存泄漏;
}
7)使用前插法创建链表
LinkList CreateLinkList(){
int x;
Node *p;
LinkList head; //头指针
head=malloc(sizeof(Node));//开辟空间,动态构建一个结点,它是头结点
head->next=NULL; //头结点用来确定首结点,使得每次数据都插入到首结点前
scandf("%d",&x);
while(x){ //x=0时结束 0为假,非0为真
p=malloc(sizeof(Node));
p->data=x;
p->next=head->next; //前插插入列表的第一个结点处
head->next=p;
scandf("%d",&x); //循环结束标志:在这判断还要不要再插入结点,不插就输入0
}
return head; //最终形成的链表的数据顺序与输入的顺序相反,时间复杂度为O(n);
}
8)清除链表中所有重复结点
算法思想:
ai为要删除的重复结点的值的第一个
当未到达链表末尾时(ai不是终点结点时)遍历删除ai+1到an结点中值为ai的结点
代码实现:
//删除链表中多余的重复结点;
void purgeLinkList(LinkList head){
Node *p,*q,*r;
q=head->next; //q指示当前检查结点的位置,置其初值指向首结点;
while(q!=NULL){//当前检查结点*q不是尾结点时,寻找并删除它的重复结点;
p=q; //工作指针p指向*q;
while(p->next!=NULL){ //当*p的后继结点存在时,将其数据域与*q数据域比较
if(p->next->data==q->data){ //若*(p->next)是*q的重复结点
r=p->next;//r指向待删结点
//移出结点* (p->next),p->next指向原来* (p->next)的后继结点
p->next = r->next;
free(r);
}else{
p=p->next;//否则,让p指向下一个结点
}
}
q=q->next; //更新检查结点
}
}
8>循环链表
1)单项循环链表的特点
*1.普通链表的终端结点的next值为NULL;
*2.循环链表的终端结点的next指向头结点
*3.在循环链表中,从任一结点出发能够扫描整个链表
*4.p->next=p;//循环指针只有一个结点p
*5.p->next=head;//p为尾结点
*6判断带头结点且头指针为head的单循环链表的是否为空的条件为:
head->next===head;
*7.设rear是指向带头结点的非空循环单链表的尾指针,则删除表首结点的操作可表示为
p=rear->next->next;//-->头结点-->首结点
rear->next->next=p->next //首结点=头指针
free(p);
2)双向循环链表
*1.概念:在链表中设置两个指针域,一个指向后继结点,
一个指向前驱结点;这样的链表叫做双向链表;
适用场合:双向链表适合应用在需要经常查找结点前驱和后继的场合,O(1);
双向链表的类型定义
struct dbnode{
DataType data;
struct dbnode *prior,*next;
};
typedef struct dbnode *doubleP;
typedef doubleP DBlinkList;
p->prior->next与p->next->prior相当 都是p的存储地址;
带头结点的双向循环链表L为空的条件
(L->next==L) && (L->prior==L);
*2.双向链表中结点的删除(*****)
#1.p->prior->next=p->next;
#2.p->next->prior=p->prior;
#3.free(p);
注:#1和#2语句执行顺序可以颠倒;
*3.双向链表中结点的插入 (谨记要双向插)
在p所指结点ai的后面插入一个新结点*t,需要修改四个指针;
①.将t的前驱和后继赋值连接
#1.t->prior=p; //t的直接前驱连接p(ai)结点
#2.t->next=p->next; //t的直接后继连接p的后继(ai+1)结点
②.将结点p(ai)的后继和结点p的next(ai+1)结点的前驱赋值连接
#3.p->next->prior=t; //p的直接后继->ai+1结点的直接前驱赋值为t
#4.p->next=t; //再将p的直接后继赋值为新结点t
注:①和②语句执行顺序可以颠倒;
第三章.栈,队列和数组(线性结构)
1.栈
1>定义:栈是只能在表的一端(表尾)进行插入和删除的线性表;
注意事项:
*1.允许插入及删除的一端(表尾)称为栈顶(Top);
*2.另一端(表头)称为栈底(Bottom).
*3.当表中没有元素时称为空栈.
S=(a1,a2,a3,...,an);//a1为栈底元素,an为栈顶元素;
进栈:在栈顶插入一元素;push
出栈:在栈顶删除一元素;pop
*4.输入是ABC 输出也是ABC,用顺序栈操作-->Push Pop Push Pop Push Pop,
不可能的输出结果为CAB;
2>特点
后进先出线性表(Last In First Out 简称LIFO),递归;
适用场合:常用于暂时保存有待处理的数据;
3>栈的基本运算
1) 初始化栈: InitStack(S);
2) 判栈空: EmptyStack(S);
3) 进栈: push(S,x);
4) 出栈: Pop(S);
5) 取栈顶: GetTop(S);
4>栈的顺序实现--顺序栈
1) 顺序栈及常用名词
*1.顺序栈--即栈的顺序实现;
*2.栈容量--栈中可存放的最大元素个数;
*3.栈顶指针top--指示当前栈顶元素在栈中的位置;取值范围为0~(maxsize-1)
*4.栈空--栈中无元素时,表示栈空;
*5.栈满--数组空间已被占满时,称栈满;
*6.下溢--当栈空时,再要求作出栈运算,则称"下溢";
*7.上溢--当栈满时,再要求作进栈运算,则称"上溢";
*8.栈所操作的是栈顶元素,顺序结构空间已经预先分配好,不能free();
2) 顺序栈的类型定义
#define maxsize 6
typedef struct seqStack{
DataType data[maxsize];
int top;//指示当前栈顶元素在栈中的位置,栈操作的是栈顶
}SeqStk;
seqStk *S; //定义一顺序栈s
注意事项:
*1 s->top==0 代表顺序栈s为空;
*2 s->top==maxsize-1 代表顺序栈s为满;
3)顺序栈的运算
*1.初始化
int InitStack(SeqStk *stk){
stk->top=0;
return 1;
}
*2.判栈空
/*栈空时返回值为1,否则返回0*/
int EmptyStack(SeqStk *stk){
if(stk->top==0)return 1;
else return 0;
}
*3.进栈
/*数据元素x进顺序栈sq*/
int Push(SeqStk *sq,DataType x){
//判断是否上溢
if(sq->top==maxsize-1){
error("栈满");//上溢
return 0;
}else{
sq->top=sq->top+1; //修改栈顶指针,指向新栈顶
sq->data[sq->top]=x;//元素x插入新栈顶中
return 1;
}
}
*4.出栈
/*顺序栈sq的栈顶元素退栈*/
int Pop(seqStk *sq){
if(sq->top==0){
error("栈空");//下溢
return 0;
}else{
sq->top--; //修改栈顶指针,指向新栈顶
return 1;
}
}
*5.取栈顶元素
DataType GetTop(SeqStk *sq){
if(EmptyStack(sq)){
return NULLData;
}else{
return sq->data[sq->top];
}
}
*6.双栈
在某些应用中,为了节省空间,让两个数据元素类型一致的栈
共享一维数组空间data[max],成为双栈,两个栈的栈底分别
设在数组的两端,让两个栈彼此迎面增长,两个栈的栈顶变量
分别为top1,top2,仅当两个栈的栈顶位置在中间相遇时:
(top1+1=top2)才会发生上溢;
*7.栈的逆置
示例:借助队列将含有n个元素的栈逆置;
先将栈中元素依次出栈并入队列,然后使该队列元素依次出队列并入栈;
5>栈的链接实现--链栈
1)链栈的定义:
栈的链式存储结构称为链栈,它是运算受限的单链表,插入和删除操作
仅限制在表头位置上进行,栈顶指针就是链表的头指针;
*.头指针Ls-->头结点-->栈顶-->...-->栈底(data,NULL)
2)链式栈的类型定义
typedef struct node{
DataType data; 下溢条件:Ls->next==NULL
struct node *next; 上溢条件:不考虑栈满现象(可用malloc分配新空间)
}LkStk; //链栈类型
3)链栈的运算
*1.初始化
void InitStack(LkStk *Ls){
LS=(LkStk*)malloc(sizeof(LkStk));//将指针强制转换为链栈指针
LS->next=NULL;
}
*2.判栈空
int EmptyStack(LKStk *LS){
if(LS->next==NULL) return 1;
else return 0;
}
*3.进栈(前插到栈顶结点前)
//将值为x的元素插入栈顶,链栈不会出现上溢现象;
void Push(LkStk *LS,DataType x){
temp=(LkStk*)malloc(sizeof(LkStk));
temp->data=x;
temp->next=LS->next;
LS->next=temp;
}
*4.出栈(删除栈顶结点)
//删除栈顶结点,并将进结点空间释放
int Pop(LKStk *LS){
LKStk *temp;
if(!EmptyStack(LS)){
temp=LS->next;
LS->next=temp->next;
free(temp)
return 1;
}else return 0;
}
*5.取栈顶元素
DataType GetTop(LKStk *LS){
if(!EmptyStack(LS))return LS->next->data;
else NULLData;
}
6>递归与递归的阅读
1)递归的定义:
如果一个函数在完成之前又调用自身,则称之为递归函数;
2)示例
求整数n的阶乘函数:
n!=1(当n=0时)或n*(n+1)! 当n>0时;
代码实现
#include
using namespace std;
//关键代码
int f(int n){
if(n==0)return 1;
else return n*f(n-1);
}
int main(){
int f(int n);
int result=0;
result=f(3);
cout<概念
1)定义:
队列(Queue)也是一种运算受限的线性表,
2)注意事项:
*1.允许删除的一端称为队头(front),
*2.允许插入的另一端称为队尾(rear).
*3.队列 Q=(a1,a2,a3,...,an);
*4.先进先服务;
3)示意图
出队<--a1(队头),a2,...,an(队尾)<--入队
在队头删一元素 在队尾插一元素
2>特点
队列也称先进先出(First In First Out)线性表,简称FIFO;
使用场合:常用于暂时保存有待处理的数据
3>队列的基本操作
*1.队列的初始化InitQueen(Q);//设置一个空队列Q;
*2.判队列空 EmptyQueue(Q); //若队列为空返回1,否则返回0;
*3.入队列 EnQueue(Q,x);//将数据元素x从队尾一端插入队列,使其成为队列的新尾元素
*4.出队列 OutQueue(Q); //删除队列首元素
*5.取队列首元素 GetHead(Q); //返回队列首元素的值
4>队列的顺序实现--顺序队列和循环队列
#1.顺序队列
1)概念:
用一维数组作为队列的存储结构;
队列容量(maxsize-1): 队列中可以存放的最大元素个数;
规定:
初始:front=rear=0;
进队:rear(队尾)增1,元素插入尾指针所在位置;
出队:front增1,取头指针所指位置元素;
队头指针front--始终指向实际头元素的前一位置
队尾指针rear--指向实际队尾元素
2)顺序队列结构定义
const int maxsize=20;
typedef struct seqQueue{
DataType data[maxsize];
int front,rear;//队首指针.对尾指针
} SeqQue;
SeqQue sq;//顺序队列的声明
3)队列的入队,出队及判空操作
*1.入队操作:
//将队尾指针抬高一个格,将data存放到新的队尾空间内
sq.rear=sq.rear+1;
sq.data[sq.rear]=x;
*2.出队操作:
//将队首指针抬高一个,指向下一个结点
sq.front=sq.front+1;
*3.空队列:
sq.rear=0,sq.front=0;
sq.rear==maxsize-1 //对满 上溢条件
sq.rear==sq.front;//队列为空,下溢条件
注意事项
sq.front=0;//规定队首结点不放数据;
sq.front+1=sq.rear;//假溢出,
#2.循环队列
1)概念:
为队列分配一块存储空间,用一维数组作为队列的存储结构,
并将这一块存储空间看成头尾相连的;
有可能sq.front的上一个结点是空的,所以用循环队列
注意事项
*1.头指针front--顺时针方向落后于实际队头元素一个位置;
*2.尾指针rear --指向实际队尾元素
*3.cq.front=0;//规定队首结点不放数据;
*4.循环实现
2)循环队列的类型定义
typedef struct CycQueue{
DataType data[maxsize];
int front,rear;队首与队尾位置
}CycQueue;
CycQueue CQ;//循环队列的声明;
3)循环队列的基本操作
*1.队列的初始化
void InitQueue(CycQue CQ){
CQ.front=0; CQ.rear=0;
}
*2.判队列空
int EmptyQueue(CycQue CQ){
//队空条件
if(CQ.rear==CQ.front) return 1;
else return 0;
}
*3.入队:
int EnQueue(CycQue CQ,DataType x){
//队列满的判断条件
if((CQ.rear+1)%maxsize==CQ.front){
error("队列已满")
return 0;
}else{
//队尾指针增1
CQ.rear=(CQ.rear+1)%maxsize; 上抬一个指针.
CQ.data[CQ.rear]=x;
return 1;
}
}
*4.出队:
int OutQueue(CycQue CQ){
if(EmptyQueue(CQ)){
error("队列为空");
return 0;
}else{
CQ.front=(CO.front+1)%maxsize;
return 1;
}
}
cq.rear=(cq.rear+1)%maxsize;
*5.取队列的首元素
DataType GetHead(cycQue cq){
if(EmptyQueue(cq)) {
error("队列为空")
return NULLData;
}else{
return cq.data[(cq.front+1)%maxsize];//保证头结点为NULL
}
}
5>队列的链式实现--链队列
1)定义:
用链式表示的队列,即它是限制仅在表头删除和表尾插入的单链表;
头指针(front)-->□-->队头-->...-->队尾(尾指针rear);
注:
*1.单链表的头指针不便于在表尾作插入操作,为此增加一个尾指针,
指向链表的最后一个结点;
*2.由于链接实现需要 动态申请 空间,故链队在一定范围内不会出现队列满的情况;
头指针front--指向表头结点,队头元素结点为front->next;
尾指针rear--指向链表最后一个结点(队尾结点)
2)链队列的类型定义
typedef struct LinkQueueNode{
DataType data;
struct LinkQueueNode *next;
}LKQueNode;
typedef struct LKQueue{
LkQueue *front,*rear;
}LinkQue;
LkQue LQ;
3)链式队列的基本运算:
#1.LQ.front--链式队列的队头指针
#2.LQ.rear --链式队列的队尾指针
#3.链式队的上溢:可不考虑(因为动态申请空间)
#4.链式队的下溢:即链式队为空时,还要求出队,此时链表中无实在结点;
#5.规定链队列为空时,令rear指针也指向表头结点;
#6.在实现队列的链表结构中,仅设置尾指针的单循环链表的时间复杂度最优
链队列下溢的条件:
LQ.front->next==NULL;或
LQ.front==LQ.rear;
*1.队列的初始化
void initQueue(LkQue *lq){
LkQueNode *temp;
temp=(*LkQueNode)malloc(sizeof(LkQueNode));
lq->front=temp;
lq->rear=temp;
(lq->front)->next=NULL;
}
*2.判队列空
int EmptyQueue(LkQue *lq){
if(lq.front==lq.rear) return 1;
else return 0;
}
*3.入队列
#1.需求:
入队--在队尾即链表尾部插入元素x
#2.算法思想
①.生产新结点p(其数据域为x,指针域为NULL);
②.将新结点p插入到表尾,并变成新的队尾结点;
#3.代码实现
void EnQueue(LkQue *lq,DataType x){
LKQueNode *temp;
temp=(LKQueNode)malloc(sizeof(LKQueNode));
temp->data=x;
temp->next=NULL;
(lq->rear)->next=temp;
lq->rear=temp;
}
*4.出队列
#1.需求:
出队--在链式队列中删除队头元素,并送至e中
#2.算法思想
①.判断是否下溢,不下溢;
②.取队头结点temp;送队头元素至x;从链式队列中删除队头结点;
③.若链式队列中原只有一个元素,则删除后队列为空,应修改队尾指针;
④.释放结点temp空间,使其回归系统;
#3.代码实现
int outQueue(LKQue *lq){
if(EmptyQueue(lq)){
error("队空")
return 0;
}else{
LKQueNode *temp
temp=lq->front->next;
lq->front->next=temp->next;
if(temp->next==NULL) lq->rear=lq->front;//队列为空;
free(temp);
return 1;
}
}
*5.取队列的首元素
DataType GetHead(LkQue lq){
LkQueNode *temp;
if(EmptyQueue(lq))return 0;
else{
temp=lq.front->next;
return temp->data;
}
}
3.数组
1>概念
数组可看成是一种特殊的线性表,其特殊在于,表中的数组元素本身也是一种线性表;
其每个元素由一个值和一个数组下标组成,其中下标个数称为数组的维数;
*1.二维数组Amn可以看成由m个行向量组成的向量,也可以看成是n个列向量组成的向量;
注意事项:
数组一旦被定义,它的维数和和维界就不再改变,因此,除了结构的初始化和销毁之外,
数组通常只有两种基本运算:
*1.读--给定一组下标,读取相应的数据元素;
*2.写--给定一组下标,修改相应的数据元素;
*3.数组只采取顺序存储结构;
*2.二维数组示意图
Amn={a00,a01,...,a0n-1,a10,a11,...,a1n-1,
...,...,...,...,am-10,am-11,...,am-1n-1}
2>寻址公式(以行为主存放)
1)已知m行n列的二维数组中的起始元素地址,及每个元素占用的存储空间,求aij的元素存储地址
#1.算法
/**
* 计算排在二维数组中aij元素的存储地址
*
* @param k:按行优先顺序存储的二维数组Amn中每个元素占k个存储单元
* @param m:二维数组的总行数,n:是二维数组中没行的总列数;
* @param aij:位于数组第i行,第j列的元素
* @param a00:数组中的第一个元素
* 算法:
* 因为aij 位于第i行,第j列,前面i行一共有i*n个元素,行号从0开始
* 第i行上又有j个元素,故前面有i*n+j个元素
* 每个元素占k个储存单元所以a00到aij共占(i*n+j)*k个存储单元
* 所以aij的存储地址为a00的存储地址+a00到aij间所占用的存储单元个数;
*/
Loc(aij)=Loc(a00)+(i*n+j)*k;
#2.示例
DataType A[3][4]={a1,a2,a3,a4,a5,a6,a7,a8,a9,a10};
已知数组A的起始地址为2000,一个元素所占空间大小为4字节,求A[1][2]的存储地址;
Loc(a7)=Loc(A[1][2])=2000+(1*4+2)*4=2000+24=2024;
2)已知m行n列的二维数组amn中aij和aov的存储地址,且Loc(aij)概念:
设A为m*n阶的矩阵(即m行n列),第i行j列的元素是a(ij),即A=a(ij)
它是纵横排列的二维数组,
2>矩阵的转置
*1.定义:
定义m*n阶矩阵A的转置为n*m阶矩阵B,即满足B=b(j,i)则a(i,j)=b(j,i),
也就是A的第i行第j列的元素是B的第j行第i列的元素;
示例
矩阵A
a b a c e
c d =
e f b d f
*2.算法实现
//设有一n阶方阵A,设计算法实现对该矩阵的转置;
void MM(int A[n][n]){
int i,j,temp;
for(i=0;i 稀疏矩阵
1)定义:
设矩阵A中有s个非零元素,若s远远小于矩阵元素的总数,则称A为稀疏矩阵;
2)稀疏矩阵的压缩存储:
作用:
二维数组要先开辟好空间,而其元素有为0或对称的,故为节省存储空间,使用矩阵并对
矩阵进行压缩存储,即为多个相同的非零元素只分配一个存储空间;对零元素不分配空间;
操作:
只存储稀疏矩阵中的非零元素
3)实现方法-->三元组表示法
由于非零元素的分布一般没有规律性,因此在存储非零元素的同时,必须同时
记下它所在的行和列的位置(i,j),反之一个三元组(i,j,aij)唯一确定了矩阵
A的一个非零元素,因此稀疏矩阵可由表示非零元的三元组及其行列数唯一确定;
三元组结点:(i,j,v)-->(行号,列号,元素值);
//三元组类型定义
const int maxNum=10;
typedef struct node{
int i, j; //非零元的行下标和列下标,行下标,列下标起始为0;
DataType v; //非零元素的值
}NODE;
4>特殊矩阵
1)概念
即非零元素或零元素的分布有一定规律的矩阵.
2)特殊矩阵压缩存储的方式
*1.对称矩阵
#1.定义:在一个n阶方阵A中,若元素满足下述性质:
aij=aji; 0<=i,j<=n-1;则称A为对称矩阵;
特点:关于左对角线对称,只存储矩阵中上三角或下三角中的元素,
让每两个对称元素共享一个存储空间;
矩阵元素:
[1 5 1 3 7
5 0 8 0 0
1 8 9 2 6
3 0 2 5 1
7 0 6 1 3]
示意图:
a00
a10 a11
a20 a21 a22
............
an-10,an-11,...an-1 n-1;
元素总数:∑(i)=n(n+1)/2;
#2.关于对称矩阵存放在一维数组中矩阵元素的行号和列号与元素位置的关系
假设以一维数组M[(n(n+1))/2]作为n阶对称矩阵A的存储结构,
设矩阵元素aij在数组M中的位置为k,(i,j)和k存在如下对应关系:
/** [j]
* @param i 元素的行号 1 5 1 3 7 1 [i]
* @param j 元素的列号 5 0 8 0 0 5 0
* @param k 元素在数组M中的位置 1 8 9 2 6 1 8 9
* i行前的每行元素个数构成等差数列 3 0 2 5 1 3 0 2 5
* sn=n(a1+an)/2 等差数列的求和公式 7 0 6 1 3 7 0 6 1 3
* 第0行元素个数为1 a1=1,
* 第i-1行元素个数为i,an=i,一共有i行(0~(i-1)) n=i;
* i行前几行的元素总个数 sn=i(i+1)/2; 又得知i行元素个数为j
* k=sn+j=i(i+i)/2+j;
*/
i(i+1)/2+j i≥j //下三角矩阵元素aij与位置k的关系
k(位置)=
j(j+1)/2+i i<j //上三角矩阵元素aij与位置k的关系
示例:M[4][3] k=4(1+4)/2+3=20/2+3=13;
第四章.树和二叉树
1.树的基本概念及其运算:
1>定义:
树:是n(n>=0)个结点的有限集T,它是非线性结构,每个结点有且只有一个直接前驱,
和一个或多个直接后继,其满足一下特性:
*1.当n=0时,称为空树 ∅;
*2.当n>0时,有且只有一个特定的称为根的结点;其余的结点可分为
m(m>=0)个互相不相交的子集T1,T2,T3,...Tm,其中每个子集Ti
又是一棵树,并称其为子树;
*3.递归是树的固有特性;
2>树的逻辑表示
*1.直观表达法
A
B C D
E F G
*2.嵌套括号法
(根(子树,子树...,子树)) (A(B(E,F),C,D(G)));
3>树的相关术语
*1.结点: 由一个数据元素及若干指向其他结点的分支所组成;
*2.度
#1.结点的度:该结点的子树数(即分支数);
#2.树的度:树中所有结点的度最大值;
*3.叶子(终端结点): 度为0的结点;
*4.非终端结点: 度不为零的结点;
*5.孩子(子节点):结点的子树的根称为该结点的孩子;
*6.双亲(父结点):一个结点称为该结点所有子树根的双亲;
*7.祖先:结点祖先指根到此结点的一条路径上的所有结点;
*8.子孙:从某结点到叶子结点的分支上所有结点称该结点的子孙;
*9.兄弟:同一双亲的孩子之间互称兄弟;
*10.结点的层次:从根算起,根为第一层,其孩子在第二层,...,L层上任何结点的孩子都在L+1层上;
*11.堂兄弟:其双亲在同一层的结点;
*12.树的深度或高度。:一棵树中所有结点层次数的最大值;
*13.有序树:若树中各结点的子树从左到右是有次序的,不能互换,称为有序树;
*14.无序树:若树中各结点的子树是无次序的,可以互换,则称为无序数;
*15.森林:是m(>=0)棵树的集合;
4>树的基本运算
*1.求根 Root(T):求树T的根结点;
*2.求双亲Parent(T,X):求结点x在树T上的双亲;若x是树T的根或X不在T上,
则结果为一特殊标志;
*3.求孩子Child(T,X,i);求树T上结点x的第i个孩子结点;若x不在T上,或
x没有第i个孩子,则结果为一特殊标志;
*4.建树Create(X,T1,T2...,Tk),k>1:建立以X为根,以T1,...Tk为第1,...k棵子树的树;
*5.剪枝Delete(T,X,i):删除树T上结点x的第i棵子树,若T无第i棵子树,则为空操作;
*6.遍历Traverse Tree(T):遍历树,即访问树中每个结点,且每个阶段仅被访问一次;
2.二叉树基本概念及其运算
1>定义
二叉树是n(n>=0)个结点的有限集合,它或为空(n=0),或由一个根及两课互不相交的左子树
和右子树组成,且左子树和右子树也均为二叉树;
注意:二叉树可以是空集合,根可以有空的左子树或空的右子树;
2>特点
*1.二叉树可以是空的,称为空二叉树;
*2.每个结点最多只能由两个孩子;
*3.子树有左,右之分且次序不能颠倒.
3>二叉树和树的比较
类型 结点 子树 结点顺序
树 n>=0 不定(有限) 无
二叉树 n>=0 <=2 有(左,右)
4>二叉树的5种基本形态
*1. ∅
*2. A
*3. A
B
*4. A
B
*5. A
B C
5>二叉树的基本运算
*1.初始化Initiate(BT):建立一棵空二叉树, BT=∅。
*2.求双亲Parent(BT,X):求出二叉树BT上结点X的双亲结点,
若X是BT的根或X根本不是BT上的结点,运算结果为NULL。
*3.求左孩子Lchild(BT,X)和求右孩子Rchild(BT,X);分别求出
二叉树BT上结点X的左、右孩子;若X为BT的叶子或X补在BT上,
运算结果为NULL。
*4.建二叉树Create(BT):建立一棵二叉树BT。
*5.先序遍历PreOrder(BT):按先序对二叉树BT进行遍历,每个结点被访问
一次且仅被访问一次,若BT为空,则运算为空操作;(先访问根结点)
*6.中序遍历InOrder(BT):按中序对二叉树BT进行遍历,每个结点被访问
一次且仅被访问一次,若BT为空,则运算为空操作。(中间访问根结点)
*7.后序遍历PreOrder(BT):按后序对二叉树BT进行遍历,每个结点被访问
一次且仅被访问一次,若BT为空,则运算为空操作。(最后问根结点)
*8.层次遍历LevelOrder(BT):按层从上往下,同一层中结点按从左往右的顺序,
对二叉树进行遍历,每个结点被访问一次且仅被访问一次,若BT为空,
则运算为空操作。
6>二叉树的性质
*1.在二叉树的第i(i>=1)层上至多有2^(i-1)个结点;
*2.深度k(k>=1)的二叉树至多有2^k-1个结点;
注:等比数列的求和公式
一个数列,如果任意的后一项与前一项的比值是同一个常数
(这个常数通常用q来表示),且数列中任何项都不为0,
即An+1/An=q;(n∈N*)-->an=am*q^(n-m)
这个数列叫等比数列,其中常数q 叫作公比。
等比数列的求和公式:
Sn=a1+a2+a3+...+an(公比为q);
qSn=a1q + a2q + a3q +...+ anq
= a2+ a3+ a4+...+ an+ a(n+1);
sn-qsn= a1-a2+a2-a3+a3+...-an+an-a(n+1);
(1-q)Sn=a1-a(n+1); //a(n+1)=a1*q^(n+1-1)=a1q^n;
(1-q)Sn=a1-a1q^n
=a1(1-q^n);
Sn=a1(1-q^n)/(1-q);(q≠1)
/**
* 求深度k(k>=1)的二叉树最多的结点数;
* a1=2^0;
* q=2;
* n=k;
*/
sk=2^0+2^1+...,+2^k+2^(k-1);//符合等比数列的求和公式
sk=2^0(1-2^k)/1-2
=1-2^k/1-2
=1-2^k/-1
=-1(1-2^k)
=2^k-1;
*3.对任何一棵二叉树,若其终点数为n0(叶子节点),度为2的结点数为n2,则n0=n2+1,
即:n0(叶子节点数)=n2(度为2的结点数)+1;
#1.证明:
long branchNum=0;//树枝个数
branchNum=n0*0+n1*1+n2*2;//树枝的个数
branchNum=n0+n1+n2(结点的总数)-1;//树枝的个数
n1+2n2=n0+n1+n2-1;
n2=n0-1;
n0=n2+1;//故n0(叶子节点数)=n2(度为2的结点数)+1;
#2.对应算法题
设一个完全二叉树共含有196个结点,求该完全二叉树中含有叶子结点(n0)的个数
n0+n1+n2=196;
n1∈[0,1];//在完全二叉树中,度为1的结点要不是没有(0),要不就1个;
//因为总结点数为196为偶数,故n1=1,n1+根结点=2,构成总结的为偶数;
n0+n2+1=196;//n0=n2+1;
2no=196;-->n0=98;
*4.具有n个结点的完全二叉树的深度k为⌊log2^n⌋+1
#1.符号⌊x⌋表示不大于x的最大整数 x是整数
向上取整, 运算称为 Ceiling,用数学符号⌈⌉(上有起止,开口向下)表示,。
向下取整, 运算称为 Floor,用数学符号 ⌊⌋ (下有起止,开口向上)表示。
注意:向上取整和向下取整是针对有浮点数而言的;若整数向上取整和向下取整,都是整数本身。
#2.算法推导:
/**
* 求具有n个结点的完全二叉树的深度k
* k为深度
* n为结点总个数
*/
//由性质2及完全二叉树的定义得
2^(k-1)-1=1)且有2^k-1个结点的二叉树;(编号从1开始)
#2.顺序:满二叉树中结点顺序编号,即从第一层结点开始自上而下,从左到右进行连续编号;
#3.示意图:
1
2 3
4 5 6 7
2)完全二叉树
#1.定义:
深度为k的二叉树中,k-1层结点数是满的(2^k-2),k层结点是左连续的(即结点编号是连续的);
#2.注:完全二叉树是满二叉树从最大编号按从右至左依次剪枝;
满二叉树是完全二叉树的特例;
#3.示意图:
a.完全二叉树: b.非完全二叉树: c.非完全二叉树:
1 1 1
2 3 2 3 2 3
4 5 6 4 5 7 6 7
3)完全二叉树结点的最大值和最小值
sk(max)=2^k-1;
sk(min)=2^(k-1)-1+1=2^(k-1);
4)假设高度为h的二叉树上只有度为0和度为2的结点,求此类二叉树中结点的最大值和最小值;
最小值: 除第一层只有根,其他h-1层,每层只有二个两个结点,结点总数=2*(h-1)+1=2h-1
最大值: 当树为满二叉树时,结点总数=2^h-1
*5.对有n个结点的完全二叉树的结点按层编号(从第1层到第⌊log2^n⌋+1层,每层从左到右)
则对任意编号为i(1<=i<=n)的结点A有:
#1.若i=1,则结点A无双亲,A是二叉树的根;
若i>1,则A的双亲Parent(A)是编号为⌊i/2⌋;
#2.如果2*i≤n,则其左孩子Lchild(A)的编号为2*i,否则,结点A无左孩子且为叶子结点;
#3.如果2*i+1≤n,则其右孩子Rchild(A)的编号为2*i+1,否则,结点A无右孩子。
7>用于描述分类过程的二叉树称为判定树;
3.二叉树的存储结构
1>二叉树的顺序存储结构
*1.实现方法--以编号为地址 的策略
即对完全二叉树进行编号,然后用一维数组存储,其中编号为i的结点
存储在数组中下标为i的分量中,数组下标为0的元素为空,对于非完全二叉树,
则用某个方法将其转化为完全二叉树,为此可设置若干个虚拟结点;
*2.特点:
此方法用于完全二叉树,则节省空间,结点位置确定方便;
用于一般二叉树尤其是单分支二叉树则存储空间浪费极大;
2>二叉树的链式存储结构:
*1.表示方法--二叉链表表示法:
结点形式: lChild data rChild---> ^(NULL) A * rChild
*2.定义类型
typedef struct btnode{
DataType data;//结点元素域
//左孩指针(指向左子节点),右孩指针(指向右子节点)
struct btnode *lChild,*rChild;
}*BinTree; //二叉链表类型;
*3.在含有n个结点的二叉链表中有2n个指针域(每个结点2个),
其中n-1个指针用来指向结点的左右孩子(除根结点不被指),
其余n+1个指针是空链域(2n-(n-1)=n+1);
*4.三叉链表表示法:
结点形式: lChild data parent(指向双亲) rChild;
3>例题:含有100个结点的二叉树采用二叉链表存储时,求空指针域NULL的个数
空指针的个数=n-1=100+1=101;
4.二叉树的遍历
1>概念:
是指按某种次序访问二叉树上的所有结点,使每个结点被访问一次且仅被访问一次;
2>遍历规则:(递归算法)
*1.算法概念:
由二叉树的递归定义知,二叉树的三个基本组成单元是:根节点D,左子树L和右子树R
D —— 访问根结点 DLR(先序遍历) 首先访问根结点,其次遍历根的左子树,最后遍历根右子树
L —— 遍历左子树 LDR(中序遍历) 首先遍历根的左子树,其次访问根结点,最后遍历根右子树
R —— 遍历右子树 LRD(后序遍历) 首先遍历根的左子树,其次遍历根的右子树,最后访问根结点
注:每种遍历对每棵子树同样按按规定的三步进行。
*2.示例:
A(根)
B(左子树) C(右子树)
D E K F
G H I
J
遍历结点排序技巧:
#1.先将二叉树变为完全二叉树,空的地方填NULL
#2.将二叉树用嵌套括号法表示出来;
注:每层结点只遍历一次,嵌套括号法表示如下
根结点:A
左子树:B(D(G,H),E)
右子树:C(K,F(I(NULL,J),NULL))
#3.结合二叉树的嵌套括号法图示,按照规定的顺序开始 "递归" 遍历排序
先序遍历(根左右): A(根), B,D,G,H,E(左子树), C,K,F,I,J(右子树);
中序遍历(左根右): G,D,H,B,E(左子树), A(根), K,C,I,J,F(右子树);
后序遍历(左右根): G,H,D,E,B(左子树),K,J,I,F,C(右子树), A(根);
从左开始是:从左子树叶子结点开始按原则顺序递归(先递归左子树,左叶子节点);
若是从根开始:是由外向内按原则顺序递归;
#1.DLR(先序遍历):
遍历算法:
1)树顺序:先遍历根结点,再遍历左子树,最后遍历右子树
2)原则:"根->左->右"的顺序遍历;
遍历结果: A(根), B,D,G,H,E(左子树), C,K,F,I,J(右子树);
代码实现:
//先序遍历以bt为根指针的二叉树
void preOrder(BinTree bit){
if(bt!=NULL){
visit(bt);//访问根结点bt
preOrder(bt->lChild);//先序遍历左子树(递归遍历)
preOrder(bt->rChild);//先序遍历右子树
}
}
#2.LDR(中序遍历):
遍历算法:
1)遍历起始位置:从二叉树左边子树结点开始遍历,
2)原则:左->根->右的顺序遍历
遍历结果: G,D,H,B,E(左子树), A(根), K,C,I,J,F(右子树);
代码实现:
//中序遍历以bt为根指针的二叉树
void inOrder(BinTree bit){
if(bt!=NULL){
inOrder(bt->lChild);//中序遍历左子树
visit(bt);//访问根结点bt
inOrder(bt->rChild);//中序遍历右子树
}
}
#3.LRD(后序遍历):
遍历算法:
1)遍历起始位置:从树最左边的叶子结点开始遍历,
2)树遍历顺序:先遍历左子树,再遍历右子树,最后遍历根结点
3)原则:左->右->根;
遍历结果: G,H,D,E,B(左子树),K,J,I,F,C(右子树), A(根)
代码实现:
//后序遍历以bt为根指针的二叉树
void postOrder(BinTree bit){
if(bt!=NULL){
postOrder(bt->lChild);//后序遍历左子树
postOrder(bt->rChild);//后序遍历右子树
visit(bt);//访问根结点bt
}
}
#4.按层遍历
遍历算法:
1)原则:按层从上到下,同层结点从左到右,按层遍历输出;
2)遍历结果:A,B,C,D,E,K,F,G,H,I,J
*3.利用二叉树遍历的递归算法,求二叉树的高度
int Height(BinTree bt){
int lh,rh;
if(bt!=NULL){
lh=Height(bt->lChild);//左子树的高度
rh=Height(bt->rChild);//右子树的高度
return 1+(lh>rh?lh:rh);
}else{
return 0;//如果是空树高度为0;
}
}
*4.二叉树叶子结点算法
int leafnode_num(BinTree bt){
if(bt==NULL)return 0;//空树
else{
if(bt->lchild==NULL&&bt->rchild==NULL) return 1;//只有根结点
else{
return leafnode_num(bt->lchild)+leafnode_num(bt->rchild);//递归求和
}
}
}
5.树和森林
1>树的存储结构(三种)
*.树的直接表示
A
B C
D E F G
H I J
*1.双亲表示法
#1.算法描述:
将树结点按层遍历存放到数组中,数组每个分量包含两个域:
数据域:用于存储树上一个结点的数据元素值
双亲域:用于存储本结点的双亲结点在数组中的序号(下标值)
根结点没有双亲,双亲域的值为-1;
#2.算法图示表示;
下标 结点 双亲
0 A -1
1 B 0
2 C 0
3 D 1
4 E 1
5 F 2
6 G 2
7 H 4
8 I 4
9 J 4
#3.树的双亲链表的类型定义
#define size 10
typedef struct{
dataType data;//结点值
int parent;//其双亲结点在表中的位置
} Node;
Node sList[size];
*2.孩子链表表示法
*.树的直接表示
A
B C
D E F G
H I J
#1.算法描述:
将树结点按层遍历存放到数组中,树中的每个结点的孩子串成一个单链表,
数组元素除了包含结点本身的信息和该结点的孩子链表的头指针之外,
#2.算法图示法
下标 结点 孩子
0 A ---->1,next-->2,NULL
1 B ---->3,next-->4,NULL
2 C ---->5,next-->6,NULL
3 D NULL
4 E ---->7,next-->8,next-->9,NULL
5 F NULL
6 G NULL
7 H NULL
8 I NULL
9 J NULL
#3.孩子链表表示法的类型定义
#define MaxND 20
typedef struct bnode{
int child;
struct bnode *next
}node,*childlink;
Typedef struct{
DataType data;
childlink hp;
}headNode;
HeadNode;link[MaxND];
*3.孩子兄弟链表表示法(二叉链表表示)
*.树的直接表示
A
B C
D E F G
H I J
#1.算法描述:
从根结点起始,首先指向长子的结点,再指向长子挨肩的弟弟结点;
结点形式: son data brother
#2.算法图示法
root->A A
↓ B
B------>C D C
↓ ↓ E F
D->E F->G H G
↓ I J
H->I->J
#3.孩子兄弟链表表示法类型定义
Typedef struct tNode{
DataType data;
//son:指向结点的第一个子(长子)结点,
//bother:指向该结点(长子结点)的下一个兄弟结点(挨肩的)
struct tnode *son,*bother;
}*Tree;
2>树,森林与二叉树的关系
*1.一般树--->为二叉树
#1.算法描述
1) 各兄弟之间加连线;
2) 对任一结点,除左孩子外,抹掉该结点
3) 以根为轴心,将连线顺时针转45°;
4) 树变为二叉树根结点没有右孩子;
#2.转化图示
原树 使用孩子兄弟链表表示法 二叉树
A A A
B C D B-> C ->D B
↓ ↓ E C
E F G H I J E->F G H->I->J F G D
H
I
J
*2.二叉树-->一般树
#1.算法描述
1) 从根结点起;
2) 该结点左孩子和左孩子右枝上的结点依次作为该结点的孩子
3) 重复 步骤 1)
#2.转化图示
二叉树 一般树
A A
B B C D
E C E F G H I J
F G D --------->
H
I
J
*3.森林-->二叉树
#1.算法描述:
1) 将每棵树转换成相应二叉树;
2) 将1)中得到的各课二叉树的根结点看做是兄弟连接起来,按T1,T2竖向连接
树1 T1: 相应二叉树 最终二叉树
A A A
B C D --> B B E
C C F G
D D H
J I
树2 T2:
E E
---> F
F
树3 T3:
G G
H I ---> H
J J I
*4.二叉树-->森林
解题思路:
#1.分树:[根+左孩子] [右孩子的左孩子] [右孩子的右孩子]
#2.拆叉:根和右子树同层级,左子树还是根的左子树
示例:
二叉树:
A
B C
D E F G
H I
森林:
A C G
B E F I
D H
3>树和森林的遍历
*1.树的遍历:
A
B C E
D
1)先序遍历:
#1.算法描述:若树非空,先访问根结点,然后依次先序遍历根的每棵子树:T1,T2,...,Tn;
#2.遍历结果:A,B,C,D,E
2)后序遍历:
#1.算法描述:若树非空则先依次后序遍历每棵子树;T1,T2,...,Tn,,最后访问根结点;
#2.遍历结果:B,D,C,E,A
3)层次遍历:
若树非空则按层从左到右依次访问每次的根节点
按层遍历-->A,B,C,E,D
*2.森林的遍历
森林直接表示图
A E G
B C D F H I
J
1)先序遍历森林
#1.算法描述:若森林非空,按森林中树的位置依次(T1,T2..)先序遍历每棵子树;
#2.遍历结果: A,B,C,D,E,F,G,H,J,I
2)中序遍历森林
#1.算法描述:若森林非空,按森林中树的位置依次(T1,T2..)后序遍历每棵子树;
#2.遍历结果:B,C,D,A,F,E,J,H,I,G
6.哈夫曼树
1>定义:
给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,
称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
权大的叶子离根近
带权路径长最小的二叉树
权小的叶子离根远
2>.算法描述:频率高的在上面(编码短),频率低的在下面(编码长),使电文总长度最小,节省空间;
示例:
设某通信系统中一个待传输的文本有6个不同字符,它们的出现频率分别是
0.5,0.8,1.4,2.2,2.3,2.8,试设计哈夫曼编码;
*1.哈佛曼树 出现频率-->字符编码
10 0.5 1000
0 1 0.8 1001
4.5 5.5 1.4 101
0 1 0 1 2.2 00
2.2 2.3 2.7 2.8 2.3 01
0 1 2.8 11
1.3 1.4
0 1
0.5 0.8
*注:哈夫曼树中:
#1.n0=n(叶子结点数),
#2.度为2的结点数n2=n-1; //(n0-1)
#3.哈夫曼树中没有度为1的分支结点;//每一分支结点都是由两棵子树合并产生的
#4.树的总结点共有2n0-1个;
3>.哈夫曼编码
概念:利用哈夫曼树构造用于通信的二进制编码称为哈夫曼编码(左子树0,右子树1);
n
电文总长= ∑ (wi*mi);
i=1
4>.带权的二叉树路径长度WPL
*1.算法描述:
/**
* @param n 叶子数
* @param pk 第k个叶子的权重
* @param lk 从根到第k个叶子的路径长度(分支数)
*/
n
WPL = ∑ (pK*lk);
k=1
*2.示例:
{34,5,12,23,8,18}为叶子结点的权值构建哈夫曼树,求其带权路径长度;
100
52 48
18 34 23 25
12 13
5 8
WPL(5)=5(叶子权重)*4(分支数)=20
WPL(8)=8(叶子权重)*4(分支数)=32
WPL(12)=12(权重)*3(分支数) =36
WPL(23)=23(权重)*2(分支数) =46
WPL(18)=18(权重)*2(分支数)=36
WPL(34)=34(权重)*2(分支数)=68
WPL=20+32+36+46+36+68=20+100+72+46=120+118=238;
第五章.图
1.图的基本概念:
1>定义:是由集合V和E组成的,记成G(图)=(V(顶点集),E(边集));
V--顶点集(非空);
E--边集(可空)
边是顶点的有序对或无序对.(边反映了两顶点间的关系)
2>两顶点之间的关系(有向与无向)
//n是图的顶点数
*1.无向图完全图:边是顶点的无序对的图(G1);
V(G1)={1,2,3,4};
E(G1)={(1,2),(1.3),(1,4),(2,3),(2,4),(3,4)}
无向图的边数n(n-1)/2;//边数=6,Cn^2
*2.有向完全图:边是顶点的有序对的图(图中每条边都用箭头指明了方向)(G2)
V(G2)={1,2,3,4};
E(G2)={<1,2>,<2,1>,<1.3>,<3,1>,<1,4>,<4,1>,
<2,3>,<3,2>,<2,4>,<4,2>,<3,4>,<4,3>}
有向图的弧数n(n-1);//弧数=12,pn^2
1 1 1
╱ | ╲ ↗↙ ↖↘ ╱ ╲
2--|--3 2 3 2 3
╲ | ╱ ↘↖ ↙↗
4 4
无向图G1 有向图G2 图G1'
*.注意事项
#1.边集可空;
#2.边集中不允许出现相同的边;
3>图的基本术语
*1.顶点(Vertex)---图中的数据元素;
*2.--有向图中,顶点Vi到顶点Vj的边,也称弧;
vi:弧头(终端点) :箭头端
vj:弧尾(初始点) :无箭头端
*3.完全图(顶点数n)
#1.无向完全图:边数=n*(n-1)/2的无向图
#2.有向完全图:边数=n*(n-1)的有向图
*4.权--与图中的边相关的数;
*5.子图:图G和G',若有V(G')⊆ V(G)和E(G')⊆E(G),则称图G'是图G的子图;如图G1和G1'
*6.邻接--若(Vi,Vj)∈E(G),则称Vi和Vj互为邻接点;
*7.关联--若(Vi,Vj)∈E(G),则称边(Vi,Vj)关联于顶点Vi和Vj;
#1.邻接是指顶点之间的关系,而关联是指边于顶点间的关系;
#2.若弧∈E(G),则称Vi是Vj的邻接点;
*8.度
1)无向图的度(D(Vi)):顶点Vi的度为与Vi相关联的边的个数;
2)有向图的度:
#1.出度(OD(Vi)):顶点Vi的出度为以Vi为尾的出边数;(箭头由vi出发)
#2.入度(ID(Vi)):顶点Vi的入度为以Vi为头的入边数;(箭头指向Vi)
#3.度(D(Vi)):有向图的度(DVi)=入度(ID(vi))+出度(OD(Vi));
注: 图中边数e与顶点的度的关系(一边带二度,两度组成一边)
n
e=(1/2)∑ D(Vi)
i=1
*9.路径:图中,顶点Vp到顶点Vq的路径是顶点序列:(起点,终点)(起点,终点)...;
*10.路径长度:路径上边或弧的数目;
*11.简单路径:除第一个顶点和最后一个顶点相同外,其余各顶点均不相同的路径;
*12.回路:第一个和最后一个顶点相同的路径,也称环;
*13.简单回路:第一个和最后一个顶点相同的简单路径;
#回路中可以有多个圈,而简单回路只能由一个圈
*14.连通:无向图中,若从顶点Vi到Vj顶点有路径,则称Vi和Vj是连通的;
*15.连通图和连通分量:
1)无向图:
#1.连通图
定义:图中每对顶点间都连通;vi~vj;
示例: 1 G1为连通图
╱ ╲
2 3
╲ ╱
4 (G1)
#2.连通分量
定义:图中极大的连通子图(再扩大一点就不连通)
示例: 1 5 G2不是连通图,
╱ ╲ ╱ 但它有两个连通分量;
2 3 6
╲ ╱ ╲
4 7
(分量1) (G2) (分量2)
2)有向图
#1.强连通图
定义:图中任意一对顶点Vi和Vj都有 图示:G3为强连通图
顶点Vi到顶点Vj的路径,也有 1
从顶点Vj到vi的路径,连个顶点 ↙ ↑
间双向连通; 2 |
↘ 3 (G3)
#2.强连通分量
定义:有向图的极大强连图子图;
图示: 1 1 G4有两个强连通分量
↙↑ ↙ ↑
2 ↑ 4 2 ↑ 4(分量2)
↘↑ ↗ ↘ ↑
3 3
(G4) (分量1)
*16.生成树
#1.概念:含有该连通图的全部顶点的一个极小连通子图(无向图).
若连通图G的顶点个数为n,则G的生成树的边数为n-1
#2.G的子图G'边数大于n-1,则G'中一定有环;
G的子图G'边数小于n-1,则G'中一定不连通;
*17.生成森林
在非连通图中,每个连通分量都可得到一个极小连通子图,也就是生成树.
这些生成树就组成了一个非连通图的生成森林;
4>图的基本运算
*01.建立图 CreateGraph(G,V,E)
*02.取顶点信息 GetVex(G,u)
*03.取边信息 GetArc(G,u,v)
*04.查询第一个邻接点 FirstVex(G,u)
*05.查询下一个邻接点 NextVex(G,u,v)
*06.插入顶点 InsertVex(G,v)
*07.删除顶点 DeleteVex(G,v)
*08.插入边 InsertArc(G,v,w)
*09.删除边 DeleteArc(G,v,w)
*10.遍历图 Travers(G,tag)
2.图的存储结构
1>邻接矩阵表示法
*1.作用:表示图的各顶点之间关系的矩阵;
*2.定义:设G=(V,E)是n个顶点的图,则G的邻接矩阵为下列n阶方阵
#1.不带权图的邻接矩阵:
1 若(Vi,Vj)或∈E(G);
A[i][j]=
0 否则 vi,vj 间无边或弧
#2.带权图的邻接矩阵:
wij 若(Vi,Vj)或∈E(G);
(wij为边或弧的权值)
A[i][j]=
∞ 否则 vi,vj 间无边或弧
*3.图转化成邻接矩阵图示
图的直接表示:
无向图 有向图
1 1
╱ | ╲ ↙↑
2 | 4 ↘2
╲ | ╱ ↓
3 (图G1) 3 (图G2)
图的邻接矩阵表示:(5分应用题)
V1 V2 V3 V4 V1 V2 V3
V1 0 1 1 1 V1 0 1 0
G1= V2 1 0 1 0 G2= V2 1 0 1
V3 1 1 0 1 V3 0 0 0
V4 1 0 1 0
结论:
1)无向图的邻接矩阵是对称的((Vi,Vj)∈E(G),则(Vj,Vi)∈E(G))
2)由无向图求出个顶点的度
#1.无向图:顶点Vi的度D(Vi)=矩阵中第i行元素之和
#2.有向图:OD(Vi)=矩阵中第i行元素之和;
ID(Vi)=矩阵中第i列元素之和;
#3.算法的时间复杂度为O(n^2);
邻接矩阵的类型定义(了解)
2>邻接表
1)定义
对图G中每个顶点都建立一个单链表,第i个单链表(称边表),链接图中与顶点Vi相邻接的所有顶点;
边表结点形式: adjvex|nextarc
注:#1.adjvex:邻接点域(顶点域):存放与顶点Vi相邻接顶点Vj的序号j;
#2.nextarc:链域:指向Vi的下一个邻接点;
表头结点:
每个链表均设一表头结点(以向量存储,称顶点表)
表头结点形式:vertex|firstarc
V[i]--第i个链表的表头结点
V[i].vertex--存放顶点Vi的信息
V[i].firstarc--指向Vi的邻接链表的第一个结点;
2)图示
*1.无向图 邻接表
v0 vertex firstAct adjvex nextAct
╱ | 0 v0 ---->1------> 2, NUll
v1 | 1 v1 ---->0------> 2, NULL
╲ | v3 2 v2 ---->1------> 2 ------------>3,NULL
╲| ╱ 3 v3 ---->2,NULL
v2
*2.有向边的 出度邻接表(箭尾所指) 入度邻接表(箭头所指)
v0
↗↙ ↑ 0 v0 -->1,NULL 0 v0 -->1,----->2,NULL
v1 ↑ v3 1 v1 -->0,-->2,NULL 1 v1 -->0,NULL
↘ ↑ ↗ 2 v2 -->0,-->3,NULL 2 v2 -->1,NULL
v2 3 v3-->NULL 3 v3-->2,NULL
3)结论:
*1.n个顶点,e条边的无向图,其邻接表的表头结点数为n,链表结点总数为2e
*2.对于无向图,第i个链表的结点数为顶点Vi的度
对于有向图,第i个链表的结点数为顶点Vi的出度;
*3.在边稀疏时,邻接表比邻接矩阵省空间;
*4.连接表表示在检测边数方面比邻接矩阵表示效率要高;
*5.邻接表时间复杂度为O(n+e);
*6.对边稀疏的图用连接表存储较省空间
3.图的遍历
1>遍历的含义及方法:
图的遍历——从图G中某一顶点v出发,系统地访问图的每个顶点,并且每个顶点只被访问一次。
2>遍历方法:
为克服顶点的重复访问,设立辅助数组visited[n]。
1 顶点i已被访问过;
visited[i]=
0 顶点i位未被访问过
*1.深度优先搜索(DFS)
#1.特点:像二叉数的先序遍历,DFS具有递归性需要用到栈;
#2.过程:
从图G(V,E)中任一顶点Vi开始,首先访问Vi,然后访问Vi的任一未访问过的邻接点Vj
再以Vj为新的出发点继续进行深度优先搜索,直到所有顶点都被访问过。
#3.注意:
搜索到达某个顶点时(图中仍有顶点未被访问),如果这个顶点的所有邻接点都被访问过,
那么就回到前一个被访问过的顶点,再从该顶点的下一未被访问的邻接点开始深度优先搜索。;
#4.深度搜索的顶点的访问序列不是唯一的
#5.如果以 邻接表 为存储结构,查找邻接点操作实际上时顺序查找链表,以链表为存储结构,
深度优先搜索算法的 时间复杂度 为 O(n+e); //n:图的顶点, e为图的边数
#6.若采用 邻接矩阵 作为存储结构,查找邻接点实际上通过循环语句顺序访问邻接矩阵的某一行
其深度优先算法的 时间复杂度 为 O(n^2); //其中n为图的顶点数
*2.广度优先搜索(BFS)
#1.特点:像二叉数的层次遍历,BFS遍历顶点的处理次序为先进先出,故具有队列的特性;
#2.过程:
从图G(V,E)中某一点Vi出发,首先访问Vi的所有邻接点(w1, w2, …, wt),
然后再顺序访问w1, w2,…, wt的 所有未被访问过的邻接点….,
此过程直到所有顶点都被访问过;
#3.深度搜索的顶点的访问序列不是唯一的
3>图的连通分量计算
从无向图的每个连通分量的一个顶点出发遍历,求得无向图的所有连通分量。
/*G为用邻接矩阵或邻接表表示的有n个顶点的无向图,求该图的连通分量*/
void trace(Graph G) {
int i;
for ( i=0; i概念:
*1.连通图G=(V,E),从任一顶点遍历,则图中边分成两部分:
E(G)=T(G)+B(G);//T(G)遍历过的边,E(G)未遍历过得边
则G'(V,T)为G的子图,称之为G的一棵生成树 V--顶点集(非空),T--边集(可空)
*2.深度优先生成树和广度优先生成树;
2>性质
*1.生成树G'是图G的极小连通子图
*2.生成树的结点为n,边为n-1;
*3.图的生成树不是唯一的;
3>最小生成树的算法
*1.概念
给定一个带权图,构造带权图的一棵生成树,使树中所有边的权总和为最小。
*2.实现算法:
#1.Prim算法(普里姆算法),
适合于求边 稠密 的带权图的最小生成树。
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
3).重复下列操作,直到Vnew = V://新的顶点集合与原顶点集合包含元素相同
①.在集合E中选取权值最小的边,其中u为集合Vnew中的元素,
而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件
即具有相同权值的边,则可任意选取其中之一;
②.将v加入集合Vnew中,将边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。
#2.Kruskal算法(克鲁斯卡尔算法):
1) 适合于求边 稀疏 的带权图的最小生成树
2) 按权值递增次序构造Tmin,即每次选权最小且不构成回路的边,直至n-1条。
5.单源最短路径(有向图)
(Dijkstra算法(迪杰斯特拉算法))
#1.算法描述:
给定一个带权有向图G=(V,E);[v:顶点集合,E:边集合],另外给定V中的一个顶点,
称为源,计算从这个源点(顶点v0)出发到其他顶点间的最短路径长度(路径上各边权值之和),
可以借助已到达的最短路径的顶点中转,去计算未到达顶点的最短路径;(最短路径递增)
#2.适用场合:求图G中两个结点之间的最短路径
#3.示例:求v0到v1,v2,v3的最短路径
v1
20 ↗ ↑ ↘ 60
v0 30 v2
40 ↘ ↑ ↗ 10
v3
step(步骤) s(已达顶点) u(顶点) dist[1] dist[2] dist[3]
1 {v0} - 20 max_int 40
2 {v0,v1} v1 20 80(20+60) 40
3 {v0,v1,v3} v3 20 50(40+10) 40
4 {v0,v1,v3,v2} v2 20 50 40
最终结果:v0到v1,v2,v3的最短路径分别为20,50,40;
6.拓扑排序(有向图)
*1.有向图拓扑排序算法的基本步骤:
#1.图中选择一个入度为 0 的顶点,输出该顶点;
#2.从图中删除该顶点及其相关联的弧,调整被删弧的弧头结点的入度(入度减 1);
#3.重复执行(1)、(2)直到所有入度为 0 的顶点均被输出,拓扑排序完成,
或者图中再也没有入度为 0 的顶点。
#4.AOV网(Activity On Vertex Network)
工程或者某种流程可以分为若干个小的工程阶段,这些小的工程或阶段就称为活动
如果以图中的顶点来表示活动,有向边表示活动之间的优先关系,这种用顶点表示
活动的有向图称为 AOV 网,AOV 网中的弧表示了活动之间存在着的制约关系。
*2.示例:
AOV网如图,求该图的拓扑排序
C2
↗ ↘
C1 C3
↘ ↗
C5 → C4
拓扑排序:C1,C2,C5,C4,C3
*3.结论:
#1.任何一个无环有向图,其全部顶点可以排成一个拓扑序列。
如果图中有环,就不能排序出图中的每个顶点;
#2.拓扑排序算法的时间复杂度为 O(n+e), n 是图的顶点个数, e 是图的弧的数目。
第六章.查找表(集合)
1.基本概念
1>查找表:
概念:查找表是一种以集合为逻辑结构,已查找为"核心"运算,同时包括其他运算的数据结构;
*注:由 同一类型 的数据元素(或记录)构成的集合;
2>关键字(键):用来 标识 数据元素的数据项称为关键字,简称键,其值称为键值;
3>主关键字:可唯一标识各个数据元素的关键字
4>查找:根据给定的某个k(key)值,在查找表寻找一个其键值(key)等于k的数据元素
5>静态查找表: 进行的是引用型运算
概念:静态查表是以具有相同特性的数据元素集合为逻辑结构
基本运算:
1) 建表
2)查找
*1.顺序查找
*2.有序表的二分查找
*3.索引顺序表的查找
3)读表中元素;
6>动态查找表: 进行的是加工型运算
动态查找表是以集合为逻辑结构,包括以下5种基本运算:
*1.初始化Initiate (ST):设置一个空的动态查找表ST;
*2.查找Search(ST,key):同静态查找表;
*3.读表中元素Get (ST, pos):同静态查找表;
*4.插入Insert (ST, key):若ST中不存在关键字值等于key的元素,则将一个关键字值等于key的新元素插入到ST中;
*5.删除Delete(ST,key):当ST中存在关键字值等于key的元素时,将其删除。
2.静态查找表的实现
1>顺序表的查找
*1.顺序表中元素的类型定义
typedef struct {
keytype key;//关键字域
...;//其他域
}
*2.顺序表的类型定义
const int maxsize=20; //静态查找表的表长
typedef struct {
TableElem elem[maxsize+1]; //一维数组,0号单元留空
int n; //最后一个元素的下标,即表长
}SqTable;
*3.查找表 --顺序查找
#1.使用设立岗哨编程设计技巧查询表
#2.算法描述
//在顺序表R中顺序查找其关键字等于k的元素,
//若找到,则函数值为该元素在表中的位置,否则为0
int SearchSqtable(Sqtable T,KeyType key){
T.elem[0].key=key; //设立岗哨(跳出循环的条件),顺序表中下标为0的元素初始定义留空
int i=T.n; //i的初始值为表长,也是最后一个表中元素的下标
while(T.elem[i].key!=key) //当i=0时肯定会跳出循环,返回0; 时间复杂度O(n)
i--; //未找到时,修改比较位置继续查找
return i;
}
#3.平均查找长度(算法分析)
1)概念:
检索过程中和关键码的平均比较次数,即平均检索长度
缩写:ASL(Average Search Length)
n(上界)
∑ ['sɪgmə] ∑ k(k从i开始取数,一直取到n,然后全部加起来)
(求和公式) i(下界)
2)公式
n
ASL=∑ pici
i=1
①.n:表中元素的个数
②.pi:查找第i个元素的概率,若不特别声明,一般认为每个元素的检索概率相等,即pi=1/n;
③.ci:找到第i个元素的比较次数;
若找的是第一位的元素的元素elem[0],则比较次数为1;
若找的是第i的元素elem[i],则比较次数为n-i+1;//由后向前数
#4.算法分析
pi(概率):1/n;
比较次数和:
n
∑ ci=1+2+...+n=n(1+n)/2 ;//ci从i开始取数,一直取到n,然后全部加起来
i=1
成功查找的ASL:
n n
ASL=∑ pici=(1/n)∑ ci =n(n+1)/2*(1/n)=(n+1)/2; //ASL=总比较次数 * pi(概率)
i=1 i=1
不成功查找的ASL:
ASL=n+1;//n-i+1=n-0+1
#5.结论:
用顺序查找方法对含有 n 个数据元素的顺序表按 从后向前 查找次序进行查找,
现假设查找其中每个数据元素的概率不相等,则
该顺序表按查找概率由低到高的顺序来存储数据元素,其 ASL 最小
2>有序表的查找
*1.概念:如果顺序表中的数据元素是按键值大小的顺序排序的,则称为有序表;
*2.二分查找(Binary Search)
每次用给定值与表的中间位置的元素的键值比较,确定给定值的所在区间,然后逐步缩小查找区间。
*3.算法描述
用给定值key与处在中间位置的数据元素 T.elem[mid]的键值T.elem[mid] .key进行比较,
可根据三种比较结果区分三种情况:
#1.key==T.elem[mid].key,查找成功, T.elem[mid]即为待查元素;
#2.keyT.elem[mid].key,说明若待查元素若在表中,则一定排在 T.elem[mid]之后。
*4.代码实现
/*
* 二分查找法,查找表中键值为key的元素的位置
* 在有序表 T 中,用二分查找法查找键值等于 key 的元素,
* 变量 low,hig 分别标记查找区间的下 界和上界
*/
int SearchBin(SqTable T,KeyType key){
int low, high, mid;
low=1;high=T.n; //初始化查找区域
while(low<=high){
mid=(low+high)/2;
if(key==T.elem[mid].key){
return mid; //查找成功
}else if(key11 -->low=mid+1-->low=12
2) 12+22=33 34/2=17 -->17 -->high=mid-1-->high=16
3) 12+16=28 28/2=14 -->14 -->low=mid+1-->low=15
4) 15+16=31 31/2=15 -->15 -->low=mid+1->low=16=high
5) 16+16=32 32/2=16 -->16 查找成功
比较的元素下标依次为:11,17,14,15,16;
3>索引顺序表的查找
*1.概念:
索引顺序表是结合了顺序查找和二分查找的优点构造的一种带索引的存储结构;
*2.构成:
一个索引顺序表由两部分组成:一个索引表和一个顺序表。
其中的顺序表在组织形式上与普通的顺序表完全相同,
而索引表本身在组织形式上也是一个顺序表。//每块索引表,地址相邻
索引表通过索引将顺序表分割为若干块,而顺序表呈现出 “按块有序” 的性质。
[e.g]
顺序表中元素
1,4,5,3,2, 8,10,7,11, 22,25,20,28 顺序表
↘index1↙ ↘index2↙ ↘index3 ↙ 索引表(顺序表)
索引表 (块内最大键值,块起始位置)
index1,index2,index3
表现形式:
index1 (5,1) {1,4,5,3,2}
index2 (11,6) {8,10,7,11}
index3 (28,10) {22,25,20,28}
*3.算法描述
#1.先确定待查数据元素所在的块;
#2.然后在块内顺序查找;
*4.平均查找长度
分块查找的平均长度等于两阶段各自的查找长度之和,顺序表有n个元素,每块索引表
含s个元素,且第一阶段采用顺序查找,则在等概率假定下,分块查找的平均查找长度为:
/**
* 求分块查找的平均查找长度
* n:顺序表中数据元素的总个数-->表长
* s:每块索引表中包含顺序表元素的个数
* s/n:索引表的块数
* (n(元素总个数)+1)/2: 顺序查找的ASL公式
* ASLb=(n/s+1)/2: ASLb为s/n块索引表的平均查找长度
* ASLs=(s+1)/2 : 每块索引表内元素的平均查找长度
* ASL(bs)=ASLb+ASLs=(n/s+1+s+1)/2=(n/s+s+2)/2
*/
ASL(bs)=(n/s+s+2)/2=(1/2)(n/s+s)+1
*5.其中 n 为顺序表中的数据元素数目,当s取√n时(n/s=s时->s=√n),ASL达到 最小值 √n+1。
4>优缺点
静态查找表的上述三种不同实现各有优缺点。
#1.顺序查找效率最低但限制最少;
#2.二分查找效率最高,但限制最强;
#3.分块查找则介于上述二者之间,在实际应用中应根据需要加以选择;
3.动态查找表
1>二叉排序树上的查找(Binary Sort Tree 动态查找的树表)
*1.算法描述:
当二叉排序树不空时,首先将 给定值 和 根结点 的关键字比较,若相等,则查找成功;
否则根据 给定值 与 根结点 关键字间的大小关系,分别在左子树或右子树上继续进行查找;
其ASL的最大值为(n+1)/2
其ASL的最大值为(n+1)/2
对序列R={k1,k2,...,kn},k1~kn均为关键字值,则按下列原则构建二叉排序树
*1.令k1为根;(第一个元素为总根)
*2.k1key) return bst; //成功时返回结点的地址
else if(keykey)
return SearchBST(bst->lchild,key); //继续在左子树中查找
else
return SearchBST(bst->rchild,key); //继续在右子树中查找
}
/**
* 在根指针bst所指的二叉排序树上递归地查找键值等于key的结点,
* 若成功,则返回指向该结点的指针,否则返回NULL,
* (这个算法了解就行)
* @param bst 二叉树根指针
* @param key 查找目标结点的键值
* @param f 指针f指向查到结点的父结点,其初始值为NULL
*/
BinTree SearchBST(BinTree bst,KeyType key,BSTNode *f){
if(bst==NULL)return NULL; //查找不成功;
else if(key==bst->key) return bst; //成功时返回结点的地址
else if(keykey)
return SearchBST(bst->lchild,key,bst); //继续在左子树中查找
else
return SearchBST(bst->rchild,key,bst); //继续在右子树中查找
}
/**
* 边查找,表插入-->动态查表法
*
* 若根指针bst所指的二叉排序树上无键值为key的结点,
* 则插入这个结点,并返回1,否则返回0(这个算法了解就行)
*
* @param bst 二叉树根指针
* @param key 要查找目标结点的键值
* *p 查找失败插入新点的位置(新的根结点)
* *t 查找成功后目标结点的位置
* *f 被插入结点p的父结点,其初始值为NULL
*/
int InsertBST(BinTree bst,KeyType key){
BSTNode *p,*t,*f;
f=NULL;
t=SearchBST(bst,key,f);
if(t==NULL){
p=malloc(sizeof(btnode));
p->lchild=NULL;
p->rchild=NULL;
if(f==NULL) bst=p; //被插入结点p为新的根结点
else if(keylchild) f->lchild=p; //被插入结点p为f左孩子
else f->rchild=p; //被插入结点p为f右孩子
return 1;
}else{
return 0; //查找成功时不用插入结点
}
}
*6.平均查找长度(ASL)
A(1) A(1)
B(2) C(2) B(2)
D(3) E(3) C(3)
D(4)
E(5)
图G1 图G2
ASL(G1)=(1+2+2+3+3)/5=11/5≈log2^5; ASL(G2)=(1+2+3+4+5)/5=15/5=3;
二叉排序树上的平均查找长度是介于O(n)和O(log2^n)之间的,其查找效率与树的形态有关。
2>散列表(哈希表)
*1.散列函数 (哈希函数)
#1.使用散列表进行查找的出发点
使数据元素的存储位置和键值之间建立某种联系,以减少查找过程中的比较次数;
#2.定义
设记录表A,长为n,ai(1<=i<=n)为表中某一元素,ki为其关键字,
则关键字ki和元素ai在表中的地址之间有一函数关系,即:
Addr(ai)=H (ki)
↗ ↖
ai在表中的地址 散列函数:关键字与元素地址的函数
*2.散列地址
由散列函数决定数据元素的存储位置,该位置称为散列地址;
*3.散列查找
散列函数转换
给定关键字-------------> 在表中的地址
↓
↓
查看此位置上有无欲查元素
有 ↙↘ 无
↙ ↘
输出信息 将它填到此位置上
*4.散列表(Hash Table):
#1.概念:
用数据元素的键值通过散列函数获取其存储位置,这种存储方式构造
的存储结构称为散列表;
#2.散列法主要工作
1)选择一个好的散列函数
①.函数计算简便,运算速度快
②.随机性好,地址尽可能均匀分布
③.冲突小
2)解决冲突
*5.散列表中相关术语
#1.冲突:
设有散列函数 H 和键值 k1、 k2(k1≠k2),但H(k1)=(k2) 的现象称为冲突,
即:不同的关键字映射到同一存储单元,并称k1和k2是同义词;
示例:
散列函数为 H(k)=k mod(%) 质数;//取余运算
k1=3;k2=16; 素数为13
k1%13=k2%13; //这时k1与k2就冲突了;
#2.堆积:
非同义词之间对同一个散列地址的争夺现象称为"堆积";
*6.常用的散列法(填空题)
#1.数学分析法
没有采用散列技术解决冲突
#2.除留余数法(******)
1)算法描述:
取关键字被某个不大于散列 表长n 的正整数p ,以键值除以p,所得余数作为散列地址;
即: H(key)=key mod p (p<=n)
2)示例
一组关键字从000,001~859999的散列地址为:0~5999 即m=6000
取 p=5999 //余数r在0~5999范围内 H=k mod 5999
设:k=172148
则:H=k mod p =172148 mod 5999=4176
3)p的取值规则
①.p不取偶数;
②.p不取关键字字符基的n倍;
③.一般选p为质数且最接近表长m的质数;(大于1且除了1和它本身以外不能被其他数整除的自然数)
#3.平方取中法
#4.基数转换法
*7.散列表的实现(解决冲突)
#1.线性探测法
1)概念
对任何键值 key,设 H (key) =d,设散列表的容量为 m,
则线性探测法生成的后继散 列地址序列为
d+1, d+2,…, m-1, 0, 1,…, d-1
1)算法描述:
计算出的散列地址已被占用,则按顺序找(从左到右)"下一个"空位;
①.HT[j]空,则R填入
②.HT[j].key=k,则输出
③.否则,按顺序一步步找"下一个"空位,将R填入;
④.是循环队列:超出键值补充原来空下的地
2)示例
p=13; H(key)=key mod 13;
Key:(13,41,15,44,06,68,25,12,38,64,19,49)
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
0 2 2 5 6 3 12 12 12 8 6 10
散列表: 0 1 2 3 4 5 6 7 8 9 10 11 12
|13|12|41|15|68|44|06|38|64|19|49| |25|
3)缺点
使用线性探测法解决冲突后存储空间连续使 非同义词之间对同一个散列地址争夺 现象
称为 "堆积" ,为了减少堆积的概率,应设法使后继散列地址尽量均匀地分散在整个散列表中;
#2.二次探测法
1)算法描述:
生成的后继散列地址不是连续的而是跳跃式的,以便为 后续数据元素留下空间
从而减少堆积。按照二次探测法,键值 key 的散列地址序列为
d0=H(key),di= (d0+i) mod m // m 为散列表的表长,i的取值i=1^2,-1^2,2^2,-2^2,…,土 k^2(k≤m/2)。
2) 示例
表长为13的散列,用二次探测法插入键值为29的元素
m=13;d0= H(key); di= (d0+i) mod m; //i=1^2,-1^2,2^2,-2^2,…,土 k^2(k≤m/2)
散列表: 0 1 2 3 4 5 6 7 8 9 10 11 12
| | |54|16|30| | | | | | | | |
0 1 2 3 4 5 6 7 8 9 10 11 12
| | |54|16|30| | | | | | | | |
d0=29/13=3-->(3+1^2)/13=4-->(3-1^2)/13=2;-->(3+2^2)=7;
2)缺点
不易探测到整个散列表的所有空间,上述后继散列地址可能难以包括散列表的所有存储位置。
#3.连地址法
1)算法描述
链地址是对每个同义词都搭建一个单链表来解决冲突,其组织方式如下
设选定的散列函数为H,H的值域(即散列的地址范围)为0~(n-1);
设置一个"指针向量" Pointer HP[n],其中每个指针HP[i]指向一个单链表
该单链表用于存储所有散列地址为i的数据元素,每一个这样的单链表称为一个
同义词子表;
H(k)=k mod(%) p(质数)
索引∈[0,p-1]; //按键值顺序依次得出散列值,再通过散列值和索引的对比,
将键值放到相应的散列地址上;冲突时按先来后到的顺序连接起来;
2)示例
若选定的散列函数为 H(key)=key mod 13,已存入键值为
26, 41, 25, 05, 07, 15,12, 49, 51, 31, 62 的散列表:
HAdd HP
0 ---->26,NULL
1 NULL
2 ---->41,next-->15,NULL
3 NULL
4 NULL
5 ---->05,next-->31,NULL
6 NULL
7 ---->07,NULL
8 NULL
9 NULL
10 ---->49,next-->62,NULL
11 NULL
12 ---->25,next-->12,next-->51,NULL
#4.多重散列法
#5.公共溢出区法
按这种方法,散列表由两个一维数组组成。一个称为基本表,它实际上就是上面所说的散列
表,另一个称为溢出表。 插入首先在基本表上进行,假如发生冲突,则将同义词存入溢出表。
这样,基本表不可能发生“堆积”。
第七章.排序
1.概述(术语)
1>数据排序
排序就是将一组对象按照规定的次序重新排列的过程,排列往往是为检索服务的;
2>稳定性
#1.概念
n个记录的序列为{R1,R2,...,Rn},其相应的键值序列为{K1,K2,...,Kn},假设Ki=kj,
若在排序前的序列中Ri在Rj之前,即i排序类型
#1.内部排序:全部数据存于内存,按方法分为以下几种:
插入排序、交换排序、选择排序、归并排序;
#2.外部排序:需要对外存进行访问排序过程;
2.插入排序
1>常用的插入排序
*1.直接插入排序
*2.折半插入排序
*3.表插入排序
*4.希尔排序
2>直接插入排序 算法描述:
对R1,...,Ri-1已排好序,有k1<=K2...<=K(i-1),
现将ki依次与ki-1,ki-2,...进行比较,并移动元素,
直到发现Ri应插在Rj与Rj+1之间(即有Kj<=ki<=kj+1),
则将Ri插到j+1号位置上,形成i个有序序列;
*直接插入排序类似图书馆中整理图书的过程。
3>排序文件的存储类型定义
#define n 100 //序列中待排序记录的总数
typedef struct{
int key; //关键字项
anytype otheritem; //其他数据项
}record;
typedef record list[n+1]; 数组表示
list r; r[i].key--->第i个元素的关键字
4> 算法代码实现
//直接插入排序算法,对r[1]...r[n]进行排序
void straightSort(list r){
for(i=2;i<=n;i++){ //n为表长,从第二个记录起进行插入
r[0]=r[i]; //r[0]初始值为r[2],第i个值复制为岗哨
j=i-1; //j的初始值为1
//r[i]依次与前面元素(i-1~1)作对比,比r[i]大的后移一格,直到j=1
while(r[0].key示例
{(49),38,65,97,76,134,27,[49],1}
*0. (49)
*1. 38,(49) //开始
*2. 38,(49),65
*3. 38,(49),65,97
*4. 38,(49),65,76,97
*5. 38,(49),65,76,97,134
*6. 27,38,(49),65,76,97,134
*7. 27,38,(49),[49],65,76,97,134
*7. 01,27,38,(49),[49],65,76,97,134
6>算法分析
*1.存储空间 n+1;//1为附加空间
*2.空间复杂度 O(1);
*2.时间复杂度 O(n^2),理想情况为O(n);//若待排序记录的数量很大时,一般不选用直接插入排序。
*3.稳定性:稳定;
3.交换排序
1>交换排序的基本思想
比较两个记录键值的大小,如果两个键值的大小出现逆序,则交互这两个记录,
这样讲键值较小的记录向序列前部移动,键值较大的记录向序列后部移动;
*注意:交换排序包括:冒泡排序和快速排序
2>冒泡排序
*1.算法描述:
对{a1,a2,...,an-1,an}排序
设置一个排序变化标识flag,初值为1,如果经过一轮排序,序列的排序有变化,
就将flag的值改为0,如果没有就flag仍为1时排序完成,
从左到右,相邻两两比较,键值大的放在后面,最终找到最大的放在最后面(an),
再进行下一轮的对比范围为(a1~an-1),排出第二大的放在an-1的位置上,
依次类推;直到flag为0时说明排序完成
*2.示例
{(49),38,65,97,76,134,27,[49],1}
*1. 38,(49) 65,76,97,27,[49],1,134
*2. 38,(49) 65,76,27,[49],1,97,134
*3. 38,(49),65,27,[49],1,76,97,134
*4. 38,(49),27,[49],1,65,76,97,134
*5. 38,27,1,(49),[49],65,76,97,134
*6. 27,1,38,(49),[49],65,76,97,134
*7. 1,27,38,(49),[49],65,76,97,134
*3.算法实现
/**
* 用冒泡排序法对r[1]...r[n]进行排序
* flag:标志文件是否已经排好序
*/
void bubbleSort(list r,int n){
int flag;
for (int i = 1; i < n - 1; i++) {
flag=1; //若循环中记录未作交换,说明序列排序已完成
for (int j = 1; j < n - i; j++) {
if(r[j+1].key快速排序(Quick Sorting)(***常考****)
*1.算法描述
首先取第一个记录,将之与表中其余记录比较并交换,从而将它放到记录的正确的最终位置,
使记录表分成两部分{其一(左边的)诸记录的关键字均小于它;
其二(右边的)诸记录的关键字均大于它};然后对这两部分
重新执行上述过程,依此类推,直至排序完毕。
第一趟 排序是指第一位键值找到合适的位置即 [key]
*2.示例以[45]为基准
用快速排序方法对 [45] 38 66 90 88 10 25 43 进行排序
第一次交换后 43 38 66 90 88 10 25 [45]
第二次交换后 43 38 [45] 90 88 10 25 66
第三次交换后 43 38 25 90 88 10 [45] 66
第四次交换后 43 38 25 [45] 88 10 90 66
第五次交换后 43 38 25 10 88 [45] 90 66
第六次交换后 43 38 25 10 [45] 88 90 66
*3.算法代码表示
在第 i 次选择操作中,通过 n-i 次键值间比较,
从 ni+1 个记录中选出键值最小的记录,并和第 i(1≤i≤n-l)个记录交换
//选择排序算法
void SelectSort (List R,int n){
int min;
for(int i=1;i<=n-1;i++){ //每次循环,选择出一个最小键值
min=i; //假设第 i 个记录键值最小
for (int j=i+1;j<=n;j++){
if (R[j] . key直接选择排序
*1.算法描述
设记录R1,R2...,Rn,对i=1,2,...,n-1,重复下列工作;
1)在Ri,...,Rn中选最小(或最大)关键字记录Rj;
2)将Rj与第i个记录交换位置,即将选到的第i小的记录换到第i号位置上
*2.示例:
46,15,13,94,17
第一趟:13,[15,46,94,17]
第二趟:13,15,[46,94,17]
第三趟:13,15,17,[94,46]
第四趟:13, 15,17,46,94
*3.算法代码实现
void SelectSort(List R,int n){
int temp,min;
for(int i=1,i堆排序(****)
*1.堆:集合{k1,k2,...,kn},对所有i=1,2,...,n/2有:
ki<=K2i且Ki<=K2i+1;则称此集合为堆(最小堆)
例:{13,40,27,88,55,34,65,92} (最小堆)
{92,65,88,40,55,34,13,27} (最大堆)
下标: 1 2 3 4 5 6 7 8
*.对应的完全二叉树:
最小堆(根最小) 最大堆(根最大)
13 92
40 27 65 88
88 55 34 65 40 55 34 13
92 27
*2.最小堆{k1,k2,...,kn}
↓概念上可看成
顺序存储的完全二叉树(Ri对应结点i)且树中双亲关键字均不超过孩子的关键字;
*3.建最小堆(筛选法)
设记录{R1,R2,...,Rn}
#1.顺序输出完成完全二叉树(以数组存储)
#2.从最后一个双亲开始,如果有较小的孩子,则将其沿左或右小的那个方向筛选;
一直到不能再筛;
#3.逐次处理完每个双亲;
*4.算法分析
#1.空间复杂度: n+1; //仅需一个记录大小的供交换用的辅助存储空间;
#2.时间复杂度: O(nlog2n)
#3.稳定性: 不稳定排序
5.归并排序
*1.算法描述:
归并的含义是将两个或两个以上的有序表合并成一个新的有序表;
合并的方法是比较各子序列的第一个记录的键值,最小的一个就是排序后序列的第一个记录的键值。
取出这个记录,继续比较各子序列现有的第一个记录的键值,便可找出排岸后的第二个记录。
如此继续下去,最终可以得到排序结果。
*2.算法的代码实现
//将 ah,…, am和 am+1,…, an 两个有序序列合并成一个有序序列 Rh, Rn
void Merge(List a,List R,int h,int m,int n){
k=h; j=m+1; //k,j 置成文件的起始位置;
while ((h<=m) && (j<=n)){ //将 a 中记录从小到大合并入 R;
if (a[h].key<=a[j].key){ //a[h]键值小,送入 R[k]并修改 h 值
R[k]=a[h];
h++;
}else { //a [ j ]键值小,送入 R[k]并修改 j 值
R[k]=a[j];
j++;
}
k++;
}
while (h<=m) {R[k] =a[h];h++; k++;} // j >n,将 ah,…,am 剩余部分插入 R 的末尾
while (j<=n) {R[k] =a[j]; j++;k++;} //h>m,将 am+1,…,an 剩余部分插入 R 的末尾
}
*3.示例:
{25, 9, 78, 6 65, 15, 58, 18, 45, 20}
一次归并: [9 25] [6 78] [15 65] [18 58] [20 45]
二次归并: [6 9 25 78 ] [15 18 58 65 ] [20 45]
三次归并: [6 9 15 18 25 58 65 78 ] [20 45]
四次归并: [6 9 15 18 20 25 45 58 65 78 ]
*4.算法分析
#1.时间复杂度为 O(nlog2n);
#2.空间复杂度:由于要用到和待排记录等数量的数组 b 来存放结果,
所以实现归并排序需要附加一倍的存储开销。空间复杂度大;
#3.二路归并排序是稳定的。
#4.在 n 较大时,归并排序的时间性能优于堆排序,但它所需的辅助存储量较多。
6.各排序方法的比较表
|-----------|-------------|----------------|-----------|-----------|----------|
| 排序类型 排序名称 平均时间 最坏情况 辅助存储 稳定性
|-----------|-------------|----------------|-----------|-----------|----------|
| 插入排序 直接插入排序 O(n^2)最好O(n) O(n^2) O(1) 稳定
|-----------|-------------|----------------|-----------|-----------|----------|
| 冒泡排序 O(n^2)最好O(n) O(n^2) O(1) 稳定
| 交换排序 |-------------|----------------|-----------|-----------|----------|
| 快速排序 O(nlog2^n) O(n^2) O(log2^n) 不稳定
|-----------|-------------|----------------|-----------|-----------|----------|
| 直接选择排序 O(n^2) O(n^2) O(1) 不稳定
| 选择排序 |-------------|----------------|-----------|-----------|----------|
| 堆排序 O(nlog2^n) O(nlog2^n) O(1) 不稳定
|-----------|-------------|----------------|-----------|-----------|----------|
| 归并排序 二路归并排序 O(nlog2^n) O(nlog2^n) O(n) 稳定
|-----------|-------------|----------------|-----------|-----------|----------|