术语 | 定义 |
---|---|
数据 | 数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号的集合 |
数据元素 | 数据元素是数据的基本单位,通常作为一个整体进行考虑和处理,含有多个数据项 |
数据项 | 是构成数据元素的不可分割的最小单位 |
数据对象 | 具有相同性质的数据元素的集合,是数据的一个子集 |
数据类型 | 一个值的集合和定义在此集合上的一组操作的总称{原子类型、结构类型、抽象数据类型} |
数据结构 | 相互之间存在一种或多种特定关系的数据元素的集合 |
数据类型
数据类型 | 定义 |
---|---|
原子类型 | 其值不可再分的数据类型 |
结构类型 | 其值可以再分解为若干成分的数据类型 |
抽象数据类型ADT | 抽象数据组织及与之相关的操作 |
抽象数据类型的定义格式
ADT 抽象数据类型名{//抽象数据类型定义格式
数据对象:<数据对象的定义> //自然语言
数据关系:<数据关系的定义> //自然语言
基本操作:<基本操作的定义>
}ADT 抽象数据类型名;
//基本操作的定义格式
基本操作名(参数表) //参数表中赋值参数只提供输入值,引用参数以&打头,可提供输入值和返回操作结果
初始条件:<初始条件描述>
操作结果:<操作结果描述>
定义:逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。
分类:
定义:存储结构是指数据结构在计算机中的表示,也称物理结构
分类:
存储结构 | 定义 | 优点 | 缺点 |
---|---|---|---|
顺序存储 | 把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现 | 随机存取,占用空间少 | 使用一整块相邻的存储单元,产生较多碎片 |
链式存储 | 不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系 | 不会出现碎片,充分利用所有存储单元 | 需要额外空间,只能顺序存取 |
索引存储 | 在存储元素信息的同时,还建立附加的索引表。 | 检索速度快 | 附加的索引表需要额外空间。增删数据修改索引表时花费时间 |
散列存储 | 根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。 | 检索、增加和删除结点的操作很快 | 可能出现元素存储单元的冲突,解决冲突会增加时间和空间开销 |
定义:施加在数据上的运算包括运算的定义和实现。定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
T ( n ) = O ( f ( n ) ) , S ( n ) = O ( g ( n ) ) T(n)=O(f(n)),S(n)=O(g(n)) T(n)=O(f(n)),S(n)=O(g(n))
函数的渐进的界 O ( g ( n ) ) O(g(n)) O(g(n))
(存在正数c和 n 0 n_0 n0使得对于一切 n ≥ n 0 n\geq n_0 n≥n0), 0 ≤ f ( n ) ≤ c g ( n ) 0\leq f(n)\leq cg(n) 0≤f(n)≤cg(n)
算法复杂度分析步骤
递归算法两类复杂度分析
T ( n ) = { O ( 1 ) n = 1 a T ( n − 1 ) + f ( n ) n > 1 T(n)= \begin{cases} O(1) & n=1\\ aT(n-1)+f(n)& n>1 \end{cases} T(n)={O(1)aT(n−1)+f(n)n=1n>1
T ( n ) = a n − 1 T ( 1 ) + ∑ i = 2 n a n − i f ( i ) T(n)=a^{n-1}T(1)+\sum_{i=2}^n a^{n-i}f(i) T(n)=an−1T(1)+i=2∑nan−if(i)
T ( n ) = { O ( 1 ) n = 1 a T ( n b ) + f ( n ) n > 1 T(n)= \begin{cases} O(1) & n=1\\ aT(\frac nb)+f(n)& n>1 \end{cases} T(n)={O(1)aT(bn)+f(n)n=1n>1
T ( n ) = n l o g b a T ( 1 ) + ∑ j = 0 l o g b n − 1 a j f ( n b j ) T(n)=n^{log_b a}T(1)+\sum_{j=0}^{log_b n-1}a^jf(\frac n{b^j}) T(n)=nlogbaT(1)+j=0∑logbn−1ajf(bjn)
线性表是具有相同数据类型的n个数据元素的有限序列。其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为 L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L=(a_1,a_2,...,a_i,a_{i+1},...,a_n) L=(a1,a2,...,ai,ai+1,...,an)。
InitList(&L):初始化表。构造一个空的线性表
Length(L):求表长
LocateElem(L,e):按值查找操作
GetElem(L,i):按位查找操作
ListInsert(&L,i,e):插入操作
ListDelete(&L,i,&e):删除操作,并用e返回删除元素的值
PrintList(L):输出操作
Empty(L):判断操作
DestoryList(&L):销毁操作
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。顺序表的特点是表中元素的逻辑顺序与物理顺序相同。
i
个元素的内存地址:&(A[0])+i*sizeof(ElemType)
静态分配的实现
#define MaxSize 50 //定义线性表的最大长度
typedef struct{
ElemType data[MaxSize]; //顺序表的元素
int length; //顺序表的当前长度
}SqList; //顺序表的类型定义
动态分配的实现
#define InitSize 100 //表长度的初始定义
typedef struct{
ElemType *data; //指示动态分配数组的指针
int MaxSize,length; //数组的最大容量和当前个数
}SqList; //动态分配数组顺序表的类型定义
//C的初始动态分配语句
L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
free(L);
//C++的初始动态分配语句
L.data = new ElemType[InitSize];
delete L;
注意算法对i的描述是第i个元素,它是以1为起点的
bool ListInset(SqList &L,int i,ElemType e){
if(i<1||i>L.length+1) return false; //判断插入位置是否有效
if(L.length>=MaxSize) return false; //判断存储空间是否已满
for(int j=L.length;j>=i;j--) L.data[j] = L.data[j-1]; //将插入位置之后的元素后移
L.data[i-1] = e; //赋值
L.length++; //线性表长度+1
return true;
}
bool ListDelete(SqList &L,int i,ElemType &e){
if(i<1||i>L.length) return false; //判断值是否有效
e = L.data[i-1]; //赋值,用于返回
for(int j = i;j<L.length;j++) L.data[j-1] = L.data[j]; //删除元素后的元素前移
L.length--; //线性表长度-1
return true;
}
int LocateElem(SqList L,ElemType e){
int i;
for(i=0;i<L.length;i++)
if(L.data[i] == e)
return i+1; //注意这里的下标
return 0;
}
typedef struct LNode{ //定义单链表结点类型
ElemType data; //数据域
struct LNode *next; //指针域
}LNode, *LinkList; //LinkList为指向结构体LNODE的指针类型
LinkList List_HeadInsert(LinkList &L){
LNode *s;int x;
L=(LinkList)malloc(sizeof(LNode));//创建头结点
L->next = NULL; //初始为空链表
scanf("%d",&x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d",&x);
}
return L;
}
LinkList List_TailInsert(LinkList &L){
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x);
while(x!=9999){
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d",&x);
}
r->next = NULL; //尾结点指针置空
return L;
}
LNode *LocateElem(LinkList L,ElemType e){//按值查找
LNode *p = L->next;
while(p!=NULL&&p->data!=e)
p = p->next;
return p;
}
typedef struct DNode{
ElemType data;
struct DNode *prior,*next; //前驱和后继指针
}DNode,*DLinkList;
借助数组来描述线性表的链式存储结构,结点也有数据域data
和指针域next
,这里的指针是节点的相对地址(数组下标),又称游标
#define MaxSize 50
typedef struct{
ElemType data;
int next;
}SLinkList[MaxSize];
InitStack(&S):初始化一个空栈S
StackEmpty(S):判断一个栈是否为空,若栈S为空则返回true,否则返回false
Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶
Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回
GetTop(S,&x):读取栈顶元素,若栈S非空,则用x返回栈顶元素
DestroyStack(&S):销毁栈,并释放栈S占用的存储空间
利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,并附设一个指针top
指示当前栈顶元素的位置
#define MaxSize 50 //定义栈中元素最大个数
typedef struct{
Elemtype data[MaxSize]; //存放栈中元素
int top;//栈顶指针
}
S.top
,初始时设置S.top=-1
;栈顶元素:S.data[S.top]
S.top==-1
;栈满条件:S.top==MaxSize-1
;栈长:S.top+1
利用栈底位置相对不变的特性,可让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶共享空间的中间延伸。
两个栈的栈顶指针都指向栈顶元素
top0=-1
时0号栈为空,top1=MaxSize
时1
号栈为空
top1-top0==1
为栈满
当0
号栈进栈时top0
先加1再赋值,1
号栈进栈时top1
先减1再赋值;出栈是刚好相反
采用链式存储的栈称为链栈,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。这里规定链栈没有头结点,Lhead
指向栈顶元素
typedef struct Linknode{
ElemType data;//数据域
struct Linknode *next;//指针域
} *LiStack;//栈类型定义
InitQueue(&Q):初始化队列,构造一个空队列Q
QueueEmpty(Q):判队列空
EnQueue(&Q,x):入队,若队列Q非满,将x加入,使之成为新的队尾
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回
GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x
队列的顺序实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针:队头指针front
指向队头元素,队尾指针rear
指向队尾元素的下一个位置
#define MaxSize 50//定义队列中元素的最大个数
typedef struct{
ElemType data[MaxSize];//存放队列元素
int front,rear;//队头指针和队尾指针
} SqQueue;
Q.front==Q.rear==0
将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。当队首指针Q.front=MaxSize-1
后,再前进一个位置就自动到0,这可以利用除法取余运算%
来实现
Q.front=Q.rear=0
Q.front=(Q.front+1)%MaxSize
Q.rear=(Q.rear+1)%MaxSize
(Q.rear+MaxSize-Q.front)%MaxSize
判断循环队列队空或队满的三种方式
牺牲一个单元来区分队空和队满,入队时少用一个队列单元,约定以“队头指针在队尾指针的下一位置作为队满的标志”
(Q.rear+1)%MaxSize==Q.front
Q.front=Q.rear
(Q.rear-Q.front+MaxSize)%MaxSize
类型中增设表示元素个数的数据成员。
Q.size==0
Q.size==MaxSize
类型中增设tag
数据成员,以区分是队满还是队空。
tag=0
时,若因删除导致Q.front==Q.rear
,则为队空
tag=1
时,若因插入导致Q.front==Q.rear
,则为队满
队列的链式表示称为链队列,它实际是一个同时带有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点。
typedef struct{//链式队列结点
ElemType data;
struct LinkNdoe *next;
}LinkNode;
typedef struct{//链式队列
LinkNode *front,*rear;//队列的队头和队尾指针
}LinkQueue;
通常将链式队列设计成一个带头结点对的单链表,这样插入和删除就统一了
双端队列是指允许两端都可进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端。
顺序扫描表达式的每一项,然后根据它的类型作出如下相应操作:若该项是操作数,则将其压入栈中;若该项是操作符
,则连续从栈中退出两个操作数Y
和X
,形成运算指令X
,并将计算结果重新压入栈中。当表达式的所有项扫描并处理完毕后,栈顶存放的就是最后的结果
(
,入栈)
,则依次把栈中的运算符加入后缀表达式,直到出现(
,从栈中删除(
(
外的栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或遇到一个左括号为止。可以将递归算法转换为非递归算法。通常需要借助栈来实现这种转换
3
操作2
数组是由n个相同类型的数据元素构成的有限序列,每个数据元素成为一个数据元素,每个元素在n个线性关系中的序号称为该元素的下标,下标的取值范围称为数组的维界
数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为其元素也是定长线性表的线性表。
多维数组的映方法:按行优先和按列优先
指为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。其目的是节省存储空间
矩阵中非零元素的个数远远小于矩阵元素的个数
使用三元组(行、列、值)或十字链表法存储,失去了随机存取特性
串(String)是由零个或多个字符组成的有限序列。记为 S = ′ a 1 a 2 . . . a n ′ S='a_1a_2...a_n' S=′a1a2...an′
串中任意多个连续的字符组成的子序列称为该串的子串
包含子串的串称为主串
由一个或多个空格组成的串称为空格串
用一组地址连续的存储单元存储串值的字符序列
#define MAXLEN 255//预定义最大串长为255
typedef struct{
char ch[MAXLEN];//每个分量存储一个字符
int length;//串的实际长度
}SString;
堆分配存储仍然以一组地址连续的存储单元存放串值的字符序列,但他们的存储空间是在程序执行过程中动态分配得到的
typedef struct{
char *ch;//按串分配存储区,ch指向串的基地址
int length;//串的长度
}HString;
在C
语言中,存在一个称之为堆的自由存储区,并用malloc()
和free()
函数来完成动态存储管理
利用malloc()
为每个新产生的串分配一块实际串长所需要的存储空间,若分配成功,则返回一个指向起始地址的指针,称为串的基地址,这个串由ch
指针来指示;若分配失败,则返回NULL
。已分配的空间可用free()
释放掉
类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性,在具体实现时,每个节点即可以存放一个字符,也可以存放多个字符,每个节点称为块,整个链表称为块链结构
StrAssign(&T,chars):赋值操作。把串T赋值为chars
StrCopy(&T,S):复制操作。由串S复制得到串T
StrEmpty(S):判空操作。若S为空串,则返回TRUE,否则返回FALSE
StrCompare(S,T):比较操作,S>T则返回值>0;k若S=T,返回0,否则返回<0
StrLength(S):求串长
SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符起长度为len的子串
Concat(&T,S1,S2):串联接。用T返回S1和S2的联接
Index(S,T):定位操作
ClearString(&S):清空
DestroyString(&S):销毁串
子串的定位操作通常称为串的模式匹配,它求的是子串在主串中的位置
PM
:字符串的前缀和后缀的最长相等前后缀长度编号 | 描述 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
S |
字符 | a | b | c | a | c |
PM |
子串右移位数=已匹配的字符数-对应的部分匹配值:Move=(j-1)-PM[j-1] |
0 | 0 | 0 | 1 | 0 |
next (PM右移一位) |
子串右移位数:Move=(j-1)-next[j] ,子串的比较指针回退到:j=next[j]+1 |
-1 | 0 | 0 | 0 | 1 |
next=next+1 |
在子串的第j 个字符与主串发生失配时,则跳到子串的next[j]位置重新与主串当前位置进行比较 |
0 | 1 | 1 | 1 | 2 |
n e x t [ j ] = { 0 j = 1 m a x { k ∣ 1 < k < j 且 ′ p 1 . . . p k − 1 ′ = ′ p j − k + 1 . . . p j − 1 ′ } 当 此 集 合 不 空 时 1 其 他 情 况 (next[ ]推导公式) next[j]= \begin{cases} 0 & j=1\\ max\{k|1
next的推导步骤,next[j]=k
,求next[j+1]
next[j]=k
表明 p 1 . . . p k − 1 = p j − k + 1 . . . p j − 1 p_1...p_{k-1}=p_{j-k+1}...p_{j-1} p1...pk−1=pj−k+1...pj−1
next[j+1]=next[j]+1
next[k]
个字符与 p j p_j pj比较,如果 p n e x t [ k ] p_{next[k]} pnext[k]与 p j p_j pj还是不匹配,那么需要寻找长度更短的相等前后缀,下一步继续用 P n e x t [ n e x t [ k ] ] P_{next[next[k]]} Pnext[next[k]]与 p j p_j pj比较,直到找到k'=next[next...[k]]
满足条件 ′ p 1 . . . p k ′ ′ = ′ p j − k ′ + 1 . . . p j ′ 'p_1...p_{k'}'='p_{j-k'+1}...p_{j}' ′p1...pk′′=′pj−k′+1...pj′,则next[j+1]=k'+1
next[1]=0
:当模式串中的第一个字符与主串的的当前字符比较不相等时,next[1]=0
,表示模式串应该右移一位,主串当前指针后移一位,再和模式串的第一个字符进行比较max{k}
:当主串的第i
个字符与模式串的第j
个字符失配时,主串i
不回溯,则假定模式串的第k
个字符与主串的第i
个字符比较,k
应满足条件 1 < k < j 且 ′ p 1 . . . p k − 1 ′ = ′ p j − k + 1 . . . p j − 1 ′ 1nextval[]
,如果出现了 p j = p n e x t [ j ] p_j=p_{next[j]} pj=pnext[j],则将next[j]
修正为next[ next[j] ]
,直到两者不相等
祖先、子孙、双亲、孩子、兄弟
结点的度、树的度
分支结点、叶子节点
节点的深度、高度、层次
有序树和无序树
路径和路径长度
森林
每个节点至多只有两棵子树,并且二叉树的子树有左右之分,不能颠倒
i
为分支结点,否则为叶子结点i
的左孩子编号为 2 i 2i 2i,否则无左孩子i
的右孩子编号为 2 i + 1 2i+1 2i+1,否则无右孩子i
所在的层次为 ⌊ l o g 2 i ⌋ + 1 \lfloor log_2i \rfloor +1 ⌊log2i⌋+1将完全二叉树上编号为i
的结点元素存储在一维数组下标为i-1
的分量中
注意:这种存储结构建议从数组下标1开始存储树中的结点,若从数组下标0开始存储,则计算其孩子结点时与之前描述的计算公式不一致,在书写程序时需要注意。
lchild |
data |
rchild |
---|
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *lchild,*rchild;//左、右孩子指针
}BiTNode,*BiTree;
n个结点的二叉链表中,含有n+1
个空链域
二叉树的遍历是按照某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。共有先序遍历(NLR)、中序(LNR)、后续(LRN)三中遍历方法
void PreOrder(BiTree T){//PreOrder-先序、InOrder-中序、PostOrder-后序
if(T!=NULL){
visit(T);//访问根结点
PreOrder(T->lchild);//遍历访问左子树
PreOrder(T->rchlid);//遍历访问右子树
}
}
void PreOrder2(BiTree T){
InitStack(S);BiTree p=T;
while(p||!IsEmpty(S)){
if(p){
visit(p);Push(S,p);
p = p->lchild;
}
else{
Pop(S,p);
p = p->rchild;
}
}
}
void Inorder2(BiTree T){
InitStack(S);BiTree p = T;//初始化S,p是遍历指针
while(p||!IsEmpty(S)){
if(p){//一路向左
Push(S,p);//当前结点入栈
p = p->lchild;//左孩子不空,一直往左走
}
else{//出栈,并转向出栈结点的左子树
Pop(S,p);visit(p);//栈顶元素出栈,访问出栈结点
p = p->rchild;//向右子树走
}
}
}
void PostOrder(BiTree T){
InitStack(S);
P = T;
r = NULL;
while(p||!IsEmpty(S)){
if(p){ //走到最左边
push(S,p);
p = p->lchild;
}
else{ //向右
GetTop(S,p); //读栈顶结点(非出栈)
if(p->rchild&&r->rchild!=r) //若右子树存在,且未被访问过
p = p->rchild; //转向右
else{ //否则,弹出结点并访问
pop(S,p); //将结点弹出
cisit(p->data); //访问该结点
r = p; //记录最近访问过的结点
p = NULL; //结点访问完,重置p指针
}
}//else
}//while
}
void LevelOrder(BiTree T){
InitQueue(Q);
BiTree p;
EnQueue(Q,T);//将根结点入队
while(!IsEmpty(Q)){
DeQueue(Q,p);
visit(p);
if(p->lchild!=NULL)EnQueue(Q,p->lchild);
if(p->rchild!=NULL)EnQueue(Q,p->rchild);
}
}
由二叉树中序遍历结果和前序、后序、层次中的一个组合,就可唯一确定一棵二叉树
在含n
个结点的二叉树中,有n+1
个空指针。引入线索二叉树正是为了加快查找结点前驱和后继的速度。
规定:若无左子树,令lchild
指向其前驱结点;若无右子树,令rchild
指向其后继结点
lchild |
ltag |
data |
rtag |
rchild |
---|
l c h i l d = { 0 , l c h i l d 域 指 示 结 点 的 左 孩 子 1 , l c h i l d 域 指 示 结 点 的 前 驱 r c h i l d = { 0 , r c h i l d 域 指 示 结 点 的 右 孩 子 1 , r c h i l d 域 指 示 结 点 的 后 继 lchild= \begin{cases} 0,&lchild域指示结点的左孩子\\ 1,&lchild域指示结点的前驱 \end{cases}\\ rchild= \begin{cases} 0,&rchild域指示结点的右孩子\\ 1,&rchild域指示结点的后继 \end{cases} lchild={0,1,lchild域指示结点的左孩子lchild域指示结点的前驱rchild={0,1,rchild域指示结点的右孩子rchild域指示结点的后继
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
线索化的实质就是遍历一次二叉树
在对其进行遍历时,只要先找到序列中的第一个节点,然后依次找结点的后继,直至后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志为“1”,则右链为线索,指示其后继,否则遍历右子树中第一个访问的结点为其后继。
后序线索二叉树上找后继时需要知道结点双亲,即需采用带标志域的三叉链表作为存储结构。
采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。
根节点的下标为0,其伪指针域为-1
该存储结构利用了每个结点只有唯一双亲对的性质,可以很快的得到每个结点的双亲结点,但求结点的孩子需要遍历整个结构。
data |
parent_pos |
---|
将每个结点的孩子结点都用单链表链接起来形成一个线性结构
这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历n个结点中孩子结点指针域指向的n个孩子链表
又称二叉树表示法。孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针。
data |
firstchild |
nextsibling |
---|
树转换为二叉树
规则:左孩子右兄弟。每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟
画法:1.在兄弟结点之间加一连线;2.对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;3.以树为轴心,顺时针旋转45°
特点:根无右子树
森林转换二叉树
二叉树转换为森林
二叉排序树的定义
二叉排序树的查找、插入、构造
二叉排序树的剔除
z
是叶子结点,则直接删除z
只有一棵左子树或右子树,则让z
的子树成为z
父节点的子树,替代z
的位置z
有左、右两棵子树,则令z
的直接后继替代z
,然后从二叉排序树中删去这个直接后继,这样就转换成了上面的两种情况二叉排序树的查找效率分析
定义
二叉排序树的插入
情况 | 具体 |
---|---|
LL平衡旋转(右单旋转) | |
RR平衡旋转(左单旋转) | |
LR平衡旋转(先左后右双旋转) | |
RL平衡旋转(先右后左双旋转) |
带权路径长度: W P L = ∑ i = 1 n w i l i WPL=\sum_{i=1}^nw_il_i WPL=∑i=1nwili
含有n
个带权结点的二叉树中,带权路径长度WPL
最小的二叉树称为哈夫曼树
将n个结点分别作为n
棵仅含有一个结点的二叉树,构成森林F
构造一个新结点,从F
中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
从F
中删除刚才选出的两棵树,同时将新得到的树加入F
中
重复2-3步骤
n-1
个结点,哈夫曼树中结点总数为2n-1
1
的节点固定长度编码:对每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
前缀编码:没有一个编码是另一个编码的前缀
使用哈夫曼树得到哈夫曼编码:默认为左边为0,右边为1(不唯一,没明确规定)
图 G G G由顶点集 V V V和边集 E E E组成,记为 G = ( V , E ) G=(V,E) G=(V,E),其中 V ( G ) V(G) V(G)表示图 G G G中顶点的有限非空集; E ( G ) E(G) E(G)表示图 G G G中顶点之间的关系集合。 V = { v 1 , v 2 , . . . , v n } V=\{v_1,v_2,...,v_n\} V={v1,v2,...,vn}, ∣ V ∣ |V| ∣V∣表示顶点个数, E = { ( u , v ) ∣ u ∈ V , v ∈ V } E=\{(u,v)|u \in V,v\in V\} E={(u,v)∣u∈V,v∈V}, ∣ E ∣ |E| ∣E∣表示图 G G G中边的条数
顶点表结点
边表的头指针和顶点的数据信息采用顺序存储
顶点域 | 边表头指针 |
---|---|
data |
firstarc |
边表结点
对每个顶点 v i v_i vi建立一个单链表,第 i i i个单链表中的结点表示依附于顶点 v i v_i vi的边,这个单链表为顶点 v i v_i vi的边表
邻接点域 | 指针域 |
---|---|
adjvex |
nextarc |
尾域 | 头域 | 链域->弧头相同的下一条弧 | 链域->弧尾相同的下一条弧 | 弧信息 |
---|---|---|---|---|
tailvex |
headvex |
hlink |
tlink |
info |
数据 | 第一条出弧 | 第一条入弧 |
---|---|---|
data |
firstin |
firstout |
标志域 | 依附的结点 | 下一条依附于ivex 的边 |
依附的结点 | 下一条依附于jvex 的边 |
边信息 |
---|---|---|---|---|---|
mark |
ivex |
ilink |
jvex |
jlink |
info |
数据 | 第一条依附于该顶点的边 |
---|---|
data |
firstedge |
Adjacent(G,x,y):判断图G是否存在边<x,y>
Neighbors(G,x):列出图G中与结点x邻接对的边
InsertVertex(G,x):在图G中插入顶点x
DeleteVertex(G,x):从图G中删除顶点x
AddEdge(G,x,y):若边<x,y>不存在,则向图G中添加该边
RemoveEdge(G,x,y):若边<x,y>存在,则从图G中删除该边
FirstNeighbor(G,x,y):求图G中顶点x的第一个邻接点,若有则返回顶点号。否则返回-1
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y外顶点x的下一个邻接点的顶点号,若没有返回-1
Get_edge_value(G,x,y):获取图G中边<x,y>的权值
Set_edge_value(G,x,y,v):设置图G中边<x,y>对应的权值为v
Breadth-Frist-Search,BFS
基本思想
首先访问起始顶点 v v v,接着由 v v v出发,依次访问v的各个未访问过的邻接顶点 w 1 , w 2 , . . . , w i w_1,w_2,...,w_i w1,w2,...,wi,然后一次访问 w 1 , w 2 , . . . , w i w_1,w_2,...,w_i w1,w2,...,wi的所有未被访问的邻接顶点。
换句话说,BFS
是以v
为起始点,由近及远依次访问和v
有路径相通且路径长度为1,2,…的顶点
bool visited[MAX_VERTEX_NUM];//访问标记数组
void BFSTraverse(Graph G){ //对图G进行广度优先遍历
for(i=0;i<G.vexnum;i++)
visited[i] = FALSE;
InitQueue(Q);
for(i=0;i<G.vexnum;++i)
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i);
}
void BFS(Graph G,int v){//从顶点v出发,广度优先遍历图G
visit(v);
visited[v] = TRUE;
EnQueue(Q,v);
while(!isEmpty(Q)){
DeQueue(Q,v);
for(w=FirstNeighbor(G,v);w>=0;w=Neighbor(G,v,w))
if(!visited[w]){
visit(w);
visited[w] = TRUE;
EnQueue(Q,w);
}
}
}
Depth-First-Search,DFS
基本思想
首先访问图中某一起始顶点 v v v,然后由 v v v出发,访问与 v v v邻接且未被访问的任一顶点 w 1 w_1 w1,再访问与 w 1 w_1 w1邻接且未被访问的任一顶点,重复上述过程
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G){
for(v=0;v<G.vexnum;++v)
visited[v] = FALSE;
for(v=0;v<G.vexnum;++v)
if(!visited[v])
DFS(G,v);
}
void DFS(Graph G,int v){
visit(v);
visited[v] = TRUE;
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
if(!visited[w]){
DFS(G,w);
}
}
性能 | 广度优先搜索/深度优先搜索 |
---|---|
空间复杂度 | $O( |
时间复杂度-邻接矩阵 | $O( |
时间复杂度-邻接表 | $O( |
生成树 | 广度深度优先生成树,邻接表不唯一,邻接矩阵唯一 |
求一个带权连通图的最小生成树Minimum-Spanning-Tree,MST
基本思想
初始时从图中任取一顶点加入树T,此时树中只含有一个顶点
之后选择一个与当前T中顶点集合距离最近的顶点,且加入后不能出现环,并将该顶点和相应的边加入T,每次操作后T中的顶点数和边数都增1。
重复直到加满
时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2 ) O(∣V∣2)
辅助数组
S
:记录以求得的最短路径的顶点dist[]
:记录从源点 v 0 v_0 v0到其他各顶点当前的最短路径长度path[]
:path[i]
表示从源点到顶点i之间的最短路径的前驱结点。可用于回溯找最短路径算法步骤
初始化:集合S
初始化为{0}
,dist[]
的初始值dist[i]=arcs[0][i]
从顶点集合V-S
中选出 v j v_j vj,满足 d i s t [ j ] = M i n { d i s t [ i ] ∣ v i ∈ V − S } dist[j]=Min\{dist[i] \ |v_i \in V-S\} dist[j]=Min{dist[i] ∣vi∈V−S},令 S = S ∪ { j } S=S\cup\{j\} S=S∪{j}
根据公式修改从 v 0 v_0 v0出发到集合V-S上任一顶点 v k v_k vk可达的最短路径长度
若dist[j]+arcs[j][k]
重复步骤2-3操作共n-1
次
时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
算法描述
定义一个n阶方阵 A ( − 1 ) , A ( 0 ) , . . . , A ( n − 1 ) A^{(-1)},A^{(0)},...,A^{(n-1)} A(−1),A(0),...,A(n−1),其中 A ( − 1 ) [ i ] [ j ] = a r c s [ i ] [ j ] A^{(-1)}[i][j]=arcs[i][j] A(−1)[i][j]=arcs[i][j],
根据递推公式重复n
次,计算出 A ( 0 ) , . . . , A ( n − 1 ) A^{(0)},...,A^{(n-1)} A(0),...,A(n−1)
A ( k ) [ i ] [ j ] = M i n { A ( k − 1 ) [ i ] [ j ] , A ( k − 1 ) [ i ] [ k ] + A ( k − 1 ) [ k ] [ j ] } , k = 0 , 1 , . . , n − 1 A^{(k)}[i][j]=Min\{A^{(k-1)}[i][j],A^{(k-1)}[i][k]+A^{(k-1)}[k][j]\},k=0,1,..,n-1 A(k)[i][j]=Min{A(k−1)[i][j],A(k−1)[i][k]+A(k−1)[k][j]},k=0,1,..,n−1
其中, A ( 0 ) [ i ] [ j ] A^{(0)}[i][j] A(0)[i][j]是从顶点 v i v_i vi到 v j v_j vj、中间路径是 v 0 v_0 v0的最短路径的长度, A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j]是从顶点 v i v_i vi到 v j v_j vj、中间顶点的序号不大于k的最短路径的长度
时间复杂度 O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)
有向无环图DAG
有向无环图是描述含有公共子式的表达式的有效工具,可实现对相同子式的共享,从而节省存储空间
AOV网
:若用AVG
表示一个工程,其顶点表示活动,用有向边 < V i , V j >
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序
A
在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径拓扑排序算法
AOV
网中选择一个没有前驱的顶点并输出AOV
网为空时间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
逆拓扑排序
AOV
网中选择一个没有后继的顶点并输出AOV
为空在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销,称之为用边表示活动的网络,简称AOE网
AOE网
中仅有一个入度为0的顶点,称为开始顶点(源点);只存在一个出度为0的顶点,称之为结束顶点(汇点)
具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动
关键路径并不唯一,只提高其中一条关键路径上的关键活动速度不能缩短整个工程的工期。
计算参数
事件 v k v_k vk的最早发生时间 v e ( k ) ve(k) ve(k)
v e ( k ) = M a x { v e ( j ) + W e i g h t ( v j , v k } ve(k)=Max\{ve(j)+Weight(v_j,v_k\} ve(k)=Max{ve(j)+Weight(vj,vk}
v l ( 汇 点 ) = v e ( 汇 点 ) vl(汇点)=ve(汇点) vl(汇点)=ve(汇点)
活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)
一个活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)和其最早开始时间 e ( i ) e(i) e(i)的差额 d ( i ) = l ( i ) − e ( i ) d(i)=l(i)-e(i) d(i)=l(i)−e(i)
typedef struct{
ElemType *elem;
int TableLen;
}SSTable;
int Search_Seq(SSTable ST,ElemType key){
ST.elem[0] = key; //哨兵
for(i=ST>TableLen;ST.elem[i]!=key;--i); //从后往前找
return i;//若表中不存在关键字为key的元素,将查找到i为0时退出循环
}
折半查找又称二分查找,仅适用于有序的顺序表
仅适用于顺序存储结构,不适用于链式存储结构
生成的判定树
基本思想
首先将给定的key
与表的中间位置的关键字比较,成功后返回;否则根据key
与关键字的大小判断查找左边还是右边
折半查找整个算法中,关于mid的取值向上/向下需要统一
int Binary_Search(SeqList L,ElemType key){
int low=0,high=L.TableLen-1,mid;
while(low<=high){
mid = (low+high)/2; //取中间位置
if(L.elem[mid]==key)
return mid;
else if(L.elem[mid]>key)
high = mid-1;//从前半部分继续查找
else
low = mid+1;//从后半部分继续查找
}
return -1; //查找失败,返回-1
}
又称索引顺序查找,它吸取了顺序查找和折半查找各自的优点,既有动态结构,又适合快速查找
基本思想
将查找表分成若干块。块内的元素可以是无序的。但块之间按照每个块的最大关键字进行排序
建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块的第一个元素的地址,索引表按关键字有序排列
分块查找的过程
索引查找的平均查找长度 L I L_I LI,块内查找的平均查找长度 L S L_S LS
查找算法 | A S L 成 功 ASL_{成功} ASL成功 | A S L 失 败 ASL_{失败} ASL失败 |
---|---|---|
顺序查找-无序表 | n + 1 2 \frac{n+1}{2} 2n+1 | n + 1 n+1 n+1 |
顺序查找-有序表 | n + 1 2 \frac{n+1}{2} 2n+1 | n 2 + n n + 1 \frac n2+\frac n{n+1} 2n+n+1n |
折半查找 | s u m ( 圆 形 结 点 ∗ 对 应 层 数 ) / n sum(圆形结点*对应层数)/n sum(圆形结点∗对应层数)/n | s u m ( 方 结 点 ∗ 对 应 层 数 − 1 ) / ( n + 1 ) sum(方结点*对应层数-1)/(n+1) sum(方结点∗对应层数−1)/(n+1) |
分块查找 | A S L = L I + L S ASL=L_I+L_S ASL=LI+LS |
B树又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m
表示。
n | P 0 P_0 P0 | K 1 K_1 K1 | P 1 P_1 P1 | K 2 K_2 K2 | P 2 P_2 P2 | . . . ... ... | K n K_n Kn | P n P_n Pn |
---|
其中, K i K_i Ki为结点的关键字, P i P_i Pi为指向子树根结点的指针,且指针 P k − 1 P_{k-1} Pk−1所指子树中所有结点的关键字均小于 K i K_i Ki, P i P_i Pi所指子树中所有结点的关键字均大于 K i K_i Ki
结点的孩子个数等于该节点中关键字个数加1
B树是所有结点的平衡因子都等于0的多路平衡查找树
B树的高度不包括最后的外部结点那一层
log m ( n + 1 ) ≤ h ≤ log ⌈ m / 2 ⌉ ( ( n + 1 ) / 2 ) + 1 \log_m(n+1) \leq h \leq \log_{\lceil m/2\rceil}((n+1)/2)+1 logm(n+1)≤h≤log⌈m/2⌉((n+1)/2)+1
m-1
,必须进行分裂
当被删关键字k
不在终端结点时,可以用k的前驱或后继k'
替代k
,然后在相应的结点中删除k'
。关键字k'
必定落在某个终端节点中,则转换成了被删关键字在终端结点中的情形
当被删关键字k
在终端结点中时
直接删除关键字:若被删除关键字所在结点的关键字个数 > ⌈ m / 2 ⌉ >\lceil m/2 \rceil >⌈m/2⌉,表明删除该关键字后仍满足B树的定义,则直接是删除该关键字
兄弟够借:若被删除关键字所在结点的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil-1 =⌈m/2⌉−1,且与此节点相邻的右(左)兄弟节点的关键字个数 ≥ ⌈ m / 2 ⌉ \geq \lceil m/2 \rceil ≥⌈m/2⌉,则需要调整该节点、右(左)兄弟结点及其双亲结点(父子换位法),以达到新的平衡
兄弟不够借:若被删除关键字所在结点的关键字个数 = ⌈ m / 2 ⌉ − 1 =\lceil m/2 \rceil-1 =⌈m/2⌉−1,且与此节点相邻的右(左)兄弟节点的关键字个数均$ \lceil m/2 \rceil-1$,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少至0,则直接将根结点删除,合并后的新结点成为根;若其双亲结点不是根结点,且关键字减少超过下限,在继续合并操作。
在B+树中查找时,非叶结点上的关键字值等于查找值时并不停止,而是继续往下找,直到叶结点上的该关键字为止。无论成功与否,每次查找都是一条从根结点到叶结点的路径
B树 | B+树 | |
---|---|---|
关键字个数为n的结点的子树个数 | n-1 | n |
结点关键字个数n范围 | ⌈ m / 2 ⌉ ≤ n ≤ m \lceil m/2 \rceil \leq n \leq m ⌈m/2⌉≤n≤m | ⌈ m / 2 ⌉ − 1 ≤ n ≤ m − 1 \lceil m/2 \rceil -1\leq n \leq m -1 ⌈m/2⌉−1≤n≤m−1 |
叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址 | ||
叶结点包含的关键字和其他结点包含的关键字是不重复的 | 叶节点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中 |
Hash(key)=Addr
常见构造函数 | 公式 | 评价 |
---|---|---|
直接定地法 | H ( k e y ) = k e y H(key)=key H(key)=key或 H ( k e y ) = a × k e y + b H(key)=a \times key+b H(key)=a×key+b | 最简单,不会产生冲突。适合关键字的分布基本连续的情况 |
除留余数法 | H ( k e y ) = k e y H(key)=key%p H(key)=key, p p p为不大于散列表表长 m m m但最接近或等于 m m m的质数 | |
数字分析法 | 设关键字是r进制数,选取数码分布比较均匀的若干位作为散列地址 | 适合于一直的关键字集合,若更换了关键字,则需要重新构造新的散列函数 |
平方取中法 | 取关键字对的平方值的中间几位作为散列值 | 适用于关键字的每位取值都不均匀或均小于散列地址所需的位数 |
开放定址法 | d i d_i di | 补充说明 |
---|---|---|
线性探测法 | 0 , 1 , 2 , . . . , m − 1 0,1,2,...,m-1 0,1,2,...,m−1 | 可能出现大量元素在相邻地址上聚集,降低查找效率 |
平方探测法 | 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 0^2,1^2,-1^2,2^2,-2^2,...,k^2,-k^2 02,12,−12,22,−22,...,k2,−k2 | 散列表长度m必须是一个可以表示成4k+3 的素数 |
再散列法 | i ∗ H a s h 2 ( k e y ) i*Hash_2(key) i∗Hash2(key) | i i i是冲突的次数 |
伪随机序列法 | d i = d_i= di=随机序列 |
拉链法
把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识
Addr=Hash(key)
Addr
的位置上是否有记录,若无记录,返回查找失败;若有记录。比较它与key的值,若相等,则返回查找成功的标志,否则执行步骤3Addr
置为此地址,转入步骤2散列表的查找效率取决于散列函数、处理冲突的方法和装填因子
基本思想:每次讲一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成
要将L(i)
插入已有序的子序列L[1...i-1]
,需要执行以下操作
查找出L(i)
在L[1...i-1]
中的插入位置k
将L[k...i-1]
中的所有元素依次后移一个位置
将L(i)
复制到L(k)
void InsertSort(ElemType A[],int n){
int i,j;
for(i=2;i<=n;i++) //依次将A[2]...A[n]插入到前面已排序的序列
if(A[i]<A[i-1]){ //若A[i]小于前驱,则将其插入前面的有序表
A[0] = A[i]; //哨兵
for(j=i-1;A[0]<A[j];--j) //从i-1开始比较,比较一次,向后移动一次
A[j+1] = A[j];
A[j+1] = A[0]; //找到插入位置,赋值
}
}
void InsertSort(ElemType A[],int n){
int i,j,low,high,mid;
for(i=2;i<=n;i++){ //依次将A[2]...A[n]插入到前面已排序的序列
A[0] = A[i]; //暂存单元,不是哨兵
low = 1;high = i-1;
while(low<=high){ //折半查找
mid = (low+high)/2;
if(A[min]>A[0]) high = mid-1;
else low = mid+1;
}
for(j=i-1;j>=high+1;--j) //统一后移元素
A[j+1] = A[j];
A[high+1] = A[0]; //赋值
}
}
基本思想:先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd]
的特殊子表,即把相隔某个“增量”的记录组成一个子表,对每个子表分别进行直接插入排序,当整个表中的元素已呈“毕本有序”时,再对全体记录进行一次直接插入排序
过程
增量序列: d 1 = n / 2 , d i + 1 = ⌊ d i / 2 ⌋ d_1=n/2,d_{i+1}=\lfloor d_i/2 \rfloor d1=n/2,di+1=⌊di/2⌋,最后一个增量等于1
void ShellSort(ElemType A[],int n){
//A[0]只是暂存单元,不是哨兵
for(dk=n/2;dk>=1;dk=dk/2) //步长变换
for(i=dk+1;i<=n;i++) //对d_i个组进行直接插入排序
if(A[i]<A[i-dk]){ //需要将A[i]插入所在的有序子表中
A[0] = A[i]; //暂存A[i]
for(j=i-dk;j>0&&A[0]<A[j];j-=dk)//寻找插入位置
A[j+dk] = A[j]; //记录后移
A[j+dk] = A[0]; //插入
}
}
基本思想:从后往前(或从前往后)两两比较相邻元素的值,若为逆序则交换,指导序列比较完。
void BubbleSort(ElemType A[],int n){
for(i=0;i<n-1;i++){
flag = false; //表示本趟冒泡是否发生交换的标志
for(j=n-1;j>i;j--) //一趟冒泡过程
if(A[j-1]>A[j]){ //若为逆序
swap(A[j-1],A[j]); //交换
flag = true;
}
if(flag==false)
return; //本趟遍历后没有发生交换,说明表已经有序
}
}
注意:冒泡排序所产生的有序子序列是全局有序的。每一趟排序都会将一个元素放置到其最终的位置上
基本思想:在待排序表L[1...n]
中任取一个元素pivot
作为枢轴,通过一趟排序将待排序表划分为独立的两个部分L[1...k-1]
和L[k+1...n]
,使得L[1...k-1]
中的所有元素小于pivot
,L[k+1...n]
中的所有元素大于等于pivot
,则pivot
放在了其最终位置L(k)
上,这个过程称为一趟快速排序。然后分别对左右两部分重复上述过程,直到每个部分只有一个元素。
void QuickSort(ElemType A[],int low,int high){
if(low<high){
//Partition()就是划分操作,将表划分成满足条件的两个子表
int pivotpos=Partition(A,low,high); //划分
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivots+1,high);
}
}
int Partition(ElemType A[],int low,int high){ //一趟划分
ElemType pivot=A[low];
while(low<high){
while(low<high&&A[high]>=pivot) --high;
A[low] = A[high]; //将比枢轴小的元素移动到左边
while(low<high&&A[low]<=pivot) ++low;
A[high] = A[low]; //将比枢轴大的元素移动到右边
}
A[low] = pivot; //枢轴放到最终位置
reutrn low;
}
快速排序是所有内部排序算法中平均性能最优的排序算法
基本思想:假设排序表为L[1...n]
,第i
趟排序即从L[i...n]
中选择关键字最小的元素与L(i)
交换,每一趟排序可以确定一个元素的最终位置,经过n-1趟排序可以使整个排序表有序
void SelectSort(ElemType A[],int n){
for(i=0;i<n-1;i++){ //一共进行n-1趟
min = i; //记录最小元素位置
for(j=i+1;j<n;j++) //在A[1...n-1]中选择最小的元素
if(A[j]<A[min]) min = j; //更新最小的元素
if(min!=j) swap(A[i],A[min]); //封装的swap()函数共移动3次
}
}
大根堆:L(i)>=L(2i) & L(i)>=L(2i+1)
,最大元素在根结点
小根堆:L(i)<=L(2i) & L(i)<=L(2i+1)
,最小元素在根结点
堆的插入:把新结点放到堆的末端,后进行向上调整
构造初始堆:
输出堆顶元素,重新构建堆,重复这一过程
void BuildMaxHead(ElemType A[],int len){
for(int i=len/2;i>0;i--) //从i=n/2开始,反复调整堆
HeadAdjust(A,i,len);
}
void HeadAdjust(ElemType A[],int k,int len){
//将元素k为根的子树进行调整
A[0] = A[k]; //A[0]暂存子树的根节点
for(i=2*k;i<=len;i*2){ //沿key较大的子节点向下筛选
if(i<len&&A[i]<A[i+1])i++; //取i为较大子结点
if(A[0]>=A[i])break; //筛选结束
else{
A[k]=A[i]; //将A[i]调整到双亲结点上
k=i; //修改k值,继续向下筛选
}
}
A[k] = A[0]; //被筛选结点的值放入最终位置
}
void HeapSort(ElemType A[],int len){
BuildMAxHeap(A,len);
for(i=len;i>1;i--){ //n-1趟的交换和建堆过程
Swap(A[i],A[1]); //输出堆顶元素,和堆底元素交换
HeadAdjust(A,1,i-1);//调整,把剩余的i-1个元素整理成堆
}
}
假定待排序表含有n个记录,则可将其视为n个有序的子表,每个子表的长度为1,然后两两合并,得到 ⌈ n / 2 ⌉ \lceil n/2 \rceil ⌈n/2⌉个长度为2或1的有序表,继续两两合并。这种排序方法称为2路归并排序。
一趟归并排序的操作是,调用 ⌈ n / 2 h ⌉ \lceil n/2h \rceil ⌈n/2h⌉次算法merge()
,将L[1...n]
中前后相邻且长度为h的有序段进行两两归并,得到前后相邻、长度为2h的有序段进行两两归并,得到前后相邻、长度为2h的有序段,整个归并排序需要进行 ⌈ l o g 2 n ⌉ \lceil log_2n\rceil ⌈log2n⌉趟
ELemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType)); //辅助数组B
void Merge(ElemType A[],int low,int mid,int high){
for(int k=low;k<=high;k++)B[k] = A[k]; //将A中元素放到B中
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j])A[k] = B[i++]; //将较小值赋值到A中
else A[k] = B[j++];
}
while(i<=mid) A[k++] = B[i++]; //若一个表未检测完,赋值
while(j<=high) A[k++] = B[j++]; //若第二个表未检测完,赋值
}
void MergeSort(ElemType A[],int low,int high){
if(low<high){
int mid = (low+high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high); //归并
}
}
MSD
:将关键字位权重递减一次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列LSD
:将关键字权重递增一次进行排序,最后形成一个有序序列排序过程:
在排序中,使用r个队列 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr−1
对 i = 0 , 1 , . . . , d − 1 i=0,1,...,d-1 i=0,1,...,d−1,依次做一次分配和收集,每个关键字结点 a j a_j aj由d元组组成
分配:开始时,把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr−1各个队列置成空队列,然后依次考察线性表中的每个结点 a j a_j aj,若 a j a_j aj的关键字 k j i = k k_j^i=k kji=k,就把 a j a_j aj放进 Q k Q_k Qk队列中
收集:把 Q 0 , Q 1 , . . . , Q r − 1 Q_0,Q_1,...,Q_{r-1} Q0,Q1,...,Qr−1各个队列中的结点依次首尾相连,得到新的结点序列,从而组成新的线性表
算法种类 | 时间复杂度-最好 | 时间复杂度-平均 | 时间复杂度-最坏 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
直接插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 是 |
冒泡排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 是 |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 否 |
希尔排序 | O ( 1 ) O(1) O(1) | 否 | |||
快速排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( log 2 n ) O(\log_2n) O(log2n) | 否 |
堆排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( 1 ) O(1) O(1) | 否 |
2路归并排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n ) O(n) O(n) | 是 |
基数排序 | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) | O ( r ) O(r) O(r) | 是 |
在许多应用中,经常需要对大文件进行排序,因为文件中的记录很多、信息量庞大,无法将整个文件复制进内存中进行排序。因此,需要将待排序的记录存储在外存之上,排序时再把数据一部分一部分地调进内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。这种排序方法就称为外部排序
外部排序的总时间=内部排序所需时间+外存信息读写的时间+内部归并所需的时间
r
个初始段归并,做k
路平衡归并,归并树可用严格k叉树来表示,树的高度= ⌈ log k r ⌉ \lceil \log_kr\rceil ⌈logkr⌉=归并趟数S做内部归并时,在k个元素中选择关键字最小的记录需要比较k-1次,S趟归并总需的比较次数是 S ( n − 1 ) ( k − 1 ) = ⌈ log k r ⌉ ( n − 1 ) ( k − 1 ) = ⌈ log 2 r ⌉ ( n − 1 ) ( k − 1 ) ⌈ log 2 k ⌉ S(n-1)(k-1)=\lceil \log_kr\rceil(n-1)(k-1)=\lceil \log_2r \rceil (n-1)(k-1)\lceil \log_2k \rceil S(n−1)(k−1)=⌈logkr⌉(n−1)(k−1)=⌈log2r⌉(n−1)(k−1)⌈log2k⌉
引入败者树后,在k个元素中选择关键字最小的记录需要比较 ⌈ log 2 k ⌉ \lceil \log_2k \rceil ⌈log2k⌉次,内部归并的比较次数与k无关。因此只要内存允许,增大归并路数k将有效减少归并树的高度,提高外部排序饿的速度
败者树
初始待排文件FI
,初始归并段输出文件为FO
,内存工作区为WA
,FO
与WA
的初始状态为空,WA
可容纳 w w w个记录
FI
输入w个记录到工作区WA
WA
中选出其中关键字取最小值的记录,记为MINIMAX
MINIMAX
就输出到FO
中去FI
不为空,则从FI输入下一个记录到WA
中WA
中鄋关键字比MINIMAX
记录的关键字大的记录中选出最小关键字记录,作为新的MINIMAX
3-5
,直至在WA
中选不出新的MINIMAX
记录为止,由此得到一个初始归并段,输出一个归并段的结束标志至FO
中去2-6
,直至WA
为空,由此得到全部初始归并段把归并段的长度作为权值,进行严格k叉树的哈夫曼树思想,构造最佳归并树