数据结构复习笔记

前言

最近马上要到数据结构的期末考试了,就自己总结了下考试的知识点,知识点会有点侧重于期考的知识点以致于没有那么全面和深入,不过是回顾一下所学的知识点罢了。由于是一学期的知识点,虽然不全面但是也是比较繁杂,如有纰漏错误烦请指出。
之后有时间了可能会视情况补充完善。
(目前排序部分不太完善)

数据

数据的逻辑结构和物理结构

数据结构的三要素:逻辑结构、存储结构、数据的运算
数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,二是算法的实现依赖于所采用的存储结构。

逻辑结构

逻辑结构分为四种类型:集合结构,线性结构,树形结构,图形结构。

1、集合结构:集合结构的集合中任何两个数据元素之间都没有逻辑关系,组织形式松散。
2、线性结构:数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。
3、树状结构:树状结构是一个或多个节点的有限集合。
4、网络结构:网络结构是指通信系统的整体设计,它为网络硬件、软件、协议、存取控制和拓扑提供标准。

物理结构(存储结构)

物理结构就是内存的存储方式,分为以下几种:

(1)顺序存储结构:是把数据元素存放在地址连续存储单元里,其数据间的逻辑关系和物理关系是一致的,
其优点是可以实现随机存取,每个元素占用最少的存储空间,缺点是只能使用相邻的一整块存储空间,因此可能产生较多的外部碎片
(2)链式存储结构:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储位置的指针来表示元素之间的逻辑关系。
> 其优点是不会出现碎片现象,能充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。

数据的逻辑结构独立于其存储结构
数据的逻辑结构是从面向实际问题的角度出发的,只采用抽象表达方式,独立于存储结构,数据的存储结构有多种不同的选择;而数据的存储结构是逻辑结构在计算机上的映射,它不能独立于逻辑结构而存在。数据结构包括三个要素,缺一不可。

算法特性和时间复杂度

算法的五大特性

算法的特性:

输入: 算法具有0个或多个输入
输出: 算法至少有1个或多个输出
有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步 骤可以在可接受的时间内完成
确定性:算法中的每一步都有确定的含义,不会出现二义性
可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成

算法的时间复杂度

时间频度

一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)

时间复杂度

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称时间复杂度

常见数量阶
常见数量阶
计算时间复杂度

1.基本操作,即只有常数项,认为其时间复杂度为O(1)
2.顺序结构,时间复杂度按加法进行计算
3.循环结构,时间复杂度按乘法进行计算
4.分支结构,时间复杂度取最大值 判断一个算法的效率时,往往只需要关注操作数量的最高次项,其它次要项和常数项可以忽略
5.在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度

空间复杂度本处省略,下图来自《王道考研数据结构》
空间复杂度

线性表

线性表两种存储结构

list

线性表的特点:

表中元素个数有限。 表中元素具有逻辑上的顺序性,表中元素有其先后次序。 表中元素都是数据元素,每个元素都是单个元素。
表中元素的数据类型都相同,这意味着每个元素占用相同大小的存储空间。
表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。

注意:线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要将其混淆

InitList(&L):初始化表。构造一个空的线性表
Length(L):求表长。返回线性表工的长度,即L中数据元素的个数。
LocateElem(L,e):按值查找操作。在表L中査找具有给定关键字值的元素。
GetElem(L,i):按位査找操作。获取表工中第1个位置的元素的值。
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e.
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值
PrintList(I):输出操作。按前后顺序输出线性表L的所有元素值
Empty(L):判空操作。若L为空表,则返回true,否则返回 false.
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

顺序表

#define LIST_INIT_SIZE 100  //顺序表存储空间的初始分配量
#define LISTINCREMENT 10    //顺序表存储空间的分配增量
typedef struct {
     ElemType *elem;   //存储空间的基地址
     int length;       //顺序表的当前长度
     int listsize;     //数组存储空间的长度
}SqList;
//初始化
Status InitList(SqList &L){
    L.elem=(ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    if(!L.elem) exit(OVERFLOW);
    L.length=0;
    L.listsize=LIST_INIT_SIZE;
    return OK;
}
//销毁
void DestroyList(SqList &L) {
  if(L.elem) free(L.elem);
  L.elem=NULL;
}
//清零
void ClearList(SqList &L)
{
       L.length=0;
}
//判空
Status ListEmpty(SqList L)
{
    if(L.length==0) return TRUE;
    else return FALSE;
}
//求表长
int ListLength(SqList L)
{
     return L.length;
}
//取值
Status GetElem(SqList L,int i,ElemType &e){
    if(i<1 || i>L.length) return ERROR;
    e=L.elem[i-1];       
    return OK;
}
//定位1
int LocateElem (SqList L,ElemType e)
{
    i=1;
    while( i<=L.length && L.elem[i-1]!=e)  i++;
    if(i<=L.length) return i;  
    else return 0;       
} 
	//定位2
	/*int LocateElem (SqList L, ElemType e, Status (*compare)(ElemType, ElemType))
	{
		i=1;  p=L.elem;
		while(i<=L.length && !(*compare)(*p++,e)) ++i;
		if(i<=L.length) return i;
		else return 0;
	} 
	//定义比较函数:
	Status GT(ElemType a,ElemType b){
		 if(a>b) return TRUE;
		 else return FALSE;
	}
	*/
//插入
Status ListInsert(SqList &L,int i,ElemType e){
    //插入元素e到顺序表L的第i个位置之前
    if(i<1 ||i>L.length+1) return ERROR;
    if(L.length==L.listize)
     {
	 L.elem=(ElemType *) realloc(L.elem, (L.listsize+LISTINCREMENT)*sizeof(ElemType) );
	if(!L.elem) exit(OVERFLOW);
	L.Listsize += LISTINCREMENT;
	 }//重新分配空间
    q=&L.elem[i-1];
    for(p=&(L.elem[L.length-1];p>=q;--p) 
         *(p+1)=*p;
    *q=e;
    ++L.length;
    return OK;
}
Status ListDelete(SqList &L,int i,ElemType &e){
    //删除顺序表L中的第i个元素,其值由e返回;
    if(i<1 ||i>L.length) return ERROR;
    p=&L.elem[i-1];
    e=*p;
    q=L.elem+L.length-1;
    for(++p;p<=q;++p) 
       *(p-1)=*p;
    --L.length;
    return OK;
}
Status ListTraverse(SqList L,Status (*visit)(ElemType))
{
  for(i=0;i<L.length;i++)
    if(!visit(L.elem[i])) return ERROR;
  return OK;
} 
//定义访问函数:
Status visit(ElemType e){
     printf(e);
     return OK;
}

顺序存储

链型表

任何两个元素的存储位置没有固定的联系 每个元素的存储位置只能由其直接前驱结点的指针指出
在单链表中获取第i个元素必须从头指针出发,沿指针链依次向后查找。因此,单链表是顺序存取的存储结构。

typedef struct LNode{
   ElemType data;       //数据域
   struct LNode *next;  //指针域
}LNode,*LinkList;

Status InitList(LinkList &L) {
    L=(LinkList)malloc(sizeof(LNode));
    if (!L) exit(OVERFLOW);
    L->next=NULL;
    return OK;
}

void DestroyList(LinkList &L) {
    while(L) {
	p=L->next;
	free(L);
	L=p;
    }
}

void ClearList(LinkList L) {
  for(p=L->next;p; p=L->next) {
	L->next=p->next;
	free(p);
  }
}
Status ListEmpty(LinkList L) {
    if( !L->next) return TRUE;
    else return FALSE;
}
int ListLength(LinkList L) {
    p=L->next; n=0;   
    while(p)
    { n++;	p=p->next; }
    return n;
}
Status GetElem(LinkList L, int i, ElemType &e) {
    j=1;
    p=L->next; 
    while(p &&j<i) { p=p->next; ++j;}
    if( !p || j>i) 
      return ERROR;   
    e=p->data ;
    return OK;
 } 
LinkList LocateElem(LinkList L,ElemType e,Status (*compare)(ElemType,ElemType)) {
    p=L->next;
    while( p && !compare(p->data,e) ) p=p->next;
    return p;
}
//定义比较函数:
Status LT(ElemType a,ElemType b) {
     if(a<b) return TRUE;
     else return FALSE;
}
//遍历
Status ListTraverse(LinkList L,Status (*visit)(ElemType)) {
    for( p=L->next; p; p=p->next )
      if( !visit(p->data) ) return ERROR;
    return OK;
}

链式存储

单循环链表、双循环链表和单链表遍历
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。
循环单链表

循环单链表遍历

双向循环链表前后均有结点
双向循环单链表

线性表分析

线性表分析

栈和队列

栈和队列

栈

使用栈存储数据元素,对数据元素的“存”和“取”有严格的规定:数据按一定的顺序存储到栈中,当需要调取栈中某数据元素时,需要将在该数据元素之后进栈的先出栈,该数据元素才能从栈中提取出来。
栈操作数据元素只有两种动作:
数据元素用栈的数据结构存储起来,称为“入栈”,也叫“压栈”。
数据元素由于某种原因需要从栈结构中提取出来,称为“出栈”,也叫“弹栈”。

栈是后进先出(LIFO)的线性表
ADT栈

Status InitStack(SqStack &S) {
//构造一个空栈,该栈由指针S指示 
  S.base=(SElemType*)malloc ((STACK_INIT_SIZE)*sizeof(SElemType));
    // 栈的连续空间分配
    //S.base=new SElemType[STACK_INIT_SIZE]; 等价
	if(!S.base)	exit(OVERFLOW);
	S.top=S.base; //空栈,初始化栈顶指针
	S.stacksize=STACK_INIT_SIZE; 
	return OK;
}//InitStack 

栈

队列

队列:只允许在表的一端进行插入,而在另一端进行删除的线性表。
队头:允许删除的一端
队尾:允许插入的一端
空队列:没有元素的队列
设队列Q=(a1,a2,a3,…an),
a1称为队头元素
an称为队尾元素
队列是先进先出(FIFO) 的线性表。

队列

ADT队列

串的基本操作

基本名词
字符串

ADT字符串

串的存储方式

顺序存储结构
---- 定长存储结构
---- 堆分配存储结构
链式存储结构
---- 块链结构

定长存储结构

特点:用长度固定的连续单元依次存储串值的字符序列
串长的表示方法
以下标为0的数组分量存放实际串长——PASCAL
串值后加一个不计入串长的结束标记字符——C、C++中用‘\0’作串的结束标记
用C语言实现第一种表示串长的串的定长存储结构

   #define MAXSTRLEN    255    //最大串长
   typedef unsigned char SString[MAXSTRLEN+1];

串截断现象:若串长超出MAXSTRLEN,则超出部分被舍去
串的定长存储表示下各基本操作的实现的实质:

字符串序列的复制
需求:T,S1,S2都是SSTring型的串变量,现要求将用T返回S1和S2联接的新串。
算法思想:串T值产生有2种情况:
S1[0]+S2[0]≤MAXSTRLEN:完整联接
S1[0]+S2[0]>MAXSTRLEN:超出最大串长部分被‘截断‘
截断现象在可能增加串长的操作中经常发生,给串相关操作的结果完整性带来很大隐患

堆分配存储结构

特点:采用动态字符数组存放串值,此时不必为数组预定义大小,以串长动态分配数组空间
用C语言实现串的堆分配存储

typedef struct {
   char *ch; 
   int  length;   
}HString;

串的堆分配存储表示下各基本操作的实现的实质:

字符串序列的复制
优点:
有顺序存储结构的特点
处理方便
操作中对串长没有限制

Status StrAssign(HString &T, char *chars){
    if(T.ch) free(T.ch);    //释放旧空间
    for(i=0,c=chars; *c; ++i,++c); //计算串常量chars的长度i      
    if(!i) {  T.ch=NULL; T.length=0;  }  //chars为空串
    else {
        if(!(T.ch=(char *)malloc(i*sizeof(char)))) exit OVERFLOW;
        T.ch[0..i-1]=chars[0..i-1];   //chars的串值依次赋给串变量
        T.length=i;
    }
    return OK;
}// StrAssign 
int Strlength(HString S){//返回串S的长度
    return S.length;
}// Strlength 

int StrCompare(HString S, HString T){
//比较串S和T:若S>T,返回正整数,若S=T返回0,若S<T,返回负整数
    for(i=0;i<S.length && i<T.length; i++)
       if(S.ch[i]!=T.ch[i]) return S.ch[i]-T.ch[i];
    return S.length-T.length;
}// StrCompare 
Status StrConcat(HString &T, HString S1, HString S2){
//由T返回将串S2联接在串S1的末尾组成的新串
  if(T.ch) free(T.ch);  //释放旧空间
  if(!(T.ch=(char *)malloc((S1.length+S2.length)*sizeof(char)))) 
	exit OVERFLOW;
  T.ch[0 .. S1.length-1]=S1.ch[0 .. S1.length-1];  
  T.length=S1.length+S2.length;
  for(i=0; i<S2.length; i++) T->ch[S1.length+i]=S2.ch[i];
  return OK;
}// StrConcat 
Status SubString(HString &Sub, HString S, int pos, int len){
   if(pos<1 || pos>S.length || len<0 || len>S.length-pos+1) 
	return ERROR;  //参数不合法
   if(Sub.ch) free(Sub.ch);   //释放旧空间
   if(!len) {Sub.ch=NULL; Sub.length=0;}  // 子串为空串
   else  {
      if(!(Sub.ch=(char *)malloc(len*sizeof(char)))) exit OVERFLOW;
      Sub.ch[0..len-1]=S.ch[pos-1..pos+len-2];
      Sub.length=len;
   }
   return OK;
}// SubString 

块链存储表示

块链存储

#define  CHUNKSIZE  80  // 可由用户定义的块大小
  typedef  struct Chunk {  // 结点结构
    char  ch[CUNKSIZE];
    struct Chunk  *next;
  } Chunk;
  typedef struct {  // 串的链表结构
     Chunk *head, *tail;  //串的头和尾指针,便于联结操作
     int   curlen;           // 串的当前长度
  } LString;

结点大小为1
优点:操作方便;
缺点:存储密度较低,占用存储量大。
结点大小>1
优点:存储密度高;
缺点:插入、删除字符时,可能会引起结点之间字符的移动,算法实现比较复杂

除了某些特定操作如联接有一定方便之处,总的来说链串不如另外两种顺序存储结构灵活:
占用空间较多
操作复杂
在实际应用中串的链式结构远不如串的顺序结构使用广泛

串的模式匹配算法KMP

略······

数组

数组下标的计算

由于数组可以是多维的,而顺序存储结构是一维的,因此数组中数据的存储要制定一个先后次序。通常,数组中数据的存储有两种先后存储方式:

以列序为主(先列后行):按照行号从小到大的顺序,依次存储每一列的元素
以行序为主(先行后序):按照列号从小到大的顺序,依次存储每一行的元素。

根据存储方式的不同,查找目标元素的方式也不同。如果二维数组采用以行序为主的方式,则在二维数组 anm 中查找 aij 存放位置的公式为:

LOC(i,j) = LOC(0,0) + (i*m + j) * L;

其中,LOC(i,j) 为 aij 在内存中的地址,LOC(0,0) 为二维数组在内存中存放的起始位置(也就是 a00 的位置)。
而如果采用以列存储的方式,在 anm 中查找 aij 的方式为:

LOC(i,j) = LOC(0,0) + (i*n + j) * L;

特殊矩阵压缩存储

特殊矩阵主要有两类:
-->对称矩阵
-->稀疏矩阵、上(下)三角矩阵

对称矩阵

结合数据结构压缩存储的思想,我们可以使用一维数组存储对称矩阵。由于矩阵中沿对角线两侧的数据相等,因此数组中只需存储对角线一侧(包含对角线)的数据即可。
对称矩阵的实现过程是,若存储下三角中的元素,只需将各元素所在的行标 i 和列标 j 代入下面的公式:
公式1

存储上三角的元素要将各元素的行标i和列标j带入另外一个公式:
公式2

三元组顺序表

   0 12  9  0  0  0  0
   0  0  0  0  0  0  0
  -3  0  0  0  0 14  0
M= 0  0 24  0  0  0  0
   0 18  0  0  0  0  0
  15  0  0 -7  0  0  0

//上面矩阵用三元组表示
i  j  v      
1  2  12
1  3  9
3  1  -3
3  6  14
4  3  24 
5  2  18
6  1  15
6  4  -7
typedef  struct
{
    int i,j;     //行坐标、列坐标
    ElemType e;  //元素
}Triple;

typedef struct
{
    Triple date[MAXSIZE+1];  //0不存储元素
    int mu,nu,tu;      //行数、列数、非零元个数
}TSMatrix;

转置

以列序为主序的转置

void TransposeSMatrix(TSMatrix *T1,TSMatrix *T2)
{
    T2->mu=T1->nu;T2->nu=T1->mu;T2->tu=T1->tu;
    if(T1->tu)
    {
        int q=1,col,p;
        for(col=1;col<=T1->nu;col++)  //矩阵列循环
        {
            for(p=1;p<=T1->tu;p++)    //遍历所有元素
            {
                if(T1->date[p].j==col)  //当元素在col列时
                {
                    T2->date[q].i=T1->date[p].j;
                    T2->date[q].j=T1->date[p].i;
                    T2->date[q].e=T1->date[p].e;
                    q++;
                }
            }
        }
    }
}
//上述代码,当矩阵运算为满时,即tu=mu*nu,其时间复杂度为O(nu*nu*mu)

快速转置

第一种算法是通过遍历所有元素的下标,从而确定其在转置后数组中的位置,快速转置的思想就是,预先确定每一列第一个非零元在对应转置后的数组date中的位置;因此需要两个辅助数组
num[ ]:用来存放每一列的非零元个数
cpot[ ]:存放第一个非零元在转置后数组date中的位置
num[ ]数组的值很好求,只需要遍历一次所有元素即可

//可发现
copt[1]=1
copt[col]=copt[col-1]+num[col-1]

void FastTransposeSMatrix(TSMatrix *T1,TSMatrix *T2)
{
    int num[T1->nu],cpot[T1->nu];
    int col,p,q,t;
    T2->mu=T1->nu;T2->nu=T1->mu;T2->tu=T1->tu;
    if(T1->tu)
    {
        //初始化每列非零元个数为0
        for(col=1;col<=T1->nu;col++)
        {
            num[col]=0;
        }
        //求每列非零元个数
        for(t=1;t<=T1->tu;t++)
        {
            ++num[T1->date[t].j];
        }
        //求每列第一个非零元转置后的位置
        cpot[1]=1;
        for(col=2;col<=T1->nu;col++)
        {
            cpot[col]=num[col-1]+cpot[col-1];
        }
        //遍历所有元素
        for(p=1;p<=T1->tu;p++)
        {
            col=T1->date[p].j;  //获取列坐标
            q=cpot[col];        //获取新位置
            T2->date[q].i=T1->date[p].j;
            T2->date[q].j=T1->date[p].i;
            T2->date[q].e=T1->date[p].e;
            ++cpot[col];   //之所以这个地方要++,因为每列非零元可能不止一个
        }  
    }
}

树与二叉树

本处知识点总结有所侧重
树与二叉树

二叉树与二叉树的链式结构

二叉树的性质:
  1.二叉树的第i层上至多有2i-1个节点
  2.深度为K的二叉树至多有2k-1个节点
  3.任何一个二叉树中度数为2的节点的个数比度数为0的节点数目少1.
  4.具有n个节点的完全二叉树的深度为 |log2N |+1
  5.若完全二叉树中的某节点编号为 i,
  则若有左孩子编号为 2i,若有右孩子编号为 2i+1,母亲节点为 i/2。

完全二叉树举例

顺序存储结构:适用于完全二叉树
特点:用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树的结点。此时可利用性质5很方便地求出结点之间的关系。
对一般二叉树,可将其每个结点与完全二叉树上同一位置上的结点对照,存储在一维数组的相应分量中。可能对存储空间造成极大的浪费。

链式存储

  typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;
  }BiTNode,*BiTree;

n个结点的二叉树的二叉链表中有n+1个空链域

二叉树的遍历

遍历二叉树:
按某条搜索路径巡访二叉树中的每一个结点,使每一个结点均被访问一次且仅被访问一次。

遍历是二叉树所有操作中最重要的操作

非线性结构遍历的困难 :
每个元素的后继有多个 需要找到一种规律,使得非线性结构的元素排成一个线性序列

应用场合:
在二叉树中查找具有某种特征的结点 对二叉树中全部结点逐一进行某种处理

先序遍历
访问根结点;
先序遍历左子树;
先序遍历右子树;

中序遍历
中序遍历左子树;
访问根结点;
中序遍历右子树;

后序遍历
后序遍历左子树;
后序遍历右子树;
访问根节点

二叉树遍历
二叉树遍历
先序遍历递归实现

void PreorderTraverse(BiTree T, void (*visit)(ElemType)) {
	if(T){
         visit(T->data); 
         PreorderTraverse(T->lchild,visit);                  
         PreorderTraverse(T->rchild,visit);                         
    }
}//PreorderTraverse

先序创建二叉树

Status CreateBiTree(BiTree &T)
{ //先序创建一棵二叉树,由指针T指向其根结点的指针
    scanf("%c",&ch);
    if(ch==‘#')  T=NULL;
    else {
	   if(!(T=(BiTree)malloc(sizeof(BiTNode))))  exit(OVERFLOW);
	   T->data=ch;
	   CreateBiTree(T->lchild);
	   CreateBiTree(T->rchild);
    }
    return OK;
}//CreateBiTree

中序遍历非递归实现

void InorderTraverse(BiTree T, void (*visit)(ElemType))
{     InitStack(S); Push(S,T); 
       while(!StackEmpty(S)) 
       {  while(GetTop(S,p)&&p)    Push(S,p->lchild); 
          Pop(S,p); 
          if(!StackEmpty(S))
        {   
         Pop(S,p); 
	      visit(p->data);
	      Push(S,p->rchild); 
             }
        }
} //InOrderTraverse

后序遍历的非递归实现

void PostorderTraverse(BiTree T, void (*visit)(ElemType)) {
      InitStack(S); Push(S,T); 
      while(!StackEmpty(S)) {
          while(GetTop(S,p)&&p)    Push(S,p->lchild); 
          Pop(S,p); 
          while((i=GetTop(S,r)) && r->rchild==p) {
         visit(r->data);
              Pop(S,p); 
          }//while
          if(i) Push(S,r->rchild); 
       }//while
} //PostOrderTraverse

层序遍历二叉树

void LevelorderTraverse(BiTree T, void (*visit)(ElemType))
{//层序遍历二叉树
    p=T;
    InitQueue(Q);
    if(p) EnQueue(Q,p);
    while(!QueueEmpty(Q))  // 队列不空
   {
	DeQueue(Q,p);  
	 visit(p->data);
	if(p->lchild)  
	 EnQueue(Q,p->lchild);      
	if(p->rchild)  
	 EnQueue(Q,p->rchild);      
    }
}// LevelorderTraverse 

遍历时间复杂度

二叉树与表达式

二叉树遍历最早是在对存储在机内的表达式求值时提出的

用二叉树表示表达式的方法

例:a+b*(c-d)-e/f
描述表达式的二叉树遍历序列
前缀表达式(波兰式):
        -+a*b-cd/ef
中缀表达式:
        a+b*c-d-e/f
后缀表达式(逆波兰式):
        abcd-*+ef/-

表达式

a+bc-(d+e)
第一步:按照运算符的优先级对所有的运算单位加括号~
式子变成拉:((a+(b
c))-(d+e))
第二步:转换前缀与后缀表达式
前缀:把运算符号移动到对应的括号前面
则变成拉:-( +(a (bc)) +(de))
把括号去掉:-+a
bc+de 前缀式子出现
后缀:把运算符号移动到对应的括号后面
则变成拉:((a(bc)* )- (de)+ )-
把括号去掉:abc*-de+- 后缀式子出现

二叉树的线索化

线索化暂略

树、二叉树、森林的互换

树转二叉树

在树的兄弟结点之间添加一条线
在树中只保留父结点与第一个孩子结点的连线

树与二叉树
森林转二叉树:

把森林中的树都转换成二叉树
从第二棵树开始,把转换后的二叉树作为钱一棵树根结点的右子树插入到第一棵树中

二叉树转换成树和森林

1.加线。在所有的兄弟结点之间加一条线。
2.去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除其他孩子结点之间的连线。
3.调整。以树的根结点为轴心,将整个树调节一下(第一个孩子是结点的左孩子,兄弟转过来的孩子是结点的右孩子)

哈夫曼树

赫夫曼树,也称“哈夫曼树”、“最优树”以及“最优二叉树”。

树的带权路径长度为树中所有叶子结点的带权路径长度之和。通常记作 “WPL” 。
WPL

步骤1
根据给定的n个权值{w1, w2, …, wn},构造n棵二叉树的集合F = {T1, T2, …, Tn},Ti(1≤i≤n)只有一个带权值wi的根结点,其左、右子树均为空。
步骤2
在F中选取两棵根结点权值最小的二叉树,分别作为左、右子树构造一棵新二叉树。置新二叉树的根结点的权值为其左、右子树上根结点的权值之和。
步骤3
在F中删去这两棵二叉树,把新的二叉树加入F 。
步骤4
重复步骤2和步骤3
直到F中仅剩下一棵树为止。
这棵树就是Huffman树。

typedef struct
{
unsigned int weight; 
unsigned int parent, lchild, rchild; 
}HTNode,*HuffmanTree;
Status CreateHuffmanTree(HuffmanTree &HT, int *w, int n) 
{  if(n<=1) return ERROR; 
   m=2*n-1; 
   HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));//0号单元未用
   for(p=HT+1,i=1;i<=n; ++i,++p,++w)    *p={*w,0,0,0}; 
   for(;i<=m; ++i, ++p)     *p= {0,0,0,0}; 
   for(i=n+1;i<=m; ++i)
   { Select(HT,i-1,s1,s2);
     HT[s1].parent=i; HT[s2].parent=i;       
     HT[i].lchild=s1; HT[i].rchild=s2;   
     HT[i].weight=HT[s1].weight+HT[s2].weight; 
   }	
   return OK;
}

在Huffman树中没有度为1的结点,这类树又称严格二叉树或正则二叉树, 此类二叉树中仅有度为0和度为2的结点。

哈夫曼编码

介绍

霍夫曼编码,首先需要根据输入文本,构造一棵二叉树,树的左链接表示比特"0",右链接表示比特"1",叶子结点表示字符。字符所对应的霍夫曼编码值就是从根结点到叶子结点的链接值。

构建哈夫曼树

#define LEN 512
struct huffman_node{
        char c;
        int weight;
        char huffman_code[LEN];
        huffman_node * left;
        huffman_node * right;
};
int huffman_tree_create(huffman_node *&root, map<char, int> &word){
        char line[MAX_LINE];
        vector<huffman_node *> huffman_tree_node;

        map<char, int>::iterator it_t;
        for (it_t = word.begin(); it_t != word.end(); it_t++){
                // 为每一个节点申请空间
                huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                node->c = it_t->first;
                node->weight = it_t->second;
                node->left = NULL;
                node->right = NULL;
                huffman_tree_node.push_back(node);
        }


        // 开始从叶节点开始构建Huffman树
        while (huffman_tree_node.size() > 0){
                // 按照weight升序排序
                sort(huffman_tree_node.begin(), huffman_tree_node.end(), sort_by_weight);
                // 取出前两个节点
                if (huffman_tree_node.size() == 1){// 只有一个根结点
                        root = huffman_tree_node[0];
                        huffman_tree_node.erase(huffman_tree_node.begin());
                }else{
                        // 取出前两个
                        huffman_node *node_1 = huffman_tree_node[0];
                        huffman_node *node_2 = huffman_tree_node[1];
                        // 删除
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        // 生成新的节点
                        huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                        node->weight = node_1->weight + node_2->weight;
                        (node_1->weight < node_2->weight)?(node->left=node_1,node->right=node_2):(node->left=node_2,node->right=node_1);
                        huffman_tree_node.push_back(node);
                }
        }

        return 0;
}

字符频率统计

int read_file(FILE *fn, map<char, int> &word){
        if (fn == NULL) return 1;
        char line[MAX_LINE];
        while (fgets(line, 1024, fn)){
                fprintf(stderr, "%s\n", line);
                //解析,统计词频
                char *p = line;
                while (*p != '\0' && *p != '\n'){
                        map<char, int>::iterator it = word.find(*p);
                        if (it == word.end()){// 不存在,插入
                                word.insert(make_pair(*p, 1));
                        }else{
                                it->second ++;
                        }
                        p ++;
                }
        }
        return 0;
}

哈夫曼树转哈夫曼编码

int get_huffman_code(huffman_node *&node){
        if (node == NULL) return 1;//层序遍历
        huffman_node *p = node;
        queue<huffman_node *> q;
        q.push(p);
        while(q.size() > 0){
                p = q.front();
                q.pop();
                if (p->left != NULL){
                        q.push(p->left);
                        strcpy((p->left)->huffman_code, p->huffman_code);
                        char *ptr = (p->left)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '0';
                }
                if (p->right != NULL){
                        q.push(p->right);
                        strcpy((p->right)->huffman_code, p->huffman_code);
                        char *ptr = (p->right)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '1';
                }
        }
        return 0;
}

实现步骤

读取输入;
统计输入中每个字符的频次;
根据频次,构造Huffman树;
构造编译表,用于将字符与变长前缀映射;
将Huffman树编码为比特字符串,并写入输出流;
将文本长度编码为比特字符串,并写入输出流;
压缩数据,即使用编译表翻译每个文本字符,写入输出流。

图的术语

图(Graph):由两个集合V(G)和E(G)组成的,记为G=(V,{E})
其中:
V 是顶点的有穷非空集;
E 是边(弧)的有限集
约定符号:

V:顶点有穷非空集合
VR:顶点关系的集合
E:边或弧的集合
n:图中顶点数目
e:边或弧的数目
G:图
N:网

无向完全图和有向完全图:
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n(n-1)/2条边。在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n(n-1) 条边。
稀疏图和稠密图:
有很少条边或弧的图称为稀疏图,反之称为稠密图,这里的概念是相对而言的。
权和网:
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网。
邻接点:
对于无向图G= (V,{E}), 如果边(v,v')属于E, 则称顶点v和v‘互为邻接点,即v和v'相邻接、边(v,v')依附于顶点v和v',或者说(v,v')与顶点v和v'相关联。
度、入度和出度:
点v的度是和v相关联的边的数目,记为TD(v)。如上图左侧上方的无向图,顶点A与B互为邻接点,边(A,B) 依附于顶点A 与B 上,顶点A 的度为3。而此图的边数是5,各个顶点度的和=3+2+3+2=10,推敲后发现,边数其实就是各顶点度数和的一半,多出的一半是因为重复两次计数。
对于有向图G= (V,{E}),如果弧<v,v'>属于E,则称顶点v邻接到顶点v',顶点v'邻接自顶点v的弧<v,v'>和顶点v, v'相关联。以顶点v为头的弧的数自称为v的入度,记为ID (v); 以v为尾的弧的数目称为v的出度,记为OD (v); 顶点v的度为TD(v) =ID(v) +OD (v)。
路径和路径的长度:
从顶点v 到顶点v'的路径是一个顶点序列。路径的长度是路径上的边或弧的数目。有向图的路径也是有向的。
回路或环:
第一个顶点到最后一个顶点相同的路径称为回路或环。
简单路径、简单回路或简单环:
序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
连通、连通图和连通分量:
在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。 如果对于图中任意两个顶点vi、vj ∈E, vi,和vj都是连通的,则称G是连通图。
稀疏图:有很少条边或弧(e<nlog2n)的图。
稠密图:有很多条边或弧的图。
ADT图

图的邻接矩阵

图的2种常用的存储形式:
数组(邻接矩阵)表示法
邻接表

邻接矩阵

#define MAX_VERTEX_NUM 20
typedef  enum {DG,DN,UDG,UDN} GraphKind; 
typedef struct ArcCell
{
	VRType adj;
	infoType *info;
}ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct 
{
	VertexType vexs[MAX_VERTEX_NUM]; //顶点向量
	AdjMatrix arcs;
	int vexnum,arcnum;
	GraphKind kind;
}MGraph;
Status CreateGraph(MGraph &G)
{//在邻接矩阵存储结构上根据图的种类调用具体构造算法
	printf("please input the kind of graph\n");
	scanf(&G.kind);
	switch(G.kind)
	{
		case DG:return CreateDG(G);//构造有向图
		case DN:return CreateDN(G);//构造有向网
		case UDG:return CreateUDG(G);//构造无向图
		case UDN:return CreateUDN(G);//构造无向网
		default:return ERROR;
	}
}
Status CreateUDN(MGraph &G) //在邻接矩阵存储结构上,构造无向网G
{	scanf(&G.vexnum,&G.arcnum);//读入顶点数和边数目for(i=0;i<G.vexnum;i++)	scanf(&G.vexs[i]);//构造顶点向量
	for(i=0;i<G.vexnum;i++) //邻接矩阵初始化
	   for(j=0;j<G.vexnum;j++)
	  	G.arcs[i][j]=INFINITY;
	for(k=0;k<G.arcnum;k++)//构造邻接矩阵
	{	scanf(&v1,&v2,&w);//读入一条边依附的顶点及权值
		i=LocateVex(G,v1);j=LocateVex(G,v2);//确定v1、v2在图中的位置
		G.arcs[i][j]=w;//边<v1,v2>的权值
		G.arcs[j][i]=G.arcs[i][j];//置<v1,v2>的对称弧<v2,v1 >
	} 	
   return OK;
}//CreateUDN

图_邻接矩阵的分析

图的邻接表

图

#define MAX_VERTEX_NUM 20
typedef struct ArcNode//表结点
{	int adjvex;
	struct ArcNode *nextarc;
	infoType info;
}ArcNode;
typedef struct Vnode//头结点
{	VertexType data;
	ArcNode *firstarc;
}VNode,AdjList[MAX_VERTEX_NUM];
typedef struct
{	AdjList vertices;
	int vexnum,arcnum;  //图的当前顶点数和弧数
	int kind;           //图的种类
}ALGraph;

邻接表的分析

图的遍历

对非线性结构线性化。遍历算法是求解图的连通性、拓扑排序、求关键路径等算法的基础。

图的两种遍历方式(有向图和无向图均适用):
深度优先搜索DFS (Depth First Search)
广度优先搜索BFS (Breadth First Search)

辅助向量visited[]的使用

① 使用原因:
因为图中任意顶点都可能和其余顶点相邻接,所以在访问了某顶点后,可能沿另外的某条路径搜索,而后又回到此顶点上,为了避免同一顶点被多次访问,在遍历图的过程中,必须记下每个已访问过的顶点。
② 使用方法:
设置一个辅助数组visited[0..n-1],它的初始值置为“假”,表示顶点未被访问过,一旦访问了顶点i,就置visited[i]的值为“真”或者被访问时的次序号。
Boolean visited[MAX_VERTEX_NUM] ;

深度优先搜索DFS

DFS的过程接近先序遍历。
算法思想:
①从图中某个顶点v出发,访问此顶点;
②依次从v的各个未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到
③若图中还有顶点未被访问(非连通图),则另选图中一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

Boolean visited[MAX_VERTEX_NUM]; 
Status (*VisitFunc)(int v); //全局函数指针变量

void DFSTraverse( Graph G, Status (*Visit)(int v))
 { //对图G进行深度优先遍历  
   VisitFunc=Visit;
   for ( v=0; v <G.vexnum; ++v )  visited[v] = FALSE;
   for ( v=0; v <G.vexnum; ++v )
     if ( !visited[v] ) DFS(G,v); 
 }//DFSTraverse
void DFS( Graph G, int v) 
 { //从v出发深度优先遍历图G
    visited[v] = TRUE; 
    VisitFunc(v); //访问顶点v
    for(w=FirstAdjVex(G,v); w>=0; w=NextAdjVex(G,v,w)) 
      if ( !visited[w] )  DFS(G,w);  
 } //DFS

1
2

在遍历图时,对每个顶点至多调用一次DFS函数,因为一旦某个顶点被标志成已被访问,就不再从它出发进行搜索。
遍历图的实质上是对每个顶点查找其邻接点的过程。其耗费的时间取决于所采用的存储结构。
用邻接矩阵存储图时,查找所有顶点的邻接点需要O(n2);
用邻接表存储图时,查找所有顶点的邻接点需要O(e);
深度优先遍历图的算法的时间复杂度与采用的存储结构有关
以邻接矩阵做图的存储结构时,深度优先遍历的时间复杂度为O(n2)
以邻接表做图的存储结构时,深度优先遍历的时间复杂度为O(n+e)。

广度优先搜索BFS

BFS的过程接近层序遍历。

算法思想:
①从图中的某个顶点v出发,访问此顶点;
②依次访问v的所有未被访问过的邻接点,之后按这些邻接点被访问的先后次序依次访问它们的邻接点,直至图中所有和v有路径相通的顶点都被访问到;
③若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

void BFSTraverse(Graph G,Status(*Visit)(int v)) 
{ //对图G进行广度优先遍历
   for(v=0;v<G.vexnum;++v)  visited[v] = FALSE;  
   InitQueue(Q);
   for( v=0;v<G.vexnum;++v )
	if(!visited[v]) //v没有被访问 
	{ visited[v]=TRUE;  	Visit(v);      //访问v
      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])  
		{  visited[w]=TRUE; Visit(w); //访问w
		   EnQueue(Q,w); //w入队列
		} //if
	  } //while
     } //if
} //BFSTraverse 

利用图的遍历算法来得到生成树或生成森林
对无向连通图G进行遍历时,由遍历过程中历经的边的集合和所有顶点的集合一起构成了G的一棵生成树。
由深度优先遍历得到的为深度优先生成树
由广度优先遍历得到的为广度优先生成树
对于非连通图,它的每个连通分量的生成树就组成了非连通图的生成森林

图的遍历算法也可以用来判断图的连通性。
对于无向图来说,若无向图是连通的,则从任一结点出发, 仅需一次遍历就能够访问图中的所有顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点。
故在BFSTraverse ()或DFSTraverse ()中添加了第二个for循环,再选取初始点,继续进行遍历,以防止一次无法遍历图的所有顶点。对于无向图,上述两个函数调用BFS (G,i)或DFS(G,i)的次数等于该图的连通分量数;而对于有向图则不是这样,因为一个连通的有向图分为强连通的和非强连通的,它的连通子图也分为强连通分量和非强连通分量,非强连通分量一次调用BFS (G, i)或DFS (G, i)无法访问到该连通分量的所有顶点。

最小生成树

一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n − 1 n-1n−1条边,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成图中的一条回路。
对于一个带权连通无向图G = ( V , E ) G=(V, E)G=(V,E),生成树不同,其中边的权值之和最小的那棵生成树(构造连通网的最小代价生成树),称为G的最小生成树(Minimum-Spanning-Tree, MST)。

GENERIC_MST(G){
	T=NULL;
	while T 未形成一棵生成树;
		do 找到一条最小代价边(u, v)并且加入T后不会产生回路;
			T=T U (u, v);
}

应用MST性质构造最小生成树的算法:
Prim算法
Kruscal算法

可参考:
https://zhuanlan.zhihu.com/p/136387766

Prim算法(普里姆算法)

设N=(V,{E})是连通网,T=(V,{TE})表示N的最小生成树,TE为最小生成树的边集,初始为空集。则Prim算法的执行过程:
Step1:令U={u},u∈V(u是网中任意一个顶点),TE={};
Step2:在u∈U,v∈V-U的边(u,v)∈E中寻找一条代价最小的边(u,v)并入TE,同时将顶点v并入U;
Step3:重复Step2,直至U=V,此时TE中必有n-1条边,而T={V,{TE}}是N的一棵最小生成树。

通俗点说就是:从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把当前形成的连通分量当做一个整体或者一个点看待,然后重复“找最短的边并添加”的操作。

为实现普里姆算法,需要附设一个辅助数组closedge,以记录从U到V-U集合具有最小代价的边。对每一个v∈V-U(设其在图中的位置为i),它在数组中存在一个相应分量closedge[i]记录一个顶点是v另一顶点是U中顶点的边中代价最小的边,closedge[i].lowcost域存储该边上的权值;closedge[i].adjvex域存储该边依附在U集合中的顶点

struct
    {
	ElemType  adjvex;  
   	VRType lowcost;     
    }closedge[MAX_VERTEX_NUM]; 

Prim算法分析:
时间复杂度:O(n2) , 只与顶点数有关, 与网中的边数无关
适用于求边稠密的网的最小生成树

Kruscal算法(克鲁斯卡尔算法)

假设连通网N=(V,{E}),T=(V,{TE})表示N的最小生成树,TE为最小生成树上边的集合。初始时令TE为空集。
Step1:令最小生成树T的初态为只有n个顶点的非连通图T=(V,{TE}),TE={}。
Step2:从权值最小的边(u,v)开始,若该边依附的两个顶点落在T的不同连通分量上,则将此边加入到TE中,即TE=TE∪(u,v),否则舍弃此边,选择下一条代价最小的边。
Step3:重复Step2,直至TE所有顶点在同一连通分量上。此时T=(V,{TE})就是N的一棵最小生成树。

与Prim算法从顶点开始扩展最小生成树不同,Kruskal 算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。

Kruscal算法分析:
时间复杂度:O(e*loge),只与边数有关, 与网中的顶点数无关
适合于求边稀疏的网的最小生成树

Prim算法
Kruscal算法

拓扑排序

基本概念和知识:

AOV(Activity On Vertex)网:有向图可用来描述一项工程或系统的完成过程。在这种图中,顶点表示活动,有向弧表示活动之间的优先关系,如
<vi,vj>表示活动vi必须先于活动vj进行。其中vi是vj的直接前驱,vj是vi的直接后继。若从顶点vi到vk有一条路径,则vi是vk的前驱、vk是vi的后继;

有向无环图(DAG图):无环的有向图
对于DAG图:
描述工程和系统的进行过程
工程能否顺利进行--->拓扑排序
估算工程完成必须的最短时间--->有向图的关键路径问题

有向图中检测是否存在环的办法:
深度优先遍历
对有向图进行拓扑排序,若网中所有顶点都在它的拓扑有序序列中,则不存在环。

偏序:若集合 X 上的关系R是传递的、自反的、反对称的,则称R是集合X上的偏序关系。可指集合中部分成员之间可比较。
全序:若关系R 是集合X 上的偏序关系,如果对于属于X的每个x,y,必有xRy 或yRx ,则称R是集合X上的全序关系。可指集合中全部成员之间可比较。
拓扑排序:由集合上的偏序得到该集合上的全序的操作。这个全序被称为拓扑有序。

拓扑
拓扑排序步骤:
Step1:在有向图中选一个无前驱的顶点输出之;
Step2:从有向图中删去此顶点及所有以它为尾的弧;
Step3:重复前2步,直到图中顶点全部输出,此时图中无环;或图不空但找不到无前驱的顶点,此时图中有环。
拓扑排序算法还是求关键路径的基础。

仅就逻辑结构:拓扑序列可能不唯一;
但若给定物理结构,按算法得到的拓扑序列唯一。

拓扑排序算法中的图采用邻接表作为存储结构。 辅助数组Indegree[]:记录每个顶点的入度 辅助结构:暂存入度为零的顶点以避免重复检测
如栈,队列,甚至是线性表

 Status TopologicalSort(ALGraph G)    
{  FindIndegree(G,indegree);   
   InitStack(S);  //用到第3章中栈的基本操作 
   for(i=0;i<G.vexnum;++i)   if (!indegree[i])  Push(S,i); 
   count=0;        //对输出顶点计数 
   while(!StackEmpty(S))
   {  Pop(S,i);    printf(i,G.vertices[i].data); 
      ++count;                  //输出顶点数加1
      for(p=G.vertices[i].firstarc; p; p=p->nextarc)
      { k=p->adjvex;  if(!(--indegree[k]))  Push(S,k); }
    }//while 
   if(count<G.vexnum)  return ERROR;   
   else  return OK;            //无回路     
}// TopologicalSort 

时间复杂度为O(n+e)

关键路径

AOE-网(Activity On Edge):一个有向无环网,顶点表示事件,弧表示活动,弧上权值表示活动持续的时间。
通常用来估算工程完成时间
路径长度:AOE网中路径上各活动持续时间之和。
关键路径:从源点到汇点路径长度最长的路径。

设活动ai在有向边<j,k>上,有:
活动ai的最早开始时间e(i):是从源点v0到vj的最长路径长度。
活动ai的最迟开始时间l(i):是不推迟工程完成的前提下,该活动允许的最迟开始时间。
e(i)= Ve(j)
l(i)=Vl(k)-dut(<j,k>)
活动ai时间余量:l(i)-e(i)
关键活动:l(i)=e(i)的活动。
关键路径上的活动都是关键活动
在这里插入图片描述

关键路径
设有向无环图G的有向边<j,k>上:
事件vk的最早发生时间Ve(k)=从源点v0到vk的最长路径长度
Ve(0)=0;
Ve(k)=Max{Ve(j)+dut(<j,k>), <j,k>∈T,所有j}
在这里插入图片描述在这里插入图片描述
事件vj的最迟开始时间Vl(j):保证汇点vn-1在Ve(n-1)时刻完成的前提下,事件vj最迟允许开始的时间。
Vl(n-1) = Ve(n-1)=从源点到汇点的最长路径长度;
Vl(j)=Min{Vl(k)-dut(<j,k>), <j,k>∈T,所有k}

关键路径

影响关键活动的因素是多方面的,任何一项活动持续时间的改变都会影响关键路径的改变
关键活动的速度提高是有限度的,只有在不改变网的关键路径的情况下,提高关键活动的速度才有效
关键路径可有多条
若网中有几条关键路径,则单提高某一条关键路径上的关键活动的速度并不能导致整个工程缩短工期,必须提高同时在几条关键路径上的活动的速度才能使整个工程缩短工期

最短路径

最短路径问题:从图中某一顶点到达另一顶点的路径可能不止一条,求其中一条路径使得沿此路径上各弧上的权值总和最小。称路径的第一个顶点为源点,最后一个顶点为终点。

参考博客:
https://www.cnblogs.com/Braveliu/p/3458671.html

查找

本部分讨论两类不同的查找表:静态查找表和动态查找表,给出在不同查找表上进行查找的不同算法和性能分析以及动态查找表的创建方法。

三大查找方法
顺序查找,二分法查找(折半查找),分块查找

基本术语

查找表:为了便于查找而专门设置的一种数据结构,是由同一类型的用于查找的数据元素(或记录)构成的集合 。
在计算机中进行查找的方法与采用的数据结构有关

对查找表经常进行的操作:

  1. 查询某个“特定的”数据元素是否在查找表中;
  2. 检索某个“特定的”数据元素的各种属性;
  3. 在查找表中插入一个数据元素;
  4. 从查找表中删除某个数据元素。
    静态查找表: 仅作上述1)和2)操作的查找表
    动态查找表: 可作上述1)、2)、3)、4)操作的查找表

关键字:数据元素中某个数据项的值,用以标识一个数据元素
主关键字:可以唯一地标识一个记录的关键字
次关键字:能识别若干记录
查找:根据给定值,在查找表中确定一个其关键字等于给定值的数据元素或记录.
查找成功:表中存在这样的记录,则给出该记录信息或指示该记录在表中的位置
查找不成功:查找表中不存在这一记录,给出“空记录”或“空指针”。

查找算法的性能分析:通常以关键字和给定值进行比较的记录个数的平均值为衡量算法好坏的依据.
查找成功的平均查找长度(ASL)(Average Search Length):查找成功时为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值
Pi: 查找表中第i个记录的概率,有
Ci:找到表中其关键字与给定值相等的第i个记录时,和给定值比较过的关键字的个数.则:
ASL

查找不成功平均查找长度: 确定查找不成功时和给定值进行比较的关键字个数的期望值称为在查找不成功时平均查找长度。
平均查找长度(Average Search Length):查找算法在查找成功时平均查找长度和查找不成功时平均查找长度之和。

静态查找表的查找

线性表适于表示静态查找表

线性表通常有两种存储方式
顺序存储结构
链式存储结构

可以顺序表或线性链表表示静态查找表

基于线性表的查找包括:
顺序查找: 适用于两种存储结构
有序表的折半查找
索引顺序表的分块查找

顺序查找

无序查找算法

从表中第一条/最后一条记录开始,逐个进行记录的关键字与给定值的比较,若某个记录的关键字和给定值比较相等,则查找成功,返回其在顺序表中的位序;反之,若直至最后一条/第一条记录其关键字和给定值比较都不等,则查找不成功,返回0。

辅助信息
监视哨:为能自动检验数组下标越界,在0下标处设置哨兵,若查找不成功,则循环会在0下标处自动终止,函数返回0。

 int  Search_Seq( SSTable  ST,  KeyType key )
 {  ST.elem[0]. key= key; //0下标为监视哨
    for(i=ST.length;!EQ(ST.elem[i].key,key ); --i);           
    return i;              
 }//Search_Seq 

在这里插入图片描述在这里插入图片描述
无论给定什么关键字,顺序查找不成功时和给定值进行比较的关键字个数均为n+1.
顺序查找的缺点:平均查找长度大,n越大,效率越低。
顺序查找的优点:算法简单;适用面广;不关心记录是否有序。
顺序查找的时间复杂度为O(n)。

折半查找

元素必须是有序的,如果是无序的则要先进行排序操作
折半查找(二分查找):先确定待查记录所在范围,逐步缩小范围,直到找到或找不到该记录止。
应用范围:有序顺序表。
例: 查找 key = 77 的结点所在的数组元素下标,设指针low和high分别指示待查元素所在范围的上界和下界,指针mid指示区间的中间位置,即mid=(low+high)/2
折半
折半查找算法

int  Search_Bin ( SSTable  ST,  KeyType key ){
    low = 1 ; high = ST.length ;
    while ( low <= high ) {      
      mid = ( low + ligh ) / 2 ; 
      if  ( EQ(ST.elem[mid]. key, key )  return mid ;
      else if (LT(key,ST.elem[mid].key)) high = mid -1 ;
      else low = mid + 1; 
    }
    return 0 ;
 }  

最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);

折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

//二分查找(折半查找),版本2
int BinarySearch1(int a[], int value, int n)
{
    int low, high, mid;
    low = 0;
    high = n-1;
    while(low<=high)
    {
        mid = (low+high)/2;
        if(a[mid]==value)
            return mid;
        if(a[mid]>value)
            high = mid-1;
        if(a[mid]<value)
            low = mid+1;
    }
    return -1;
}
 
//折半查找,递归版本
int BinarySearch2(int a[], int value, int low, int high)
{
    int mid = low+(high-low)/2;
    if(low > high)
        return -1;
    if(a[mid]==value)
        return mid;
    if(a[mid]>value)
        return BinarySearch2(a, value, low, mid-1);
    if(a[mid]<value)
        return BinarySearch2(a, value, mid+1, high);
}

来源:https://blog.csdn.net/sayhello_world/article/details/77200009

判定树:用二叉树描述折半查找过程,树中每个结点表示一个记录,用结点中的值为该记录在表中的位置。每个非终端结点的左子树表示的是在该结点位置的前半区进行折半查找的过程,其右子树表示的是在该结点位置的后半区进行折半查找的过程。

查找成功和不成功时和给定值比较的关键字个数最多不超过树的深度:log2n+1

一般情况下,表长为n的折半查找的判定树的深度和含有n个结点的完全二叉树的深度相同,设 n=2h-1 且查找概率相等,则折半查找成功的平均查找长度
ASLbs
折半查找优缺点:查找效率高,但仅适用于有序的顺序表

分块查找

索引顺序查找(分块查找):顺序表+索引表组成
应用范围:有序或分块有序的顺序表。
分块有序:第二个子表中的所有记录的关键字都大于第一个子表的最大关键字,依次类推。
分块查找
查找性能分析:
设长度为n的表均匀分成b块,每块含有s个记录,则b=n/s ,每块查找概率1/b,块中每个记录的查找概率是1/s。则ASLbs=Lb+Lw, 其中:
Lb: 在索引表中确定所在块的平均查找长度。
Lw :在块中查找元素的平均查找长度。

分块查找

动态查找表

二叉排序树查找

二叉排序树的定义:
采用链式结构存储的树表示动态查找表的特点:

链式结构适于进行插入和删除操作;
树结构本身的排序特性使得查找过程变得高效。

二叉排序树(二叉查找树,BST):
空树或具有下列性质的二叉树:

根的左子树若非空,则左子树上所有结点的关键字值均小于根结点的关键字值;
根的右子树若非空,则右子树上所有结点的关键字值均大于根结点的关键字值; 它的左右子树同样是二叉排序树。

二叉排序树的查找过程:
若二叉排序树为空,则查找失败,返回空指针;
若二叉排序树不空,首先将给定值和根结点的关键字比较,若相等则查找成功,返回根结点的地址。
若给定值小于根结点的关键字值,则在其左子树上继续查找;
若给定值大于根结点的关键字值,则在其右子树上继续查找。

二叉排序树查找

BiTree SearchBST(BiTree T, KeyType key)
{ if((!T) || EQ(key,T->data.key)) return T;
  if(LT(key,T->data.key)) 
    return SearchBST(T->lchild,key); 
  else return SearchBST(T->rchild,key);
} //SearchBST 

中序遍历二叉排序树可得到一个关键字的有序序列。
因此一个无序序列可通过构造一棵二叉排序树而变为一个有序序列

构造BST的过程
查找失败时元素不断插入的过程。
二叉排序树的插入
当树中不存在关键字等于给定值的结点时插入,新插入的结点一定是新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的孩子。

所有新插入的结点都是二叉排序树上的叶子,因此进行插入时,不必移动其他结点,仅需改动某个结点的指针即可。这相当于在有序序列上插入一个记录而不需移动其他记录。

插入算法:
首先进行查找,查找失败时记录查找路径上访问的最后一个结点(待插入结点的双亲结点)
生成待插入结点,判断它是其双亲的哪个孩子,将它作为叶子结点插入
若二叉树为空,则首先单独生成根结点

Status SearchBST(BiTree T, KeyType key, BiTree f, BiTree &p)
{  //修改后的查找算法
   if(!T)  {  p=f;  return FALSE;  } 
   else if EQ(key,T->data.key)     
     {  p=T;   return TRUE;  }
   else if LT(key,T->data.key)          
	   return SearchBST(T->lchild,key,T,p);    
   else  return SearchBST(T->rchild,key,T,p); 
}// SearchBST 
//插入
Status InsertBST(BiTree &T, ElemType e){
   if(!SearchBST(T,e.key,NULL,p))       
   {
     s=(BiTree)malloc(sizeof(BiTNode));             
     if(!s)   exit(OVERFLOW);
     s->data =e;     s->lchild= s->rchild=NULL; 
     if(!p) T=s;                   
     else if LT(e.key, p->data.key)   p->lchild=s;
     else   p->rchild=s;                 
     return TRUE;
   }
   else return FALSE;                    
} 

二叉排序树的删除
 在删除二叉排序树上某个结点之后,仍然保持二叉排序树的特性,即:二叉排序树中任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。
 

删除结点有三种情况
  1.被删除的结点是叶子
  2.被删除的结点只有左子树或者只有右子树
  3.被删除的结点既有左子树,也有右子树

被删除的结点*p是叶子
由于要删除的结点p即无左子树,又无右子树,
因此删除结点p之后不会破坏二叉排序树结构的完整性,
只要将其双亲结点f原来指向p的指针改为指向空即可

f->lchild=NULL;
free( p );

被删除的结点*p只有左子树或者只有右子树
要删除的结点p只有左子树PL或者右子树PR,
这时候只要将p的左子树PL或p的右子树PR
直接作为其双亲结点f 的相应左子树或右子树即可

f->lchild=p->lchild;(p只有左子树)
f->lchild=p->rchild;(
p只有右子树)

被删除的结点*p既有左子树,也有右子树。

令S是删除前中序遍历序列中被删结点*p的直接前驱结点,令指针s指向它 。

第一种处理方法:

f->lchild=p->lchild;
s->rchild=p->rchild;

第二种处理方法:

p->data=s->data;
q->rchild=s->lchild;
free(s);

查找成功的情况:
查找走过一条从根结点到该结点的路径,与关键字比较次数等于路径长度+1,总的比较次数不超过树的深度
查找不成功的情况:
查找走过一条从根结点到叶子结点的路径,与关键字比较次数等于路径长度+1,总比较次数不超过树的深度

二叉排序树的平均查找长度分析:
最坏情况:当插入关键字有序,二叉排序树蜕变成单支树,深度为n,ASL=(n+1)/2
最好情况:二叉排序树与折半查找判定树相同,ASL与log2n成正比。
(最好的情况下能达到折半查找的效率)

在等概率情况下,完全二叉树的查找效率最高。在随机的情况下,二叉排序树的平均查找长度和logn是等数量级的

平衡二叉排序树

基本术语:
平衡二叉树(AVL树):
它或者是一棵空树,或者是满足下列性质的二叉树:
(1)其左、右子树深度之差的绝对值不大于1
(2)其左、右子树都是平衡二叉树
结点的平衡因子BF=该结点的左子树深度-右子树深度
平衡二叉树上结点的平衡因子取值={-1,0,+1}
二叉树上有结点的平衡因子的绝对值大于1,则它就不是平衡的。
为什么引入平衡二叉树:
若一棵二叉排序树是平衡的,则树上任何结点的左右子树的深度之差不超过1,二叉树的深度与log2n同数量级。可以保证它的平均查找长度与log2n同数量级
平衡二叉树

构造平衡的二叉排序树的方法是:
根据初始序列,从空树开始插入新结点
在插入过程中,一旦有结点的平衡因子的绝对值大于1(失去平衡),则在保持二叉排序树特性的前提下,采用平衡旋转技术,对最小不平衡子树(其左、右子树均平衡,只有子树的根结点不平衡)进行调整,使其平衡。

在插入过程中构造平衡的二叉排序树
采用平衡旋转技术,设在二叉排序树上插入结点而失去平衡的最小子树根结点指针为a,失去平衡的情况有四种。
旋转操作的正确性证明:
保持二叉排序树的性质
即中序遍历所得关键字序列自小到大有序
为什么只对最小不平衡子树调整?
经旋转处理的子树深度与插入之前相同,不影响插入路径上所有祖先的平衡度

LL型(右单旋转):
由于在A的左孩子的左子树上插入结点,A的平衡因子由+1变为+2,需进行一次向右的顺时针旋转操作
LL
RR型(左单旋转):
由于在A的右孩子的右子树上插入结点,A的平衡因子由-1变为-2,需进行一次向左的逆时针旋转操作。
RRLR型(先左后右双旋转):
由于在A的左孩子的右子树上插入结点,A的平衡因子由1变为2,需进行两次(先左后右)的旋转操作
LRRL型(先右后左双旋转):
由于在A的右孩子的左子树上插入结点,A的平衡因子由-1变为-2,需进行两次(先右后左)的旋转操作。
RL

B-树与B+树

B-B-树也称B树。

所有的叶子结点都出现在同一层上,不带信息(可看成是外部结点或查找失败的出口,实际并不存在)。

B

从根开始查找,如果 Ki = KEY 则查找成功。
若 Ki < KEY < Ki+1; 查找 Ai 指向的结点
若 KEY < K1; 查找 A0 指向的结点
若 KEY > Kn; 查找 An指向的结点
若 找到叶子,则查找失败。

B-树的插入
B-树的生成从空树开始,在查找的基础上逐个插入关键字而得。
由于B-树结点中的关键字个数必须≥m/2-1,每次插入一个关键字不是在树中增加一个叶子结点,而是在最底层的某个非终端结点中添加一个关键字。此时有可能打破B-树对结点个数上限的要求,有2种情况:
1、若插入前该结点的关键字个数<m-1
2、若插入前该结点的关键字个数=m-1

1、若插入前该结点的关键字个数<m-1
直接将关键字和其它信息按序插入到该结点中。
在这里插入图片描述2、若插入前该结点的关键字个数=m-1,则插入后>m-1
在这里插入图片描述
在这里插入图片描述
B-树的删除
在B-树删除一个关键字,首先必须找到待删关键字所在结点,从中删除之。
若该结点为最下层的非终端结点,且删除前其关键字个数不少于m/2 ,则删除完成;否则要进行结点的合并。
若该结点不是最底层的非终端结点,被删关键字是其第i个关键字,则将Ai-1或Ai所指子树中的最大(或最小)关键字Y移上来代替Ki,然后在相应的结点中删去Y。

1)删除前被删关键字所在结点关键字数目≥m/2
直接删除该关键字Ki和Ai即可。
2)删除前被删关键字所在结点关键字数目=(m/2)-1 ,而与该结点相邻的右兄弟(左兄弟)结点中关键字数目大于(m/2)-1
将该兄弟结点中最小(最大)关键字上移至双亲结点
双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在结点。
3)被删关键字所在结点和其相邻的兄弟结点中的关键字数目均等于(m/2)-1,设该结点有右兄弟且其右兄弟结点地址由双亲结点中的指针Ai指示
删去关键字后,它所在结点的剩余关键字和指针,加上双亲结点的关键字Ki一起,合并到Ai所指兄弟结点中。
如果因此使得双亲结点的关键字个数小于m/2-1,则依次类推作相应处理,即合并操作有可能向上传递。结点合并的极端情况是使树的高度减少1。

在这里插入图片描述在这里插入图片描述B+树:
B+在这里插入图片描述

散列表

基础术语:

已讲查找表的共同点

记录关键字的值和其在表中的位置之间无确定关系
查找过程是给定值依次和关键字集合中各关键字的比较
查找效率取决于和给定值进行比较的关键字个数。

理想的查找表:不经过比较,一次存取就能得到所要查找的记录,即根据记录的关键字就能确定其存储位置
实现途径:在记录的关键字和其在表中的存储位置之间建立一种确定的、一对一的关系

散列(Hash)函数
在记录的关键字和其在表中位置之间建立的一种函数关系,即以f(key)作为关键字为key的记录在表中的存储位置。

散列(Hash)函数是一个映象
将关键字的集合映射到某地址集合
通常地址集的大小由关键字的个数决定

冲突:不同关键字得到同一散列地址,即:
key1key2,而f(key1)=f(key2)
同义词:在一个散列函数中具有相同函数值的不同关键字。

一般情况下,关键字的取值范围比较大,而关键字的个数比较小。因此散列函数具有“压缩”功能。
由于散列函数通常是一个压缩映像,因此不可避免地会产生“冲突”现象

改进散列函数只能减少冲突,而不能绝对避免冲突
选定处理冲突的方法

散列(Hash)表:根据设定的散列函数H(key)和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集(区间)上,并以关键字在地址集中的“像”作为相应记录在表中的存储位置,这种表被称为散列表(哈希表)。
散列(Hash)造表或散列:映象过程
散列(Hash)地址:关键字的存储位置

设计Hash表的步骤
1.考虑选择一个“好”的、均匀的散列函数
2.选择一种处理冲突的方法

直接定址法:
取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key)=a×key+b(a,b为常数)。这种函数也叫自身函数。
特点:
直接定址所得地址集合和关键字集合的大小相同。因此不同关键字不会发生冲突,但在实际中使用很少。

数字分析法:
取关键字分布均匀的若干位或组合作散列地址
特点:
适于关键字位数较多而关键字个数较少的情况
且关键字已知

平方取中法
取关键字平方后的中间几位为散列地址。
依据:
1)通过“平方”扩大差别;
2)平方值的中间几位受到关键字中每一位的影响
特点:
适于无法预知全部关键字情况,或关键字的每一位都有某些数字重复出现频度很高
在这里插入图片描述折叠法:
将关键字分割成位数相同的几部分,取这几部分的叠加和为散列地址。
有:移位叠加和间界叠加两种
特点:
适于关键字位数很多,且每一位数字分布大致均匀
在这里插入图片描述除留余数法
取关键字被某个不大于散列表长m的数p除后所得余数作为散列地址。即:
H(key) = key MOD p (p≤m)
p的选择:一般p为≤m且接近m的质数或不含20以内质因数的合数。若p选不好易产生同义词
特点:
简单常用,也可与前面各方法结合使用

随机数法
取关键字的随机函数值为散列地址。即:
H(key)=Random(key)
特点:
当关键字长度不等时采用此法比较恰当。

采用散列函数需要考虑的因素
1.计算散列函数所需时间
2.关键字长度
3.散列表大小
4.关键字各位的分布
5.记录的查找频率

处理冲突:
在这里插入图片描述1.开放定址法:
在这里插入图片描述
在这里插入图片描述
2.再哈希法:在这里插入图片描述
3.链地址法
将所有关键字为同义词的记录存储在同一单链表中。每个地址的链表的头指针组织成一个向量。
4.公共溢出区法:
HashTable[0..m-1]:基本表,每个分量存放一个记录。
OverTable[0..v]:溢出表,所有关键字和基本表中关键字为同义词的记录,一旦发生冲突, 均填入溢出表。

散列造表的过程
在查找失败时在失败位置插入元素
散列表的查找:
查找过程和造表过程一致。给定K值,根据造表时设定的散列函数求得散列地址,若表中此位置上没有元素,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据造表时设定的处理冲突的方法找“下一地址”,直至散列表某个位置为“空”或者表中所填元素的关键字等于给定值时为止。
查找不成功:
若r[i] == NULL
查找成功:若r[i].key == K

分析:
虽然散列表在关键字与记录的存储位置之间建立了直接映象,但是由于冲突的存在,散列表的查找过程仍是一个给定值和关键字进行比较的过程,因此仍可以使用ASL作为散列表查找效率的量度。
查找过程中需和给定值比较的关键字个数取决于三个因素:
选用的哈希函数;
选用的处理冲突的方法;
散列表的装填因子α=n/m
其中
n:表中填入的记录数;
m:散列表长
(α越大,发生冲突的可能性越大 )
在这里插入图片描述在这里插入图片描述

ASL计算:
可参考:
https://www.cnblogs.com/ygsworld/p/10238729.html

排序

排序
八大排序

图片来源:https://www.cnblogs.com/hokky/p/8529042.html
在这里插入图片描述

直接插入排序

直接插入排序的核心思想就是:
将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
因此,从上面的描述中我们可以发现,直接插入排序可以用两个循环完成:

第一层循环:遍历待比较的所有数组元素
第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
如果:selected > ordered,那么将二者交换

我们在这里再次运用到了前面使用过的 “监视哨” 概念

监视哨:为能自动检验数组下标越界,在0下标处设置哨兵,若查找不成功,则循环会在0下标处自动终止,函数返回0。
监视哨的意义是防止下标越界,提高速度

在这里插入图片描述
使用监视哨版的直接插入算法:
在这里插入图片描述
未使用监视哨:

void print(int a[], int n ,int i){
	cout<<i <<":";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<" ";
	}
	cout<<endl;
}
 
 
void InsertSort(int a[], int n)
{
	for(int i= 1; i<n; i++){
		if(a[i] < a[i-1]){           //若第i个元素大于i-1元素,直接插入。
		                             //小于的话,移动有序表后插入
			int j= i-1;	
			int x = a[i];		 //复制为哨兵,即存储待排序元素
			a[i] = a[i-1];           //先后移一个元素
			while(x < a[j]){	 //查找在有序表的插入位置
				a[j+1] = a[j];
				j--;		 //元素后移
			}
			a[j+1] = x;		 //插入到正确位置
		}
		print(a,n,i);			//打印每趟排序的结果
	}
	
}
 
int main(){
	int a[8] = {3,1,5,7,2,4,9,6};
	InsertSort(a,8);
	print(a,8,8);
}

直接插入排序算法的性能分析
空间效率:
仅使用了常数个辅助单元,因而空间复杂度为O(1).
时间效率:
在排序过程中,向有序子表中逐个地插入元素的操作进行了nー1趟,每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为O(n).
在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,为∑i,总的移动次数也达到最大,为∑(i+1)
平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为n2/4。因此,直接插入排序算法的时间复杂度为O(n2).
稳定性:由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序方法。
适用性:直接插入排序算法适用于顺序存储和链式存储的线性表。为链式存储时,可以从前往后查找指定元素的位置。

希尔排序(插入排序)

希尔排序又叫缩小增量排序
基本思想:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

操作方法:

选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; 按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。

希尔排序在这里插入图片描述
版本2:
来源:https://blog.csdn.net/Void_leng/article/details/87812430

//C语言
void InsertSortWithgap(int arr[], int size, int gap)
{
	assert(arr);
	int i;
	int j;
	int temp;
	for (i = gap; i < size; ++i)//从下标为gap的元素作为带插入元素
	{
		temp = arr[i];//存储gap下标的元素
		for (j = i - gap; j >= 0; j -= gap)//从第 0 个元素进行比较
		{
			if (temp > arr[j])//降序
			{
				arr[j + gap] = arr[j];//gap位置的元素换成 j 所在下标的元素
			}
			else
			{
				break;
			}
		}
		arr[j + gap] = temp;//j 所在下标位置存储  temp
	}
}
	//希尔
void ShellSort(int arr[], int size)
{
	int gap = size;
	while (1)
	{
		gap = gap / 3 + 1;//改变gap 的值
		InsertSortWithgap(arr, size, gap);
		if (gap == 1)
		{
			break;	//gap的值为一时停止
		}
	}
}

希尔排序算法的性能分析
空间效率:
仅使用了常数个辅助单元,因而空间复杂度为O(1).
时间效率:
由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当n在某个特定范围时,希尔排序的时间复杂度约为O(n^1.3)
在最坏情况下希尔排序的时间复杂度为O(n^2).
稳定性:
当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,因此希尔排序是一种不稳定的排序方法。例如,图8.2中49与49的相对次序已发生了变化。
适用性:
希尔排序算法仅适用于线性表为顺序存储的情况。

简单选择排序

选择排序就是在待排序的数据中选择一个最大的或者最小的放在带待排序数据的末尾。也可以是已经排好序的数据的开头。
在这里插入图片描述简单选择排序算法的性能分析:
空间效率:
仅使用常数个辅助单元,故空间效率为O(1).
时间效率:
从上述伪码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过3(n-1)次,最好的情况是移动0次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是n(n-1)2次,因此时间复杂度始终是O(n^2)
稳定性:
在第i趟找到最小元素后,和第i个元素交换,可能会导致第个元素与其含有相同关键字元素的相对位置发生改变。例如,表L=(2,2,1},经过一趟排序后L={1,2,2},最终排序序列也是L={1,2,2},显然,2与2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序方法

堆排序(选择排序)

堆排序就要用到前面学到的数据结构二叉树的知识了。堆的作用是什么呢?就是寻找最值。所以根节点肯定最大(大堆)或者最小(小堆)。我们把根节点和最后一个结点进行交换,数组最后一个数肯定就是最大或者最小的。最后又在剩下的数据里面建堆。 要注意的是升序建大堆,降序建小堆。

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

在这里插入图片描述 堆排序的基本思想是:
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
在这里插入图片描述…………

快速排序

快速排序是对冒泡排序的改进,又称划分交换排序。

基本思想:

在待排序序列中任选一个记录作为枢轴,通过一趟排序将待排序记录分割成独立的两部分,其中前一部分记录的关键字均小于等于枢轴的关键字,后一部分记录的关键字都大于等于枢轴的关键字;
分别对这两个子序列按照同样方法再进行快速排序(划分),直到分割出的每个子序列只包含一个记录为止; 此时整个序列达到有序

一趟快排的过程:

选取待排序序列的第一个记录作为枢轴(或称支点),将其关键字暂存在变量pivotkey中;
设置两个指针(实际上是整型数,指示数组下标)low和high,其初值位置分别为待排序序列的下界和上界位置;
先从high所指位置开始向左搜索,找到第一个关键字小于pivotkey的记录,将它和枢轴记录互换位置;
接着从low所指位置开始向右搜索,找到第一个关键字大于pivotkey的记录,将它和枢轴记录互换位置;
重复以上两步,直至low=high为止。

一趟快速排序

int Partition(SqList &L,int low,int high)
 {  pivotkey=L.r[low].key;
    while(low<high)                             
    {     
       while(low<high && L.r[high].key>=pivotkey)  --high;
       L.r[low]<->L.r[high]; 
       while(low<high && L.r[low].key<=pivotkey)   ++low;
       L.r[low]<->L.r[high];
    }//while
    return low;       //返回枢轴所在位置 
 }// Partition

在这里插入图片描述
改进的一趟快速排序

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[low]=L.r[high];               
       while(low<high && L.r[low].key<=pivotkey)   ++low;
       L.r[high]=L.r[low];               
    }//while
    L.r[low]=L.r[0];
    return low;                                       
  }// Partition 

快速排序

 void QSort ( SqList &L,int low, int  high )
{  if  (low < high)
  {  pivotloc=Partition(L,low,high); 
     QSort (L, low, pivotloc-1) ;    //对低端子序列递归排序
     QSort (L, pivotloc+1, high );   //对高端子序列递归排序
  }
}// QSort

void QuickSort ( SqList &L )
{ //对L指示的顺序表进行快速排序
  QSort ( L, 1, L.length ); 
}  // QuickSort 

空间复杂度:O(logn),需要一个栈空间
若每趟排序都能将记录序列均匀分割成长度相近的两个子序列,则栈的最大深度为 (包括最外层参数进栈)
若每趟排序后枢轴都偏向子序列的一端,则栈的最大深度为n。
改进:在一趟排序后比较分割出的2个子序列的长度,然后先对长度短的子序列进行下一趟快排,这样栈的最大深度可降为O(logn)
稳定性:不稳定

时间复杂度:O(nlogn)
就平均时间而言,快速排序性能最好
若经过每一趟快速排序得到的两个子序列的长度基本相等,则快速排序的时间效率最高: O(nlogn)
若初始序列按关键字有序或基本有序(正序或逆序),则枢轴总是子序列中关键字最小或最大的记录,这样每趟快排划分出的两个子序列都有一个接近空序列,此时快速排序将蜕化为冒泡排序:O(n2)
在这里插入图片描述

归并查找

posted @ 2021-12-20 19:25  CK_0ff  阅读(2271)  评论(2编辑  收藏  举报