一、绪论

程序=数据结构+算法

(1)基本的数据结构

     1. 线性结构- 线性表- 栈和队列- 串- 数组和广义表
  1. 非线性结构

用计算机解题一个问题的步骤

  1. 具体问题抽象为数学模型
  2. 设计算法
  3. 编程、调试、运行

数据结构是一门研究非数值计算的程序设计中计算机的操作对象以及它们之间的关系和操作的学科

(2)基本概念和术语

  1. 数据(Data)

    是能输入计算机且能被计算机处理的各种符号的集合

    • 信息的载体
    • 对客观事物符号化的表示
    • 能够被计算机识别、存储和加工

    数值型的数据:整数、实数等

    非数值型的数据:文字、图像、图形、声音等

  2. 数据元素(Data Element)

    • 是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理
    • 也简称为元素,或称为记录、结点和顶点
  3. 数据项(Data Item)

    • 构成数据元素的不可分割的最小单位
  4. 数据对象(Data Object)

    • 是性质相同的数据元素的集合,是数据的一个子集
  5. 数据结构(Data Structure)

    • 数据元素相互之间的关系称为结构
    • 是指相互之间存在一种或多种特定关系的数据元素集合
    • 数据结构是带结构的数据元素的集合

    a. 数据元素之间的逻辑关系,称为逻辑结构

    b. 数据元素及其关系在计算机内存中的表示(映像),称为物理结构或存储结构

(3)数据结构的两个层次

  1. 逻辑结构
  • 描述数据元素之间的逻辑关系

    • 与数据的存储无关,独立于计算机
    • 是从具体问题抽象出来的数学模型
  1. 物理结构(存储结构)
  • 数据元素及其关系在计算机存储器中的结构(存储方式)

    • 是数据结构在计算机中的表示

关系:

  • 存储结构是逻辑关系的映像与元素本身的映像
  • 逻辑结构是数据结构的抽象,存储结构是数据结构的实现

(4)逻辑结构的种类

  1. 线性结构

有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后继。

例如:线性表、栈、队列、串

  1. 非线性结构

一个结点可能有多个直接前驱和直接后继

例如:树、图

(5)四种基本逻辑结构

  1. 集合结构
  2. 线性结构
  3. 树形结构
  4. 图形结构或网状结构

(6)四种基本的存储结构

  1. 顺序存储结构

    • C语言中用数组来实现顺序存储结构
  2. 链式存储结构
    • C语言中用指针来实现链式存储结构
  3. 索引存储结构
    • 在存储结点信息的同时,还建立附件的索引表
  4. 散列存储结构

(7)数据类型

(Data Type)

定义:数据类型是一组性质相同的值的集合以及定义于这个值集合上的一组操作的总称

  • C语言中,提供int,char,float,double等基本数据类型
  • 数组、结构、共用体、枚举等构造数据类型
  • 指针、空(void)类型
  • 自定义数据类型(typedef)

(8)抽象数据类型

(Abstract Data Type,ADT)

是指一个数学模型以及定义在此数学模型上的一组操作。

  • 由用户定义,从问题抽象出数据模型(逻辑结构)
  • 还包括定义在数据模型上的一组抽象运算(相关操作)
  • 不考虑计算机内的具体存储结构与运算的具体实现算法

抽象数据类型可用(D,S,P)三元组表示:D是数据对象,S是D上的关系集,P是对D的基本操作集

一个抽象数据类型的定义格式如下:

ADT 抽象数据类型名{数据对象:<数据对象的定义> //伪代码数据关系:<数据关系的定义>   //伪代码基本操作:<基本操作的定义>   //基本操作名(参数表)、初始条件、操作结果
}ADT 抽象数据类型名

参数表:赋值参数,只为操作提供输入值

​ 引用参数,以&打头,除可提供输入值外,还将返回操作结果

初始条件:操作执行之前数据结构和参数应满足的条件

操作结果:操作正常完成之后,数据结构的变化状况和应返回的结果

例如:Circle的定义

ADT Cirle{数据对象:D={r,x,y|r,x,y均为实数}数据关系:R={<r,x,y>|r是半径,<x,y>是圆心坐标}基本操作:Circle(&C,r,x,y)操作结果:构造一个圆double Area(C)初始条件:圆已存在。操作结果:计算面积。double Circumference(C)初始条件:圆已存在。操作结果:计算周长。
}ADT Circle

复数的定义

ADT Complex{D={r1,r2|r1,r2都是实数}S={<r1,r2>|r1是实部,r2是虚部}assign(&C,v1,v2)初始条件:空的复数C已存在操作结果:构造复数C,r1,r2分别被赋以参数v1,v2的值destroy(&C)初始条件:复数C已存在操作结果:复数C被销毁
}ADT Complex

(9)用C语言实现抽象数据类型

复数的实现

typedef struct{float realpart;   /*实部*/float imagpart;   /*虚部*/
}Complex            /*定义复数抽象类型*/
//函数声明
void assign(Complex *A,float real,float imag);      /*赋值*/
void add(Complex *A,flaot real,float imag);         /*A+B*/
void minus(Complex *A,flaot real,float imag);       /*A-B*/
void multiply(Complex *A,flaot real,float imag);    /*A*B*/
void divide(Complex *A,flaot real,float imag);      /*A/B*/
//函数定义
Void assign(Complex *A,float real,float imag){A->realpart=real;     /*实部赋值*/A->imagpart=imag;       /*虚部赋值*/
}
void add(Complex *c,Complex A,Complex B){   /*c=A+B*/c->relpart=A.realpart+B.realpart;       /*实部相加*/c->imagpart=A.imagpart+B.imagpart;     /*虚部相加*/
}

(10)算法和算法分析

  1. 算法的定义

    对特定问题求解方法和步骤的一种描述,它是指令的有限序列。

  2. 算法的描述

    自然语言:英语、中文

    流程图:传统流程图、NS流程图

    伪代码:类语言:类C语言

    程序代码:C语言程序、JAVA语言程序

  3. 算法与程序

    算法是解决问题的一种方法或一个过程,考虑如何将输入转换成输出,一个问题可以有多种算法。

    程序是用某种程序设计语言对算法的具体实现。

    程序=数据结构+算法

  4. 算法特性

    有穷性、确定性、可行性、输入、输出

  5. 算法设计的要求

    正确性(Correctness)、可读性(Readability)、健壮性(Robustness)、高效性(Efficiency)

  6. 算法的效率

    • 时间效率:算法所耗费的时间
    • 空间效率:算法执行过程中所耗费的存储空间

    时间效率和空间效率有时候是矛盾的。

  7. 算法时间效率的度量

    • 事后统计:将算法实现,测算其时间和空间开销
    • 事前分析:对算法所消耗资源的一种估算方法

    算法运行时间=一个简单操作所需的时间*简单操作次数

    for(i=1;i<=n;i++)                             //n+1次for(j=1;j<=n;j++)                         //n(n+1)次c[i][j]=0;                               //n*n次for(k=0;k<n;k++)                        //n*n*(n+1)次c[i][j]=c[i][j]+a[i][k]*b[k][j]; //n*n*n次
    
  8. 算法的渐进时间复杂度

    T(n)=O(f(n)) O是数量级的符号

    简称时间复杂度。

    方法:

    ​ 忽略所有低次幂项和最高次幂系数

    • ​ 找出语句频度最大的那条语句最为基本语句

    • ​ 计算基本语句的频度得到问题规模n的某个函数f(n)

    • ​ 取其数量级用符号“O”表示

    //分析以下程序段的时间复杂度
    i=1;
    while(i<=n)i=i*2;
    //若循环执行1次:i=1*2=2
    //若循环执行2次:i=2*2
    //若循环执行3次:i=2*2*2
    //若循环执行x次:i=2^x
    //因为i<=n,所以2^x<=n,所以x<=log2n,所以T(n)=O(log2n)=O(lgh)
    
    • 最坏时间复杂度、平均时间复杂度、最好时间复杂度

    • 时间复杂度T(n)按数量级递增顺序为:

      常数阶 对数阶 线性阶 线性对数阶 平方阶 立方阶 K次方阶 指数阶
      O(1) O(log2n) O(n) O(nlog2n) O(n^2) O(n^3) O(n^k) O(2^n)
  9. 渐进空间复杂度

    算法所需存储空间的度量

    记作:S(n)=O(f(n)) n为问题的规模(或大小)

二、线性表

(1)线性表的定义和特点

  • 线性表(Linear List)是具有相同特性的数据元素的一个有限序列。
  • 由n(n>=0)个数据元素(结点)a1,a2,…,an组成的有限序列
  • 数据元素的个数n定义为表的长度
  • 当n=0时称为空表

顺序存储结构存在的问题:

  • 存储空间分配不灵活
  • 运算的空间复杂度高

(2)线性表的类型定义

基本操作 功能 操作结果
InitList(&L) 初始化 构建一个空的线性表L
DestroyList(&L) 销毁 销毁线性表L
ClearList(&L) 清除 将线性表L重置为空表
ListEmpty(L) 判断是否为空 若线性表L为空表,则返回TRUE,否则返回FALSE
ListLength(L) 求长度 返回线性表L中的数据元素个数
GetElem(L,i,&e) 获取元素 用e返回线性表L中第i个数据元素的值(1<=i<=ListLength(L))
LocateElem(L,e,compare()) 查找搜索 返回L中第一个与e满足compare()的数据元素的位序。元素不存在则返回值为0
PriorElem(L,cur_e,&pre_e) 求前驱 cur_e不是第一个数据元素
NextElem(L,cur_e,&next_e) 求后继 cur_e不是第最后个数据元素
ListInsert(&L,i,e) 插入 在L的第i个位置之前插入新的数据元素e,L的长度加一(1<=i<=ListLength(L)+1)
ListDelete(&L,i,&e) 删除 删除L的第i个数据元素,并用e返回其值,L的长度减一(1<=i<=ListLength(L))
ListTraverse(&L,visited()) 遍历 依次对线性表中每个元素调用visited()

(3)线性表的顺序表示

线性表的顺序表示又称为顺序存储结构或顺序映像

顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构

  1. 顺序存储结构:
  • 依次存储,地址连续——中间没有空出存储单元,是一个典型的线性表顺序存储结构。

  • 地址不连续——中间存在空的存储单元,不是一个线性表顺序存储结构。

    LOC(ai+1)=LOC(ai)+l

所有数据元素的存储位置均可由第一个数据元素的存储位置得到:

​ LOC(ai)=LOC(a1)+(i-1)*l

  1. 优点:
  • 以物理位置相邻表示逻辑关系
  • 任一元素均可随机存取
  1. 表示

    顺序表(元素)>地址连续、依次存放、随机存取、类型相同>数组(元素)==>用一维数组表示顺序表

    线性表长可变,数组长度不可动态定义

    #define LIST_INIT SIZE 100  //线性表存储空间的初始分配量
    typedef struct{ElemType elem[LIST INIT SIZE;]int length;            //当前长度
    }SqList;
    

    多项式的顺序存储结构类型定义:

    #define MAXSIZE 1000     //多项式可能达到的最大长度
    typedef struct{float p;             //系数int w;                  //指数
    }Ploynomial;
    typedef struct{Polynomial *elem     //存储空间的基地址int length;               //多项式中当前项的个数
    }SqList;                    //多项式的顺序存储结构类型为SqList
    

    图书表的顺序存储结构类型定义:

    #define MAXSIZE 10000        //图书表可能达到的最大长度
    typedef struct{             //图书信息定义char no[20];            //图书ISBNchar name[50];          //图书名字float price;          //图书价格
    }Book;
    typedef struct{Book *elem;              //存储空间的基地址int length;               //图书表中当前图书个数
    }SqList;                    //图书表的顺序存储结构类型为SqList
    

    顺序表(Sequence List)

    • 逻辑位序(0开始)和物理位序相差1
    //数组动态分配
    typedef struct{ElemType *elem;int length;
    }SqList;
    L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);//malloc(m)函数,开辟m字节长度的地址空间,并返回这段空间的首地址
    //sizeof(x)运算,计算变量x的长度
    //free(p)函数,释放指针p所指变量的存储空间,即彻底删除一个变量
    //需要加载头文件:<stdlib.h>
    
    //数组静态分配
    #define MAXSIZE 100
    typedef struct{ElemType elem [MAXSIZE];int length;
    }SqList;    //定义顺序表类型
    SqList L;       //定义变量L,L是SqList这种类型的,L是个顺序表//引用成员L.elem、L.length
    SqList *L       //引用成员L->elem、L->length
    
    typedef char ElemType;
    typedef int ElemType;
    

    预定义常量和类型:

    //函数结果状态代码
    #define TRUE 1
    #define FALSE 0
    #define OK 1
    #define ERROR 0
    #define INFEASIBLE -1
    #define OVERFLOW -2
    //Status 是函数的类型,其值是函数结果状态代码
    typedef int Status;
    typedef char ElemType;
    

(4)线性表的顺序实现

  1. 线性表L的初始化

    Status InitList_Sq(SqList &L){      //构造一个空的顺序表LL.elem=new ElemType[MAXSIZE];  //为顺序表分配空间if(!L.elem) exit(OVERFLOW);       //存储分配失败L.length=0;                        //空表长度为0return OK;
    }
    
  2. 销毁线性表

    void DestroyList(SqList &L){if(L.elem)delete L.elem;    //释放存储空间
    }
    
  3. 清空线性表L

    void ClearList(SqList &L){L.length=0;                 //将线性表的长度置为0
    }
    
  4. 求线性表L的长度

    int GetLength(SqList){return (L.length);
    }
    
  5. 判断线性表L是否为空

    int IsEmpty(SqList L){if(L.length==0) return 1;else return 0;
    }
    
  6. 顺序表的取值

    根据位置i获取相应位置数据元素的内容

    int GetElem(SqList L,int i,ElemType &e){if(i<1||i>L.length)return ERROR;   //判断i值是否合理,若不合理返回errore=L.elem[i-1];                        //第i-1的单元存储着第i个数据return OK;
    }
    
  7. 顺序表的查找

    int LocateElem(SqList L,ElemType e){
    //在线性表L中查找值为e的数据元素,返回其序号(是第几个元素)for(i=0;i<L.length;i++)if(L.elem[i]==e) return i+1;     //查找成功,返回序号return 0;                             //查找失败,返回0
    }
    

    平均查找长度ASL(Average Search Length)

  8. 顺序表的插入

    Status ListInsert_Sq(SqList &L,int i,ElemType e){if(i<1||i>L.length+1) return ERROR;        //i值不合法if(L.length==MAXSIZE) retrun ERROR;        //当前存储空间已满for(j=L.length-1;j>=i-1;j--)L.elem[j+1]=L.elem[j];             //插入位置及之后的元素后移L.elem[i-1]=e;                           //将新元素e放入第i个位置L.length++;                             //表长增1return OK;
    }
    
  9. 顺序表的删除

    Status ListDelete_Sq(SqList &L,int i){if((i<1)||(i>L.length)) return ERROR;for(j=i;j<=L.length-1;j++)L.elem[j-1]=L.elem[j];L.length--;return OK;
    }
    
  10. 顺序表的操作算法分析

    • 时间复杂度

      查找、插入、删除算法的平均时间复杂度为O(n)

    • 空间复杂度

      S(n)=O(1) (没有占用辅助空间)

  • 优点

    (1)存储密度大

    (2)可以随机存取表中任一元素

  • 缺点

    (1)在插入、删除某一元素时,需要移动大量元素

    (2)存储空间不灵活,浪费存储空间

    (3)属于静态存储形式,数据元素的个数不能自由扩充

(5)线性表的链式表示

  • 链式存储结构:结点在存储器中的位置时任意的,即逻辑上相邻的数据元素在物理上不一定相邻

  • 线性表的链式表示又称为非顺序映像或链式映像

  • 用一组物理位置任意的存储单元来存放线性表的数据元素

  • 这组存储单元既可以是连续的,也可以是不连续的

  • 链表中元素的逻辑次序和物理次序不一定相同

  • 单链表是由头指针唯一确定,因此单链表可以用头指针的名字来命名

  • 各结点有两个域组成:

    数据域:存储袁术数值数据

    指针域:存储直接后继结点的存储位置

  • 相关术语:

    1.结点:数据元素的存储映像。由数据域和指针域两部分组成

    2.链表:n个结点由指针链组成一个链表,它是线性表的链式存储映像,称为线性表的链式存储结构

  • 分类:

    1.单链表:结点只有一个指针域的链表,称为单链表或线性链表

    2.双链表:结点有两个指针域的链表

    3.循环链表:首尾相接的链表

  • 空表:

    无头结点是,头指针为空时表示空表

    有头结点时,当头结点的指针域为空时表示空表

  • 特点:

    1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻

    2.访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等(顺序存取法)

(6)单链表的定义

typedef struct Lnode{        //生命结点的类型和指向结点的指针类型ElemType data;           //结点的数据域struct Lnode *next;     //结点的指针域
}Lnode,*LinkList;           //LinkList为指向结构体Lnode的指针类型
LinkList L;  //定义链表L:
Lnode *p;
LinkList p; //定义结点指针p:
typedef Struct student{char num[8];char name[8];int score;struct student *next;
}Lnode,*LinkList;
typedef Struct{char num[8];char name[8];int score;
}ElemType;typedef struct Lnode{ElemType data;struct Lnode *next;
}Lnode,*LinkList;

(7)单链表基本操作

  1. 单链表的初始化

    即构造一个空表

    Status InitList L(LinkList &L){L=new LNode;  //或L=(LinkList)malloc(sizeof(LNode));L->next=NULL;return OK;
    }
    
  2. 判断链表是否为空

    int ListEmpty(LinkList){ //若L为空表,则返回1,否则返回0if(L->next)              //非空return 0;elsereturn 1;
    }
    
  3. 单链表的销毁

    Status DestroyList_L(LinkList &L){Lnode *p;            //或LinkList p;while(L){p=L,L=L->next;delete p;}return OK;
    }
    
  4. 清空单链表

    Status  ClearList(LinkList &L){ //将L重置为空表Lnode *p,*q;               //或LinkList p,q;p=L->next;while(p){                 //没到表尾q=p->next;delete p;p=q;}L->next=NULL;                //头结点指针域为空return OK;
    }
    
  5. 求单链表的表长

    int ListLengt_L(LinkList L){      //返回L中数据元素个数LinkList p;p=L->next;                       //p指向第一个结点i=0;while(p){                     //遍历单链表,统计结点数i++;p=p->next;}return i;
    }
    
  6. 取值

    //获取线性表中的某个数据元素的内容,通过变量e返回
    Status GetElem_L(LinkList L,int i,ElemType &e){p=L->next;j=1;                      //初始化while(p&&j<i){              //向后扫描,知道p指向第i个元素或p为空p=p-next;++j;}if(!p||j>i)return ERROR;    //第i个元素不存在e=p->data;return OK;
    }//GetElem_L
    
  7. 查找

    //1.
    Lnode *LocateElem_L(LinkList L,Elemtype e){//在线性表L中查找值为e的数据元素//找到,则返回L中值为e的数据元素的地址,查找失败返回NULLp=L->next;while(p&&p->data!=e)p=p->next;return p;
    }
    //2.
    int LocateElem_L(LinkList L,Elemtype e){//在线性表L中查找值为e的数据元素的位置序号//返回L中值为e的数据元素的位置序号,查找失败返回0p=L-next;j=1;while(p&&p->data!=e){p=p->next;j++;}if(p) return j;else return 0;
    }
    
  8. 插入

    Status ListInsert_L(LinkList &L,int i,ElemType e){p=L;j=0;while(p&&j<i-1){p=p->next;++j;            //寻找第i-1个结点,p指向i-1结点}if(!p||j>i-1)return ERROR;   //i大于表长+1或者小于1,插入位置非法s=new LNode;              //生成新结点s,将结点s的数据域置为es->data=e;                   //将结点s插入L中s-next=p-next;p-next=s;return OK;
    }//ListInsert_L
    
  9. 删除

    //将线性表L中第i个数据元素删除
    Status ListDelete_L(LinkList &L,int i,ElemType &e){p=L;j=0;while(p-next&&j<i-1){p=p-next;++j;                   //寻找第i个结点,并令p指向其前驱}if(!(p->next)||j>i-1)return ERROR;  //删除位置不合理q=p->next;                         //临时保存被删结点的地址以备释放p->next=q->next;                    //改变删除结点前驱结点的指针域e=q->data;                          //保存删除结点的数据域delete q;                           //释放删除结点的空间return OK;
    }//ListDelete_L
    
  10. 单链表的建立

    • 头插法——元素插入在链表头部
    void CreateList_H(LinkList &L,int n){L=new LNnode;L->next=NULL;          //先建立一个带头结点的单链表for(i=n;i>0;--i){p=new LNode;       //生产新结点//p=(LNode*)malloc(sizeof(LNode));scanf(&p->data);   //输入元素值p->next=L-next;      //插入到表头L->next=p;}
    }//CreateList_H
    //算法的时间复杂度是O(n)
    
    • 尾插法——元素插入在链表尾部
    void CreateList_R(LinkList &L,int n){L=new LNode;L->next=NULL;r=L;          //尾指针r指向头结点for(i=0;i<n;++i){p=(LNode*)malloc(sizeof(LNode));;scanf(&p->data);p->next=NULL;r->next=p;r=p;}
    }//CreateList_R
    //算法的时间复杂度是O(n)
    

(8)单链表算法时间效率分析

  1. 查找

    因线性链表只能顺序存取,即在查找时要从头指针找起,查找时间复杂度为O(n)

  2. 插入和删除

    因线性表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)

(9)循环链表

头尾相接的链表——表中最后一个结点的指针域指向头结点,整个链表形成一个环

优点:从表中任一结点出发均可找到表中其他结点

循环条件:

​ p != L

​ p->next != L

带尾指针循环链表的合并:

LinkList Connect(LinkList Ta,LinkList Tb){   //假设Ta、Tb都是非空的单循环链表p=Ta->next;                              //p存表头结点Ta->next=Tb->next-next;                  //Tb表头连结Ta表尾delete Tb->next;                     //释放Tb表头结点Tb->next=p;                               //修改指针return Tb;
}

(10)双向链表

在单链表的每个结点里再增加一个指向其直接前驱的指针域prior,形成两个方向不同的链

  • 双向循环链表

    头结点的前驱指针指向链表的最后一个结点

    最后一个结点的后继指针指向头结点

  • 双向链表的对称性

    p->prior->next = p = p->next->prior

  1. 双链表结构定义:

    typedef struct DuLNode{Elemtype data;struct DuLNode *prior,*next;
    }DuLNode,*DuLinkList;
    
  2. 双链表的插入

    void ListInsert_DuL(DuLinkList &L,Int i,ElemType e){//在带头结点的双向循环链表L中第i个位置之前插入元素eif(!(p=GetElemP_Dul(L,i))) return ERROR;s=new DulNode;s->data=e;s->prior=p->prior;p->prior->next=s;s->next=p;p->prior=s;return OK;
    }//ListInsert_DuL
    
  3. 双向链表的删除

    void ListDelete_DuL(DuLink &L,Int i,ElemType &e){if(!(p=GetElemP_DuL(L,i))) return ERROR;e=p->data;p->prior->next=p->next;p->next->prior=p->prior;free(p);return OK;
    }//ListDelete_DuL
    

(11)顺序表和链表的比较

  • 链式存储结构
  1. 优点

    • 结点空间可以动态申请和释放
    • 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动元素
  2. 缺点

    • 存储密度小,每个结点的指针域需额外占用存储空间

      (存储密度=结点数据本身占用的空间/结点占用的空间总量)

      存储密度越大,存储空间的利用率就越高。顺序表存储密度为1(100%),链表的存储密度小于1

    • 链式存储结构是非随机存取结构,对任一结点的操作都要从头指针依指针链查找到该结点,增加了算法的复杂度

顺序表 链表
存储空间 预先分配,会导致空间闲置或溢出现象 动态分配
存储密度 不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度=1 需要借助指针来体现元素间的逻辑关系,存储密度小于1
存取元素 随机存取,按位置访问元素的时间复杂度为O(1) 顺序存取,按位置访问元素的时间复杂度为O(n)
插入、删除 时间复杂度为O(n) 不需移动元素,时间复杂度为O(1)
适用情况 1. 表变化不大,且能事先确定变化的范围2. 很少进行插入或删除操作,经常按元素位置序号访问数据元素 1. 长度变化较大2. 频繁进行插入或删除操作

(12)线性表的合并

void union(List &La,List Lb){La_len=ListLength(La);Lb_len=ListLength(Lb);for(i=1;i<=Lb_len;i++){GetElem(Lb,i,e);if(!LocateElem(La,e))ListInsert(&La,++La_len,e);}
}

(13)有序表的合并

  • 顺序表
void  MergeList_Sq(SqList LA,SqList LB,SqList &LC){pa=LA.elem;pb=LB.elem;          //指针pa和pb的处置分别指向两个表的第一个元素LC.length=LA.length+LB.length;   //新表长度为待合并两表的长度之和LC.elem=new ElemType[LC.length];//为合并后的表分配一个数组空间pc=LC.elem;          //指针pc指向新表的第一个元素pa_last=LA.elem+LA.length-1;  //指针pa_last指向LA表的最后一个元素pb_last=LB.elem+LB.length-1;   //指针pb_last指向LB表的最后一个元素while(pa<=pa_last && pb<=pb_last){   //两个表都非空if(*pa<=*pb) *pc++=*pa++;  //依次“摘要”两表中值较小的结点else *pc++=*pb++;}while(pa<=pa_last)  *pc++=*pa++;//LB表已到达表尾,将LA中剩余元素加入LCwhile(pb<=pb_last)   *pc++=*pb++;//LA表已到达表尾,将LB中剩余元素加入LC
}//MergeList_Sq
  • 链表
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){pa=La->next;pb=Lb->next;pc=Lc=La;         //用La的头结点作为Lc的头结点while(pa&&pb){if(pa->data <= pb->data){pc->next=pa;pc=pa;pa=pa->next;}else{pc->next=pb;pc=pb;pb=pb->next;}}pc->next=pa?pa:pb;  //插入剩余段delete Lb;           //释放Lb的头结点
}

(14)多项式的实现

void CreatePolyn(Polynomial &P,int n){//输入m项的系数和指数,建立表示多项式的有序链表pP=new PNode;p->next=NULL;        //先建立一个带头结点的单链表for(i=2;i<=n;++i){    //依次输入n个非零项s=new PNode;    //生成新结点cin>>s->code>>s->expn; //输入系数和指数pre=P;            //pre用于保存q的前驱,值为头结点q=P->next;        //q初始化,指向首元结点while(q&&q->expn<s->expn){ //找到第一个大于输入项指数的项*qpre=q;q=q->next;}s->next=q;pre->next=s;}
}

(15)图书信息管理

struct Book{char id[20];char name[50];int price;
};typedef struct{Book *elem;int length;
}SqList;typedef struct LNode{Book data;struct LNode *next;
}LNode,*LinkList;

三、栈和队列

栈和队列是限定插入和删除只能在表的“端点“进行的线性表(操作受限)

(1)栈

1.定义

  • 栈(stack),一种特殊的线性表,是限定表尾进行插入和删除操作的线性表
  • 又称为后进先出(Last In First Out)的线性表,简称LIFO结构
  • 表尾称为栈顶Top;表头称为栈底Base
  • 插入元素到栈顶(即表尾)的操作,称为入栈
  • 从栈顶(即表尾)删除最后一个元素的操作,称为出栈
  • “入”=压入=PUST(x) “出”=弹出=POP(y)

2.表示

操作 描述
InitStack(&S) 初始化操作
DestoryStack(&S) 销毁栈操作
StackEmpty(S) 判定S是否为空栈
StackLength(S) 求栈的长度
GetTop(S,&e) 取栈顶元素
ClearStack(&S) 栈置空操作
Push(&S,e) 入栈操作
Pop(&S,&e) 出栈操作

3.实现

A. 顺序栈

  • top指针,指示栈顶元素之上
  • base指针,指示栈底元素
  • stacksize表示栈可使用的最大容量
  • 空栈:base==top是栈空标志
  • 栈满;top-base==stacksize
  • 栈满时的处理方法:
    • 1.报错,返回操作系统
    • 2.分配更大的空间,作为栈的存储空间,将原栈的内容移入新栈
  • 使用数组作为顺序栈存储方式的特点:简单、方便、但易产生溢出(数组大小固定)
  • 上溢(overflow)、下溢(underflow)
#define MAXSIZE 100
typedef struct{SElemType *base; //栈底指针SElemType *top;       //栈顶指针int stacksize;        //栈可用最大容量
}SqStack;
  1. 顺序栈的初始化
Status InitStack(SqStack &S){    //构造一个空栈S.base=new SElemType[MAXSIZE];//S.base=(SElemType*)malloc(MAXSIZE*sizeof(SElemType));if(!S.base)exit (OVERFLOW);  //存储分配失败S.top=S.base;  //栈顶指针等于栈底指针S.stacksize=MAXSIZE;return OK;
}
  1. 判断栈是否为空
Status StackEmpty(SqStack S){if(S.top==S.base)return TRUE;elsereturn FALSE;
}
  1. 求栈的长度
int StackLength(SqStack S){return S.top-S.base;
}
  1. 清空栈
Status ClearStack(SqStack S){if(S.base)S.top=S.base;return OK;
}
  1. 销毁顺序栈
Status DestoryStack(SqStack &S){if(S.base){delete S.base;S.stacksize=0;S.base=S.top=NULL;}return OK;
}
  1. 入栈
Status Push(SqStack &S,SElemType e){if(S.top-S.base==Stacksize)//栈满return ERROR;*S.top++=e;return OK;
}
  1. 出栈
Status Pop(SqStack &S,SElemType &e){if(S.top==S.base) return ERROR;e=*--S.top;return OK;
}

B. 链栈

  1. 表示
typedef struct StackNode{SElemType data;struct StackNode *next;
}StackNode,*LinkStack;
LinkStack S;
  1. 初始化
void InitStack(LinkStack &S){//构造一个空栈,栈顶指针置为空S=NULL;return OK;
}
  1. 判断栈是否为空
Status StackEmpty(LinkStack S){if(S==NULL)return TRUE;else return FALSE;
}
  1. 入栈
Status Push(LinkStack &S,SElemType e){p=new StackNode;p->data=e;p->next=S;S=p;return OK;
}
  1. 出栈
Status Pop(LinkStack &S,SElemType &e){if(S==NULL)return ERROR;e=S->data;p=S;S=S->next;delete p;return OK;
}
  1. 取栈顶元素
SElemType GetTop(LinkStack S){if(S!=NULL)return S-data;
}

4. 栈与递归

  • 若一个对象部分地包含它自己,或用它自己给自己定义,则称这个对象是递归的;

  • 若一个过程直接地或间接地调用自己,则称这个过程是递归的过程。

  • 函数调用过程

    • 调用前,系统完成

      1. 将实参,返回地址等传递给被调用函数
      2. 为被调用函数的局部变量分配存储区
      3. 将控制转移到被调用函数的入口
    • 调用后,系统完成:
      1. 保存被调用函数的计算结果
      2. 释放被调用函数的数据区
      3. 依照被调用函数保存的返回地址将控制转移到调用函数
  • 嵌套调用:遵循后调用的先返回

  • 递归工作栈——递归程序运行期间使用的数据存储区

long Fact(long n){if(n==0)return 1;else return n*Fact(n-1);
}
//两者等同
long Fact(long n){t=1;for(i=1;i<=n;i++)t=t*i;return t;
}

(2)队列

1.定义

  • 队列(queue)是一种先进先出(First In First Out----FIFO)的线性表。

  • 在表一端插入(表尾),在另一端(表头)删除

  • 入队、出队

  • 表达式的组成

    操作数(operand):常数、变量

    运算符(operator):算术运算符、关系运算符、逻辑运算符

    界限符(delimiter):左右括弧和表达式结束符

2.表示

操作 描述
InitQueue(&Q) 构造空队列Q
DestoyQueue(&Q) 销毁队列
ClearQueue(&Q) 清空队列
QueueLength(Q) 求队长
GetHead(Q,&e) 返回对头元素
EnQueue(&Q,e) 插入队尾元素
DeQueue(&Q,&e) 删除对头元素

3.实现

A. 顺序队列

  1. 循环队列的类型定义
#define MAXSIZE 100
Typedef struct{QElemType *base; //初始化的动态分配存储空间int front;            //头指针int rear;          //尾指针
}SqQueue;
  1. 队列的初始化
Status InitQueue(SqQueue &Q){Q.base=new QElemType[MAXQSIZE];        //分配数组空间//Q.base=(QElemType*)malloc(MAXQSIZE*sizeof(QElemType));if(!Q.base)exit(OVERFLOW);         //存储分配失败Q.front=Q.rear=0;                 //头指针尾指针置为0,队列为空return OK;
}
  1. 求队列的长度
int QueueLength(SqQueue Q){return((Q.rear-Q.front+MAXQSIZE)%MAXQSIZE);
}
  1. 循环队列的入队
Status EnQueue(SqQueue &Q,QElemType e){if((Q.rear+1)%MAXQSIZE==Q.front)return ERROR;      //队满Q.base[Q.rear]=e;                                  //新元素加入队尾Q.rear=(Q.rear+1)%MAXQSIZE;                          //队尾指针+1return OK;
}
  1. 循环队列的出队
Status DeQueue(SqQueue &Q,QElemType &e){if(Q.front==Q.rear)return ERROR;               //队空e=Q.base[Q.front];                             //保存队头元素Q.front=(Q.front+1)%MAXQSIZE;                 //队头指针+1return OK;
}
  1. 取队头元素
QElemType GetHead(SqQueue Q){if(Q.front!=Q.rear)            //队列不为空return Q.base[Q.front];  //返回队头指针元素的值,队头指针不变
}

B. 链式队列

  1. 链队列的类型定义
#define MAXQSIZE 100         //最大队列长度
typedef struct Qnode{QElemType data;struct Qnode *next;
}QNode,*QueuePrt;typedef struct{QueuePrt front;     //队头指针QueuePrt rear;        //队尾指针
}LinkQueue;
  1. 链队列初始化
Status InitQueue(LinkQueue &Q){Q.front=Q.rear=(QueuePrt)malloc(sizeof(QNode));if(!Q.front)exit(OVERFLOW);Q.front->next=NULL;return OK;
}
  1. 销毁链队列
Status DestroyQueue(LinkQueue &Q){while(Q.front){p=Q.front->next;free(Q.front);Q.front=p;}return OK;
}
  1. 链队列的入队
Status EnQueue(LinkQueue &Q,QElemType e){p=(QueuePtr)malloc(sizeof(QNode));if(!p)exit(OVERFLOW);p->data=e;p->next=NULL;Q.rear->next=p;Q.rear=p;return OK;
}
  1. 链队列的出队
Status DeQueue(LinkQueue &Q,QElemType &e){if(Q.front==Q.rear)return ERROR;p=Q.front->next;e=p->data;Q.front->next=p->next;if(Q.rear==p)Q.rear=Q.front;delete p;return OK;
}
  1. 求链队列的队头元素
Status GetHead(LinkQueue Q,QElemType &e){if(Q.front==Q.rear)return ERROR;e=Q.front->next->data;return OK;
}

四、串、数组和广义表

(1) 串

1. 串的定义

  • 串(String)——零个或多个任意字符组成的有限序列

  • s=“a1a2…an”(n>=0)

  • 空串——n=0

  • 子串:串中任意个连续字符组成的子序列称为该串的子串

    真子串:指不包含自身的所有子串

  • 主串:包含字串的串相应地称为主串

  • 字符位置:字符在序列中的序号为该字符在串中的位置

  • 字串位置:字串第一个字符在主串中的位置

  • 空格串:由一个或多个空格组成的串

  • 串相等:当且仅当两个串的长度相等并且各个对应位置上的字符都相同时,这两个串才是相等的

    所有空串是相等的

2. 串的操作

操作 描述
StrAssign(&T,chars) 串赋值
StrCompare(S,T) 串比较
StrLength(S) 求串长
Concat(&T,S1,S2) 串连结
SubString(&Sub,S,pos,len) 求子串
StrCopy(&T,S) 串拷贝
StrEmpty(S) 串判空
ClearString(&S) 清空串
Index(S,T,pos) 子串的位置
Replace(&S,T,V) 串替换
StrInsert(&S,pos,T) 子串插入
StrDelete(&S,pos,len) 子串删除
DestroyString(&S) 串销毁

3. 串的存储结构

A.顺序存储结构

#define MAXLEN 255
typedef struct{char ch[MAXLEN+1];      //存储串的一维数组int length;               //串的当前长度
}SString;

B.链式存储结构——块链结构

#define CHUNKSIZE 90         //块的大小
typedef struct Chunk{char ch[CHUNKSIZE];struct Chunk *next;
}Chunk;typedef struct{Chunk *head,*tail;            //串的头指针和尾指针int curlen;                  //串的当前长度
}LString;                       //字符串的块链结构

4. 串的运算

A. 串的模式匹配算法

  1. 算法目的:确定主串中所含子串(模式串)第一次出现的位置(定位)
  2. 算法应用:搜索引擎、拼写检查、语言翻译、数据压缩
  3. 算法种类:BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)、KMP算法(特点:速度快)

B. BF算法

亦称简单匹配算法,采用穷举法的思路

int Index_BF(SString S,SString T){int i=1,j=1;while(i<=S.length && j<=T.length){if(s.ch[i]==t.ch[j]){++i;++j;}   //主串和子串依次匹配下一个字符else {i=i-j+2;j=1;}              //主串、子串指针回溯重新开始下一次匹配}if(j>=T.length)return i-T.length;  //返回匹配的第一个字符的下标else return 0;                       //模式匹配不成功
}

C. KMP算法

int Index_KMP(SString S,SString T,int pos){i=pos,j=1;while(i<S.length && j<T.length){if(j==0 || S.ch[i]==T.ch[j]){i++;j++;}elsej=next[j];           //i不变,j后退}if(j>T.length) return i-T.length;   //匹配成功else return 0;                        //匹配失败
}void get_next(SString T,int &next[]){i=1;next[1]=0;j=0;while(i<T.length){if(j==0||T.ch[i]==T.ch[j]){++i;++j;next[i]=j;}elsej=next[j];}
}

(2) 数组

1. 基本要点

  • 数组:按一定格式排列起来的,具有相同类型的数据元素的集合

  • 一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组

  • 一维数组的逻辑结构:线性结构。定长的线性表。

  • 声明格式:数据类型 变量名称[长度];

  • 在C语言中,一个二维数组类型也可以定义为一维数组类型(其分量类型为一维数组类型)即:

    typedef elemtype array2[m] [n];

    等价于:

    typedef elemtype array1[n];

    typedef array1 arrray2[m];

  • 线性表结构是数组结构的一个特例,而数组结构又是线性表结构的扩展。

  • 数组特点:结构固定——定义后,维数和维界不再改变。

  • 基本操作:

操作 描述
InitArray(&A,n,bound1,…boundn) 构造数组A
DestroyArray(&A) 销毁数组A
Value(A,&e,index1,…,indexn) 取数组元素值
Assign(A,&e,index1,…,indexn) 给数组元素赋值

2. 数组的顺序存储

3. 特殊矩阵的压缩存储

对称矩阵、三角矩阵、对角矩阵、稀疏矩阵

(3)广义表

  • 又称列表Lists

  • LS=(a1,a2,…,an)

  • 表头head(LS)=a1

  • 表尾tail(LS)=(a2,…,an),表尾一定是一个表

    • A=() 空表,长度为0
    • B=() 长度为1,表头、表尾均为()
    • C=(a,(b,c)) 长度为2,由原子a和子表(b,c)构成。表头为a,表尾为((b,c))
    • D=(x,y,z) 长度为3,每一项都是原子。表头为x,表尾为(y,z)
  • 广义表的长度定义为最外层所包含元素的个数

  • 广义表的深度定义为该广义表展开后所含括号的重数

  • 原子的深度为0,空表的深度为1

  • 广义表可以是一个递归表,例如F=(a,F)=(a,(a,(a,…))),递归表的深度是无穷值,长度是有限值

  • 广义表可以看成是线性表的推广,线性表是广义表的特例

  • 广义表的结构灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构

  • 基本运算:

功能 描述
GetHead(L) 求表头
GetTail(L) 求表尾

五、树、二叉树

(1) 树

  • 树形结构(非线性结构):节点之间有分支、具有层次关系
  • 树(Tree)是n(n>=0)个结点的有限集。
    • n=0,称为空树
    • n>0,则它满足两个条件:
      1. 有且仅有一个特定的称为根(Root)的结点
      2. 其余结点可分为m(m>=0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并成为根的子树(SubTree)
  • 树的定义是一个递归的定义
  • 树的表示方式:层次结构、嵌套集合、广义表、凹入表示
  • 基本术语:
    • 根节点:非空树中无前驱结点的结点
    • 结点的度:结点拥有的子树数
    • 树的度:树内各结点的度的最大值
    • 叶子:终端节点,度=0
    • 分支节点:度!=0,非终端节点,根节点以外的分支节点称为内部结点
    • 树的深度:树中结点的最大层次(高度)
    • 有序树:树中结点的各子树从左至右有次序
    • 无序树:树中结点的各子树无次序
    • 森林:是m(m>=0)棵互不相交的树的集合
  • 树一定是森林,森林不一定是树
  • 树的表示法:双亲表示法、孩子表示法、孩子兄弟表示法

(2) 二叉树

1. 基本概念

  • 二叉树的结构最简单,规律性最强
  • 所有树都能转为唯一对应的二叉树
  • 由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成
    • 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)
    • 子树有左右之分,其次序不能颠倒
    • 二叉树可以是空集合,根可以有空的左子树或空的右子树
  • 二叉树不是树的情况:
    • 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分
    • 树当结点只有一个孩子时,就无需区分它是左还是右的次序。
  • 二叉树的五种基本形态:
    • 空二叉树
    • 根和空的左右子树
    • 根和左子树
    • 根和右子树
    • 根和左右子树
  • 基本操作:
功能 描述
CreateBiTree(&T,definition) 按definition构造二叉树T
PreOrderTraverse(T) 先序遍历T
InOrderTraverse(T) 中序遍历T
PostOrderTraverse(T) 后序遍历T
  • 满二叉树:一棵深度为k且有2^k-1个结点的二叉树称为满二叉树

    • 每一层上的结点数都是最大结点数(即每层都满)
    • 叶子结点全部在最底层
    • 编号规则:自上而下,自左而右
  • 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树。(Complete binary tree)
    • 在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树
  • 满二叉树一定是完全二叉树

2. 二叉树的性质

  1. 性质1:在二叉树的第i层上至多有2^(i-1)个结点(i>=1)

    ​ 第i层上至少有1个结点

  2. 性质2:深度为k的二叉树至多有2^k-1个结点(k>=1)

    ​ 深度为k时至少有k个结点

  3. 性质3:对任何一棵二叉树T,如果其叶子树为n0,度为2的结点树为n2,则n0=n2+1

  4. 性质4:具有n个结点的完全二叉树的深度为|log2n|+1

    ​ |x|:称作x的低,表示不大于x的最大整数

    ​ 表明了完全二叉树结点数n与完全二叉树深度k之间的关系

  5. 性质5:如果对一棵有n个结点的完全二叉树(深度为|log2n|+1)的结点按层序编号(从第一层到第|log2n|+1层,每层从左到右),则对任一结点i(1<=i<=n),有:

    a. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点|i/2|

    b. 如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i

    c. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1

3. 二叉树的顺序存储结构

实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素

#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXSTIZE]
SqBiTree bt;

缺点:最坏情况,深度为k的且只有k个结点的单支树需要长度2^k-1的一维数组

特点:结点间关系蕴含在其存储位置中

​ 浪费空间,适于存满二叉树和完全二叉树

4. 二叉树的链式存储结构

A. 二叉链表

指向两个孩子

typedef struct BiNode{TElemType data;struct BiNode *lchild,*rchild;      //左右孩子指针
}BiNode,*BiTree;

在n个结点的二叉链表中,有_________个空指针域:

必有2n个链域。除根结点外,每个结点有且仅有一个双亲,所以只会有n-1个结点的链域存放指针,指向非空子女结点。

空指针数目=2n-(n-1)=n+1

B. 三叉链表

指向两个孩子和老父亲

typedef struct TriTNode{
TelemType data;
struct TriTNode *lchild,*parent,*rchild;
}TriTNode,*TriTree;

(3) 遍历二叉树

1. 概念

  • 遍历定义:顺着某一条搜索路径寻访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游)。
  • 遍历目的:得到树中所有结点的一个线性排列
  • 遍历用途:是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心
  • L:遍历左子树 D:访问根结点 R:遍历右子树
    • 遍历整个二叉树方案共有:DLR、LDR、LRD、DRL、RDL、RLD六种
    • 若规定先左后右:DLR(先(根)序遍历)、LDR(中(根)序遍历)、LRD(后(根)序遍历)
  • 先序:前缀表示(波兰式)
  • 中序:中缀表示
  • 后序:后缀表示(逆波兰式)
  • 根据遍历序列确定二叉树:
    • 若二叉树中各节点的值均不同,则二叉树结点的线序序列、中序序列和后序序列都是唯一的
    • 由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一颗二叉树

2. 二叉树先序遍历算法

Status PreOrderTraverse(BiTree T){if(T==NULL)  return OK;              //空二叉树else{visit(T);                        //访问根结点PreOrderTraverse(T->lchild);  //递归遍历左子树PreOrderTraverse(T->rchild);    //递归遍历右子树}
}void Pre(BiTtee *T){if(T!=NULL){printf("%d\t",T->data);pre(T->lchild);pre(T->rchild);}
}

3. 中序遍历算法:

Status InOrderTraverse(BiTree T){if(T==NULL)return OK;         //空二叉树else{InOrderTraverse(T->lchild);   //递归遍历左子树visit(T);                  //访问根结点InOrderTraverse(T->rchild);   //递归遍历右子树}
}

4. 后序遍历算法:

Status PostOrderTraverse(BiTree T){if(T==NULL)return OK;           //空二叉树else{PostOrderTraverse(T->lchild); //递归遍历左子树PostOrderTraverse(T->rchild);   //递归遍历右子树visit(T);                      //访问根结点}
}

5. 中序非递归算法

Status InOrderTraverse(BiTree T){BiTree p;InitStack(S);p=T;while(p||!StackEmpty(S)){if(p){Push(S,p);p=p->lchild;}else{Pop(S,q);printf("%c",q->data);p=q->rchild;}      }return OK;
}

6. 层次遍历算法

typedef struct{BTNode data[MaxSize];         //存放队中元素int front,rear;                 //队头和队尾指针
}SqQueue;                           //顺序循环队列类型void LevelOrder(BTNode *b){BTNode *p;SqQueue *qu;InitQueue(qu);                   //初始化队列enQueue(qu,b);                   //根结点指针进入队列while(!QueueEmpty(qu)){          //队不为空,则循环deQueue(qu,p);             //出队结点pprintf("%c",p->data);       //访问结点pif(p->lchild!=NULL)enQueue(qu,p->lchild); //有左孩子是将其进队if(p->rchild!=NULL)enQueue(qu,p->rchild); //有右孩子是将其进队}
}

7. 二叉树的建立

按先序遍历序列建立二叉树的二叉链表

Status CreateBiTree(BiTree &T){scanf(&ch);if(ch=="#")    T=NULL;else{if(!(T=(BiTNode *)malloc(sizeof(BiTNode))))exit(OVERFLOW);        //T=new BiTNode;T->data=ch;                //生成根结点CreateBiTree(T->lchild);      //构造左子树CreateBiTree(T->rchild);      //构造右子树}return OK;
}//CreateBiTree

8. 复制二叉树

int Copy(BiTree T,BiTree &NewT){if(T==NULL){NewT=NULL;retrun 0;}else{NewT=new BiTNode;NewT->data=T->data;Copy(T->lChild,NewT->lchild);Copy(T->rChild,NewT->rchild);}
}

9. 计算二叉树的深度

int Depth(BiTree T){if(T==NULL)return 0;else{m=Depth(T->lChild);n=Depth(T->rChild);if(m>n)return(m+1);else return(n+1);}
}

10. 计算二叉树结点总数

int NodeCount(BiTree T){if(T==NULL)return 0;elsereturn NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}

11. 计算二叉树叶子结点数

int LeadCount(BiTree T){if(T==NULL)    return 0;if(T->lchild==NULL && T->rchild==NULL)   return 1;else return LeafCount(T->lchild)+LeafCount(T->rchild);
}

(4) 线索二叉树

  • 利用某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;

  • 如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继;

    ​ ——这种改变指向的指针称为"线索"

  • 加上了线索的二叉树称为线索二叉树(Threaded Binary Tree)

  • 对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化

  • 为区分lchild和rchild指针到底是指向孩子的指针,还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:

    ​ Itag=0 lchild指向该结点的左孩子

    ​ ltag=1 lchild指向该结点的前驱

    ​ rtag=0 rchild指向该结点的右孩子

    ​ rtag=1 rchild指向该结点的后继

typedef struct BiThrNode{int data;int ltag,rtag;struct BiThrNode *lchild,*rchild;
}BiThrNode,*BiThrTree;
  • 增设了一个头结点:

    ltag=0,lchild指向根结点,rtag=1,rchild指向遍历序列中最后一个结点

    遍历序列中第一个结点的lchild域和最后一个结点的rc域都指向头结点

(5) 树和森林

1. 树的存储结构

A.双亲表示法

  • 实现:定义结构数组,存放树的结点,每个结点含两个域(数据域、双亲域)
  • 特点:找双亲容易,找孩子难
typedef struct PTNode{TElemType data;int parent;         //双亲位置域
}PTNode;#define MAX_TREE_SIZE 100
typedef struct{PTNode nodes[MAX_TREE_SIZE];int r,n;         //根结点的位置和结点个数
}PTree;

B. 孩子链表

  • 把每个结点的孩子排列起来,看成是一个线性表,用单链表存储。则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
//孩子结点结构
typedef struct CTNode{int child;sturct CTNode *next;
}*ChildPtr;//双亲结点结构
typedef struct{TElemType data;ChildPtr firstchild;  //孩子链表头指针
}CTBox;//树结构
typedef struct{CTBox nodes[MAX_TREE_SIZE];int n,r;              //结点数和根结点的位置
}CTree;
  • 特点:找孩子容易,找双亲难。

C. 孩子兄弟表示法

又称二叉树表示法,二叉链表表示法

  • 实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
typedef struct CSNode{ElemType data;struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;

2. 树与二叉树的转换

  • 将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作。

  • 由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系。

  • 给定一棵树,可以找到唯一的一棵儿茶素与之对应

  • 树变二叉树:兄弟相连留长子

  • 二叉树变树:左孩右右连双亲,去掉原来右孩线

3. 森林与二叉树的转化

  • 森林变二叉树:树变二叉根相连
  • 二叉树变森林:去掉全部右孩线,孤立二叉再还原

4. 树的遍历

A. 先根(次序)遍历

B. 后根(次序)遍历

C. 按层次遍历

5. 森林的遍历

A. 先序遍历

B. 中序遍历

(6) 哈夫曼树

1. 基本概念

  • 也称最优二叉树
  • 路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
  • 结点的路径长度:两结点间路径上的分支数
  • 树的路径长度:从树根到每一个结点的路径长度之和,记作TL
  • 结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
  • 权(weight):将书中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
  • 结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和,记作WPL(Weighted Path Length)
  • 哈夫曼树:最优树 带权路径长度(WPL)最短的树
  • 注:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等
  • 构造这种树的算法是由哈夫曼教授于1952年提出的,称为哈夫曼树,相应的算法称为哈夫曼算法
  • 满二叉树不一定是哈夫曼树
  • 哈夫曼树中权越大的叶子离根越近
  • 具有相同带权结点的哈夫曼树不唯一

2. 构造算法

贪心算法:

  • 构造哈夫曼树时首先选择权值小的叶子结点
  • 口诀:
  1. 构造森林全是根
  2. 选用两小造新树
  3. 删除两小添新人
  4. 重复2、3剩单根
  • 哈夫曼树的结点的度数为0或2,没有度为1的结点
  • 包含n个叶子结点的哈夫曼树中共有2n-1个结点

3. 算法实现

//结构类型定义
typedef struct{int weight;int parent,lch,rch;
}HTNode,*HuffmanTree;

哈夫曼树中共有2n-1个结点,不使用0下标,数组大小为2n

void CreateHuffmanTree(HuffmanTree HT,int n){if(n<=1)return 0;m=2*n-1;                  //数组共2n-1个元素HT=new HTNode[m+1];           //0号单元未用,HT[m]表示根结点for(i=1;i<=m;++i){         //将2n-1个元素的lch、rch、parent置为0HT[i].lch=0;HT[i].rch=0;HT[i].parent=0;        }for(i=1;i<=n;++i)ch>>HT[i].weight;      //输入前n个元素的weight值//初始化结束,下面开始建立哈夫曼树for(i=n+1;i<=m;i++){      //合并产生n-1个结点——构造Huffman树Select(HT,i-1,s1,s2);   //在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回它们在                                HT中的序号s1和s2HT[s1].parent=i;HT[s2].parent=i;       //表示从F中删除s1,s2HT[i].lch=s1;HT[i].rch=s2;          //s1,s2分别作为i的左右孩子HT[i].weight=HT[s1].weight+HT[s2].weight;    //i的权值为左右孩子权值之和}
}

4. 哈夫曼编码思想

  1. 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)

  2. 利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短

  3. 在哈夫曼树的每个分支上标上0或1:

    结点的左分支标0,右分支标1

    把从根到每个叶子的路径上的标号连结起来,作为该叶子代表的字符的编码

  • 为什么哈夫曼编码能够保证是前缀编码?

    因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀

  • 为什么哈夫曼编码能够保证字符编码总长最短?

    因为哈夫曼树的带权路径长度最短,故字符编码的总长最短

性质1:哈夫曼编码是前缀码

性质2:哈夫曼编码是最优前缀码

5. 哈夫曼编码的算法实现

void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中HC=new char *[n+1];              //分配n个字符编码的头指针矢量cd=new char[n];                    //分配临时存放编码的动态数组空间cd[n-1]='\0';                   //编码结束符for(i=1;i<=n;++i){                //逐个字符求哈夫曼编码start=n-1;c=i;f=HT[i].parent;while(f!=0){               //从叶子结点开始向上回溯,直到根结点--start;                  //回溯一次start向前指一个位置if(HT[f].lchild==c) cd[start]='0';   //结点c是f的左孩子,则生成代码0else cd[start]='1';      //结点c是f的右孩子,则生成代码1c=f;f=HT[f].parent;     //继续向上回溯}                           //求出第i个字符的编码HC[i]=new char[n-start];     //为第i个字符串编码分配空间strcpy(HC[i],&cd[start]);    //将求得的编码从临时空间cd复制到HC的当前行中}delete cd;                      //释放临时空间
}//CreateHuffanCode

6. 文件的编码和解码

六、图

(1) 图的定义和术语

  • 图:G=(V,E) Graph=(Vertex,Edge)

  • V:顶点(数据元素)的有穷非空集合

  • E:边的有穷集合

  • 无向图:每条边都是无方向的

  • 有向图:每条边都是有方向的

  • 完全图:任意两个点都有一条边相连

  • 无向完全图:n个顶点,n(n-1)/2条边

  • 有向完全图:n个顶点,n(n-1)条边

  • 稀疏图:有很少边或弧的图(e<nlogn)

  • 稠密图:有较多边或弧的图

  • 网:边/弧带权的图

  • 邻接:有边/弧相连的两个顶点之间的关系

  • 关联(依附):边/弧与顶点之间的关系

  • 顶点的度:与该顶点相关联的边的数目,记为TD(v)

    在有向图中,顶点的度等于该顶点的入度与出度之和

    顶点v的入度是以v为终点的有向边的条数,记作ID(v)

    顶点v的出度是以v为终点的有向边的条数,记作OD(v)

  • 当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是一棵树,而且是一棵有向树

  • 路径:接续的边构成的顶点序列

  • 路径长度:路径上边或弧的数目/权值之和

  • 回路(环):第一个顶点和最后一个顶点相同的路径

  • 简单路径:除路径起点和终点可以相同外,其余顶点均不相同的路径

  • 简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径

  • 连通图(强连通图):在无(有)向图G=(V,{E})中,若对任何两个顶点v、u都存在从v到u的路径,则称G是连通图(强连通图)

  • 权:图中边或弧所具有的相关数称为权,表明从一个顶点到另一个顶点的距离或耗费

  • 网:带权的图

  • 子图

  • 连通分量:无向图G的极大连通子图称为G的连通分量

  • 强连通分量:有向图G的极大强连通子图

  • 极大连通子图:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通

  • 极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,子图不再连通

  • 生成树:包含无向图G所有顶点的极小连通子图

  • 生成森林:对非连通图,由各个连通分量的生成树的集合

(2) 图的基本操作

操作 描述
Create_Graph() 创建图
GetVex(G,v) 求顶点v的值
Create_Graph(&G,V,VR) V是图的顶点集,VR是图中弧的集合,按V和VR的定义构造图G
DFSTraverse(G) 对图进行深度优先遍历
BFSTraverse 对图进行广度优先遍历

(3) 图的存储结构

图的逻辑结构:多对多

图没有顺序存储结构,可以借助二维数组来表示元素间的关系——数组表示法(邻接矩阵)

链式存储结构:多重链表(邻接表、邻接多重表、十字链表)

1. 邻接矩阵(数组)表示法

  • 建立一个顶点表(记录各个顶点信息)和一个邻接矩阵(表示各个顶点之间关系)

    • 设图A=(V,E)有n个顶点,则顶点表Vexs[n]

    • 图的邻接矩阵是一个二维数组A.arcs[n] [n],定义为

      A.arcs[i] [j]=1 如果<i,j>属于E 或者 (i,j)属于E

      A.arcs[i] [j]=0 否则

A. 无向图的邻接矩阵表示法

  • 无向图的邻接矩阵是对称的
  • 顶点i的度=第i行(列)中1的个数
  • 特别:完全图的邻接矩阵中,对角元素为0,其余1

B. 有向图的邻接矩阵表示法

  • 第i行含义:以结点vi为尾的弧(即出度边)
  • 第j列含义:以结点vi为头的弧(即入度边)
  • 有向图的邻接矩阵可能是不对称的
  • 顶点的出度=第i行元素之和
  • 顶点的入度=第i列元素之和
  • 顶点的度=第i行元素之和+第i列元素之和

C. 网(即有权图)的邻接矩阵表示法

  • 定义为A.arcs[i] [j] =Wij <vi,vj>或(vi,vj) 属于VR

    ​ 无穷 无边(弧)

D. 邻接矩阵的存储表示

用两个数组分别存储顶点表和邻接矩阵

#define MaxInt 32767     //表示极大值,即无穷
#define MVNum 100           //最大顶点数
typedef char VerTexType;    //设顶点的数据类型为字符型
typedef int ArcType;        //假设边的权值类型为整型
typedef struct{VerTexType verxs[MVNum];     //顶点表ArcType arcs[MVNum][MVNum];        //邻接矩阵int vexnum,arcnum;                //图的当前点数和边数
}AMGraph;                   //Adjacency Matrix Graph

E. 采用邻接矩阵表示法创建无向网

  1. 输入总顶点数和总边数
  2. 依次输入点的信息存入顶点表中
  3. 初始化邻接矩阵,使每个权值初始化为极大值
  4. 构造邻接矩阵
Status CreateUDN(AMGraph &G){cin>>G.vexnum>>G.arcnum;    //输入总顶点数,总边数for(i=0;i<G.vexnum;++i)cin>>G.vexs[i];           //依次输入点的信息for(i=0;i<G.vexnum;++i)     //初始化邻接矩阵for(j=0;j<G.vexnum;++j)G.arcs[i][j]=MaxInt;//边的权值均置为极大值for(k=0;k<G.arcnum;++k){   //构造邻接矩阵cin>>v1>>v2>>w;           //输入一条边所依附的顶点及边的权值i=LocateVex(G,v1);j=LocateVex(G,v2);        //确定v1和v2在G中的位置G.arcs[i][j]=w;         //边<v1,v2>的权值置为wG.arcs[j][i]=G.arcs[i][j];   //置<v1,v2>的对称边<v2,v1>的权值为w} //forreturn OK;
}//CreateUDNint LocateVex(AMGraph G,VertexType u){//图G中查找顶点u,存在则返回顶点表中的下标,否则返回-1int i;for(i=0;i<G.vexnum;++i)if(u==G.vexs[i]) return i;return -1;
}

F. 邻接矩阵的优点:

  1. 直观、简单、好理解

  2. 方便检查任意一对顶点间是否存在边

  3. 方便找任一顶点的所有“邻接点”(有边直接相连的顶点)

  4. 方便计算任一顶点的“度”(从该点发出的边数为"出度",指向该点的边数为"入度")

    无向图:对应行(或列)非0元素的个数

    有向图:对应行非0元素的个数是"出度";对应列非0的个数是"入度"

G. 邻接矩阵的缺点:

  1. 不便于增加和删除顶点
  2. 浪费空间——存稀疏图(点很多而边很少)有大量无效元素
  3. 浪费时间——统计稀疏图中一共有多少边

2. 邻接表(链式)表示法

顶点:按编号顺序将顶点数据存储在一维数组中;

关联同一顶点的边(以顶点为尾的弧):用线性链表存储

A. 无向图

  1. 邻接表不唯一
  2. 若无向图中有n个顶点、e条边,则其邻接表需n个头结点和2e个表结点。适宜存储稀疏图
  3. 无向图中顶点vi的度为第i个单链表中的结点数

B. 有向图

  1. 顶点vi的出度为第i个单链表中的结点个数
  2. 顶点vi的入度为整个单链表中邻接点域值是i-1的结点个数
  3. 找出度易,找入度难

逆邻接表:找入度易,找出度难

C. 图的邻接表存储表示

typedef struct VNode{VerTexType data;            //顶点信息ArcNode *firstarc;            //指向第一条依附该顶点的边的指针
}VNode,AdjList[MVNum];          //AdjList表示邻接表类型
//AdjList v;    相当于     VNode v[MVNum];
//弧(边)的结点结构
#define MVNum 100               //最大顶点数
typedef struct ArcNode{         //边结点int adjvex;                    //该边所指向的顶点的位置struct ArcNode *nextarc;   //指向下一条边的指针OtherInfo info;              //和边相关的信息
}ArcNode;typedef struct{AdjList vertices;           //vertices--vertex的复数int vexnum,arcnum;         //图的当前顶点数和弧数
}ALGraph;//邻接表操作举例说明
ALGraph G;                      //定义了邻接表表示的图G
G.vexum=5;                     //图G包含5个顶点,5条边
G.arcnum=5;                        //图G中第2个顶点是b
G.vertices[1].data='b';          //图G中第2个顶点是b
p=G.vertices[1].firtarc;       //指针p指向顶点b的第一条边结点
p->adjvex=4;                    //p指针所指边结点是到下标为4的结点的边

D. 采用邻接表示法创建无向网

算法思想:

  1. 输入总顶点数和总边数

  2. 建立顶点表

    依次输入点的信息存入顶点表中

    使每个表头结点的指针域初始化为NULL

  3. 创建邻接表

    依次输入每条边依附的两个顶点

    确定两个顶点的序号i和j,建立边结点

    将此边结点分别插入到vi和vj对应的两个边链表的头部

Status CreateUDG(ALGraph &G){    //采用邻接表表示法,创建无向图Gcin>>G.vexnum>>G.arcnum;    //输入总顶点数,总边数for(i=0;i<G.vexnum;++i){   //输入各点,构造表头结点表cin>>G.vertices[i].data  //输入顶点值G.vertices[i].firstarc=NULL //初始化表头结点的指针域}//forfor(k=0;k<G.arcnum;++k){   //输入各边,构造邻接表cin>>v1>>v2;         //输入一条边依附的两个顶点i=LocateVex(G,v1);j=LocateVex(G,v2);p1=newArcNode;         //生成一个新的边结点*p1p1->adjvex=j;         //邻接点序号为jp1->nextarc=G.vertices[i].firstarc;G.vertices[i].firstarc=p1; //将新结点*p2插入顶点vi的边表头部p2=newArcNode;         //生成另一个对称的新的边结点*p2p2->adjvex=i;         //邻接点序号为ip2->nextarc=G.vertices[j].firstarc;G.vertices[j].firstarc=p2; //将新结点*p2插入顶点vj的边表头部}//forreturn OK;
}//CreateUDG

E. 邻接表特点

  1. 方便找任一顶点的所有“邻接点”

  2. 节约稀疏图的空间

    需要N个头指针+2E个结点(每个结点至少2个域)

  3. 对无向图方便计算任一顶点的“度”

  4. 对有向图只能计算“出度”;需要构造“逆邻接表”(存指向自己的边)来方便计算“入度”

  5. 不方便检查任意一对顶点间是否存在边

F. 邻接矩阵与邻接表表示法的关系

  1. 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数
  2. 区别:
    • 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)
    • 邻接矩阵的空间复杂度为O(n^2),而邻接表的空间复杂度为O(n+e)
  3. 用途:邻接矩阵多用于稠密图,而邻接表多用于稀疏图

3. 十字链表

用于有向图

邻接表——有向图——缺点:求结点的度困难——十字链表

邻接表——无向图——缺点:每条边都要存储两遍——邻接多重表

  • 十字链表(Orthogonal List)是有向图的另一种链式存储结构(可以看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表)
  • 有向图中的每一条弧对应十字链表中的一个弧结点,同时有向图中的每一个定点在十字链表中对应有一个结点,叫做顶点结点

4. 邻接多重表

(4)图的遍历

遍历的定义:

​ 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,叫做图的遍历,它是图的基本运算。

遍历实质:

​ 找每个顶点的邻接点的过程

图的特点:

​ 图中可能存在回路,且图的任一顶点都可能与其他顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点

怎样避免重复访问?

解决思路:设置辅助数组visited[n]用来标记每个被访问过的顶点

  • 初始状态visited[i]为1
  • 顶点i被访问,改visited[i]为1,防止被多次访问

1. 深度优先搜索

(Depth_First Search——DFS)

连通图的深度优先遍历类似于树的先根遍历

邻接矩阵表示的无向图深度遍历实现:

void DFS(AMGraph G,int v){                   //图G为邻接矩阵类型cout<<v;                               visited[v]=true;                       //访问第v个顶点for(w=0;w<G.vexnum;w++)                  //依次检查邻接矩阵v所在的行if((G.arcs[v][w]!=0)&&(visited[w]))DFS(G,w);//w是v的邻接点,如果w未访问,则递归调用DFS
}

2. 广度优先搜索

(Breadth_First Search——BFS)

void BFS(Graph G,int v){         //按广度优先非递归遍历连通图Gcount<<v;visited[v]=true;                //访问第v个顶点InitQueue(Q);                  //辅助队列Q初始化,置空EnQueue(Q,v);                   //v进队while(!QueueEmpty(Q)){         //队列非空DeQueue(Q,u);             //队头元素出队并置为ufor(w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))if(!visited[w]){cout<<w;visited[w]=true;EnQueue(Q,w);      //w进队}//if}//while
}//BFS

DFS与BFS算法效率比较:

  • 空间复杂度相同,都是O(n)(借用了堆栈或队列);
  • 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关

(5)图的应用

1. 最小生成树

  • 生成树:所有顶点均由边连接在一起,但不存在回路的图
  • 一个图可以有许多棵不同的生成树
  • 所有生成树具有以下共同特点:
    • 生成树的顶点个数与图的顶点个数相同
    • 生成树是图的极小连通子图,去掉一条边则非连通
    • 一个有n个顶点的连通图的生成树有n-1条边
    • 在生成树中再加一条边必然形成回路
    • 生成树中任一两个顶点间的路径是唯一的
  • 无向图的生成树:
    • 深度优先生成树
    • 广度优先生成树
  • 最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树
  • 构造最小生成树(Minimum Spanning Tree) MST性质
  • 普里姆(Prim)算法
  • 克鲁斯卡尔(Kruskal)算法
  • 最小生成树可能不唯一
算法名 普里姆算法 克鲁斯卡尔算法
算法思想 选择点 选择边
时间复杂度 O(n^2)(n为顶点数) O(eloge)(e为边数)
适应范围 稠密图 稀疏图

2. 最短路径

  • 问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径
  • 最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边
  • 第一类问题:两点间最短路径——单源最短路径——用Dijkstra(迪杰斯特拉)算法
  • 第二类问题:某源点到其他各点最短路径——所有顶点间的最短路径——用Floyd(弗洛伊德)算法
  • Dijkstra算法
  • Floyd算法

3. 拓扑排序

  • 有向无环图:无环的有向图,简称DAG图(Directed Acycline Graph)

  • AOV网:(拓扑排序)

    用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。

    AOV网中不允许有回路。

  • 拓扑排序:

    在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧<i,j>存在,则在这个序列中,i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排列的算法称为拓扑排序。

  • 检测AOV网中是否存在环方法:

    对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。

4. 关键路径

AOE网:(关键路径)

用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称AOE网(Activity On Edge)。

  • 源点——表示整个工程的开始,入度为0的顶点

  • 汇点——表示整个工程的结束,出度为0的顶点

  • 关键路径——路径长度最长的路径

  • 路径长度——路径各活动持续时间之和

  • 求解关键路径:

    • ve(vj)——表示事件vj的最早发生时间
    • vl(vj)——表示事件vj的最迟发生时间
    • e(i)——表示活动ai的最早开始时间
    • l(i)——表示活动的最迟开始时间
    • l(i)-e(i)——表示完成活动ai的时间余量
    • 关键活动——关键路径上的活动,即l(i)==e(i)的活动
    • 若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动
    • 如果一个活动处在所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间
    • 处于所有的关键路径上的活动完成时间不能缩短太多,否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。

七、查找

(1) 基本概念

  • 查找表是由同一类型的数据元素(或记录)构成的集合。由于“集合”中的数据元素之间存在着松散的关系,因此查找表是一种应用灵便的结构。
  • 查找——根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)
  • 关键字——用来表示一个数据元素(或记录)的某个数据项的值
    • 主关键字——可唯一地标识一个记录的关键字是主关键字
    • 次关键字——用以识别若干记录的关键字是次关键字
  • 若查找表存在这样一个记录,则称"查找成功",查找结果给出整个记录的信息,或指示该记录在查找表中的位置
  • “查找不成功”,查找结果给出"空记录"或"空指针"
  • 查找的目的:
    • 查询某个"特定的"数据元素是否在查找表中;
    • 检索某个"特定的"数据元素的各种属性
    • 在查找表中插入一个数据元素
    • 删除查找表中的某个数据元素
  • 查找表:
    • 静态查找表:仅作"查询"(检索)操作的查找表
    • 动态查找表:作"插入"和"删除"操作的查找表
  • 评价查找算法的指标
    • 关键字的平均比较次数,也称平均查找长度ASL(Average Search Length)
    • 关键字比较次数的期望值

(2) 线性表的查找

1. 顺序查找(线性查找)

  • 应用范围:

    • 顺序表或线性链表表示的静态查找表
    • 表内元素之间无序
  • 数据元素类型定义:

    typedef struct{KeyType key;           //关键字...                        //其他域
    }ElemType;
    
    typedef struct{                //顺序表结构类型定义ElemType *R;         //表基址int length;                //表长
    }SSTable;                   //Sequential Search Table
    SSTable ST;                 //定义顺序表ST
    
    int Search_Seq(SSTable ST,KeyType key){        //若成功返回其位置信息,否则返回0for(i=ST.length;i>=1;--1)if(ST.R[i].key==key)return i;return 0;
    }
    //其他形式
    int Search_Seq(SSTable ST,KeyType key){for(i=ST.length;ST.R[i].key!=key;--i)if(i<=0)break;//for(i=ST.length;ST.R[i].key!=key&&i>0;--i);if(i>0)retrun i;else retrun 0;
    }
    //改进:把待查关键字key存入表头("哨兵""监视哨")
    int Search_Seq(SSTable ST,KeyType key){ST.R[0].key=key;for(i=ST.length;ST.R[i].key!=key;--i);return i;
    }
    
  • 比较次数

    • 查找第i个元素,需要比较n+1-i次
    • 查找失败,需比较n+1次
  • 时间复杂度:O(n)

    • 查找成功时的平均查找长度:ASL(n)=(1+2+…+n)/n=(n+1)/2
  • 空间复杂度:一个辅助空间——O(1)

  • 顺序查找的特点:

    • 优点:算法简单,逻辑次序无要求,且不同存储结构均适用
    • 缺点:ASL太长,时间效率太低

2. 折半查找(二分或对分查找)

  • 有序表表示静态查找表——折半查找

  • 折半查找——每次将待查记录所在区间缩小一半

  • 折半查找算法:(非递归算法)

    • 设表长为n,low、high和mid分别指向待查元素所在区间的上界、下界和中点,key为给定的要查的值
    • 初始时,另low=1,high=n,mid=(low+high)/2
    • 让k与mid指向的记录比较
      1. 若key==R[mid].key,查找成功
      2. 若key<R[mid].key,则high=mid-1
      3. 若key>R[mid].key,则low=mid+1
    • 重复上述操作,直至low>high时,查找失败
    int Search_Bin(SSTable ST,KeyType key){low=1;high=ST.length;           //置区间初值while(low<=high){mid=(low+higt)/2;if(ST.R[mid].key==key) return mid;     //找到待查元素else if(key<ST.R[mid].key)               //缩小查找区间high=mid-1;                            //继续在前半区间进行查找else low=mid+1;                          //继续在后半区间进行查找}return 0;                                 //顺序表中不存在待查元素
    }//Search_Bin
    
  • 折半查找——递归算法

    int Search_Bin(SSTable ST,keyType key,int low,int high){if(low>high)return 0;               //查找不到时返回0mid=(low+high)/2;if(key==ST.elem[mid].key) return mid;else if(key<ST.elem[mid].key)......      //递归,在前半区间进行查找else...... //递归,在后半区间进行查找
    }
    
  • 判定树

    • 查找成功比较次数=路径上的结点数=结点的层数
    • 比较次数<=树的深度<=log2n+1
  • 平均查找长度ASL约等于log2(n+1)-1(n>50)

  • 折半查找的特点

    • 优点:效率比顺序查找高
    • 缺点:只适用于有序表,且限于顺序存储结构(对线性链表无效)

3. 分块查找

  • 又称索引顺序查找
  • 条件
    • 将表分成几块,且表或者有序,或者分块有序
    • 建立"索引表"(每个结点含有最大关键字域和指向本快递一个结点的指针,且按关键字有序)
  • 查找效率
    • ASL=对索引表查找的ASL+对块内查找的ASL
  • 分块查找的特点:
    • 优点:插入和删除比较容易,无需进行大量移动
    • 缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算
    • 适用情况:如果线性表既要快速查找有警察动态变化,则可采用分块查找

4. 比较

顺序查找 折半查找 分块查找
ASL 最大 最小 中间
表结构 有序表、无序表 有序表 分块有序
存储结构 顺序表、线性链表 顺序表 顺序表、线性链表

(3) 树表的查找

1. 基本概念

  • 动态查找表——几种特殊的树

    • 二叉排序树
    • 平衡二叉树
    • 红黑树
    • B-树
    • B+树
    • 键树
  • 表结构在查找过程中动态生成
  • 对于给定值key,若表中存在,则成功返回;否则,插入关键字等于key的记录

2. 二叉排序树

  • 二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树

  • 二叉排序树或是空树,或是满足如下性质的二叉树:

    • 若其左子树非空,则左子树上所有结点的值均小于根节点的值
    • 若其右子树非空,则右子树上所有结点的值均大于等于根结点的值
    • 其左右子树本身又各是一棵二叉排序树
  • 二叉排序树的性质:中序遍历非空的二叉排序树,所得到的数据元素序列是一个按关键字排列的递增有序序列

  • 二叉排序树查找递归算法:

    typedef struct{KeyType key;            //关键字项Info Type otherinfo;  //其他数据域
    }ElemType;
    
    typedef struct BSTNode{ElemType data;          //数据域struct BSTNode *lchild,*rchild;        //左右孩子指针
    }BSTNode,*BSTree;BSTree T;                  //定义二叉排序树T
    
    BSTree SearchBST(BSTree T,KeyType key){if((!T) || key==T->data.key)return T;else if(key<T->data.key)return SearchBST(T->lchild,key); //在左子树中继续查找else return SearchBST(T->rchild,key); //在右子树中继续查找
    }//SearchBST
    
  • 二叉排序树效率分析

    • 含有n个结点的二叉排序树的平均查找长度和树的形态有关
    • 最好情况:域折半查找中的判定树相同,树的深度为log2n+1,时间复杂度O(log2n)
    • 最坏情况:查找效率域顺序查找情况相同,时间复杂度O(n)
  • 提高形态不均衡的二叉排序树的查找效率:做"平衡化"处理,即尽量让二叉树的形状均衡——平衡二叉树

  • 一个无序序列可通过构造二叉排序树而变成一个有序序列

  • 构造树的过程就是对无序序列进行排序的过程

  • 不同插入次序的序列生成不同形态的二叉树排序树

  • 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变

  • 二叉排序树的操作——删除

    1. 被删除的结点是叶子结点:直接删去该结点,其双亲结点中相应指针域的值改“空”
    2. 被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换它(结点替换),其双亲结点的相应指针域的值改为"指向被删除节点的左子树或右子树"
    3. 被删除的结点既有左子树,也有右子树
      • 以其中序前趋值替换之(值替换),然后再删除该前趋结点(前趋是左子树中最大的结点)
      • 也可以用其后继替换之,然后再删除该后继结点(后继是右子树中最小的结点)

3. 平衡二叉树

  • 平衡二叉树(balanced binary tree),又称AVL树(Adelson-Velskii and Landis)

  • 一棵平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:

    • 左子树与右子树的高度只差的绝对值小于等于1
    • 左子树和右子树也是平衡二叉排序树
  • 平衡因子(BF)——该结点左子树与右子树的高度差

    平衡因子=结点左子树的高度-结点右子树的高度

    根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1、0、或1

  • 失衡二叉排序树的调整

    • 平衡调整的四种类型

      1. LL型
      2. LR型
      3. RL型
      4. RR型
    • 调整原则:1)降低高度2)保持二叉排序树性质

4. B-树

5. B+树

(4) 哈希表的查找

1. 散列表的基本概念

  • 基本思想:记录的存储位置与关键字之间存在对应关系

    对应关系——hash函数

    Loc(i)=H(keyi)

  • 散列方法(杂凑法)

    选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;

    查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素键码进行比,确定查找是否成功

  • 散列函数(杂凑函数):散列方法中使用的转换函数

  • 散列表(杂凑表)

  • 冲突:不同的管家吗映射到同一个散列地址

    key1!=key2,但是H(key1)=H(key2)

    在散列查找方法中,冲突是不可能避免的,只能尽可能减少

  • 同义词:具有相同函数值的多个关键字

2. 散列函数的构造

  • 考虑因素

    1. 执行速度(即计算散列函数所需时间)
    2. 关键字的长度
    3. 散列表的大小
    4. 关键字的分布情况
    5. 查找频率
  • 方法

    1. 直接定址法
    2. 数字分析法
    3. 平方取中法
    4. 折叠法
    5. 除留余数法
    6. 随机数法
  • 直接定址法

    Hash(key)=a.key+b (a、b为常数)

    优点:以关键码key的某个线性函数值为散列地址,不会产生冲突

    缺点:要占用连续地址空间,空间效率低

  • 除留余数法

    Hash(key)=key mod p (p是一个整数)

3. 处理冲突的方法

A. 开放定址法(开地址法)

  • 基本思想:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入
  • 常用方法:线性探测法、二次探测法、伪随机探测法

B. 链地址法(拉链法)

  • 基本思想:相同散列地址的记录链成一单链表,m个散列地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构
  • 建立散列表步骤:
    • 取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表
    • 若不为空,则根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入此链表
  • 链地址法的优点:
    • 非同义词不会冲突,无"聚集"现象
    • 链表上借点空间动态申请,更适合于表长不确定的情况

C. 再散列法(双散列函数法)

D. 建立一个公共溢出区

4. 散列表的查找

(5) 多路查找树(B树)

1. 概念

  • Muitl-Way Search Tree
  • 其每一个节点的孩子数可以多于两个,且每一个结点处可以存储多个元素

2. 2-3树

  • 每个结点都具有两个孩子或三个孩子
  • 一个2结点包含一个元素和两个孩子(或没有孩子)(不能只有一个孩子)
  • 一个3结点包含一小一大两个元素和三个孩子(或没有孩子)
  • 2-3树中所有的叶子都在同一层次上

3. 2-3-4树

  • 一个4结点包含小中大3个元素和4个孩子(或没有孩子)

4. B树

  • B树(B-tree)是一种平衡的多路查找树,结点最大的孩子数目称为B树的阶(order)
  • 2-3树是3阶B树,2-3-4树是4阶B树
  • 一个m阶的B树具有如下属性:
    1. 如果根节点不是叶子结点,则其至少有两棵子树
    2. 每一个非根的分支节点都有k-1个元素和k个孩子
    3. 所有叶子结点都位于同一层次

5. B+树

八、排序

(1) 概念

  • 排序:将一组杂乱无章的数据按一定规律顺次排列起来,即将无序序列排成一个有序序列(由小到大或由大到小)的运算

  • 分类:

    • 按数据存储介质:内部排序和外部排序

    • 按比较器个数:串行排序和并行排序

    • 按主要操作:比较排序和基数排序

    • 按辅助空间:原地排序和非原地排序

    • 按稳定性:稳定排序和非稳定排序

    • 按自然性:自然排序和非自然排序

      1. 内部排序:数据量不大、数据在内存,无需内外存交换数据

      2. 外部排序:数据量较大、数据在外存(文件排序)

      3. 串行排序:单处理机(同一时刻比较一对元素)

      4. 并行排序:多处理机(同一时刻比较多对元素)

      5. 比较排序:用比较的方法(插入排序、交换排序、选择排序、归并排序)

      6. 基数排序:不比较元素的大小,仅仅根据元素本身的取值确定其有序位置

      7. 原地排序:辅助空间用量为O(1)的排序方法(所占的辅助存储空间与参加排序的数据量大小无关)

      8. 非原地排序:辅助空间用量超过O(1)的排序方法

      9. 稳定排序:能够使任何数值相等的元素,排序以后相对次序不变

      10. 非稳定性排序:不是稳定排序的方法

        排序的稳定性只对结构类型数据排序有意义

      11. 自然排序:输入数据越有序,排序的速度越快的排序方法

      12. 非自然排序:不是自然排序的方法

  • 按排序依据原则

      1. 插入排序:直接插入排序、折半插入排序、希尔排序2. 交换排序:冒泡排序、快速排序3. 选择排序:简单选择排序、堆排序4. 归并排序:2-路归并排序5. 基数排序
    
  • 按排序所需工作量

    1. 简单的排序方法:T(n)=O(n^2)
    2. 基数排序:T(n)=O(d. n)
    3. 先进的排序方法:T(n)=O(nlogn)
  • 存储结构——记录序列以顺序表存储

    #define MAXSIZE 20           //设记录不超过20个
    typedef int KeyType;        //设关键字为整型量(int)
    
    Typedef struct{                //定义每个记录(数据元素)的结构KeyType key;           //关键字InfoType otherinfo;        //其它数据项
    }RedType;                  //Record Type
    
    Typedef struct{                //定义顺序表的结构RedType r[MAXSIZE+1];    //存储顺序表的向量int length;               //顺序表的长度
    }SqList;
    

(2) 插入排序

  • 基本思想:每一步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,知道对象全部插入为止

  • 即边插入边排序,保证子序列中随时都是排好序的

  • 在插入a[i]前,数组a的前半段(a[0]a[i-1])是有序段,后半段(a[i]a[n-1])是停留于输入次序的"无序段"

1. 直接插入排序

顺序法定位插入位置

1. 复制插入元素 x=a[i]
2. 记录后移,查找插入位置 for(j=i-1;j>=0&&x<a[j];j--)a[j+1]=a[j];
3. 插入到正确位置 a[j+1]=x;
1. 复制为哨兵L.r[0]=L.r[i];
2. 记录后移,查找插入位置for(j=i-1;L.r[0].key<L.r[j].key;--j)L.r[j+1]=L.r[j];
3. 插入到正确位置L.r[j+1]=L.r[0]
void InsertSort(SqList &L){int i,j;for(i=2;i<-L.length;++i){if(L.r[i].key<L.r[i-1].key){            //若"<",需将L.r[i]插入有序子表L.r[0]=L.r[i];                       //复制为哨兵for(j=i-1;L.r[0].key<L.r[j].key;--j){L.r[j+1]=L.r[j];              //记录后移}L.r[j+1]=L.r[0];                   //插入到正确位置}}
}

​时间复杂度结论:

         1. 原始数据越接近有序,排序速度越快2. 最坏情况下(输入数据是逆有序的),Tw(n)=O(n^2)3. 平均情况下,耗时差不多是最坏情况的一半,Te(n)=O(n^2)4. 要提高查找速度:减少元素的比较次数、减少元素的移动次数

2. 二分插入排序

二分法定位插入位置

void BInsertSort(SqList &L){for(i=2;i<=L.length;++i){     //依次插入第2~第n个元素L.r[0]=L.r[i];               //当前插入元素存到"哨兵"位置low=1;                       //采用二分法查找插入位置high=i-1;                 while(low<=high){mid=(low+high)/2;if(L.r[0].key<L.r[mid].key)high=mid-1;else low=mid+1;}//循环结束,high+1则为插入位置for(j=i-1;j>=high+1;--j)L.r[j+1]=L.r[j]; //移动元素L.r[high+1]=L.r[0];             //插入到正确位置}
}//BInsertSort
  • 折半哈如排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
  • 减少了比较次数,但没有减少移动次数
  • 平均性能优于直接插入排序

3. 希尔排序

(Donald.L.Shell) 缩小增量多变插入排序

  • 基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,带整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序

  • 特点:

    • 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
    • 最后一次只需要少量移动
    • 增量序列必须是递减的,最后一个必须是1
    • 增量序列应该是互质的
    void ShellSort(Sqlist &L,int dlta[],int t){//按增量序列dlta[0..t-1]对顺序表L作希尔排序for(k=0;k<t;++k)ShellInsert(L,dlta[k]);        //一趟增量为dlta[k]的插入排序
    }//ShellSort
    
    void ShellInsert(SqList &L,int dk){//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子for(i=dk+1;i<=L.length;++i)if(r[i].key<r[i-dk].key){r[0]=r[i];for(j=i-dk;j>0&&(r[0].key<r[j].key);j=j-dk)r[j+dk]=r[j];r[j+dk]=r[0];}
    }
    
    • 希尔排序法是一种不稳定的排序方法
    • 最后一个增量值必须为1,无除了1之外的公因子
    • 不宜在链式存储结构上实现

(3) 交换排序

1. 冒泡排序

  • 基本思想:每趟不断将记录两两比较,并按"前小后大"规则交换
void bubble_sort(SqList &L){ //冒泡排序算法int m,i,j;                  RedType x;                  //交换时临时存储for(m=1;m<=n-1;m++){        //总共需m趟for(j=1;j<=n-m;j++)if(L.r[j].key>L.r[j+1].key){//发生逆序x=L.r[j];L.r[j]=L.r[j+1];L.r[j+1]=x;        //交换}//endif}//for
}
//改进
void bubble_sort(SqList &L){//赶紧的冒泡排序算法int m,i,j,flag=1;       //flag作为是否有交换的标记RedType x;for(m=1;m<=n&&flag==1;m++){flag=0;for(j-1;j<=m;j++)if(L.r[j].key>L.r[j+1].key){//发生逆序flag=1;     //发生交换,flag置位1x=L.r[j]; //交换L.r[j]=L.r[j+1];L.r[j+1]=x;}//endfi}//for
}

2. 快速排序

  • 基本思想

    • 任取一个元素为中心(pivot:枢轴、中心点)
    • 所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表
    • 对各子表重新选择中心元素并依此规则调整
    • 直到每个子表的元素只剩一个
void main(){QSort(L,1,L.length);
}
void Qsort(SqList &L,int low,int high){//对顺序表L快速排序if(low<high){                  //长度大于1pivotloc=Partition(L,low,high);//将L.r[low...high]一分为二,pivotloc为枢轴元素排好序的位置QSort(L,low,pivotloc-1);    //对低子表递归排序QSort(L,pivotloc+1,high);    //对高子表递归排序}//endif
}//QSort
int Partition(SqList &L,int low,int high){L.r[0]=L.r[low];pivotkey=L.r[low].key;while(low<high){while(low<high&&L.r[high].key>=pivotkey) --high;L.r[row]=L.r[high];while(low<high&&L.r[low].key<=pivotkey) ++low;L.r[high]=L.r[low];}L.r[low]=L.r[0];return low;
}
  • 快速排序不适于对原本有序或基本有序的记录序列进行排序

(4) 选择排序

1. 简单选择排序

  • 基本思想:在待排序的数据中选出最大(小)的元素放在其最终的位置
void SelectSort(SqList &K){for(i=1;i<L.length;++i){k=i;for(j=i+1;j<=L.length;j++)if(L.r[j].key<L.r[k].key)k=j;    //记录最小值位置if(k!=i)L.r[i]=L.r[k];               //交换}
}

2. 堆排序

  • 若n个元素的序列(a1,a2…an)满足ai<=a2i且ai<=a2i+1,或ai>=a2i且ai=a2i+1,则分别称该序列{a1,a2…an}为小根堆和大根堆
  • 堆实质是满足如下性质的完全二叉树:
    1. 二叉树中任一非叶子结点均小于(大于)它的孩子结点
  • 堆排序——若在输出堆顶的最小值(最大值)后,使得剩余n-1个元素的序列重又建成一个堆,则得到n个元素的次小值(次大值)…如此反复,便能得到一个有序序列,这个过程称之为堆排序
  • 堆调整
    1. 输出堆顶元素之后,以堆中最后一个元素替代之
    2. 然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换
    3. 重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为"筛选"
void HeapAdjust(elem R[],int s,int m){rc=R[s];for(j=2*s;j<=m;j*=2){   //沿key较大的孩子结点向下筛选if(j<m&&R[j]<R[j+1])++j;//j为key较大的记录的下标if(rc>=R[j])break;R[s]=R[j];              //rc应插入在位置s上s=j;}//forR[s]=rc;                    //插入
}//HeapAdjust
  • 堆排序算法:
void HeapSort(elem R[]){ //对R[1]到R[n]进行堆排序int i;for(i=n/2;i>=1;i--)HeapAdjust(R,i,n);   //建初始堆for(i=n;i>1;i--){Swap(R[1],R[i]); //根与最后一个元素交换HeapAdjust(R,1,i-1);    //对R[1]到R[i-1]重新建堆}
}//HeapSort

(5) 归并排序

  • 基本思想:将两个或两个以上的有序子序列"归并"为一个有序序列
  • 在内部排序中,通常采用的是2-路归并排序

(6) 基数排序

  • 基本思想:分配+收集
  • 也叫桶排序或箱排序

(7) 外部排序

(8) 排序的比较

类别 排序方法 时间复杂度 空间复杂度 稳定性
最好情况 最坏情况 平均情况 辅助存储
插入排序 直接插入排序 O(n) O(n^2) O(n^2) O(1) 稳定
希尔排序 O(n) O(n^2) O(n^1.3) O(1) 不稳定
交换排序 冒泡排序 O(n) O(n^2) O(n^2) O(1) 稳定
快速排序 O(nlogn) O(n^2) O(nlogn) O(nlogn) 不稳定
选择排序 直接选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(n^2) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
基数排序 k:待排元素的维数m:基数的个数 O(n+m) O(k*(n+m)) O(k*(n+m)) O(n+m) 稳定

未完待续。。。

数据结构笔记(C语言版)相关推荐

  1. 《数据结构(C语言版)》笔记-1.1 什么是数据结构

    title: <数据结构(C语言版)>笔记-1.1 什么是数据结构 date: 2022-10-22 15:26 tags: [数据结构] 文章目录 〇.问题 一.前言 二.什么是数据结构 ...

  2. 数据结构(C语言版)学习笔记2-单链表

    数据结构(C语言版)学习笔记2-单链表 1.单链表定义 typedef int ElemTypes; typedef struct node {ElemTypes data; //数据域struct ...

  3. 数据结构(C语言版) 第 八 章 排序 知识梳理 + 习题详解

    目录 一.归并排序 二.交换排序 1.快速排序 2.冒泡排序 三.插入排序 1.直接插入排序(基于顺序查找) 2.折半插入排序(基于折半查找) 3.希尔排序(基于逐趟缩小增量) 四.选择排序 0.直接 ...

  4. 数据结构(C语言版) 第 六 章 图 知识梳理 + 习题详解

    目录 一. 图的基本定义和术语 一.图的基本概念 1.度 2.连通 (1)连通图 (2)强连通/强连通图 3.回路 4.完全图 二.图的三种存储结构 1.邻接矩阵表示法 2.邻接表(链式)表示法 3. ...

  5. 数据结构(C语言版) 第 三 章 栈与队列 知识梳理 + 作业习题详解

    目录 一.栈 0.栈的基本概念 1.栈的实现 2.栈与递归 3.Hanoi塔问题 二.队列 0.队列的基本概念 1.队列的实现 2.循环队列 2.1循环队列的相关条件和公式: 3.链队列 4.链队列完 ...

  6. 数据结构(C语言版) 第二章 线性表 知识梳理+作业习题详解

    目录 一.线性表顺序存储结构(顺序表) 0.线性表的基本概念 1.样例引入:多项式相加 二.线性表链式存储结构(链表) 0.链表的基本概念 1.前插法代码实例 2.链表尾插法完整代码附带各种操作 三. ...

  7. <初识数据结构+算法实现>数据结构(C语言版)

    新晓本已悦人心,故知更加理解深! 目录 前言: ●本篇博文基于<数据结构>(C语言版)严蔚敏教授.吴伟民教授.李冬梅教授编著的教材知识及框架主线,以及参考借阅其他相关资料,结合作者的学习所 ...

  8. 资料分享:送你一本《数据结构(C语言版)》电子书!

    要想写出可复用.可扩展.易维护.灵活性好的代码,「数据结构」这一关必须要过啊! 在数据结构与算法的众多教材中,奉为经典的当属清华大学严蔚敏老师的著作.很多学校也选择这本书作为考研指定教材. 正在学习数 ...

  9. 资料分享:送你一本《数据结构(C#语言版)》电子书!

    对于信息类专业的学生而言,数据结构与算法是一门必修的课程.只有学好这门课程,熟练掌握线性表.栈.队列.树.图等基本结构,以及在这些结构上的各种算法,才能利用计算机去解决实际问题. 如何学好这门课程呢, ...

最新文章

  1. AWS pytorch-model API
  2. Android sqlite 数据库保存Date 类型
  3. vb计算机考试试题及答案,计算机二级考试《VB》操作试题及答案2016
  4. 统计学习方法笔记(六)-非线性支持向量机原理及python实现
  5. 各种登录源码来了!基础登录、验证码登录、小程序登录...全都要!
  6. 关于自主开发和研究代码
  7. C# log4net 不输出日志
  8. php.ini文件中的include_path设置
  9. 文件的创建与读取 文件的数据添加
  10. textview 结束后释放_等待按键释放,你的代码如何写?
  11. MySQL Root密码丢失解决方法总结
  12. C语言函数程序实例(超全)
  13. 物联网在工业企业的应用与实践 (1) 物联网与工业4.0
  14. Tushare实战分析美国国债收益率与利率的关系
  15. win7密码破解之“替换法”
  16. H.264区分NALU startCode和NALU 内部和startCode相同的内容
  17. Odoo开发应该怎么学习?
  18. Google Earth Engine(GEE)——在线计算列表二维ee.List对象为线性回归方程计算slope和残差
  19. OpenCV系列之图像去噪 | 五十八
  20. python爬虫优势和缺点_三种分布式爬虫策略的优缺点

热门文章

  1. 如何使用idea把自己的项目打包jar包发给别人使用
  2. 学习jQuery这一篇就够了
  3. 域名过期什么时候才能注册
  4. 数据分析之Hadoop详解
  5. 图解系统(六)——调度算法
  6. 基于 Squid 实现爬虫代理服务
  7. Java PPT转PDF 亲测无水印
  8. php 模板渲染,ThinkPHP6.0模板渲染 - ThinkPHP6.0快速开发手册(案例版) - php中文网手册...
  9. java将邮件保存到本地文件夹_JavaMail 邮件文件夹管理
  10. hotmail服务器密码已修改密码,修改过密码的hotmail无法在mac和iphone上登陆和收取邮件...