数据结构

数据结构

第一章 绪论

1.1 数据结构的研究内容

如何合理地组织数、高效地处理数据,这就是数据结构研究的问题。

1.2 基本概念和术语

1.2.1 数据、数据元素、数据项和数据对象

数据(Data):是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。

数据元素(Data Element):是数据的基本单位,在计算机中通常作为一个整体进行考虑和处理。在有些情况下,数据元素也称为元素、记录等。

数据项(Data Item):是组成数据元素的、有独立含义的、不可分割的最小单位。例如:学生信息中性别、姓名都是数据项。

数据对象(Data Object):是性质相同的数据元素的集合,是数据的一个子集。

数据>数据对象>数据元素>数据项

1.2.2 数据结构

数据结构(Data Structure)是相互之间存在一种或多种特定关系的数据元素的集合。换句话说,数据结构就是带“结构”的数据元素,“结构”就是指数据元素之间存在的关系。

1. 逻辑结构

​ 数据的逻辑结构是从逻辑关系上描述数据,它与数据的储存无关,是独立于计算机。

​ 逻辑结构有两个重要因素:一是数据元素,二是关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nF5engNe-1606220592952)(http://www.donaldlee.cn/img/逻辑结构.jpg)]

(1)集合结构

数据元素之间除了“属于同一集合”外,没有其他关系。

(2)线性结构

数据元素之间存在一对一的关系。

(3)树结构

数据元素之间存在一对多的关系。

(4)图结构

数据元素之间存在多对多的关系。

2. 存储结构

​ 数据对象在计算机中的存储表示称为数据的存储结构,也称为物理结构

(1)顺序存储结构

​ 顺序存储结构是借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系,通常用数组类型来描述。

(2)链式存储结构

​ 链式存储结构无需占用一整块存储空间。但为了表示节点之间的关系,需要给每个节点附加指针字段,用于存放后继元素的储存地址,通常用链表来描述。

1.2.3 数据类型和抽象数据类型
1.数据类型

数据类型是一个值的集合和定义在这个值集上的一组操作的总称。

2.抽象数据类型

抽象数据类型一般指由用户定义的、表示应用问题的数学模型,以及定义在这个模型上的一组操作的总称,具体分为三个部分:

数据对象、数据对象上关系的集合、对数据对象的基本操作的集合。

例:ADT 抽象数据类型名{

​ 数据对象:{数据对象的定义}

​ 数据关系:{数据关系的定义}

​ 数据操作:{数据操作的定义}

​ }ADT 抽象数据类型名

1.3 抽象数据类型的表示与实现

见课本P7

1.4 算法和算法分析

1.4.1 算法的定义及特性

​ **算法(Algorithm)**是为了解决某类问题而规定的有限长的操作序列。

​ 一个算法必须满足以下五个特性:

​ 一、有穷性

​ 二、确定性

​ 三、可行性

​ 四、输入

​ 五、输出

1.4.2 评价算法优劣的基本标准
  1. 正确性

    1. 可读性
    2. 健壮性
    3. 高效性
1.4.3 算法的时间复杂度
1.问题规模和语句频度

问题规模是算法求解的问题的输入量的多少,是问题大小的本质表示,一般用整数n表示。

语句频度是一条语句重复执行的次数。

2.算法的时间复杂度的定义

基本语句指的是算法中重复执行次数和算法的执行时间成正比的语句。

​ 一般情况下,算法中,基本语句重复执行次数是问题规模n的某个函数f(n),算法的时间量记作
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
它表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度

3.最好、最坏和平均时间复杂度

见课本P15

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)

1.4.4 算法的空间复杂度

​ 我们用渐进空间复杂度作为算法所需的存储空间的量度,它同样是问题规模n的函数,记作:
s ( n ) = O ( f ( n ) ) s(n)=O(f(n)) s(n)=O(f(n))

第二章 线性表

2.1 线性表的定义和特点

线性表是由n(n>=0)个数据特性相同的元素构成的有限序列。

线性表特点是:

1. 存在唯一一个被称作“第一个”的数据元素。
2. 存在唯一一个被称作“最后一个”的数据元素。
3. 除第一个以外,结构中每个元素均只有一个前驱。
4. 除最后一个以外,结构中的每个数据据元素均只有一个后继。

2.2 线性表的类型定义

见课本P21~22(使用抽象数据类型的方式来定义)

2.3 线性表的顺序表示和实现

2.3.1 线性表的顺序储存表示

用一组地址连续的存储单元依次存储线性表的数据元素,这种存储方式的线性表被称为顺序表

具体代码见课本P24.

2.4.2 顺序表中基本操作的实现
1.初始化

构造一个空的顺序表。

具体代码见课本P25

2.取值

根据位置序号i获取第i个数据。

具体代码见课本P26.

3.查找

根据指定的元素值e,在顺序表中查找第一个与e相等的元素并返回其序号。

具体代码见课本P26。

4.插入

在线性表的第i个位置插入一个新的数据元素e,使线性表的长度加一。

具体代码见课本P27.

5.删除

将表的第i个元素删去,使表的长度减一。

具体代码见课本P28.

2.4 线性表的链式表示和实现

2.4.1 单链表的定义和表示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPmWNGu9-1606220592954)(http://www.donaldlee.cn/img/单链表.png)]

单链表的定义:用任意一组存储单元存储数据元素的线性表,无论这组存储单元是不是连续的。且每个数据元素除了需要储存本身的数据外还需要存储后继元素的位置。(单链表只需要存储后继元素的地址。)

头指针->头结点(数据域(数据——为空或其他例如数组长度)+指针域(指针))->首元结点(数据域(数据——第一个数据)+指针域(指针))->其他节点

当链表不设置头结点时

​ 头指针L应该指向首元结点,当链表为空时,头指针L为空(L==null)。

当链表设置头结点时

​ 无论链表是否为空,头指针都是指向头结点。当链表为空时,头结点的指针域为空(L->next==null)。

2.4.2 单链表的基本操作的实现
1.初始化

构造一个空的链表。

具体代码见课本P32

2.取值

根据位置序号i,从首元结点出发,逐个访问,直到获取到第i个数据。

具体代码见课本P32.

3.查找

根据指定的元素值e,在顺序表中从首元结点出发,逐个访问,直到查找到第一个与e相等的元素并返回其序号。

具体代码见课本P33。

4.插入

生成一个新的结点,使数据域等于想要插入的元素值,然后依次修改插入位置的前一个结点与此新生成的结点的后继指针所指结点的地址,最终使线性表的长度加一。

具体代码见课本P34.

5.删除

将表的第i个元素删去,修改被删除结点的前一结点的指针域为被删除结点的后一结点,最终使表的长度减一。

具体代码见课本P35.

6.创建单链表

创建一个已经有n(n>0)个结点的链表

  1. 前插法

    将每一个新结点都插入到头结点之后,使其变成首元结点。

    L->NULL+NULL

    L->c+NULL

    L->b+()->c+NULL

    L->a+()->b+()->c+NULL

  2. 后插法

    将每个新结点都插入到最后一个结点之后。

    L->NULL+NULL

    L->a+NULL

    L->a+()->b+NULL

    L->a+()->b+()->c+NULL

2.4.3 循环链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tP2FU9jS-1606220592955)(http://www.donaldlee.cn/img/循环链表.png)]

循环链表即为最后一个结点的指针指向头结点(或首元结点)的单链表。

2.4.4 双向链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v5JiNZSb-1606220592956)(http://www.donaldlee.cn/img/双向链表.png)]

双向链表即每个结点都设置了两个指针域,一个指向前一个结点,一个指向下一个结点。

2.5 顺序表和链表的比较

2.5.1 空间性能比较
(1)存储空间的分配

顺序表的存储空间需要预先分配,元素个数扩充受到一定限制,易造成空间浪费或者空间溢出。

链表不需要为其预先分配空间,只要内存空间允许,链表中的元素个数就没有限制。

(2)存储密度大小

存储密度=数据元素本身占用的存储量/结点结构占用的存储量。

顺序表的存储密度为1。

链表的存储密度小于1。

2.5.2 时间性能比较
(1)存取元素的效率

若线性表主要操作是和元素位置精密相关的这类取值操作,很少做插入或删除时,宜采用顺序表作为存储结构。

(2)插入和删除的效率

对于链表,插入和删除的时间复杂度为O(1),顺序表为O(n)。

所以,若线性表频繁的经行插入或删除的操作,宜采用链表作为存储结构。

2.6 线性表的应用

2.6.1 线性表的合并

A:{2,3,4,6,8}

B:{0,2,5}

AB的合并只需要将B中的元素剔除A中已存在的并加入到A中即可。

具体代码见课本P42

2.6.2 有序表的合并

若AB都为有序表,合并后的表也要求是有序表,则只需要定义一个长度为A+B的空表C,并将AB中的元素依次有序的插入即可。

具体代码见课本P43.

第三章 栈和队列

3.1 栈和队列的定义和特点

栈(stack)是限定只在表尾进行插入或删除操作的线性表。所以表的尾端称为栈顶(top),表头称为栈底(bottom),不含元素的栈称为空栈。因此,栈又被称为**后进先出(Last In First Out-LIFO)**的线性表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jJnsrQBG-1606220592957)(http://www.donaldlee.cn/img/栈.jpg)]

队列(queue)是一种先进先出(First In First Out-FIFO)的线性表。它只允许在表的一端经行插入,而在另一端删除元素。允许插入的一端称为队尾(rear),允许删除的一端则称为队头(front)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uigYtro1-1606220592959)(http://www.donaldlee.cn/img/队列.png)]

3.2 栈的表示和操作的实现

3.2.1 栈的类型定义

采用抽象类型定义的方式,具体见课本P57

3.2.2 顺序栈的表示和实现

顺序栈是指利用顺序存储结构实现的栈。

1. 设置指针

top:指示栈顶元素所在位置。

​ 通常的习惯做法是,以top=0来表示空栈。

base:指示栈底元素所在的位置。

​ 当top==base时,表示空栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FqzyZE7X-1606220592959)(http://www.donaldlee.cn/img/栈中元素与栈指针的关系.jpg)]

2. 实现
  1. 初始化

    为顺序栈动态分配一个预定义大小的空间。

    具体代码见课本P58

  2. 入栈

    在栈顶插入一个新的元素。

    具体代码见课本P59

  3. 出栈

    将栈顶元素删除。

    具体代码见课本P59

  4. 取栈顶元素

    当栈非空时,此操作返回当前栈顶元素的值,栈顶指针保持不变。

    具体代码见课本P59

3.2.3 链栈的表示和实现

链栈是指采用链式存储结构实现的栈。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssiDEZye-1606220592960)(http://www.donaldlee.cn/img/链栈.png)]

  1. 初始化

    构造一个空栈,只需要将栈顶指针设为空即可。

    具体代码见课本P60.

  2. 链栈的入栈

    链栈在入栈前不需要判断栈是否已满,只需要为入栈元素动态分配一个结点空间,如上图所示。

    具体代码见课本P60.

  3. 链栈的出栈

    需要先判断链栈是否为空,出栈后需要释放出栈元素的栈顶空间。

    具体代码见课本P61.

  4. 取栈顶元素

    与顺序栈一样,当栈非空时,此操作返回当前栈顶元素的值,栈顶指针S保持不变。

    具体代码见课本P61.

3.3 栈与递归

栈有一个重要的应用就是在程序设计的时候实现递归(即调用自身的算法)

3.3.1 采用递归算法解决的问题
1. 定义是递归的

有很多数学函数是递归定义的,例如:阶乘函数,斐波那契数列等。

具体解释见课本P61

2. 数据结构是递归的

某些数据结构本身具有递归特性,则它们的操作可以递归地描述。

例如链表,其结点(LNode)的定义由数据域(data)和指针域(next)组成,而指针域(next)是一种指向结点(LNode)类型的指针,即结点(LNode)的定义中又使用到了其自身,所以链表是一种递归的数据结构。

遍历输出链表中的各个结点的递归算法:

void TraverseList(LinkList p){
    if(p==null) return;
    else{
        cout<<p->data<<endl;
        TraverseList(p->next);
    }
}
3. 问题的解法是递归的

问题本身没有明显的递归结构,但是用递归求解比迭代求解更简单。

例如:汉诺塔问题,八皇后问题,迷宫问题。

具体解释见课本P64.

3.3.2 递归过程与递归工作栈

见课本P65~67

3.3.3递归算法的效率分析
  1. 时间复杂度分析

  2. 空间复杂度分析

    皆见课本P69

3.3.4 利用栈将递归方法转换为非递归的方法
  1. 设置一个工作栈存放递归工作记录(包括实参、返回地址及局部变量)
  2. 进入非递归调用入口(即被调用程序开始处)将调用程序传来的实参和返回地址入栈(递归程序不可以作为主程序,因而可以认为初始是被某个程序调用的)。
  3. 进入递归调用入口:当不满足递归结束条件时,逐层递归,将实参、返回地址及局部变量入栈,这一过程可用循环语句来实现——模拟递归分解的过程。
  4. 递归结束条件满足,将到达递归出口的给定常数作为当前的函数值。
  5. 返回处理:在栈不空的条件下反复退出栈顶记录,根据记录中的返回地址经行题意规定的操作,即逐层计算当前函数值,直至栈空为止。——模拟递归求值过程。

3.4 队列的表示和操作实现

3.4.1 队列的类型定义

见课本P69

3.4.2 循环队列——队列的顺序表示和实现
1. 指针设置

front-队列头元素

rear-队列尾元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tFVnJ9b-1606220592960)(http://www.donaldlee.cn/img/顺序分配的队列中头、尾指针和元素间的关系.jpg)]

为解决(d)所示情况在插入新元素时的溢出现象,可使用循环队列。

2. 循环队列

循环队列中,头尾指针以及队列元素之间的关系不变,只是在循环队列中,头尾指针的“依环增一”的操作可以用“模“运算来实现。通过取模,头指针和尾指针就可以在顺序表空间里以头尾相接的方式”循环“移动。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aZpaFVEy-1606220592960)(http://www.donaldlee.cn/img/循环队列.jpg)]

3. 基本操作

具体代码见课本P71~73

  1. 初始化

    动态分配一个预定义的大小为MAXQSIZE的数组空间。

  2. 求队列长度

    对于非循环队列,尾指针和头指针的差即为队列的长度。

    对于循环队列,差值可能为负数,所以要加上MAXQSIZE,然后再与MAXQSIZE求模。

  3. 入队

    在队尾插入一个新元素

  4. 出队

    将队头的元素删除

  5. 取队头元素

    当队列非空时,此操作返回当前队头元素的值,队头指针不变。

3.4.3 链队——队列的链式表示和实现
基本操作

具体代码见课本P73~75

  1. 初始化

    构造一个只有一个头结点的空队。

  2. 入队

    不需要检查队列是否已满,但需要为入队元素动态分配一个结点空间。

  3. 出队

    将队头的元素删除,并释放结点空间

  4. 取队头元素

    当队列非空时,此操作返回当前队头元素的值,队头指针不变。

第四章 串、数组和广义表

4.1 串的定义

**串(String)(或字符串)**是由零个或多个字符串组成的有限序列,一般记为: s = " a 1 , a 2 , a 3 , . . . . , a n " ( n ⩾ 0 ) s="a_1,a_2,a_3,....,a_n"(n\geqslant0) s="a1,a2,a3,....,an"(n0).

其中,s是字符串的名,n是字符串的长度。零个字符的串称为空串。

串中任意个连续的字符组成的子序列称为该串的字串,包含字串的串称为主串

4.2 串的类型定义、储存结构及其运算

4.2.1 串的抽象类型定义

见课本P89~90

4.2.2 串的储存结构
1. 串的顺序存储

类似于线性表的顺序存储结构,用一组连续的存储单元存储串值的字符序列。

这种定义方式是静态的,在编译的时候就已经确定了串空间的大小。而在多数情况下,串的的操作都是以穿的整体的形式进行的,串变量之间的长度相差较大,在操作中串值长度的变化也较大,为串变量设定的固定大小的空间不合理。因此最好是根据实际的需要,在程序执行的过程中动态地为每个新产生的的串动态地分配和释放字符数组的空间。

在c语言中,存在一个称为“堆”的自由存储区,可以为每个新产生的串动态分配一块儿实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基址,同时为了处理方便,约定串长也作为存储结构的一部分。这种字符串的存储方式也称为串的堆形式顺序存储结构。

2. 串的链式存储

使用单链表方式存储串。

单链表的每一个结点可以存储一个或多个字符。所以最后一个结点不一定全被串值占满,此时通常补上“#”或其他的非串值字符(通常“#”不属于串的字符集,是一个特殊符号)。

4.2.3 串的模式匹配算法

字串的定位算法通常称为串的模式匹配串匹配

著名的匹配算法是BF算法KMP算法

1. BF算法

最简单直接的匹配模式算法。

模式匹配不一定从主串的第一个位置开始,可以指定主串中查找的起始位置pos。

算法步骤:

  1. 分别利用计数指针i和j指示主串S和模式T中当前正待比较的字符位置,i初始值为pos,j初始值为1。
  2. 如果两个串均未比较到串尾,即i和j均分别小于等于S和T的长度时,则循环执行以下操作:
    1. S.ch[i]和T.ch[j]比较,若相等,则i和j分别指示串中下个位置,继续比较后续字符;
    2. 若不等,指针后退重新开始匹配,从主串的下一个字符(i=i-j+2)起再重新和模式的第一个字符(j=1)开始比较。
  3. 如果j>T.length,说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则匹配成功,返回和模式T中的第一个字符相等的字符在主串中的符号(i-T.length);否则称匹配不成功。

算法时间复杂度:

  1. 最好:O(n+m)
  2. 最坏:O(n*m)
2. KMP算法

该算法可以在O(n+m)的时间数量级上完成串的模式匹配操作。

其改进在于,每一次匹配过程中出现字符比较不相等时,不需要回溯i指针,而是利用已得到的”部分匹配“的结果将模式向右尽可能的”滑动“一段距离之后,继续进行比较。

本质上此算法就是在失配时检查除去失配元素外前所有已经成功配对的元素串头尾有无相同字串,由则可以在下一轮匹配中掠过头部这一部分的匹配.

算法步骤:

  1. 分别利用计数指针i和j指示主串S和模式T中当前正待比较的字符位置,i初始值为pos,j初始值为1。

  2. 如果两个串均未比较到串尾,即i和j均分别小于等于S和T的长度时,则循环执行以下操作:

    1. S.ch[i]和T.ch[j]比较,若相等,则i和j分别指示串中下个位置,继续比较后续字符;

    2. 若不等,则主串指针i不需要回溯。

      设主串的第i个字符与模式中的第j个字符失配。

      设k为模式中的某个字符,将模式滑动到主串字符i与模式字符k开始比较。

      可以得到k的模式必须满足以下条件:

      主串: s 1 , s 2 , s 3 , . . . , s n s_1,s_2,s_3,...,s_n s1,s2,s3,...,sn,指针:i

      模式: t 1 , t 2 , t 3 , . . . , t m t_1,t_2,t_3,...,t_m t1,t2,t3,...,tm,指针:j

      1. t 1 , t 2 , . . . , t k − 1 = s i − k + 1 , s i − k + 2 , . . . , s i − 1 t_1,t_2,...,t_{k-1}=s_{i-k+1},s_{i-k+2},...,s_{i-1} t1,t2,...,tk1=sik+1,sik+2,...,si1
      2. t j − k + 1 , t j − K + 2 , . . . , t j − 1 = s i − k + 1 , s i − k + 2 , . . . , s i − 1 t_{j-k+1},t_{j-K+2},...,t_{j-1}=s_{i-k+1},s_{i-k+2},...,s_{i-1} tjk+1,tjK+2,...,tj1=sik+1,sik+2,...,si1
      3. 所以: t 1 , t 2 , . . . , t k − 1 = t j − k + 1 , t j − K + 2 , . . . , t j − 1 t_1,t_2,...,t_{k-1}=t_{j-k+1},t_{j-K+2},...,t_{j-1} t1,t2,...,tk1=tjk+1,tjK+2,...,tj1
  3. 如果j>T.length,说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则匹配成功,返回和模式T中的第一个字符相等的字符在主串中的符号(i-T.length);否则称匹配不成功。

    上述的大概意思为,当模式串t的第j个元素与主串第i个元素失配时,如果模式串自j-1开始向前k个元素与从模式串第一个元素开始到第k-1个元素是匹配的,那么即将模式串向后滑动k个元素的位置。

next函数:

​ 若令next[j]=k,则next[j]表明当模式中第j个字符与主字符串中相应的字符失配时,在模式中重新和主串中该字符串中该字符进行比较的字符的位置k。则可以引出模式串的next函数的定义:
KaTeX parse error: Unknown column alignment: o at position 34: …\begin{array}{co̲ls} 0,j=1(t_1与s…
即,当 t j 与 s i t_j与s_i tjsi失配时,指针i不动, s i s_i si t n e x t [ j ] − 1 t_{next[j]-1} tnext[j]1开始比较,依次循环——若是第一个元素即失配,则将模式串的第一个元素与主串的下一个元素开始匹配。

例:模式串abaabcac的next函数各项输出

j12345678
模式串abaabcac
next[j]0(头元素失配为0)1(除去本元素外不足满足条件)1( a ≠ b a\neq b a=b不满足条件)2(a=a)2(a=a)3(ab=ab)1( a ≠ c a\neq c a=c不满足条件)2(a=a)

详细代码见P95

4.3 数组

4.3.1 数组的类型定义

见P98-99

4.3.2 数组顺序存储

LOC(n):求下标为n的一维数组的元素的存储位置。

LOC(i,j):求下表为i,j的二维数组的元素的存储位置。

L:单个元素所需要的空间

一维数组的储存位置的计算:

LOC(n)= LOC(0)+ nL

二维数组的存储位置的计算:

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

n:第一维下标代表的每个维度中元素的个数。

n维数组的存储位置的计算:

LOC( j 1 , j 2 , . . . , j n j_1,j_2,...,j_n j1,j2,...,jn)= LOC(0,0,…,0)+ ∑ i = 1 n C i j i \sum^n_{i=1}C_ij_i i=1nCiji

4.3.3 特殊矩阵的压缩矩阵

**压缩储存:**为多个值相同的元素只分配一个存储空间。

1. 对称矩阵

若n阶矩阵的元素满足以下条件,即为n阶对称矩阵:
a i j = a j i a_{ij}=a_{ji} aij=aji
假设以一维数组sa[n(n+1)/2]作为对称矩阵A的存储结构,则sa[k]和矩阵元素 a i j a_{ij} aij存在着一一对应的关系:
KaTeX parse error: Unknown column alignment: o at position 28: …\begin{array}{co̲ls} {i(i-1)}/2+…
对于任意指定的一组下标(i,j),均可在sa中找到矩阵元素 a i j a_{ij} aij.由此,称sa[n(n+1)/2]作为n阶对称矩阵A的压缩存储。

2. 三角矩阵
  1. 上三角矩阵

    类似上述的对应关系为:
    KaTeX parse error: Unknown column alignment: o at position 28: …\begin{array}{co̲ls} {(i-1)(2n-i…

  2. 下三角矩阵
    KaTeX parse error: Unknown column alignment: o at position 28: …\begin{array}{co̲ls} {(i-1)i}/2+…

3. 对角矩阵

见课本P102

4.4 广义表

4.4.1 广义表的定义

广义表是线性表的推广,也称为列表。一般记作:
L S = ( a 1 , a 2 , . . . , a n ) LS=(a_1,a_2,...,a_n) LS=(a1,a2,...,an)
LS是广义表的名称。

n是广义表的长度。

a i a_i ai可以是广义表也可以是单个元素。

值得注意的是,广义表()与广义表(())不同,前者为空表,长度为0,后者长度为1,可分解为其表头和表为均为空表().

4.4.2 广义表的存储结构
1. 头尾链表的存储结构

广义表可以有两种结点结构:

  1. 表结点:此类型结点代表此结点是广义表。一个表结点可以用三个域表示:
    1. 标志域:1表示为子表
    2. 指示表头的指针域
    3. 指示表尾的指针域
  2. 原子结点:此类型结点表示此结点是原子。一个原子结点可以用两个域表示:
    1. 标志域:0表示为原子
    2. 值域

2. 扩展线性链表的存储结构

此种存储方式中,无论原子结点还是表结点都用三个域来表示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nyn8cbk1-1606220592962)(http://www.donaldlee.cn/img/扩展线性表存储结构的广义表.jpg)]

第五章 树和二叉树

5.1 树和二叉树的定义

5.1.1 树的定义

​ **树(tree)**是n( n ⩾ 0 n\geqslant 0 n0)个结点的有限集,它或为空树(n=0);或为非空树T:

  1. 有且仅有一个称之为根的结点
  2. 除根节点外的其余结点可分为m(m>0)个互不相交的有限集 T 1 , T 2 , . . . , T m T_1,T_2,...,T_m T1,T2,...,Tm其中每个集合本身又是一棵树,并且称之为根的子树(SubTree)
5.1.2 树的基本术语
  1. 结点:树中的一个独立单元.
  2. 结点的度:结点拥有的子树数.
  3. 树的度:树内各结点度的最大值.
  4. 叶子:度为零的结点,也称为终端结点.
  5. 非终端结点:度不为零的结点,也称为分支结点.
  6. 双亲和孩子:结点的子树的根称为孩子,相应地,该结点称为孩子的父亲.
  7. 兄弟:同一个双亲的孩子之间称为兄弟.
  8. 祖先:从根到该结点所经分支上的所有结点.
  9. 子孙:以某结点为根的子树中的任意一个结点都称为该结点的子孙.
  10. 层次:结点的层次从根开始定义起,根为第一层,根的孩子是第二层.
  11. 堂兄弟:双亲在同一层的结点互为堂兄弟.
  12. 树的深度:树中结点的最大层次数.
  13. 有序树和无序树:如果将树中结点的各子树看成从左到右是有次序的(即不能互换),则称该树为有序树,否则为无序树.
  14. 森林:是m(m ⩾ 0 \geqslant 0 0)颗互不相交的树的集合.
5.1.3 二叉树的定义

**二叉树(Binary Tree)**是n( n ⩾ 0 n\geqslant0 n0)个结点所构成的集合.对于非空树T:

  1. 有且仅有一个称为根的结点
  2. 除根结点以外的其余结点分为两个互不相交的子集 T 1 和 T 2 T_1和T_2 T1T2,分别称为T的左子树和右子树,且两者本身又都是二叉树.

二叉树与树的主要区别:

  1. 二叉树每个结点至只有两棵子树.
  2. 二叉树的子树有左右之分,其次序不能任意颠倒.

5.2 树和二叉树的抽象数据类型定义

p115-118

5.3 二叉树的性质和存储结构

5.3.1 二叉树的性质

**性质一:**在二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i1个结点 ( i ⩾ 1 ) (i\geqslant1) (i1)

**性质二:**深度为k的二叉树至多有 2 k − 1 2^k-1 2k1个结点

**性质三:**对任意一棵二叉树T,如果其终端结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1–即二叉树的终端结点数个数为所有度数为二的结点数加一.

特殊二叉树:

  1. **满二叉树:**深度为k且含有 2 k − 1 2^k-1 2k1个结点的二叉树.

  2. **完全二叉树:**深度为k的,有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VPhQD1Ir-1606220592962)(http://www.donaldlee.cn/img/满,完全二叉树.jpg)]

**性质四:**具有n个结点的完全二叉树的深度为 不 大 于 ( l o g 2 n ) 的 整 数 + 1 不大于(log_2n)的整数+1 (log2n)+1

**性质五:**如果对一棵有n个结点的完全二叉树的结点按层序编号(从第一层到第 不 大 于 ( l o g 2 n ) 的 整 数 + 1 不大于(log_2n)的整数+1 (log2n)+1层,每层从左到右),则对结点i ( 1 ⩽ i ⩽ n ) (1\leqslant i\leqslant n) (1in)

  1. 如果i=1,则点i是二叉树的根,无双亲;如果i>1,则其双亲是结点(i/2)取不大于其的整数.
  2. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i.
  3. 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1.
5.3.2 二叉树的存储结构
  1. 顺序存储结构

    为了能在存储结构中表示出结点之间的关系,二叉树中的结点必须按照以下规则存放:

    1. **对于完全二叉树:**只需要从根起按层存储即可.依自上而下,自左而右的顺序存储元素.

    2. **对于一般二叉树:**将其每个结点与完全二叉树上的结点相对照,不存在的项即即为0.

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H0tBNqBj-1606220592963)(http://www.donaldlee.cn/img/二叉树的顺序存储结构.jpg)]

  2. 链式存储结构

    1. **二叉链表:**结点分为三个域:数据域,左右指针域

    2. **三叉链表:**结点分为三个域:数据域,左右指针域,指向双亲结点的指针域.

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BgcnXZRn-1606220592963)(http://www.donaldlee.cn/img/二叉树的链表存储结构.gif)]

5.4 遍历二叉树和线索二叉树

5.4.1 遍历二叉树
1. 遍历二叉树算法描述

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

三种遍历方式:

  1. 先序遍历:

    若二叉树为空,则空操作;否则:

    1. 访问根节点
    2. 先序遍历左子树
    3. 先序遍历右子树
  2. 中序遍历:

    若二叉树为空,则空操作;否则:

    1. 中序遍历左子树
    2. 访问根节点
    3. 中序遍历右子树
  3. 后序遍历:

    若二叉树为空,则空操作;否则:

    1. 后序遍历左子树
    2. 后序遍历右子树
    3. 遍历根节点

详细代码将p122-126

2. 根据遍历序列确定二叉树

由二叉树的先序和中序序列,或者后序和中序序列均能唯一确定一棵二叉树.

详细见p125

3. 二叉树遍历算法的应用
  1. 创建二叉树的存储结构–二叉链表 p126
  2. 复制二叉树 p127
  3. 计算二叉树深度 p127
  4. 统计二叉树中的结点个数 p128
5.4.2 线索二叉树
1. 线索二叉树的基本概念

在使用二叉链表结构储存二叉树时,有n个结点的二叉树必定存在n+1个空链域,使得存储密度下降,所以可以将空链域利用起来,使空的左右子树指针分别指向本结点的前驱和后继,这种链表二叉树被叫做线索链表,被利用起来的空指针域叫做线索.

线索二叉树的结点形式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hi2nCbTq-1606220592964)(http://www.donaldlee.cn/img/线索二叉树的结点形式.jpg)]

当ltag=0时,表示此结点的左指针域lchild指向左子树

当ltag ≠ 0 \neq0 =0时,表示此结点的左指针域lchild指向前驱

当rtag=0时,表示此结点的右指针域rchild指向右子树

当rtag ≠ 0 \neq0 =0时,表示此结点的左指针域rchild指向后继

2. 构造线索二叉树

构造过程实际上是将空指针改为指向前驱或后继.p129-130

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RsA77gLL-1606220592965)(http://www.donaldlee.cn/img/中序线索二叉树.jpg)]

3. 遍历线索二叉树

线索二叉树的遍历和在指定的持续下查找结点的前驱和后继算法都变得简单.

因此,若需要经常查找结点在所遍历线性序列中的前驱和后继,则采用线索链表作为存储结构.

线索化过程中,可以根据想要采用的遍历方式(前序,中序,后序)将二叉树变成一个线性序列,既可以看出相应的空结点的前驱和后继是哪个结点.

所以,线索二叉树分为前序,中序和后序线索二叉树三种类型.p129-131

在线索化二叉树过程中查确定结点前驱和后继的三种情况:

  1. 在中序线索二叉树中查找:

    1. 查找某结点的前驱
      1. 若此结点Ltag为1,则表示该结点无左子树,所以该结点的左链表示其前驱.
      2. 若此结点Ltag为0,则表示该结点有左子树,结点前驱为遍历左子树时最后访问的结点(左子树中最右下的结点).
    2. 查找某节点的后继
      1. 若此结点Rtag为1,则该结点无右子树,所以该结点的右链指示其后继.
      2. 若此结点Rtag为0,则该结点有右子树,所以该结点的右链指示其遍历其右子树时访问的第一个结点,即右子树的最左下结点.
  2. 在先序线索二叉树中查找

    同上,见p131

  3. 在后序线索二叉树中查找:

    同上,见p131

5.5 树和森林

5.5.1 树的存储结构
1. 双亲表示法

在这种表示法中,以一组连续的存储单元存储树的结点,每一个结点除了数据域data以外,还付设一个parent域用以指示其双亲结点的位置.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y8gJki0Q-1606220592965)(http://www.donaldlee.cn/img/树的双亲表示法.png)]

2. 孩子表示法

由于每个结点可能有多棵子树,则可以用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根节点.

例,用孩子表示法表示上述树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZrJWViZU-1606220592966)(http://www.donaldlee.cn/img/孩子表示法.png)]

3. 孩子兄弟法

又称二叉树表示法,或二叉链表表示法,即以二叉链表做树的存储结构.

链表中结点的两个链域分别指向该结点的第一个孩子和下一个兄弟结点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QcJRUS2-1606220592966)(http://www.donaldlee.cn/img/孩子兄弟法节点构成.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxPQL4Aj-1606220592967)(http://www.donaldlee.cn/img/孩子兄弟法.png)]

5.5.2 森林与二叉树的转换
1. 森林转化成二叉树

而森林是由多棵树组成,为了便于对森林的遍历等操作,需要将森林中的所有树都组合成一颗大的二叉树,转化步骤为:

  1. 首先将森林中树各自转化为二叉树,通过孩子兄弟法;
  2. 森林中第一棵二叉树的树根作为转化后二叉树的树根;
  3. 其他树的树根作为第一棵树树根的兄弟结点,进行连接;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YwxHkpLs-1606220592968)(http://www.donaldlee.cn/img/森林转化成二叉树.png)]

2. 二叉树转化成森林

将一棵二叉树转化成森林,可按如下步骤进行:

①抹线:将二叉树根结点与其右孩子之间的连线,以及沿着此右孩子的右链连续不继搜索到的右孩子间的连线抹掉。这样就得到了若干棵根结点没有右子树的二叉树。

②将得到的这些二叉树用前述方法分别转化成一般树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBG9zUfr-1606220592968)(http://www.donaldlee.cn/img/二叉树转化成森林.jpg)]

5.5.3 树和森林的遍历
1. 树的遍历

根据树的定义可引出两种次序遍历树的方法(类似于前中后序遍历):

  1. **先根(次序)遍历树:**先访问树的根结点,然后依次先根遍历根的每棵子树
  2. **后根(次序)遍历树:**先依次后根遍历每棵子树然后访问根结点.
2. 森林的遍历
  1. 先序遍历森林

    若森林不为空,则可按以下规则遍历树林

    1. 访问森林中第一棵树的根结点
    2. 先序遍历第一棵树的根结点的子树森林
    3. 先序遍历除去第一棵树之后剩余的树构成的森林
  2. 中序遍历森林

    若森林非空,则可按以下规则遍历.森林的中序遍历由于森林中树不一定为二叉树,所以根结点最后在遍历,相当于二叉树的后序遍历.

    1. 中序遍历森林中第一棵树的根结点的子树森林.
    2. 访问第一棵树的根结点
    3. 中序遍历除去第一棵树之后剩余的树构成的森林

5.6 哈夫曼树及其应用

5.6.1 哈夫曼树的基本概念

哈夫曼树又称为最优树,是一类带权路径长度最短的树.

  1. **路径:**从树中的一个结点到另一个结点之间的分支构成这两个结点之间的路径.

  2. **路径长度:**路径上的分支数目称作路径长度.

  3. **树的路径长度:**从树根到每一个结点的路径长度之和.

  4. **权:**赋予某个实体的一个量,是对尸体的某个或某些属性的数值化描述.

  5. **结点的带权路径长度:**从该结点到树根之间的路径长度与该结点上权的乘积.

  6. 树的带权路径长度:树中所有叶子节点的带权路径长度之和.

  7. **哈夫曼树:**设有m个权值,可以构造一个含n个叶子结点的二叉树,每个叶子结点的权值为 w i w_i wi,则其中树的带权路径长度最小的二叉树称为哈夫曼树.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gmVWlMC1-1606220592968)(http://www.donaldlee.cn/img/具有不同带权路径长度的二叉树.jpg)]

5.6.2 哈夫曼树的构造算法
1. 哈夫曼树的构造过程
  1. 根据指定的n个权值构造n棵只有根结点的二叉树,这n棵二叉树构成一个森林F.
  2. 在森林F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且重置新的二叉树的根结点的权值为其左右子树上根结点的权值之和.
  3. 在森林F中删除这两棵树,同时将新的到的二叉树加入到F中.
  4. 重复2,3步,直到F只含有一棵树便是哈夫曼树.
2. 哈夫曼算法的实现

由于哈夫曼树没有度为一的结点,则一棵有n个叶子结点的哈夫曼树共有2n-1个结点,可以存储在大小为2n-1大小的数组中.

哈夫曼树结点的形式:权值,双亲指针,左指针,右指针

详细编码见P138-139

5.6.1 哈夫曼编码
1. 哈夫曼编码的主要思想:

有关编码的两个概念:

  1. **前缀编码:**如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码.
  2. **哈夫曼编码:**对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到叶子路径上各分支的赋值组成一个二进制串,该二进制串就称为哈夫曼编码.

哈夫曼编码的性质:

  1. 哈夫曼编码都是前缀编码.
  2. 哈夫曼编码是最优前缀编码.
2. 哈夫曼编码的算法实现

在构造了哈夫曼树之后,依次以叶子结点为出发点,向上回溯至根结点为止,回溯时走左分支则生成代码0,走右分支则生成代码0.

详细代码实现见P141

3. 文件的编码和译码
  1. **编码:**有了字符集的哈夫曼编码表之后,对数据文件的编码过程为:
    1. 依次读入文件中的字符c
    2. 在哈夫曼编码表中找到此字符
    3. 将字符c转化为编码表中的字符串
  2. **译码:**译码必须借助哈夫曼树
    1. 依次读入文件的二进制码
    2. 从哈夫曼树的根结点出发,若当前读入0,则走向左孩子,否则走向右孩子
    3. 一当到达某一叶子结点时便译出相应的字符编码,然后重新从根出发继续译码.

第六章 图

6.1 图的定义和基本术语

6.1.1 图的定义

图(Gragh)G由两个集合V和E组成,记作 G = ( V , E ) G=(V,E) G=(V,E),其中V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合,这些顶点偶对称之为.V(G)和E(G)通常分别表示图G的顶点集合和边集合.

若E(G)为有向边的集合,则称该图为有向图;若E(G)为无向边的集合,则称该图为无向图.

6.1.2 图的基本术语
  1. **子图:**假设有两个图G=(V,E)和G’=(V’,E’),如果V’ ⊆ \subseteq V且E’ ⊆ \subseteq E,则称G’为G的子图.

  2. **无向完全图和有向完全图:**对于无向图,若具有n(n-1)/2条边,则称为无向完全图.对于有向图,则具有n(n-1)条弧,则称为有向完全图.

    即每个顶点都与其他顶点拥有一条边.

  3. **稀疏图和稠密图:**有很少条边或弧的图称为稀疏图,反之称为稠密图.

  4. **权和网:**在实际应用中,每条边可以表上具有某种意义的值,该数值称为该边上的权.带权的图通常称为网.

  5. **邻接点:**对于无向图,两个直接相连的点称为邻接点.

  6. 度,入度和出度:顶点的是指和顶点相关联的边的数目,记作TD(v).对于有向图,顶点的度分为出度和入度.

    1. 入度是以顶点为头(即指向该顶点的边)的弧的数目.
    2. 出度是以顶点为尾(即从该顶点指出的边)的弧的数目.
  7. **路径和路径长度:**无向图中,从顶点v到顶点v’的路径是一个顶点序列.路径长度是一条路径上经过的边或弧的数目.

  8. **回路或环:**第一个顶点和最后一个顶点相同的路径称为回路或环.

  9. **简单路径,简单回路或简单环:**序列中顶点不重复出现的路径称为简单路径.除了第一和最后一个顶点之外,其他顶点不重复的回路称为简单回路或者简单环.

  10. 连通,连通图和连通分量:在无向图中,如果两个顶点之间有路径,则称两顶点是连通的.对于图中的任意两个顶点都是连通的,则此图称为连通图.无向图中的极大连通子图称为连通分量.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rE6kkDrx-1606220592969)(http://www.donaldlee.cn/img/无向图及其连通分量.jpg)]

  11. 强连通图和强连通分量:在有向图中,如果对于每一对顶点之间都存在路径,则称该有向图为强连通图.有向图中的极大强连通子图称作强连通分量.

  12. 连通图的生成树:一个极小连通子图,它含有图中的全部顶点,但只有足以构成一棵树的n-1条边,这样的连通子图称为连通图的生成树.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1EO98rc-1606220592970)(http://www.donaldlee.cn/img/连通图的生成树.jpg)]

  13. 有向树和生成森林:有一个顶点的入度为0,其余顶点的入度都为1的有向图称为有向树.

    一个有向图的生成森林是由若干棵有向树组成,含有图中的所有顶点,但只含有足以构成若干棵不相交的有向树的弧.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aMKIeSeu-1606220592971)(http://www.donaldlee.cn/img/有向图及其生成森林.png)]

6.2 图的类型定义

见P152-153

6.3 图的存储结构

6.3.1 邻接矩阵

图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系,即图没有顺序存储结构,但可以借用二维数组来表示元素之间的关系,即邻接矩阵表示法.

1. 邻接矩阵表示法

邻接矩阵是表示顶点之间相邻关系的矩阵.

设G(V,E)是具有n个顶点的(无向)图,则G的邻接矩阵是具有以下性质的n阶方阵.
KaTeX parse error: Unknown column alignment: o at position 33: …\begin{array}{co̲ls} 1,若<v_i,v_j…
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7y0IQHjm-1606220592971)(http://www.donaldlee.cn/img/图的邻接矩阵.jpg)]

注: < v i , v j > <v_i,v_j> <vi,vj>代表有向图中 v i 到 v j v_i到v_j vivj中间的一条弧. ( v i , v j ) (v_i,v_j) (vi,vj)代表 v i 到 v j v_i到v_j vivj之间的一条边.

上式的意思为,若点 v i 到 v j v_i到v_j vivj之间有边或弧,那么对应的邻接矩阵中的值 A [ i ] [ j ] A[i][j] A[i][j]即为1,否则此项为0.

若是网(即有向图),者可定义为:
KaTeX parse error: Unknown column alignment: o at position 33: …\begin{array}{co̲ls} w_{i,j},若<v…
w i , j 为 < v i , v j > 的 权 值 w_{i,j}为<v_i,v_j>的权值 wi,j<vi,vj>

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

以无向网为例说明创建图的算法.

算法步骤:

  1. 输入总顶点数.
  2. 依次输入点的信息存入定点表中.
  3. 初始化邻接矩阵,使每个权值初始化为极大值.
  4. 构造邻接矩阵.依次输入每条边依附的顶点和其权值,确定两个顶点在图中的位置之后,使相应边赋予相应的权值,同时使其对称边赋予相同的权值.

具体代码见:P155

3. 邻接矩阵表示法的优缺点
  1. 优点:
    1. 便于判断两个顶点之间是否有边,即根据Aij=1或0来判断.
    2. 便于计算各个点的度.对于无向图,邻接矩阵第i行元素之和即为顶点i的度;对于有向图,第i行元素之和即为顶点i的出度,第i列元素之和即为顶点i的入度.
  2. 缺点:
    1. 不便于增加和删除顶点.
    2. 不便于统计边的数目,需要扫描邻接矩阵的所有元素才能确认.
    3. 空间复杂度高.
6.3.2 邻接表
1. 邻接表表示法

邻接表是图的一种链式存储结构.在邻接表中,对图中每个顶点 v i v_i vi建立一个单链表,把与 v i v_i vi相邻接的顶点全都放在这个链表中.

邻接表的每一个单链表的第一个结点存放有关顶点的信息,把这一结点看成链表的表头,其余结点存放有关边的信息.,这样邻接表便由两部分组成:

  1. **表头结点表:**所有表头结点以顺序结构的形式存储,以便可以随机访问任意一顶点的边列表.

    表头结点包括**数据域(data)链域(firstarc)**两部分.

  2. 边表:由表示图中顶点间关系的2n个边链表组成.边链表中边结点包括邻接点域,数据域,链域三部分.

    1. 邻接点域表示与表头结点表示的顶点所邻接的点(此边结点代表的点)在表头结点表中的位置.
    2. 数据域存储与边有关的数据,如权值.
    3. 链域指示与顶点所邻接的下一个点的位置.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjweBbFV-1606220592972)(http://www.donaldlee.cn/img/表头结点和边结点.jpg)]

顶点 v i v_i vi的度恰为第i个链表中的结点数.

在有向图中,第i个链表中的结点个数只是顶点 v i v_i vi的出度;而要求入度必须遍历整个邻接表.在所有链表中,其邻接点域的值为i的结点1个数是顶点 v i v_i vi的入度.

可以建立一个有向图的逆邻接表,即对每个顶点都建立一个链接所有进入此顶点的边的表.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wp8lHLMc-1606220592972)(http://www.donaldlee.cn/img/逆邻接表.jpg)]

2. 采用邻接表表示法创建无向图

基于上述方法,要创建一个图则需要创建其相应的顶点表和边表.以一个无向图为例:

算法步骤:

  1. 输入总点数和总边数
  2. 依次输入点的信息存入顶点表中,使每个表头结点的指针域初始化为NULL.
  3. 创建邻接表.依次输入每条边依附的两个结点,确定这俩个顶点的序号i和j之后,将此边结点分别插入 v i , v j v_i,v_j vi,vj对应的两个边链表的头部.
3. 邻接表表示法的优缺点
  1. 优点:
    1. 便于增加和删除顶点
    2. 便于统计边的数目,按顶点表顺序扫描所有边表可得到变得数目(无向图中所有边表结点数除二即为变数,有向图中所有边表结点数即为边数.)
    3. 空间效率高.
  2. 缺点:
    1. 不便于判断顶点之间是否有边,要判断两个顶点之间是否有边,就需要扫描其中一个点的边表.
    2. 不便于计算有向图各个顶点的度.
6.3.3 十字链表

十字链表是有向图的另一种链式存储结构.可以看成是将有向图的邻接表和逆邻接表结合起来得到的一种链表.

在十字链表中,对应于有向图中每一条弧有一个结点,对应于每个顶点也有一个结点.

即对于邻接表来说,在每一个边及顶点结点都加一个指针域,两个指针域分别指向此结点指向及被指向的结点.

弧结点有五个域:

  1. 尾域(tailvex):指示弧尾
  2. 头域(headvex):指示弧头
  3. 链域(hlink):指向弧头相同的下一条弧
  4. 链域(tlink):指向弧尾相同的下一条弧
  5. 信息域(info):指向该弧的相关信息

顶点结点有三个域:

  1. 数据域(data):存储和顶点有关的信息
  2. **firstin指针域:**指向以该顶点为弧头的第一个弧顶点
  3. **firstout指针域:**指向以该结点为弧尾的第一个弧结点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tgtItHAh-1606220592973)(http://www.donaldlee.cn/img/有向图的十字链表.jpg)]

6.3.4 邻接多重表

多重链表是图的另一种链式存储结构.虽然邻接表的同一条边会被这条边的两个顶点分别存储一次,对于需要对边进行的操作这样会造成很多麻烦.

邻接多重表中,每个边都用一个结点表示,由六个域组成:

  1. **标志域(mark)😗*可以用来标记这条边是否被搜索过
  2. **两个顶点指针(ivex,jvex)😗*分别指向该边依附的两个顶点.
  3. **两条边指针(ilink,jlink)😗*分别指向下一条依附于点ivex的边和下一条依附于jvex的边.
  4. **信息域(info)😗*存储各种信息.

每一个顶点也用一个结点表示,由两个域组成:

  1. 数据域:存储数据
  2. 指针域(firstedge):指示第一条依附于该顶点的边.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TN1jQqqO-1606220592973)(http://www.donaldlee.cn/img/多重邻接表.jpg)]

6.4 图的遍历

6.4.1 深度优先搜索
1. 深度优先搜索遍历的过程

深度优先搜索遍历类似于树的先序遍历,是树的先序遍历的推广.

对于一个连通图,深度优先搜索遍历的过程如下:

  1. 从图中的某个顶点v出发,访问v.
  2. 找出刚访问过的顶点的第一个未被访问过的邻接点,访问该顶点.以该顶点为新顶点,重复此步骤,直到访问过的顶点没有未被访问过的邻接点为止.
  3. 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问过的邻接点,访问该顶点.
  4. 重复步骤2,3,直到图中的所有顶点都被访问过,搜索结束.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PP3iqXkh-1606220592974)(http://www.donaldlee.cn/img/图的深度优先搜索.jpg)]

在图经过深度优先搜索遍历完成后,可以形成一棵以开始顶点为根的树,称为深度优先生成树

2. 深度优先搜索遍历的算法实现

深度优先搜索是一个递归地过程,为了能在遍历的过程中区分顶点是否已经被访问过,需要设置访问标志数组visited[n],其初始值为"false"一旦某个顶点被访问过时,则其相应的分量置为"true".

算法步骤:

  1. 从图中某个顶点出发,访问v,并置visited[v]的值为true.
  2. 依次检查v的所有邻接点w,如果visited[w]的值为false,再从w出发进行递归遍历,直到图中的所有顶点都被访问过.

注:若图为非连通图,上述遍历过程执行过之后,图中一定还有顶点未被访问,需要从图中另选一个未被访问过的顶点作为起始点,重复上述深度优先搜索过程.

具体代码见课本P162

3.深度优先搜索遍历的算法分析
  1. 当用邻接矩阵表示图时,查找每个顶点的邻接点的时间复杂度为 O ( n 2 ) O(n^2) O(n2)其中n为图中顶点数.
  2. 当用邻接表作为图的存储结构时,查找每个顶点的邻接点的时间复杂的为 O ( e ) O(e) O(e),其中e为图中的边数.因此,深度优先搜索遍历图时的时间复杂度为 O ( n + e ) O(n+e) O(n+e).
6.4.2 广度优先搜索
1. 广度优先搜索遍历的过程

广度优先搜索遍历类似与树的按层次遍历的过程.

广度优先搜索遍历的过程如下:

  1. 从图中的某个顶点v出发,访问v.
  2. 依次访问v的各个未曾访问过的邻接点.
  3. 分别从这些邻接点出发依次访问它们的邻接点,并使"先被访问的顶点的邻接点"先于"后被访问的顶点的邻接点"被访问.重复步骤三,直至图中所有已被访问的邻接点都被访问到.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfS7tS66-1606220592975)(http://www.donaldlee.cn/img/广度优先搜索与深度优先搜索的比较.gif)]

2. 广度优先搜索遍历算法的实现

广度优先搜索的特点是:尽可能先对横向进行搜索.

广度优先搜索不仅需要访问标志是数组visited[n],也需要一个队列来保存已被访问过的顶点,以便对已被访问过的顶点的邻接点进行按顺序的访问.

算法步骤:

  1. 从图中某个顶点v出发,访问顶点v,并置visited[v]的值为true,然后将v进队.
  2. 只要队列不空,则重复以下操作:
    1. 队头顶点u出队.
    2. 依次检查u所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队.
3. 广度优先搜索遍历的算法分析

时间复杂度与深度优先搜索相同.

6.5 图的应用

6.6.1 最小生成树

在一个连通图的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的最小生成树.

构造最小生成树算法很多都利用了MST的性质:

假设 N = ( V , E ) N=(V,E) N=(V,E)是一个连通网,U是顶点集V的一个非空子集.若(u,v)是一条具有最小权值(代价)的边,其中 u ∈ U , v ∈ V − U u\in U,v \in V-U uU,vVU,则必然存在一棵包含边(u,v)的最小生成树.

其含义为:如果一条边的权值为连通图中最小的,那么该连通图的最小生成树一定包含这条边.

下面是两个使用了MST原理的最小生成树构造算法:

1. 普里姆算法-加点法
  1. 普里姆算法的构造过程

    假设 N = ( V , E ) N=(V,E) N=(V,E)是连通网,TE是N上最小生成树中边的集合.

    1. U(还未被纳入最小生成树的顶点)={ u 0 , u 0 ∈ V u_0,u_0\in V u0,u0V},TE={}.
    2. 在所有 u ∈ U , v ∈ V − U ( 即 已 被 纳 入 最 小 生 成 树 的 顶 点 ) u\in U,v\in V-U(即已被纳入最小生成树的顶点) uU,vVU()中找一条权值最小的边( u 0 , v 0 u_0,v_0 u0,v0)并入集合TE,同时, v 0 v_0 v0并入U.
    3. 重复2,直至U=V为止.

    普里姆算法也可以称作"加点法".

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8mulyd2-1606220592975)(http://www.donaldlee.cn/img/普里姆算法.jpg)]

  2. 普利姆算法实现

    详细代码见课本P167

2. 克鲁斯卡尔算法-加边法
  1. 克鲁斯卡尔算法的构造过程

    假设连通网N=(V,E),将N中的边按权值从小到大排列.

    1. 初始状态为只有n个顶点二无边儿的非连通图T(V,{}),图中的每个顶点自生成一个连通分量.
    2. 在E中选择权值最小的边,若该边依附的顶点落在T中不同的连通分量上(既不生成回路),则将此边加入到T中,否则舍去此边二选择下一条权值最小的边.
    3. 重复2,直至T中所有顶点都在同一连通分量上.

    克鲁斯卡尔算法也可以称为"加边法"

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4s6b6prQ-1606220592976)(http://www.donaldlee.cn/img/克鲁斯卡尔算法.jpg)]

  2. 克鲁斯卡尔算法的实现

    P169

6.6.2 最短路径

在带权有向图中,习惯上称路径上的第一个顶点为源点,最后一个点为终点.

主要有两种最短路径问题,一种是求从某个源点到其余各顶点的最短路径,另一种是求每一对顶点之间的最短路径.

1. 从某个源点到其余各顶点的最短路径–迪杰斯特拉算法.

单源点的最短路径问题:给定带权有向图G和源点 v 0 v_0 v0求从 v 0 v_0 v0到G中其余各顶点的最短路径.

**迪杰斯特拉算法:**按路径长度递增的次序产生最短路径的算法.

  1. 迪杰斯特拉算法的求解过程:

对于网N=(V,E),将N中的顶点分成两组:

第一组S:以求出的最短路径的终点集合(初始时只包含源点 v 0 v_0 v0).

第二组V-S:尚未求出的最短路径的顶点集合(初始为V-{ v 0 v_0 v0}).

算法将按各顶点与 v 0 v_0 v0之间最短路径长度递增的次序,逐个将集合V-S中的顶点加入到集合S中,在这个过程中,总保持从 v 0 v_0 v0到集合中各点的路径长度始不大于到集合V-S中各顶点的路径长度.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-orxCXHCU-1606220592977)(http://www.donaldlee.cn/img/迪杰斯特拉算法.jpg)]

  1. 迪杰斯特拉算法的实现:

假设用带权的邻接矩阵arcs来表示带权有向网G,G.arcs [ i ] [ j ] [i][j] [i][j]表示弧 < v i , v j > <v_i,v_j> <vi,vj>上的权值.若 < v i , v j > <v_i,v_j> <vi,vj>不存在,则置G.arcs [ i ] [ j ] [i][j] [i][j] ∞ \infin ,源点为 v 0 v_0 v0.

算法引入以下辅助的数据结构:

  1. 一维数组S[i]:记录从源点 v 0 v_0 v0到终点 v i v_i vi是否已被确定最短路径长度,true表示确定,false表示尚未确定.
  2. 一维数组Path[i]:记录从源点 v 0 v_0 v0到终点 v i v_i vi的当前最短路径上 v i v_i vi的直接前驱顶点序号.其初始值为:如果从 v 0 v_0 v0 v i v_i vi有弧,则Path[i]为 v 0 v_0 v0;否则为-1.
  3. 一维数组D[i]:记录从源点 v 0 v_0 v0到终点 v i v_i vi的当前最短路径长度.其初始值为:如果从 v 0 v_0 v0 v i v_i vi有弧,则D[i]为弧上的权值;否则为 ∞ \infin

算法步骤:

  1. 初始化:

    1. 将源点 v 0 v_0 v0加到S中,即S[ v 0 v_0 v0]=true;
    2. v 0 v_0 v0到各个终点的最短路径长度初始化为权值,即D[i]=G.arcs [ i ] [ j ] , ( v i ∈ V − S ) [i][j],(v_i\in V-S) [i][j],(viVS);
    3. 如果 v 0 v_0 v0和顶点 v i v_i vi之间有弧,则将 v i v_i vi的前驱置为 v i v_i vi即Path[i]= v 0 v_0 v0,否则Path[i]=-1.
  2. 循环n-1次,执行以下操作:

    1. 选择下一条最短路径的终点 v k v_k vk,使得:
      D [ k ] = M i n { D [ i ] ∣ v i ∈ V − S } D[k]=Min\{D[i]|v_i\in V-S\} D[k]=Min{D[i]viVS}
  3. v k v_k vk加入到S中,即S[k]=true.

  4. 根据条件更新从 v 0 v_0 v0出发到集合V-S上任一顶点的最短路径长度,若条件 D [ k ] + G . a r c s [ k ] [ i ] < D [ i ] D[k]+G.arcs[k][i]<D[i] D[k]+G.arcs[k][i]<D[i]成立,则更新 D [ i ] = D [ k ] + G . a r c s [ k ] [ i ] D[i]=D[k]+G.arcs[k][i] D[i]=D[k]+G.arcs[k][i],同时更改 v i v_i vi的前驱为 v k v_k vk,Path[i]=k.

2. 每一对顶点之间的最短路径-弗洛伊德算法

算法讲解:https://www.cnblogs.com/wangyuliang/p/9216365.html

弗洛伊德算法仍然使用带权的1邻接矩阵arcs来表示有向网G,求从顶点 v 0 v_0 v0 v j v_j vj的最短路径.

算法的实现需要引入以下辅助的数据结构:

  1. 二维数组 P a t h [ i ] [ j ] Path[i][j] Path[i][j]:最短路径上顶点 v j v_j vj的前一顶点的序号.
  2. 二维数组 D [ i ] [ j ] D[i][j] D[i][j]:记录顶点 v i v_i vi v j v_j vj之间的最短路径长度.

算法步骤:

v i v_i vi v j v_j vj的最短路径长度初始化,即 D [ i ] [ j ] = G . a r c s [ i ] [ j ] D[i][j]=G.arcs[i][j] D[i][j]=G.arcs[i][j],然后进行n次比较和更新.

  1. v i v_i vi v j v_j vj之间加入顶点 v 0 v_0 v0比较 ( v i , v j ) (v_i,v_j) (vi,vj) ( v i , v 0 , v j ) (v_i,v_0,v_j) (vi,v0,vj)的路径长度,取其中较短者为 v i v_i vi v j v_j vj的中间顶点序号不大于0的最短路径.
  2. v i v_i vi v j v_j vj之间加入点 v 1 v_1 v1,得到 ( v i , . . . , v 1 ) (v_i,...,v_1) (vi,...,v1) ( v 1 , . . . , v j ) (v_1,...,v_j) (v1,...,vj),其中 ( v i , . . . v 1 ) (v_i,...v_1) (vi,...v1) v i v_i vi v 1 v_1 v1的且中间顶点的序号不大于0的最短路径, ( v 1 , . . . , v j ) (v_1,...,v_j) (v1,...,vj) v 1 v_1 v1 v j v_j vj且中间顶点的序号不大于0的最短路径,这两条路径已经在上一步中求出.比较 ( v − I , . . . , v 1 , . . . . , v j ) (v-I,...,v_1,....,v_j) (vI,...,v1,....,vj)与上一步求出的 v i v_i vi v j v_j vj的中间顶点序号不大于0的最短路径,取其中较短者作为 v i v_i vi v j v_j vj的中间顶点序号不大于1的最短路径…
  3. 依此类推,在 v i v_i vi v j v_j vj之间加入顶点 v k v_k vk,若 ( v i , . . . , v k ) (v_i,...,v_k) (vi,...,vk) ( v k , . . . , v j ) (v_k,...,v_j) (vk,...,vj)分别是从 v i v_i vi v k v_k vk和从 v k v_k vk v j v_j vj的中间顶点的序号不大于k-1的最短路径,则将 ( v i , . . . , v k , . . . , v j ) (v_i,...,v_k,...,v_j) (vi,...,vk,...,vj)和以得到的从 v i v_i vi v j v_j vj且中间顶点序号不大于k-1的最短路径比较,其长度短者便是从 v i v_i vi v j v_j vj的中间顶点的序号不大于k的最短路径.这样,经过n次比较之后,最后求得的必是从 v i v_i vi v j v_j vj的最短路径.按此方法,可以同时求得各对顶点间的最短路径.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIkCSzLU-1606220592977)(http://www.donaldlee.cn/img/弗洛伊德算法1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AlYtHwCB-1606220592977)(http://www.donaldlee.cn/img/弗洛伊德算法2.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98YyiKv3-1606220592979)(http://www.donaldlee.cn/img/弗洛伊德算法3.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qc6662cD-1606220592979)(http://www.donaldlee.cn/img/弗洛伊德算法4.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4a9puxtV-1606220592980)(http://www.donaldlee.cn/img/弗洛伊德算法5.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDgn626e-1606220592981)(http://www.donaldlee.cn/img/弗洛伊德算法6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CKP6RvHd-1606220592981)(http://www.donaldlee.cn/img/弗洛伊德算法7.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JiXz7h2X-1606220592981)(http://www.donaldlee.cn/img/弗洛伊德算法.png)]

6.6.3 拓朴排序
1. AOV-网

一个无环的有向图称作有向无环图,简称DAG图.有向无环图通常是描述一项工程或系统的进行过程的有效工具.通常把计划,施工过程,生产流程程序流程等都当成一个工程.

一般工程都可以分为若干个称作活动的子工程,而这些子工程之间一般受着一些条件的约束,如其中某些子工程的开始必须是在另一些子工程完成之后.

这种用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网,简称AOV-网.

在AOV-网中,不应该出现环.检测的办法是对有向图的顶点进行拓扑排序,若网中所有顶点都在他的拓扑有序序列中,则该AOV-网中必定不存在环.

所谓拓扑排序就是将AOV-网中的所有顶点排成一个线性序列,该序列满足:若在AOV-网中由顶点 v i v_i vi到顶点 v j v_j vj有一条路经,则在该线性序列中的顶点 v i v_i vi必定在 v j v_j vj之前.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33l4uvRd-1606220592982)(http://www.donaldlee.cn/img/拓扑排序.png)]

2. 拓扑排序的过程
  1. 在有向图中选一个无前驱的顶点且输出它.
  2. 从图中删除该顶点和所有以它为尾的弧.
  3. 重复1和2,直至不存在无前驱的顶点.
  4. 若此时输出的顶点数小于有向图中的顶点数,则说明有向图中的环,否则输出的顶点序列即为一个拓扑序列.
3. 拓扑排序的实现

P177-178

6.6.4 关键路径 *
1. AOE-网

与AOV-网相对应的是AOE-网,即以边表示活动的网.

AOE-网是一个带权有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的事件.

通常,AOE-网可用来估算工程的完成时间.

AOE-网在工程计划和经营管理中有广泛的应用,针对实际的问题,通常需要解决以下两个问题:

  1. 估算完成整项工程至少需要多少时间.
  2. 判断哪些活动是影响工程进度的关键.

由于整个工程只有一个开始点和一个完成点,故在正常状况(无环)下,网中只有一个入度为零的点,称作源点,也只有一个出度为零的点,称为汇点.在AOE-网中,一条路径各弧上的权值之和称为该路径的带权路径长度.

要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径,称为关键路径.关键路径上的活动叫做关键活动.

2. 关键路径求解过程
  1. 对图中顶点进行排序,在排序过程中按拓扑序列求出每个事件的最早发生时间ve(i).
  2. 按逆拓扑序列求出每个事件的最迟发生时间.
  3. 求出每个活动 a i a_i ai的最早开始时间e(i).
  4. 求出每个活动 a i a_i ai的最晚开始时间l(i).
  5. 找出e(i)=l(i)的活动 a i a_i ai,即为关键活动.由关键活动形成的由源点到汇点的每一条路径就是关键路径,关键路径有可能不止一条.
3. 关键路径算法的实现

P182-183

第七章 查找

7.1 查找的基本概念

概念和术语:

  1. **查找表:**查找表是由同一类型的数据元素(或记录)构成的集合.
  2. 关键字:是数据元素(或记录)中某个数据项的值,用它可以表示一个数据元素(或记录).若此关键字可以唯一标识一个记录,则此关键字为主关键字(对于不同的记录,其主关键字均不同).反之为次关键字.
  3. **查找:**是根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素.
  4. 动态查找表和静态查找表:托在查找的同时对表做出修改操作(如插入和删除),则相应的表为动态查找表,否则称为静态查找表.
  5. 平均查找长度:为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为该查找算法的平均查找长度.

7.2 线性表查找

7.2.1 顺序查找

顺序查找的查找过程为:从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功,反之则查找失败.

7.2.2 折半查找

折半查找也称二分查找.折半查找使用效率较高的查找方式,但是折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列.

**折半查找的查找过程为:**从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或小于中间记录的关键字,则在表中大于或小于中间记录的那一半中查找,这样重复操作,知道查找成功,或在某一部中查找区间为空.

7.2.3 分块查找

分块查找又称索引顺序查找,是一种介于顺序查找与折半查找之间的一种查找方法.

在此查找方法中,除表本身外,尚需建立一个"索引表".

索引表包括两项内容:

  1. 关键字项:其值为该指标内的最大关键字
  2. 指针项:只是该子表的第一个记录在表中的位置.

索引表按关键字有序,则表或者分块有序.

所谓分块有序指的是第二子表中所有记录的关键字均大于第一个子表的关键字,依此类推.

因此,分块查找过程需分两步进行,先确定待查记录所在的块(子表),然后再块中顺序查找.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utsQ4fNh-1606220592982)(http://www.donaldlee.cn/img/分块查找.png)]

7.3 树表的查找

7.3.1 二叉排序树

二叉排序树又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树.

1. 二叉排序树的定义

二叉排序树或者是一棵空树,或者是具有以下性质的二叉树:

  1. 若它的左子树不空,二左子树上所有结点的值均小于他根结点的值.
  2. 若他的右子树不空,则右子树上的所有结点的值均大于它根结点的值.
  3. 它的左右子树也分别为二叉排序树.

重要性质:中序遍历一棵二叉排序树时可以得到一个结点值递增的有序序列.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qjv8RXFq-1606220592983)(http://www.donaldlee.cn/img/二叉排序树.jpg)]

2. 二叉排序树的查找

在二叉排序树上进行查找和折半查找类似,也是一个逐步缩小查找范围的过程.

算法过程:

  1. 若二叉排序树为空,则查找失败,返回空指针.
  2. 若二叉排序树非空,将给定值Key与根结点的关键字T->data.key进行比较.
    1. 若key等于T->data.key,则查找成功,返回根结点地址.
    2. 若key小于T->data.key,则递归查找左子树.
    3. 若key大于T->data.key,这递归查找右子树.
3. 二叉排序树的插入

二叉排序树的插入是以查找为基础的.

要建一个关键字值为key的结点*S插入到二叉排序树中,则需要从根结点向下查找,当树中不存在关键字等于key的结点时才进行插入.新插入的结点一定是叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点.

算法步骤:

  1. 若二叉排序树为空,则待插入结点*S作为根结点插入到空树中.
  2. 若二叉排序树非空,则将key与根结点的关键字T->data.key进行比较:
    1. 若key小于T->data.key,则将*S插入左子树.
    2. 若key大于T->data.key,则将*S插入右子树.
4. 二叉排序树的创建

二叉排序树的创建是从空的二叉排序树开始的,每输入一个结点,经过查找操作,将新的结点插入到当前二叉排序树的合适位置.

算法步骤:

  1. 将二叉排序树T初始化为空树.
  2. 读入一个关键字为key的结点.如果读入的关键字key不是输入结束标志,则循环执行以下操作:
    1. 将此结点插入到二叉排序树T中.
    2. 读入一个关键字为key的结点.
5. 二叉排序树的删除

被删除的结点可能是二叉排序树中的任何结点,删除结点后,要根据其位置不同修改其双亲结点及相关结点的指针,以保持二叉排序树的特性.

算法步骤:

首先从二叉排序树的根结点开始查找关键字为key的待查结点,如果树中不存在此结点,则不做任何操作;否则,假设被删结点为*P(指向结点指针为p),其双亲结点为*f(指向结点的指针为f), p l 和 p R p_l和p_R plpR分别表示其左子树和右子树.

为不失一般性,可设*P是*F的左孩子(右孩子类似).下面分三种情况进行讨论:

  1. 若*P结点为叶子结点,即 P R 和 P L P_R和P_L PRPL均为空树.由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可.

    f->lchild=NULL

  2. 若*P结点只有左子树 P L P_L PL或者只有右子树 P R P_R PR,此时只要令 P L P_L PL P R P_R PR直接成为其双亲结点*f的左子树即可.

    f->lchild=p->lchild(或f->lchild=p->rchild)

  3. 若*P结点的左子树和右子树均不空.从下图b知,再删去*p结点之前,中序遍历该 C L C_L CL,二叉树得到的序列为{ . . . C L , C . . . Q L , Q , S L , S , P , P R , F . . . ...C_L,C...Q_L,Q,S_L,S,P,P_R,F... ...CL,C...QL,Q,SL,S,P,PR,F...},再删去*p之后,为保持其他元素之间的相对位置不变,可以有两种处理办法:

    1. 令*P的左子树为*f的左子树,而*p的右子树为*s的右子树,如图c所示.

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

    2. 令*P的直接前驱(或直接后继)替代*P,然后再从二叉排序树中删去它的直接前驱(或直接后继).

      如图d所示,当以直接前驱*s替代*p时,由于*s只有左子树 S L S_L SL,则在删去*s之后,只要令 S L S_L SL为*s的双亲*q的右子树即可.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jdagaBNw-1606220592983)(http://www.donaldlee.cn/img/二叉排序树删除结点.jpg)]

7.3.2 平衡二叉树
1. 平衡二叉树的定义

二叉树排序树查找算法的性能取决于二叉树的结构,而二叉排序树的形状则取决于其数据集.

如果数据呈有序序列,则二叉排序树是线性的,查找的时间复杂度为O(n);反之,如果二叉排序树的结构合理,则查找速度较快,查找时间复杂度为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n).事实上,速度高度越小,查找速度越快.

因此,希望二叉树的高度尽可能小,即出现了平衡二叉树(AVL树).

平衡二叉树或者是空树,或者具有以下特征的二叉排序树:

  1. 左子树和右子树的深度之差的绝对值不超过1.
  2. 左子树和右子树也是平衡二叉树.

二叉树上的平衡因子定义为:该结点左子树和右子树的深度之差.

2. 平衡二叉树的平衡调整方法

若插入了结点后破坏了平衡二叉树的特性,需对平衡二叉树进行调整.

调整方法为:找到离插入结点最近且平衡因子绝对值超过1的祖先结点,以该结点为根的子树称为最小不平衡子树,可见重新平衡的范围局限为这颗子树.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mo4tGsrX-1606220592984)(http://www.donaldlee.cn/img/平衡树的生成过程.jpg)]

一般情况下,假设最小不平衡子树的根结点为A,则失去平衡后进行调整的规律可归纳为下列四种:

  1. **LL型:**由于在A左子树根结点的左子树上插入结点,A的平衡因子由1增至2,致使以A为根的子树失去平衡,则需先以B为根进行一次现有的逆时针旋转操作,使 b L b_L bL成为A的左子树的根,再整体顺时针旋转,变为以B为根,最后以 B R B_R BR为根逆时针旋转,变为最终形态.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LsYTcwT-1606220592985)(http://www.donaldlee.cn/img/平衡二叉树 LL型调整操作.jpg)]

  2. **RR型:**由于在A的右子树根结点的右子树上插入结点,A的平衡因子由-1变为-2,致使以A为结点的子树失去平衡,则需先以B为根进行一次向右的顺时针旋转操作,再整体向左做逆时针操作,变为以B为根,再以 B R B_R BR为根向右顺时针旋转,变为最终形态.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d1bb4PcL-1606220592985)(http://www.donaldlee.cn/img/平衡二叉树 RR型调整操作.jpg)]

  3. **LR型:**由于在A的左子树的根结点的右子树上插入结点,A的平衡因子由1增至2,致使以A为根结点的指数失去平衡,则需要进行两次旋转操作.第一次对B及其右子树进行逆时针旋转,C转上去成为B的根,这时变成了LL型,所以第二次进行LL型的顺时针旋转即可恢复平衡.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQ9NdA5t-1606220592986)(http://www.donaldlee.cn/img/平衡二叉树 LR型调整操作.jpg)]

  4. **RL型:**由于在A的右子树根结点的左子树上插入结点,A的平衡因子由-1变为-2,致使以A为根结点的子树失去平衡,则旋转方法与LR型相对称,先顺时针转,再逆时针转.

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U4xfg1QL-1606220592987)(http://www.donaldlee.cn/img/平衡二叉树 RL型调整操作.jpg)]

3. 平衡二叉树的插入

在平衡二叉树BBST上插入一个新的元素e的递归算法可描述如下:

  1. 若BBST为空树,则插入一个数据元素为e的新结点作为BBST的根结点,树的深度增1.
  2. 若e的关键字和BBST的根结点的关键字相等,则不进行插入.
  3. 若e的关键字小于BBST的根结点的关键字,而且在BBST的左子树中不存在和e有相同关键字的结点,则将e插入在BBST的左子树上,并且当插入后的左子树深度加一时,分别就一下不同情况处理:
    1. BBST的根结点的平衡因子为-1(右子树的深度大于左子树的深度):则将根结点的平衡因子更改为0,BBST深度不变.
    2. BBST的根结点的平衡因子为0(左右子树深度相等):则将根结点的平衡因子更改为1,BBST深度加一.
    3. BBST的根结点的平衡因子为1(左子树的深度大于右子树的深度):若BBST的左子树的根结点的平衡因子为1,则需要进行单向右旋平衡处理,并且在右旋处理之后,将根结点和其右子树根结点的平衡因子更改为0,树的深度不变.
    4. 若BBST的左子树根结点的平衡因子为-1,则需要进行先向左,后向右的双向旋转平衡处理,并且在旋转处理之后,修改根结点和其左右子树的平衡因子,树的深度不变.
  4. 若e的关键字大于BBST的根结点的关键字,而且在BBST的右子树中不存在和e有相同关键字的结点,则将e插入在BBST的右子树上,并且当插入后的右子树深度加一时,分别就不同情况处理.其处理操作和3中所述相对称.
7.3.3 B-树

以上方法皆可称为內查找法,适用于查找存储在计算机中内存中较小的文件.

若是文件很大且存放于外存进行查找时,內查找法便不适用了.內查找法以结点为单位进行查找,需要反复进行内外存的交换.

B-树既是一种适用于外查找的平衡二叉树.

1. B-树的定义

一棵m阶的B-树,或为空树,或为满足以下特性的m叉树:

  1. 树中的每个结点至多有m棵子树.

  2. 若根结点不是叶子结点,则至少右两棵子树.

  3. 除根以外的所有非终端结点至少有(m/2)(取不大于其的整数)棵子树.

  4. 所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点(失败结点并不存在,指向这些结点的指针为空.引入失败结点是为了便于分析B-树的查找性能).

  5. 所有的非终端结点最多有m-1个关键字:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BHXGURL4-1606220592987)(http://www.donaldlee.cn/img/3阶的B-Tree.png)]

    特点:

    1. 所有结点均在同一层次
    2. 树中结点中的关键字都是有序的,且关键字 K i K_i Ki的左子树中的关键字均小于 K i K_i Ki而其右子树中的关键字均大于 K i K_i Ki.
    3. 除叶子结点外,有的结点有一个关键字,两棵子树,有的结点中有两个关键字,三棵子树,三阶B-树最多有两个关键字,三个子树.
2. B-树的查找

与二叉树类似.例如,在上图中查找关键字28,流程如下:首先,从根开始,磁盘块1有两个关键字,且17<28<35,所以,如果存在关键字为28的结点,就一定存在于 P 2 P_2 P2所指的子树,于是与磁盘块3进行顺序比较,又因为26<28<30,所以如果关键字存在,则一定存在于磁盘块8中,所以与磁盘块8顺序比较,则发现28=28,若在叶结点中还没有查找到该关键字,则查找失败.

3. B-树的插入

由于B-树中除根之外的所有非终端结点中的关键字个数必须大于等于(m/2)(向上取整)-1,因此,每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最底层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1,则插入完成,否则表明结点已满,要产生结点的"分裂",将此结点在同一层分成两个结点.

一般情况下,结点分裂方法是:以中间关键字为界把结点一分为二,成为两个结点,并把中间关键字向上插入到双亲结点上,若双亲结点已满,则采用同样的方法继续分解.最坏的情况下,一直分裂到树根结点,B-树的高度减一.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y8mmVtHV-1606220592988)(http://www.donaldlee.cn/img/B-树中进行插入.jpg)]

代码见P126

4. B-树的删除

删除操作是在B-树的某个结点中删除指定的关键字及其邻近的一个指针,删除后应该进行调整使该树仍然满足B-树的定义,也就是保证每个结点的关键字数目范围为[(m/2)向上取整-1,m].

删除记录后,结点的关键字个数如果小于(m/2)向上取整-1,则需要进行"合并"操作.

除了删除记录,还要删除该记录临近的指针.若该结点为最下层的非终端结点,由于其指针均为空,删除后不会影响其他结点,可以直接删除;若该结点不是最下层的非终端结点,邻近的指针则指向一棵子树,不可直接删除.此时可以做如下处理:将要删除记录用其右(左)边邻近指针指向的子树中关键字最小(大)的记录(该记录必定在最下层的非终端结点中)替换.

我们以下面的树为基础,进行删除操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mH20y9Q6-1606220592989)(http://www.donaldlee.cn/img/20181227211140622.png)]

首先明确一下这个树的定义。它是一个5阶树。所以,每个节点内元素个数为2~4个。

我们依次删除8、16、15、4这4个元素。

首先删除8,因为删除8后,不破坏树的性质,所以直接删除即可。得到如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BazqynVk-1606220592990)(http://www.donaldlee.cn/img/20181227211420300.png)]

然后删除16,这导致该节点只剩下一个13节点,不满足节点内元素个数为2~4个的要求了。所以需要调整。这里可以向孩子借节点,把17提升上来即可,得到下图。这里不能和兄弟节点借节点,因为从3,6节点中把6借走后,剩下的3也不满要求了。另外,也不能把孩子中的15提升上来,那样会导致剩下的14不满足要求。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cB2qWUyn-1606220592990)(http://www.donaldlee.cn/img/20181227211706731.png)]

然后删除15,删除15后同样需要调整。调整的方式是,18上升,17下降到原来15的位置,得到下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d0NHgWFd-1606220592991)(http://www.donaldlee.cn/img/20181227212017482.png)]

然后删除元素4,删除4后该节点只剩下5,需要调整。可是它的兄弟节点也都没有多余的节点可借,所以需要进行节点合并。节点合并时,方式会有多种,我们选择其中的一种即可。这里,我们选择父节点中的3下沉,和1,2,以及5进行合并,如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QHCeyS7c-1606220592992)(http://www.donaldlee.cn/img/20181227212413845.png)]

但这次调整,导致6不符合要求了。另外,6非根节点,但只有2个孩子,也不符合要求。需要继续调整。调整的方式是,将10下沉,和6,以及13,18合并为根节点,如下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FHp9S4ec-1606220592992)(http://www.donaldlee.cn/img/20181227212827164.png)]

结束。

7.3.4 B+树

B+树是一种B-树的变形树,更适用于文件索引系统.

1. B+树和B-树的差异

一棵m阶的B+树和m阶的B-树的差异在于:

  1. 有n棵子树的结点中含有n个关键字.
  2. 所有叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,其叶子结点本身依本身关键字的大小自小而大顺序链接.
  3. 所有的非终端结点可以看成是索引的一部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QvvreMKK-1606220592992)(http://www.donaldlee.cn/img/B+树.jpg)]

2. B+树的查找,插入和删除

在B+树上进行查找,插入和删除的过程基本上与B-树相似.

  1. **查找:**若非终端结点上的关键字等于给定值,并不终止,而是继续向下直到叶子结点.因此,在B+中,不管查找成功与否,每次查找都是走了一条从根到叶子的路径.
  2. **插入:**尽在叶子结点上进行插入,当结点中的关键字个数大于m时要分裂成两个结点,它们所包含的关键字个数分别为((m+1)/2)向下取整和((m+1)/2)向上取整;并且,它们的双亲结点中应该同时包含这两个结点中的最大关键字.
  3. **删除:**B+树的删除也是仅在叶子结点进行,当叶子结点中最大关键字被删除时,其在非终端结点中的值可以作为一个"分解关键字"存在.若因删除而是结点中关键字的个数少于(m/2)向上取整时,其和兄弟结点的合并过程亦和B-树类似.

7.4 散列表的查找

7.4.1 散列表的基本概念

散列查找法的思想,就是将元素的存储位置与其关键字之间建立某种直接关系,那么在进行查找时就无需作比较或做很少次的比较,按照这种关系直接由关键字找到记录.

散列法常用术语:

  1. **散列函数和散列地址:**在记录的存储位置P和其关键字key之间建立一个确定的对应关系H,使p=H(key),称这个对应关系H为散列函数,P为散列地址.
  2. **散列表:**一个有限连续的地址空间,用以存储按散列函数计算得到相应散列地址的数据记录.通常散列表的存储空间是一个一维数组,散列地址是数组下标.
  3. **冲突和同义词:**对不同的关键字可能得到同一散列地址,即 k e y 1 ≠ k e y 2 , H ( k e y 1 ) = H ( k e y 2 ) key_1\neq key_2,H(key_1)=H(key_2) key1=key2,H(key1)=H(key2)这种现象称为冲突.具有相同函数值的关键字对散列函数来说称作同义词.
7.4.2 散列函数的构造方法

构造散列表的方法有很多,通常需要考虑以下几个因素:

  1. 散列表的长度
  2. 关键字的长度
  3. 关键字的分布情况
  4. 计算散列函数所需的时间
  5. 记录的查找频率

构造一个好的散列函数需要遵循以下两条规则:

  1. 函数计算要简单,每一关键字只能有一个散列地址与之对应.
  2. 函数的值域需要在表长的范围内,计算出的散列地址的分布应均匀,尽可能减少冲突.
1. 数字分析法

如果事先知道关键字集合,且每个关键字的位数比散列表的地址码数还要多,每个关键字由n位组成,如 k 1 k 2 k 3 . . . k n k_1k_2k_3...k_n k1k2k3...kn,则可以从关键字中提取数字分布比较均匀的若干位作为散列地址.

2. 平方取中法

平方取中法即是一种对于数字分析法的扩展.平方取中法既是将关键字数字化,然后将数字化的关键字乘平方,然后从所得到的数字串中选取合适位置的若干位作为散列地址.

3. 折叠法

折叠法即是将关键字分裂成几个位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址.

4. 除留余数法

假设散列表表长为m,选择一个不大于m的数p,用p去除关键字,除后说的余数为散列地址,即:
H ( k e y ) = k e y % p H(key)=key\%p H(key)=key%p
这个方法的关键是选取适当的p,一般情况下,可以选p为小于表长的最大质数.例如:表长m=100,可取p=97.

7.4.3 处理冲突的方法
1. 开放地址法

**开放地址法的基本思想是:**把记录都存储在散列表数组中,当某一记录关键字key的初始散列地址 H 0 = H ( k e y ) H_0=H(key) H0=H(key)发生冲突时,以 H 0 H_0 H0为基础,采取合适方法计算得到另一个地址 H 1 H_1 H1,如果仍然发生冲突,以 H 1 H_1 H1为基础再求下一个地址 H 2 H_2 H2,若 H 2 H_2 H2仍有冲突,再求得 H 3 H_3 H3,依此类推.

这种方法在寻找下一个空位时,原来的数组的空间对所有元素都是开放的,所以此方法被叫做"开放地址法",通常把寻找下一个空位的过程称为"探测".

此方法可表示为:
H i = ( H ( k e y ) + d i ) % m , i = 1 , 2 , 3 , . . . , k ( k ⩽ m − 1 ) H_i=(H(key)+d_i)\%m,i=1,2,3,...,k(k\leqslant m-1) Hi=(H(key)+di)%m,i=1,2,3,...,k(km1)
其中,H(key)表示散列函数,m为散列表长, d i d_i di为增量序列.根据 d i d_i di的取值不同,可分为以下三种探测方法:

  1. 线性探测法:
    d i = 1 , 2 , 3 , . . . , m − 1 d_i = 1,2,3,...,m-1 di=1,2,3,...,m1
    发生冲突时从冲突地址的下一单元顺序寻找空单元.

  2. 二次探测法:
    d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 . ( k ⩽ m / 2 ) d_i = 1^2,-1^2,2^2,-2^2,...,k^2,-k^2.(k\leqslant m/2) di=12,12,22,22,...,k2,k2.(km/2)

  3. 伪随机探测法:
    d i = 伪 随 机 数 列 d_i = 伪随机数列 di=

**二次聚集:**在处理同义词的冲突过程中又添加了非同义词的冲突.

2. 链地址法

链地址法的基本思想是:把具有相同散列地址的记录放在同一个单链表中,称为同义词链表.(HashMap?)

7.4.4 散列表的查找

与创建方式基本一致.

算法步骤:

  1. 给定待查找的关键字key,根据造表时设定的散列函数计算 H 0 = H ( k e y ) H_0=H(key) H0=H(key).
  2. 若单元 H 0 H_0 H0为空,则所查元素不存在.
  3. 若单元 H 0 H_0 H0中元素的关键字为key,则查找成功.
  4. 否则重复以下解决冲突过程:
    1. 按冲突处理方法,计算下一个散列地址 H i H_i Hi;
    2. 若单元 H i H_i Hi为空,则所查元素不存在;
    3. 若单元 H i H_i Hi中元素的关键字为key,则查找成功.

第八章 排序

8.1 基本概念和排序方法概述

8.1.1 排序的基本概念
  1. **排序:**是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作.
  2. 排序的稳定性:假设 K i = K j , ( 1 ⩽ i ⩽ n , 1 ⩽ j ⩽ n , i ≠ j ) K_i=K_j,(1\leqslant i \leqslant n,1\leqslant j \leqslant n,i\neq j) Ki=Kj,(1in,1jn,i=j),且在排序前的序列中 R i R_i Ri领先于 R j R_j Rj(即i<j).若在排序后的序列中 R i R_i Ri仍领先于 R j R_j Rj,则称所用的排序方法是稳定的,反之则是不稳定的.
  3. **内部排序和外部排序:**内部排序指的是待排序记录全部存放在计算机内存中进行排序的过程;外部排序指的是待排序记录的数量很大,以致于内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程.
8.1.2 内部排序方法的分类

可以将排序的记录区分为两个区域:有序序列区和无序序列区

使有序区中记录的数目增加一个或几个的操作称为一趟排序.

根据逐步扩大记录有序序列长度的原则不同,可以将内部排序可以分为以下几类:

  1. **插入类:**将无序子序列中的一个或几个记录"插入"到有序序列中,从而增加记录的有序序列的长度.主要分为:直接插入排序,折半插入和希尔排序.
  2. **交换类:**通过"交换"无序序列中的记录从而得到其中关键字最小或最大记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度.主要包括:冒泡排序和快速排序.
  3. **选择类:**从记录的无序子序列中"选择"关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度.主要包括:简单选择排序,树形选择排序和堆排序.
  4. **归并类:**通过"归并"两个或两个以上的记录有序子序列,逐步增加有序序列的长度.2-路归并排序是最为常见的归并排序算法.
  5. **分配类:**是唯一一类不需要进行关键字之间比较的排序方法,排序时主要利用分配和收集两种基本操作来完成.基数排序是主要的分类排序方法.
8.1.3 待排序记录的存储方式
  1. 顺序表
  2. 链表
  3. 待排序记录本身存储在一组地址连续的存储单元内,同时另设一个指示各个记录存储位置的地址向量,在排序过程中不移动记录本身,而移动地址向量中这些记录的"地址",在排序结束后再按照地址向量中的值调整记录的存储位置.这种排序方式称为地址排序.
8.1.4 排序算法效率的评价指标
  1. **执行时间:**对于排序操作,时间主要消耗在关键字之间的比较和记录的移动上,排序算法的时间复杂度主要由这两个指标决定.
  2. **辅助空间:**空间复杂度主要是由排序算法所需的辅助空间决定.

8.2 插入排序

8.2.1 直接插入排序

直接插入排序是一种最简单的排序算法,其基本操作是将一条记录插入到已排好序的有序表中,从而得到一个新的,记录数量增1的有序表.

算法步骤:

  1. 设待排序的记录放在数组r[1…n]中,r[1]是一个有序序列.
  2. 循环n-1次,每次使用循序查找法,查找r[i] (i=2,…,n)在已排好序的序列r[1…i-1]中的插入位置,然后将r[i]插入到表长为i-1的有序序列r[1,…,n-1],最后得到一个表长为n的有序序列.

算法评价:

  1. 时间复杂度: O ( n 2 ) O(n^2) O(n2).
  2. 空间复杂度: O ( 1 ) O(1) O(1).

算法特点:

  1. 稳定排序.
  2. 算法简便,且容易实现.
  3. 也适用于链式存储结构,只是在单链表上无需移动记录,只需修改相应的指针.
  4. 更适合于初始记录基本有序的情况,当初始记录无序,n较大时,此算法时间复杂度较高,不宜采用.
8.2.2 折半插入排序

直接插入排序采用顺序查找法查找当前记录在已排好序的序列中的位置,这种查找也可以用折半查找来实现,由此进行插入排序的排序称为"折半插入排序".

算法步骤:

  1. 设待排序的记录存放在数组r[1…n]中,r[1]是一个有序的序列.
  2. 循环n-1次,每次使用折半查找法,查找r[i] (i=2,…,n)在以排序好的序列r[1…n-1]中的插入位置,然后将r[i]插入表长为i-1的有序序列r[1,i-1],直到将r[n]插入表长为n-1的有序序列r[1…n-1],最后得到一个表长为n的有序序列.

算法评价:

  1. 时间复杂度: O ( n 2 ) O(n^2) O(n2).
  2. 空间复杂度: O ( 1 ) O(1) O(1).

算法特点:

  1. 稳定排序
  2. 因为要折半查找,所以只能用于顺序结构,不能用于链式结构.
  3. 适合初始记录无序,n较大时的情况.
8.2.3 希尔排序 *

希尔排序又称为"缩小增量排序",是插入排序的一种.

希尔排序是从"减小记录个数"和"序列基本有序"两个方面对直接插入排序进行了改进.

算法步骤:

希尔排序实质上是采用分组插入的方法.先将整个待排序记录序列分割成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组.这样进行几次分组排序之后,整个序列中的记录基本有序时,再进行全体记录的直接插入排序.

  1. 第一趟取增量 d 1 ( d 1 < n ) d_1(d_1<n) d1(d1<n)把全部的记录分成 d 1 d_1 d1个组,所有间隔为 d 1 d_1 d1的记录分在同一组,在各个注重进行直接插入排序.
  2. 第二趟取增量 d 2 < d 1 d_2<d_1 d2<d1,重复上述的分组和排序.
  3. 依此类推,知道所取的增量 d 1 = 1 , ( d t < d t − 1 < . . . < d 2 < d 2 ) d_1=1,(d_t<d_{t-1}<...<d_2<d_2) d1=1,(dt<dt1<...<d2<d2),所有记录在同一组中进行直接插入排序为止.

8.3 交换排序

8.3.1 冒泡排序

冒泡排序是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上"漂浮"(左移),或者使关键字如石块一样逐渐向下"坠落"(右移).

算法步骤:

  1. 设待排序的记录存放在数组r[1,…,n]中.首先将第一个记录的关键字和第二个记录的关键字进行比较,若为逆序,则交换两个记录.然后比较第二个与第三个记录的关键字.依此类推.,直至第n-1个记录的关键字进行过比较为止.上述过程称为第一趟起泡排序,其结果使最大的关键字的记录被安置到最后一个记录的位置上.
  2. 然后进行第二趟起泡排序,对前n-1个记录进行相同的操作,其结果是使关键字第二大的记录被安置到第n-1个记录的位置上.
  3. 重复上述过程,直到某一趟中没有任何进行过交换额记录,这完成排序.

算法评价:

  1. 时间复杂度: O ( n 2 ) O(n^2) O(n2).
  2. 空间复杂度: O ( 1 ) O(1) O(1).
8.3.2 快速排序

快速排序是由冒泡排序改进而得的.在冒泡排序过程中,只对相邻的两个记录进行比较,因此每次交换一次记录只能消除一个逆序,.如果能通过两个(不相邻)记录的一次交换,消除多个逆序,则会大大加快排序的速度.

算法步骤:

在待排序的记录中任取一个记录(一般取第一个记录)作为枢轴,设其关键字为pivokey.经过依次排序之后,把所有小于pivotkey的记录交换到其前面,大于的记录交换到后面,结果将待排序的记录分成两个子表,最后将枢轴放在中间位置.然后对其左右子表重复上述过程,直至每个子表只有一个记录时,排序完成.

一趟快速排序的具体步骤如下:

  1. 选择待排序列表中的第一个记录作为枢轴,将枢轴记录暂存在r[0]的位置上.附设两个指针low和high,初始时分别指向表的上界和下界(第一趟时,low=1,high=L.Length).
  2. 从表的最右侧位置依次向左搜索,找到第一个关键字小于枢轴关键字pivotkey的记录,将其移到low处.具体操作时:当low<high时,若high所指记录的关键字大于等于pivotkey,则向左移动指针high,否则将high所指记录与枢轴记录交换.
  3. 然后从表的最左侧位置,依次向右找到第一个关键字大于pivotkey的记录和枢轴记录交换.
  4. 重读2,3直到low和high相等为止.

8.4 选择排序

8.4.1 简单选择排序

算法步骤:

  1. 设待排序的记录存放在数组r[1…n]中.第一趟从r[1]开始,通过n-1次比较,从n个记录中选出关键字最小的记录,记为r[k],交换r[1]和r[k].
  2. 第二趟从r[2]开始,通过n-2次比较,从n-1个记录中选出关键字最小的记录,记为r[k],交换r[2]与r[k].
  3. 依此类推,经过n-1趟完成.
8.4.2 树形选择排序

树形选择排序又叫做锦标赛排序,是一种按照锦标赛的思想进行排序的选择排序方法.

首先对n个记录进行两两比较,然后在其中[n/2]个较小者之间在进行两两比较,如此重复,直至选出最小关键字的记录为止.

8.4.5 堆排序

堆排序是一种树形选择排序,在排序过程中,将待排序的记录r[1…n]看成是一棵完全二叉树的顺序存储结构,利用二叉树中双亲结点和孩子结点的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录.

堆排序利用率大根堆(或小根堆)堆顶记录最大(或最小)的特性.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZvEWGh4-1606220592993)(http://www.donaldlee.cn/img/堆排序.jpg)]

用大根堆进行排序的步骤如下:

  1. 按堆得定义将待排序的序列r[1…n]调整为大根堆(这个过程称为初建堆),交换r[1]和r[n]则r[n]为关键字最大的记录.
  2. 将r[1…n-1]重新调整为堆,交换r[1]和r[n-1],则r[n-1]为关键字次大的记录.
  3. 循环n-1次,知道交换了r[1]和r[2]为止,得到了一个非递减的有序序列r[1…n].

因此,实现堆排序需要解决以下问题:

  1. **建初堆:**如何将一个无序序列建成一个堆?
  2. **调整堆:**去掉堆顶元素,在堆顶元素改变之后,如何调整剩余元素成为一个新的堆.
1.调整堆

如上图中大根堆的根结点50与叶子结点10互换,将50从堆当中移除,剩余的堆除了根结点的10以外都满足大根堆的定义,所以将根结点10与左右结点对比,如果左右结点大于根结点,则交换该结点与根结点,这时与其交换的子树儿掉完整性被破环,在该子树当中循环上述操作,直至调整到叶子结点,则调整完毕.这种方法称为筛选法.

筛选法算法步骤:

从r[2s]和r[2s+1]中选出关键字最大者,假设r[2s]的关键字较大,比较r[s],r[2s]的关键字.

  1. 若r[s].key>=r[2s].key,说明以r[s]为根的子树已经是堆,不必做调整.
  2. 若r[s].key<r[2s].key,交换r[s]和r[2s].交换后,以r[2s+1]为根的子树仍是堆,如果以r[2s]为根的子树不是堆,则重复上述过程,将以r[2s]为根的指数调整为堆,直至进行到叶子结点1为止.
2. 建初堆

要建一个无序序列调整为堆1,就必须将其所对应的完全二叉树中以每一结点为根的子树都调整为堆.

在完全二叉树中,所有序号大于(n/2)向下取整的结点都是叶子,因此以这些节点为根的子树均以是根(只有一个结点的树为堆).这样只需要用筛选法,从最后一个分支结点(n/2)向下取整开始,依次将编号向前的结点作为根的子树都调整为堆即可.

算法步骤:

对于无序序列r[1…n],从i=n/2开始,反复利用筛选法依次将以r[i],r[i-1],…,r[1]为根的子树调整为堆.

8.5 归并排序

归并排序就是将两个或两个以上的有序表合并成一个有序表的过程.将两个有序表合并成一个有序表的过程称为2-路归并.

归并排序的思想是:

假设初始序列含有n个记录,这可以看成是n’个有序的子序列,每个子序列的长度为1,然后两两归并,得到(n/2)向上取整个长度为2或1的有序子序列;两两归并,…,如此重复,直到得到一个长度为n的有序序列为止.

相邻两个有序子序列的归并:

设两个有序表存放在同一组中相邻的位置上:R[low…mid],R[mid+1…high],每次分别从两个表中取出一个记录进行关键字比较,将较小者放入T[low…high]中,重复此过程,直至其中一个表为空,最后将另一非空表中余下部分直接复制到T中.

2-路归并排序算法步骤:

2-路归并排序将R[low…high]中的记录归并排序后放入T[low…high]中.当序列长度等于1时,递归结束,否则:

  1. 将当前序列一分为二,求出分裂点mid=((low…mid)向下取整/2).
  2. 对子序列R[low…mid]递归,进行归并排序,结果放入S[low…high]中.
  3. 对子序列R[mid+1…high]递归,进行归并排序,结果放入S[mid+1…high]中.
  4. 调用上一个算法,将有序的两个子序列S[low…mid]和S[mid+1…high]归并为一个有序的序列T[low…high].

8.6 基数排序

前述的各种算法都建立在关键字比较的基础上,而分配排序不需要比较关键字的大小,它是根据关键字中各位的值,通过对待排序记录进行若干趟"分配"与"收集"来实现排序的.基数排序就是典型的分类排序.

8.6.1 多关键字排序

例如:52张扑克牌中的关键字有两个:花色(梅花<方块<红心<黑桃)和面值(2<3<…<A),且花色的地位高于面值,在比较任意两张牌面大小的时候,必须先比较花色,再比较面值.'由此,将扑克整理成如上所述的次序关系时,有两个排序法:

  1. **最高位优先法:**先按花色不同分成有次序的4堆,每一堆的牌均具有相同的花色,然后分别对每一堆按面值大小整理有序.
  2. **最低位优先法:**这是一种分配与收集的交替进行的方法.先按不同面值分成13堆,然后将这13堆牌自小至大叠在一起,然后将每堆按照面值次序收集在一起.再重新对这些牌按照不同花色分成4堆,然后再按花色顺序再收集到一起.
8.6.2 链式基数排序

基数排序的思想类似于"最低位优先法",是借助"分配"和"收集"两种操作对单逻辑关键字进行排序的一种内部排序法.假设记录的逻辑关键字由d个"关键字"组成,每个关键字可能取rd个值.只要从最低数位关键字起,按关键字得不同值将序列中记录分配到rd个队列中后再收集,如此重复d次完成排序.这种方法称为基数排序,其中指的是rd的取值范围.

具体见P257

8.7 外部排序

8.7.1 外部排序的基本方法

外部排序基本上由两个相对独立的阶段组成.首先,可按内存的大小,将外存上含有n个记录得文件分成若干份长度为l得子文件或者,一次读入内存并利用有效的内部排序方法对它们进行排序.并将排序后得到的有序子文件重新写入外存,通常称这些有序子文件为归并段顺串;然后,对这些归并段进行逐趟归并,使归并段(有序的子文件)逐渐由小至大,直到得到整个有序文件为止.

8.7.2 多路平衡归并的实现

由于k-路归并的归并时间随k的增长而增长,而k越大,分段越大,所需读写的次数越少,速度越快.所以,在进行k-路归并时可以利用"败者树",来实使在k个记录中选出关键字最小的记录时仅需进行( log ⁡ 2 k \log_2k log2k)向下取整次比较,从而使总的归并时间变为 ( log ⁡ 2 m 向 上 取 整 ( n − 1 ) t m g ) (\log_2m向上取整(n-1)t_{mg}) (log2m(n1)tmg),这时时间将不再谁k的增长而增长.

败者树:

败者树既是类似于锦标赛类型的排序方法,只不过可以进行下一轮比较的是更小的选择.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78K9sf23-1606220592994)(http://www.donaldlee.cn/img/实现4-路归并的败者树.jpg)]

8.7.3 置换-选择排序 *

置换-选择排序是在树形选择排序的基础上得来的,它的特点是:在整个排序(得到所有初始归并段)的过程中,选择最小(或最大)的关键字输入,输出交叉或平行进行.

8.7.4 最佳归并树 *

对长度不等的m个初始归并段,构造一个哈夫曼树作为归并树,便可使在进行外部排序时所需对外读写次数达到最少.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值