数据结构、算法与应用

目录

 

一、绪论

1.1 什么是数据结构

1.1.1  数据的逻辑结构

1.1.2 数据的存储结构

1.2 算法与算法设计

1.3 算法分析

二、线性表

2.1 线性表的基本概念

2.2.1 线性表的定义

2.2.2 线性表的存储结构

2.2 顺序表

2.3 链表

2.3.1 链表的实现

2.3.2 线性表实现方法的比较

2.4 栈

2.4.1 顺序栈

2.4.2 链式栈

2.4.3 栈与递归

2.5 队列

2.5.1 顺序队列

2.6 字符串

2.6.1 有关字符串的概念

 2.6.2 字符串的模式匹配

三、树

3.1 树的基本概念

3.1.1 树的定义和基本术语

3.1.2 树的基本性质

3.1.3 树的逻辑表示方法

3.2 二叉树

3.2.1 二叉树的概念和几种特殊的二叉树

3.2.2 二叉树的性质

3.2.3 二叉树的存储结构

3.2.4 二叉树的遍历

3.2.5 线索二叉树

3.2.6 二叉搜索树(BST)

3.2.6 平衡二叉树(ASL)

3.2.7 堆和优先队列

3.2.8 Huffman编码树

3.3 树和森林

树、二叉树、森林的互相转化

树和森林的深度遍历

3.3.1 树的存储

四、图

4.1 图的基本概念

4.2 图的存储及基本操作

4.2.1 图的邻接矩阵表示

4.2.2邻接表

4.2.3 十字链表和邻接多重表

4.3 图的遍历

4.3.1 深度优先(DFS)

递归实现

非递归实现

4.3.2 广度优先 (BFS)

4.4 最小生成树

3.1  普里姆(Prim)算法

3.2 克鲁斯卡尔(Kruskal)算法

四、最短路径  一般针对带权有向图

4.1  Dijkstra算法---->单源最短路径

4.2  Floyd算法--->顶点对之间的最短路径

五、拓扑排序

 六、关键路径

五、查找

5.1 静态查找

5.1.1顺序查找

5.1.2 折半查找

5.1.3 分块查找

5.2 动态查找

5.3 散列

5.3.1散列函数

5.3.2冲突解决方法

5.3.3 散列查找例子

六、排序

6.1 排序的基本概念

6.2 插入排序

6.2.1 直接插入排序

6.2.2 折半插入排序

6.2.3 希尔排序

6.3 交换排序

6.3.1 冒泡排序

6.3.2 快速排序

6.4 选择排序

6.4.1 简单选择排序

6.4.2 堆排序

6.5 归并排序

6.6 比较排序算法的时间复杂度下界

6.7 基数排序


一、绪论

1.1 什么是数据结构

程序=数据结构+算法。数据结构是对现实世界中数据及其关系的某种映射。

数据结构的组成部分:逻辑结构、物理结构和数据操作

数据结构描述的是按照一定的逻辑关系组织起来的待处理数据的表示及相关操作,涉及到数据之间的逻辑关系、数据在计算机中的存储和数据之间的操作。

数据元素:数据的基本单位,由国歌数据项组合。

数据项:数据的不可分割的最小单位

数据对象:性质相同的数据元素的集合。例如字符集合、

数据结构:相互之间有一定关系的集合

1.1.1  数据的逻辑结构

  数据的逻辑结构是从具体问题中抽象出来的数学模型,体现了事物的组成与事物之间的逻辑关系。数据的逻辑结构由数据结点和连接两个结点的边组成。一个逻辑结构可以用一个二元组(K,R)进行表示。其中K是结点集合,R是结点间的关系集合。

  1. 结点的数据类型:整数、实数、布尔、字符、指针、复合类型

  2. 结构的分类

  • 线性结构:每个结点最多只有一个前驱和一个后继结点。一对一。
  • 树形结构:所有结点只有唯一的前驱结点,但可以具有若干个后继结点。一对多。
  • 图结点:对结点的前驱和后继不加约束,多对多。

1.1.2 数据的存储结构

数据的存储结构主要用来解决各种逻辑结构在计算机中物理存储表示的问题

元素之间的关系在计算机中有两种不同的表示方法:顺序表示和非顺序表示

数据存储的主要任务是利用主存储器的“空间相邻”和“随机访问”两个特性。利用地址空间相邻来表达数据结构的结点,每个结点通常被存储在一片连续地址的紧凑存储区域内。

  1. 四种常用存储映射的方法

  • 顺序方法:把一组结点存放在一片地址相邻的存储单元中,结点间的逻辑关系用存储单元间的自然关系表示。顺序存储结构通常也成为紧凑存储结构。紧凑性可以用存储密度来衡量:所存储的数据占用的存储空间和该结构占用的整个存储空间大小之比
  • 链接方法:在结点的存储结构中附加指针域来存储结点间的逻辑关系。包括数据域和指针域。
  • 索引方法
  • 散列方法

1.2 算法与算法设计

算法设计取决于逻辑结果

算法实现依赖于物理结构

算法:对特定方法求解方法的一种描述,指令的有限序列

  1. 算法一般具有一下性质:
  • 通用性、有效性、确定性、有穷性

     2.算法设计:

  • 穷举法、回溯法、分治法和递归法、贪心法、动态规划法

1.3 算法分析

方法:事后统计、事前分析。

1.3.1 算法的渐近分析 (大O表示法,\theta表示法、Omega表示法)

通俗来讲,算法的渐近复杂度就是忽略掉低阶项以及常数系数(常数是指数的除外,如n^2n,这里的2就不能忽略)来比较最高阶的项。如果从严格的数学定义出发,则需要引入三个符号:O、θ、Ω。

O(f(n))表示一个函数集合,这个集合中的所有函数T(n)满足:存在一个正数c,以及N,当n >= N时,T(n) <= cf(n)。表示了算法最多会差到什么程度。最深层循环语句的执行次数。

Ω(f(n))表示一个函数集合,这个集合中的所有函数T(n)满足:存在一个正数d,以及N,当n >= N时,T(n) >= df(n)。表示了算法最多会好到什么程度

当T(n)既满足O又满足Ω时,T(n) = Θ(n)。


O(1)))

算法的时间复杂度多通过设计过程中需要实施的赋值、比较等基本运算数目的多少来衡量。

大O表示法的特性:

  • 任何对数不管底数为何值,都具有相同的增长率

1.3.2 算法的空间分析

算法的空间复杂度指的是辅助空间。一维数组O(n)


二、线性表

2.1 线性表的基本概念

2.2.1 线性表的定义

线性表是n个数据特性相同的元素的组成有限序列,是最基本且常用的一种线性结构(线性表,栈,队列,串和数组都是线性结构),同时也是其他数据结构的基础。

对于非空的线性表或者线性结构的特点:

(1)存在唯一的一个被称作“第一个”的数据元素;

(2)存在唯一的一个被称作“最后一个”的数据元素;

(3)除第一个外,结构中的每个数据元素均只有一个前驱;

(4)除最后一个外,结构中的每个数据元素均只有一个后继;

2.2.2 线性表的存储结构

存取结构:随机存取O(1),顺序存取O(n)

二、线性表的两种实现方式

  • 顺序表示(顺序表)定长

概念:用一组地址连续的存储单元依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。

特点:逻辑上相邻的数据元素,物理次序也是相邻的。

  只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,因为高级语言中的数组类型也是有随机存取的特性,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。

  线性表长度可增长或缩短。

  • 链表

概念:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的),包括数据域和指针域,数据域存数据,指针域指示其后继的信息。

2.2 顺序表

数组两种结构:

  • 静态结构:不可扩充
  • 动态结构:数组链表

顺序存储的线性表,也称为向量。主要有一下特征:

  • 线性表的逻辑顺序与物理顺序一致;
  • 数据元素之间的关系是以元素在计算机内“物理位置相邻”来体现。
  • 元素的数据类型相同
  • 在程序中使用常量作为向量长度

读取:O(1)    插入、删除、按内容查找:O(n)

数据结构、算法与应用_第1张图片

多维数组逻辑特征:一个结点可能有多个直接前驱和直接后继

二维数组的两种顺序存储方式:列优先顺序表、行优先顺序表

如果计算各个元素存储地址的时间相等,则存取数组中的任一元素的时间也相等。具有这一特点的存储结构成为随机存储结构。因此,数组顺序表就是一个随机存储结构。

2.3 链表

2.3.1 链表的实现

顺序表的局限:

  1. 大小不可变
  2. 插入、删除时间复杂度高

链表可以动态的申请空间

为了每个数据元素ai与其后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储本身的信息之外,还需要存储一个指示其后继元素的信息(即直接后继元素的存储位置)。

为了便于实现,为每个单链表加上一个头结点,位于单链表的第一个结点之前。目的是将逻辑上第一个结点泛化称普通结点。

检索:O(n)  插入、删除O(1)

插入方法:头插、尾插。头插可以实现逆序。

对于单链表,只要涉及到钩链,如果没有给出直接后继,钩链的次序必须是从右到左。

若La,Lb两个链表的长度分别为m,n,则链表合并的时间复杂度为O(m+n),最差m+n-1,最优O(min(m,n))

静态链表:指针域用0,1,2,3,4.。。代替。

单链表的不足之处在于其指针域仅指向其后继结点,因此从一个结点不能有效的找到其前驱,引入双链表,其基本结构是在每个结点中加入一个指向前驱结点的指针。

在结点p后插入一个新节点q

q->prec=p;
q->next=p->next;
p->next=q;
q->next-prev=q;

删除指针变量p所指的结点

p->prev->next=p->next;
p->next->prev=p->prev;

循环链表:最后一个结点的指针指向头结点。好处:从哪一个结点出发,都能访问到表中的其它结点。

2.3.2 线性表实现方法的比较

链表优点

  1. 插入、删除不需移动其他元素,只需改变指针.
  2. 链表各个节点在内存中空间不要求连续,空间利用率高

缺点

  1. 查找需要遍历操作,比较麻烦

顺序表优点

    随机访问特性,查找O(1)时间,存储密度高;
    逻辑上相邻的元素,物理上也相邻;
    无须为表中元素之间的逻辑关系而增加额外的存储空间;

缺点:

    插入和删除需移动大量元素;
    当线性表长度变化较大时,难以确定存储空间的容量;
    造成存储空间的“碎片”

2.4 栈

:一种特殊的线性表,其实只允许在固定的一端进行插入或删除操作。进行数据插入和删除的一端称为栈顶,另一端称为栈底。不含任何元素的栈称为空栈,栈又称为后进先出的线性表

应用:括号匹配,计算式计算

中缀计算:一个符号栈,一个操作数栈,有一个操作符优先级表

后缀计算:一个栈

中缀转后缀

优先级

乘除高,加减低,栈内高,栈外低,(栈内最高,栈外最低。)相反

首先我们应该知道,要想将中缀转化为后缀,需要借助堆栈实现。(不准备画图了,画图有点浪费时间)我会用最简单明了的语言使读者弄懂。[举个例子吧:比如将:2*(9+6/3-5)+4转化为后缀表达式2 9 6 3 / +5 -  * 4 +    ]

1、任何中缀表达式都由运算数,运算符,括号(大,中,小),这三部分组成。

2、从中缀表达式的左边开始扫描(脑中自己想像的),若遇到运算数时,则直接将其输出(不压入堆栈)。

3、若遇到左括号,则将其压栈。

4、若遇到右括号,表达括号内的中缀表达式已经扫描完毕。这时需将栈顶的运算符依次弹出并输出,直至遇到左括号[左括号弹出但不输出]。

5、若遇到的是运算符:a、如果该运算符的优先级大于栈顶运算符的优先级时,将其压栈

                                    b、如果该运算符的优先级小于栈顶运算符的优先级时,将栈顶运算符弹出并输出,接着和新的栈顶运算 符比较,若大于,则将其压栈,若小于,继续将栈顶运算符弹出并输出......(一直递归下去,直至运算符大于栈顶云算符为止)。

6、最后一步,若扫描到中缀表达式的末尾[即扫描结束],若堆栈中还有存留的运算符依次弹出并输出即可。

肯定有一些读者还是没完全弄懂(毕竟全都是文字)。接下来,举一个例子:帮助大家消化

(1)out:2                                                                          stack:

(2)out:2                                                                          stack:*

(3)out:2                                                                          stack: *  (

(4)out:2    9                                                                    stack :*   (

(5)out:2    9                                                                    stack :*   ( +     注:在堆栈中括号的优先级最低

(6)out:2    9   6                                                               stack :*   ( +

(7)out :2   9   6                                                               stack :*   ( +   /

(8)out :2   9   6    3                                                         stack :*   ( +  /

(9)out :2   9   6   3    /                                                     stack :*   ( +   

(10)out: 2   9   6   3    /   +                                              stack : *     (   

(11)out:2   9   6  3   /    +                                                stack : *     (   -

(12)out : 2    9   6  3   /    +   5                                           stack : *    ( -   遇到了右括号

(13)out:2   9   6  3   /   +    5   -                                       stack:*   (      

(14)out:2  9   6   3   /   +    5   -                                       stack:*

(15)out:2  9   6   3   /   +   5    -                                       stack :*    括号弹出但不输出

(16)out :2   9    6   3   /   +   5   -   *                                 stack  :           遇到了+

(17)out:2   9  6   3   /  +   5   -    *                                    stack :+

(18)out:2  9  6   3   /  +   5  -    *   4                                 stack  : +

(19)out:2   9   6   3   /  +  5  -  *  4  +                              stack :      


终于结束了,写到我要吐。注:红笔标记的地方很重要,虽然步骤比较多,当是写起来非常快。至此第一种方法结束。接下来我会用程序实现将中缀表达式转化为后缀表达式,并通过后缀表达式来求值。
 

2.4.1 顺序栈

采用顺序存储方式的栈成为顺序栈

为了避免溢出,在对栈进行操作时要检查栈是否已满或者是否为空

2.4.2 链式栈

直接头插链表,入栈就是头插,出栈就是删除第一个结点

2.4.3 栈与递归

递归有两部分组成,第一部分是递归基础,也称为递归出口,是保证递归结束的前提;第二部分是递归规则,确定了由简单情况求解复杂情况的规则。

根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分为以下4个部分:

  (1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。

  (2)数据区:用于存储全局变量等。

  (3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。

  (4)栈区:用于动态地存储函数之间的关系,以保证被调用函数在返回时恢复到母函数中继续执行。

在函数栈帧中,一般包含以下几类重要信息。

  (1)局部变量:为函数局部变量开辟的内存空间。

  (2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过栈帧平衡计算得到),用于在本栈被弹出后恢复出上一个栈帧。

  (3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

       (4)参数

     (5)控制链、访问链

尾部递归的特点:在每个函数实现的末尾只有一个递归调用,可以用循环代替。

2.5 队列

队列(quene),或成为队,也是一种容器,可存入元素,访问元素,删除元素。
队列中也没有位置的概念,只支持默认方式的元素存入和取出。
特点就是在任何时候访问或删除的元素,都是在此之前最早存入队列而至今未删除的那个元素,因此队列也是先进先出(FIFO)

假溢出:明明可以进去显示满了

实现方法包括顺序和栈式两种

2.5.1 顺序队列

实际上尚有空闲位置而发生上溢的现象成为假溢出,解决的方法是采用循环。front指向头元素,rear指向尾元素的后一个位置。把数组看出一个环。牺牲一个元素的空间来简化操作和提高效率。

两种方式:

一:少用一个元素空间

  • 队列空:front==rear
  • 队列满:  (rear+1)%maxSize==front;

二、另设一个标志位

  • 队列空:count=0;
  • 队列满:count=MaxSIze;

2.6 字符串

2.6.1 有关字符串的概念

串的定义:是由零个或多个字符组成的有限序列,也称为字符串

串当中的字符数目n称为是串的长度,零个字符的串可以称为是空串,需要注意的是这个空串和空格串是不同的,空格串是只包含空格的串,空格串是有长度的,可以不止有一个空格

子串与主串,串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串,子串在主串中的位置就是子串的第一个字符在主串中的序号

字符串的比较:

如果是一个字符串会小于另外一个字符串,可能出现的情况如下所示

给定两个串:s=”a1a2……an”,t=”b1b2……bm”,当满足以下条件之一时,s < t

    1.n < m,且ai=bi(i=1,2,……,n),其中n代表s字符串的长度,s代表t字符串的长度,前面字符串都相同,但是s字符串的长度比t字符串小也就说明s字符串小于t字符串
    2.存在某个k≤min(m,n)这是k>1的时候,使得ai=bi(i=1,2,……,k-1),ak < bk,如果两个字符串从第一个字符就开始不相等也就是说a1 != b1的话就直接只比较a1和b1的大小

    其实我们的英语词典就是按照字符串大小的顺序来进行排列的,所以我们查找单词的时候其实也就是在比较字符串的大小

字符串的顺序存储:一般的存储方案使用char数组。‘/0’表示结束。

串与线性表的区别:

  • 串的数据约束为字符集
  • 串的基本操作和线性表有很大区别:

 线性表的基本操作中,太多以单个元素作为操作对象,如查找某个元素、在某个位置上插入或删除一个元素。字符串通常以串的整体作为操作对象。

 2.6.2 字符串的模式匹配

模式匹配,在目标中找到一个给定的模式,返回匹配的第一个字串的首字符位置。通常目标串较大,模式串较小

精确匹配:在目标T中至少一处存在模式P,则称匹配成功

近似匹配:如果T和P有某种程度上的相似,则称匹配成功。基本操作由字符串的插入、删除、替换组成

1.朴素的模式匹配算法:从首字母开始依次比较  O(TP)

2.KMP  O(T+P)

2.7 特殊矩阵压缩到数组

  • 一,相关概念

    ㈠特殊矩阵:矩阵中存在大多数值相同的元,或非0元,且在矩阵中的分布有一定规律。

    ⒈对称矩阵:矩阵中的元素满足

                       aij=aji    1≤i,j≤n

    ⒉三角矩阵:上(下)三角矩阵指矩阵的下(上)三角(不包括对角线)中的元素均为常数c或0的n阶矩阵。

    ⒊对角矩阵(带状矩阵):矩阵中所有非0元素集中在主对角线为中心的区域中。

    ㈡稀疏矩阵:非0元素很少(≤ 5%)且分布无规律。

    二,存储结构

    1、对称矩阵

    存储分配策略: 每一对对称元只分配一个存储单元,即只存储下三角(包括对角线)的元, 所需空间数为:  n(n+1)/2。

    存储分配方法: 用一维数组sa[n(n+1)/2]作为存储结构。

                 sa[k]与aij之间的对应关系为:

    2、三角矩阵

    也是一个n阶方阵,有上三角和下三角矩阵。下(上)三角矩阵是主对角线以上(下)元素均为零的n阶矩阵。设以一维数组sb[0..n(n+1)/2]作为n阶三角矩阵B的存储结构,仍采用按行存储方案,则B中任一元素bi,j和sb[k]之间仍然有如上的对应关系,只是还需要再加一个存储常数c的存储空间即可。如在下三角矩阵中,用n(n+1)/2的位置来存储常数。

    对特殊矩阵的压缩存储实质上就是将二维矩阵中的部分元素按照某种方案排列到一维数组中,不同的排列方案也就对应不同的存储方案

    2、稀疏矩阵

    常见的有三元组表示法、带辅助行向量的二元组表示法(也即行逻辑链表的顺序表),十字链表表示法等。

    1)、三元组表示法

    三元组表示法就是在存储非零元的同时,存储该元素所对应的行下标和列下标。稀疏矩阵中的每一个非零元素由一个三元组(i,j,aij)唯一确定。矩阵中所有非零元素存放在由三元组组成的数组中。

    2)十字链表。行横链表,列竖链表

2.7.1 广义表

 广义表(Lists,又称列表)是一种非线性的数据结构,是线性表的一种推广。即广义表中放松对表元素的原子限制,容许它们具有其自身结构。它被广泛的应用于人工智能等领域的表处理语言LISP语言中。在LISP语言中,广义表是一种最基本的数据结构,就连LISP 语言的程序也表示为一系列的广义表。

GetHead是第一个,GetTail是除了第一个元素的其它元素。


三、树

3.1 树的基本概念

3.1.1 树的定义和基本术语

树是递归定义的,一颗树是由n(n>=0)个元素组成的有限集合,其中:

  •     每个元素称为结点(node);
  •     至少存在一个结点,它没有前驱(其余的结点都有唯一的一个前驱结点,每个结点可以有0或多个后继结点),这个结点称为根节点(root);
  •     除根节点外,其余结点能分成m(m>=0)个互不相交的有限集合T0,T1,T2,……,Tm-1,其中每个子集又是一颗树,这些集合称为这棵树的子树。
     

数据结构、算法与应用_第2张图片

  • 父结点,子结点,兄弟结点:每个结点的子树成为该结点的子结点(儿子),相应的该结点成为子结点的父结点(父亲),具有同一父结点的结点成为兄弟节点。上图中A是B,C,D的父结点,BCD是兄弟结点。
  • 结点的度:一个结点的子树的数量称为结点的度,结点A有3个子树,A的度为3.
  • 堂兄弟:同一层次的结点
  • 树的度:树中结点最大的度数,上图中树的度为3.
  • 树的深度:指的是树的最大层数,上图中做大层数为4,则树的深度为4。
  • 分支节点:度不为0的节点称为分支节点,分支节点又称非终端节点。一棵树中排除叶结点外的所有节点都是分支节点。
  • 节点:节点包括一个数据元素及若干指向其他子树的分支。
  • 节点的度:节点所拥有子树的个数称为节点的度。
  • 树的度:树中所有节点的度的最大值成为该树的度。
  • 节点的层次:从根节点到树中某节点所经路径上的分支也称为该节点的层次,根节点的层次为0,其他节点层次是双亲节点层次加1.
  • 树的深度:树中所有节点的层次的最大值称为该树的深度。空树的深度为0.图中深度为3
  • 树的高度:等于树的深度加一。
  • 祖先节点:从根节点到该节点所经分支上的所有节点。
  • 子孙节点:以某节点为根节点的子树中所有节点
  • 树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则成为无序树
  • 森林(Forest)是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此也可以森林和树相互递归的定义来描述树。
  • 路径:从树的一个结点m到结点n,如果存在一个有限集合S={s1,s2,s3,...}.使得,,...,都是该树中的边,则称从m到n存在一条路径。

3.1.2 树的基本性质

  1. 树中的结点数等于所有的结点度+1

  2. 度为m的树,其第i层上至多有m^{i}个结点(根结点为第i层,i>=0)

  3. 高度为h(深度为h-1)的度为m的树至多有\frac{m^{h}-1}{m-1}个结点(m>1)

  4. 具有n个结点的树,最小高度为\left \lceil log_{m}(n(m-1)+1) \right \rceil

3.1.3 树的逻辑表示方法

  1. 树形表示法
  2. 文氏图表示法
  3. 凹入表表示法
  4. 嵌套括号表示法

3.2 二叉树

3.2.1 二叉树的概念和几种特殊的二叉树

  •    一棵二叉树是节点的一个有限集合,该集合或者为空,或者是由一个根节点加上两颗分别称为左子树和右子树的二叉树组成。每棵子树的根节点有且只有一个前驱,可以由0个或多个后继。因此,树是递归定义的。每个节点最多有两棵子树,即二叉树不存在度大于2的节点。二叉树的子树有左右之分,其子树的次序不能颠倒。
  •     满二叉树:在一棵二叉树中,如果所有分支节点都存在左子树和右子树,而且所有叶子节点都在同一层上。
  •     完全二叉树:如果一棵具有N个节点的二叉树的结构与满二叉树的前N个节点的结构相同,称为完全二叉树。满二叉树是特殊的完全二叉树。叶子结点出现在k或k-1层

3.2.2 二叉树的性质

  1. 对于一颗二叉树,如果叶结点数为n0,而度为2的结点总数为n2,则n0=n2+1。
  2. 在二叉树中,第i层的结点总数最多有2^{i-1}(i>=1)  
  3. 在深度为K的二叉树中最多有2^{k}-1个结点(K>=1),最少有K个结点。
  4. 具有n个结点的完全二叉树的树的深度k为 \left \lceil log _{2}(n+1)\right \rceil
  5. 有n个结点的完全二叉树各结点如果用顺序方式存储,对任意结点i,有如下关系:
  •     如果i!=1,则父结点的编号为i/2;
  •     如果2*i<=n,则其左孩子的编号为2*i,若2*i>n,则无左子树,是叶结点,即2*i>n的结点肯定是叶结点,上图G结点编号为7,i=7,2*7>12,即i是叶结点;
  •     如果2*i+1<=n,则其右孩子的编号为2*i+1,若2*i+1>n,则无右子树

3.2.3 二叉树的存储结构

  • 顺序存储结构:一般是由一个一维数组构成的,二叉树上的结点按照某种规定的次序逐个保存到数组的各个单元。使用一维数组保存二叉树的结点,关键在于定义结点的存储次序,这种次序应该能反应结点之间的逻辑关系,即父子,兄弟等关系。如果需要顺序存储的二叉树不是完全二叉树,这样结点和数组中的序号没有了对应的关系。解决的方法是:将非完全二叉树转化为完全二叉树,把缺少的及诶单虚设为无数据的结点

  • 使用链式存储结构比较符合二叉树的逻辑结构,一个结点由结点元素和两个分别指向左右子树的指针组成,这样的结构称:二叉链表结构。有时为了方便查找父结点,还再增加一个结点指针,这种结构称为三叉链表结构。

    对于二叉树,每个结点都有指向该结点的边,假设一个二叉树有n个结点,则有n-1条边(除了根结点其余各个结点有对应一条边),而按照二叉链式存储结构,一个结点有2n个指针域,其中n-1个用来指向自己的左右孩子,剩余n+1个指针域为空。

    二叉链表从根结点开始很方便的找到左右孩子,但是找到父结点比较麻烦。

3.2.4 二叉树的遍历

  1. 广度优先遍历

使用队列实现,按层次遍历

public static List levelOrder(TreeNode root) {
    	List list=new LinkedList<>();
    	Queue queue=new ArrayDeque<>();
    	if(root!=null) {
    		queue.add(root);
    	}
    	while(!queue.isEmpty()) {
    		TreeNode node=queue.poll();
            list.add(node.val);
            if(node.left!=null) {
            	queue.add(node.left);
            }
            if(node.right!=null) {
            	queue.add(node.right);
            }
    	}
    	return list;
    }

  .2. 深度优先遍历

  • 前序遍历
    public static List PreOrderWithoutRecusion(TreeNode root) {
    	List list=new LinkedList<>();
    	Stack stack=new Stack<>();
    	
    	TreeNode pointer=root;
    	while(!stack.isEmpty()||pointer!=null) {
    		if(pointer==null) {
    			pointer=stack.pop();
    		}
            list.add(pointer.val);
            if(pointer.right!=null) {
            	stack.add(pointer.right);
            }
            pointer=pointer.left;
    	}
    	return list;
    }
    
  • 中序遍历
 public static List InOrderWithoutRecusion(TreeNode root) {
    	List list=new LinkedList<>();
    	Stack stack=new Stack<>();
    	
    	TreeNode pointer=root;
        
    	while(!stack.isEmpty()||pointer!=null) {
    		if(pointer!=null) {
            	stack.push(pointer);
            	pointer=pointer.left;
            }else {
    		
    			pointer=stack.pop();
    			list.add(pointer.val);
    			pointer=pointer.right;
    		
    		}
    	}
    	return list;
    }
  • 后序遍历
  public static List PostOrderWithoutRecusion(TreeNode root) {
    	List list=new LinkedList<>();
    	Stack stack=new Stack<>();
    	
    	TreeNode pointer=root;
    	TreeNode pre=root;
        
    	while(pointer!=null) {
    		for(;pointer.left!=null;pointer=pointer.left) {
    			stack.push(pointer);
    		}
    	while(pointer!=null&&(pointer.right==null||pointer.right==pre)) {
    		list.add(pointer.val);
    		pre=pointer;
    		if(stack.empty()) {
    			return list;
    		}else {
    			pointer=stack.pop();
    		}
    		
    	}
    	stack.push(pointer);
    	pointer=pointer.right;
    	}
    	return list;
    }
  • 递归遍历

public static void PreOrder(TreeNode node,List list) {
    	if(node!=null) {
    		list.add(node.val);
    		PreOrder(node.left,list);
    		PreOrder(node.right,list);
    	}
    }

3.2.5 线索二叉树

      按照某种遍历方式对二叉树进行遍历,可以把二叉树中所有结点排序为一个线性序列。在改序列中,除第一个结点外每个结点有且仅有一个直接前驱结点;除最后一个结点外每一个结点有且仅有一个直接后继结点。这些指向直接前驱结点和指向直接后续结点的指针被称为线索(Thread),加了线索的二叉树称为线索二叉树。

    当用二叉链表作为二叉树的存储结构时,因为每个结点中只有指向其左、右儿子结点的指针,所以从任一结点出发只能直接找到该结点的左、右儿子。在一般情况下靠它无法直接找到该结点在某种遍历序下的前驱和后继结点。如果在每个结点中增加指向其前驱和后继结点的指针,将降低存储空间的效率。

    我们可以证明:在n个结点的二叉链表中含有n+1个空指针。因为含n个结点的二叉链表中含有个指针,除了根结点,每个结点都有一个从父结点指向该结点的指针,因此一共使用了n-1个指针,所以在n个结点的二叉链表中含有n+1个空指针。

    因此可以利用这些空指针,存放指向结点在某种遍历次序下的前驱和后继结点的指针。这种附加的指针称为线索,加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(ThreadedBinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

(1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;

(2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;

3.2.6 二叉搜索树(BST)

对于一棵二叉搜索树,如果不为空,它应该满足以下三个特点:

1、树上的任一结点,该结点的值都大于它的非空左子树的值。

2、树上的任一结点,该结点的值都小于它的非空右子树的值。

3、任一结点的左右子树都是二叉搜索树。

将关键码集合转化为对应的二叉搜索树,实际上是对集合的关键码进行了排序

对于二叉搜索树的查找,思路方法是:

1、从根结点开始查找,如果树为空,就返回NULL。

2、如果树不空,就让数据X和根结点的数据Data作比较。

3、如果X的值大于根结点的Data,就往右子树中进行搜索;如果X的值小于根结点的Data,就往左子树中进行搜索。

4、如果X的值等于Data,就表示查找完成,返回该结点。

代码:

 public static boolean research(TreeNode root,int val) {
    	TreeNode p=root;
    	if(root==null) {
    		return false;
    	}
    	while(p!=null) {
    		if(val>p.val) {
    			p=p.right;
    		}else if(val

删除

最后到删除操作,删除操作分三种情况:

第一种情况,要删除的结点是叶结点,也就是没有左右子树。这样的情况只要把该结点的父结点指向NULL即可。

第二种情况,要删除的结点有一个子树(左子树或右子树),这时删除该结点的方法就是让该结点的父结点指向该结点的子树即可。

第三种情况,要删除的结点有左右两个子树。这种情况的解决方法有两种:

  1. 合并删除:查找到被删除的结点p的左子树中按中序遍历的最后一个结点r,将结点r的有指针赋值为指向结点p的右子树的根,然后用结点p的左子树的根代替被删除的结点p,最后删除p.
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
//合并删除
class Solution {
    TreeNode r;
    public void Delete(TreeNode parent,TreeNode node,int L,int R){
         if(node.left!=null){
            Delete(node,node.left,L,R);
        }
        if(node.right!=null){
            Delete(node,node.right,L,R);
        }
        if(node.valR){
            if(node.left==null&&node.right==null){   //左右子树都为空
                    if(parent.left!=null&&parent.left.val==node.val){
                        parent.left=null;
                    }else{
                        parent.right=null;
                    }
                }
            else if(node.left==null||node.right==null){   //有一根子树为空
                 ///删除结点不是根结点
                    if(parent.left!=null&&parent.left.val==node.val){
                        if(node.left!=null)
                           parent.left=node.left;
                        else if(node.right!=null){
                            parent.left=node.right;
                        }
                    }else{
                        if(node.left!=null)
                           parent.right=node.left;
                        else{
                            parent.right=node.right;
                        }
                    }
            }else{              //两根子树都不为空,合并删除法
                TreeNode temp=node;
                temp=temp.left;
                while(temp.right!=null){
                    temp=temp.right;
                }
                temp.right=node.right;
                if(parent.left.val==node.val){
                    parent.left=node.left;
                }else{
                    parent.right=node.left;
                }
            }
        }
       
    }
    
    public TreeNode trimBST(TreeNode root, int L, int R) {
        if(root==null) return null;
        if(root.left!=null){
            Delete(root,root.left,L,R);
        }
        if(root.right!=null){
            Delete(root,root.right,L,R);
        }
        if(root.valR){
            if(root.left==null&&root.right==null){
                root=null;
            }
            else if(root.right==null||root.left==null){
                if(root.right!=null)
                    root=root.right;
                else
                    root=root.left;
            }
            else if(root.left!=null&&root.right!=null){
                TreeNode temp=root;
                temp=temp.left;
                while(temp.right!=null){
                    temp=temp.right;
                }
                temp.right=root.right;
                root=root.left;
            }
        }
        return root;
    }
}
  1. 复制删除:选取一个合适的结点r,并将该结点的关键码复制给被删除结点p,然后将r结点删除。结点的选取方式有两种:结点p的左子树中关键码值最大的结点或者结点p的右子树中关键码值最小的结点。如果r结点有左子树或者右子树,则将其唯一的子树放置在r结点原来的位置。
 public static void DeleteByCopying(TreeNode node) {
    	TreeNode parent=research(root,node.val);
        TreeNode pointer=node;
        if(node==root) {
        	   TreeNode nnode=node;
        	   pointer=node.left;
    	       while(pointer.right!=null) {
    	    	   pointer=pointer.right;
    	       }
        	   root.val=pointer.val;
        	   if(nnode!=node) {
           	       nnode.right=pointer.left;
           	       }else {
           	    	   nnode.left=pointer.left;
           	       }
        	return;
        }
    	int flag=0;
    	if(parent.left==node) {flag=1;}
    	if(node.left==null) {
    	     if(flag==1) {
    	    	 parent.left=node.right;
    	     }else {
    	    	 parent.right=node.right;
    	     }
    	}
    	else if(node.right==null) {
   	     if(flag==1) {
   	    	 parent.left=node.left;
   	     }
   	  else {
	    	 parent.right=node.left;
	     }
   	    }else {
   	    	TreeNode sNode=pointer;
   	    	pointer=node.left;
   	    	
   	       while(pointer.right!=null) {
   	    	   sNode=pointer;
   	    	   pointer=pointer.right;
   	       }
   	       node.val=pointer.val;
   	       if(sNode!=node) {
   	       sNode.right=pointer.left;
   	       }else {
   	    	   sNode.left=pointer.left;
   	       }
   	    }
    	
    }

3.2.6 平衡二叉树(ASL)

平衡二叉树,是一种二叉排序树,其中每一个节点的左子树和右子树的高度差最多等于1。由3位科学家共同发明,用他们首字母命名 又被称为AVL树。从平衡二叉树的名称,你也可以体会到它是一种高度平衡的二叉排序树。我们将二叉树上结点的左子树深度减去右子树的深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只能是-1,0,1。
为了使复杂度保持在O(nlogn)

二、插入操作的不同情况

1、如果在查找插入位置的过程中,所有途经结点的BF值均为0,那么插入新的结点后,不会导致这些途经结点失衡,只会让它们的BF值从0变为1或者-1。

  如下图所示,插入结点3,查找时途经结点为5,4和2,其BF只是从原来的0都变为了1或-1,整棵树仍然是平衡的。

数据结构、算法与应用_第3张图片

2、如下图,插入结点4.7。左图中最小非平衡子树以结点4为根结点a,结点4.7被插入到这棵子树较低的子树中,此时这棵子树还是平衡的,只需要修改这棵子树的从根结点直至插入结点路径上所有结点的BF值。

数据结构、算法与应用_第4张图片

以上两种情况都不需要调整树的结构

3、需要调整树结构的情况又分为了四种情况:

(1)LL型调整:a的左子树较高,新结点插入在a的左子树的左子树。进行右旋转。

在下图的图a中,a是最小非平衡子树的根,b的BF一定是0(否则a就不是最小非平衡子树的根了)。结点2被插入到了a的左子树的左子树,需要进行LL型调整:将结点2-3-4-5-8看做一条可以转动的链子,将其向右旋转(顺时针)一个结点,然后将原来b结点的右子树,接到a结点的左子结点上,调整完成。

再说明一下插入结点的位置:插入结点不必像上图一样,必须插在某个结点的左子结点,也可以像下图一样,插在某个结点的右子结点,调整的方法还是一样的。这也是定义中说:新结点插入在a的左子树的‘左子树’,而不是左子树的左子结点的原因。

    数据结构、算法与应用_第5张图片

数据结构、算法与应用_第6张图片

(2)RR型调整:a的右子树较高,新结点插入在a的右子树的右子树。进行左旋转。

 

RR型调整与LL型正好是对称的,操作步骤类似。在下图的图a中,a是最小非平衡子树的根,b的BF一定是0。结点9被插入到了a的右子树的右子树,需要进行RR型调整:同样地,将结点4-5-6-8-9看做一条可以转动的链子,将其向左旋转(逆时针)一个结点,然后将原来b结点的左子树,接到a结点的右子结点上,调整完成。

数据结构、算法与应用_第7张图片

同样地,插入结点也可以插入在结点8的左子结点处,调整步骤是一样的。

   

(3)LR型调整:a的左子树较高,新结点插入在a的左子树的右子树。先进行左旋转,再进行右旋转。

在下图的图a中,a是最小非平衡子树的根,b的BF一定是0,c的BF也一定是0。结点4.1被插入到了a的左子树的右子树(图b中4.1插入到了c结点的左子树,当然也可以插到c结点的右子树,其调整过程都是一样的),需要进行LR型调整。

数据结构、算法与应用_第8张图片

图c中,首先将c结点的左右子树分别摘下来,然后将结点4.5-4-3-2看做一条可以转动的链子,对其进行左旋转(逆时针)一个结点,就得到了图d,然后再将结点2-3-4-4.5-5-8-9看做一条转动的链子,将其进行右旋转(顺时针)一个结点,就得到了图e。

最后将原来c结点的左子树接到b结点的右子结点上,将原来c结点的右子树接到a结点的左子结点上,调整完成。

数据结构、算法与应用_第9张图片

数据结构、算法与应用_第10张图片

(4)RL型调整:a的右子树较高,新结点插入在a的右子树的左子树。先进行右旋转,再进行左旋转。

 

RL型调整与LR型正好是对称的,操作步骤类似。在下图的图a中,a是最小非平衡子树的根,b的BF一定是0,c的BF也一定是0。结点5.5被插入到了a的右子树的左子树(图b中5.5插入到了c结点的左子树,当然也可以插到c结点的右子树,其调整过程都是一样的),需要进行RL型调整。

图c中,首先将c结点的左右子树分别摘下来,然后将结点7-9-10-11看做一条可以转动的链子,对其进行右旋转(顺时针)一个结点,就得到了图d,然后再将结点3-4-5-7-9-10-11看做一条转动的链子,将其进行左旋转(逆时针)一个结点,就得到了图e。

最后将原来c结点的左子树接到a结点的右子结点上,将原来c结点的右子树接到b结点的左子结点上,调整完成。

数据结构、算法与应用_第11张图片

数据结构、算法与应用_第12张图片

数据结构、算法与应用_第13张图片

平衡二叉树的删除

和插入差不多,也是向上回溯。发现谁的平衡变了,就旋转调整

3.2.7 堆和优先队列

堆本身也是一棵树,其实堆也有很多种,我们在这里主要使用二叉树来表示堆,说白了,二叉堆就是满足一些特殊性质的二叉树:

1)二叉堆是一棵完全二叉树

2)堆中某个节点的值总是不大于其父节点的值(所以也叫做最大堆),注意:层次大的元素值不一定小于层次小的元素

堆的插入:新元素必须插入到二叉树的末尾(即堆尾)。如果该数组构成的二叉树不满足堆的性质,则需要重新排列元素—“上浮”操作。

堆的筛选法构建:时间复杂度O(n)

堆的删除:删除操作必须删除该二叉树的根节点(即堆顶)。堆中最后一个元素用来补空,再重新排列元素—“下沉”操作

 

public void Insert(int val){
        //这个地方是堆的插入
        if(currentSizeval){
                    int temp=heapArray[parent];
                    heapArray[parent]=heapArray[currentSize];
                    heapArray[currentSize]=temp;
                    if(parent==0) break;
                    parent=(parent-1)/2;
                }else{
                    break;
                }
            }
            currentSize++;
         //这个地方可以改造成堆的优化构造 ,其实也是堆的删除操作
        }else if(val>heapArray[0]){
            int i=0;
            int j=2*i+1;
            int temp=val;
            while(j<=currentSize-1){
                if((j<=currentSize-2)&&(heapArray[j]>heapArray[j+1]))
                    j++;
                if(temp>heapArray[j]){
                    heapArray[i]=heapArray[j];
                    i=j;
                    j=2*j+1;
                }else
                    break;
            }
            heapArray[i]=temp;
        }
        System.out.println(Arrays.toString(heapArray));
    }

 

优先队列和其实是队列的一种

普通队列:先进先出;后进先出

优先队列:出队顺序和入队顺序无关;和优先级相关

可以使用最大堆来实现优先队列

3.2.8 Huffman编码树

Huffman树的存储结构:结构数组。

Huffman树形成的关键:如何在一堆带权节点中每次选取两个权值最小的节点。

Huffman编码树的构建:

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:


(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);


(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;


(3)从森林中删除选取的两棵树,并将新树加入森林;
 

3.3 树和森林

树、二叉树、森林的互相转化

树和森林的深度遍历

树和森林的广度遍历就是和二叉树差不多

3.3.1 树的存储

  • 孩子表示法

定长结点的多重链表(浪费)

不定长结点的多重链表

  • 孩子,兄弟表示法

每个结点保存第一个孩子和下一个兄弟

  • 双亲表示法

每个结点保存父亲


四、图

4.1 图的基本概念

在图结构中,数据元素中间的关系可以是任意的,图中任意两个元素之间都可能相关。

图根据边的分类分为有向图和无向图,有向图的边是有向边,它就像公路的单行道一样,只能从一个方向到另一个方向。无向图的边是无向边,当然它就像双向车道一样可以互相到达,而且两个顶点是没有区别的。


当且仅当(u,v)是图的边,称顶点v和u是邻接的。边(u,v)关联于顶点u和v。对于无向图这种邻接和关联是对等的,而有向图是单向的,它仅仅从u到v。


权:在图的一些应用中,可能要为每条边赋予一个表示大小的值,这个值就称为权。例如从城市A到城市B存在一条公路,而可以使用权表示这条公路的距离。


路径:一个顶点序列i1,i2........ik是图的一条路径,当且仅当边(i1,i2)(i2,i3).........(ik-1,ik)都在图中。如果除了第一个顶点和最后一个顶点之外,其余的顶点均不相同,那么这条路径称为简单路径。如果构成回路的路径是简单路径,则称此回路为简单回路。不带回路的图称为无环图。


连通图:设图G是无向图,当且仅当G的每一对顶点之间都有一条路径,则称G是连通图。


子图:如果图H的顶点和边的集合是图G的子集,那么称图H是图G的子图。


生成树:如果图H是图G的子图,且他们的顶点集合相同,并且H是没有环路的无向连通图(即一棵树),则称H是G的一棵生成树。
 

在具有n个顶点的无向图中,边数为e,则有0\leq e\leq \frac{n(n+1)}{2}

在具有n个顶点的有向图中,边数为e,则有0\leq e\leq n(n+1)

包含所有边的图为完全图,边数相对较少的为稀疏图,反之称为稠密图。

顶点的度(degree):在无向图中,一个顶点v的度是依附于顶点v的边的条数,记作TD(v)。在有向图中,以顶点v为始点的有向边的条数称为顶点v的出度,记作OD(v);以顶点v为终点的有向边的条数称为顶点v的入度,记作ID(v)。有向图中顶点v的度等于该顶点的入度与出度之和:TD(v)=ID(v)十OD(v)。

连通图与连通分量:在无向图中,若从顶点vi到顶点vj有路径,则称顶点vi与vj是连通的。如果无向图中任意两个顶点都是连通的,则称此无向图是连通图。非连通图的极大连通子图(包括所有连通的顶点和这些顶点依附的所有的边)叫做连通分量。任何联通图的联通分量只有一个,即本身。而分联通图有多个连通分量。

强连通图与强连通分量(strongly connected digraph):在有向图中,若对于顶点vi和vj,存在一条从vi到vj和从vj到vi的路径,则称顶点vi和顶点vj是强连通。如果有向图中任意两个顶点都是强连通的,则称此有向图为强连通图。非强连通图的极大强连通子图叫做强连通分量。

生成树(spanning tree):一个连通图的生成树是它的极小连通子图,它包含图中全部n个顶点和仅使这n个顶点连通的n-1条边。如果一个有向图只有一个入度为零的顶点,且其它顶点的入度均为1,则称这个有向图为有向树。一个有向图的生成森林由若干棵有向树组成,生成森林含有图中所有的顶点,且只有足以构成若干棵互不相交的有向树的弧。

 

 

 

/**
 * 边类型
 * @author Roy wang
 *
 */
class Edge implements Comparable{
	public int start;
	public int end;
	public int weight;
	public Edge(int start,int end,int weight) {
		this.start=start;
		this.end=end;
		this.weight=weight;
	}
	@Override
	public int compareTo(Object o) {
		// TODO Auto-generated method stub
		return weight-((Edge)o).weight;
	}
	
}

/**
 * 所有图的基类型
 * @author Roy wang
 *
 */

abstract class Graph{
	public int vertexNum;   //顶点数
	public int edgeNum;   //边数
	int Mark[];         //标记是否被访问过
	Graph(int vertexNum){
		this.vertexNum=vertexNum;
		edgeNum=0;
		Mark=new int[vertexNum];
		for(int i=0;i0&&oneEdge.weight=0)
			return true;
		else {
			return false;
		}
	}
	int startVertex(Edge oneEdge) {
		return oneEdge.start;
	}
	int endVertex(Edge oneEdge) {
		return oneEdge.end;
	}
	int weight(Edge oneEdge) {
		return oneEdge.weight;
	}
	abstract void setEdge(int start,int end,int weight);
	abstract void delEdge(int start,int end);
}

4.2 图的存储及基本操作

图的存贮结构应根据具体问题的要求来设计。常用的存贮结构有邻接矩阵、邻接表、邻接多重表和十字链表

4.2.1 图的邻接矩阵表示

在图的邻接矩阵表示中,除了记录每一个顶点信息的顶点表外,还有一个表示各个顶点之间关系的矩阵,称之为邻接矩阵。若设图G=(V,E)是一个有n个顶点的图,则图的邻接矩阵是一个二维数组Arcs[n][n],它的定义为:

邻接矩阵

对于网络(或带权图),邻接矩阵定义如下:

数据结构、算法与应用_第14张图片

邻接矩阵是指用矩阵来表示图。它是采用矩阵来描述图中顶点之间的关系(及弧或边的权)。 
假设图中顶点数为n,则邻接矩阵定义为:

数据结构、算法与应用_第15张图片

 无向图的邻接矩阵是堆成的。使用邻接矩阵很容易获得顶点的度

public class AdjGraph extends Graph{
	int [][]matrix;
	
	public AdjGraph(int verticesNum) {
		super(verticesNum);
		int i,j;
		
		matrix=new int[vertexNum][];
		for(i=0;i

4.2.2邻接表

链表中的结点称为边结点。包含三个域:邻接点的编号,边的信息,指向下一条关联边的边结点

存储无向图,顶点vi的度就是第i条链表中的边结点的数目。占用n+2e个存储单元

对于有向图,可以方便的计算出度,但是计算入度必须遍历整个邻接表。存储空间n+e

由于邻接表不便于找到入度。使用逆邻接表存储(差不多,就是指出变为指入)

/**
 * 临界表边界点的数据定义
 * @author Roy wang
 *
 */
class listData{
	public int vertex;
	public int weight;
}

/**
 * 边节点的数据定义
 * @author Roy wang
 *
 */
class ListNode{
	public listData element;
	ListNode next;
	public ListNode(int vertex,int weight,ListNode next) {
		element.vertex=vertex;
		element.weight=weight;
		this.next=next;
	}
	public ListNode() {}
	
}

/**
 * 每个边关联的边表
 * @author Roy wang
 *
 */
class EdgeList{
	public ListNode head=new ListNode();
	
}

/**
 * 图的邻接表表示
 * @author Roy wang
 *
 */

class ListGraph extends Graph{
	EdgeList[] graList;
	public ListGraph(int verticesNum) {
		super(verticesNum);
		graList=new EdgeList[verticesNum];
		
	}
	
	
	@Override
	Edge FirstEdge(int oneVertex) {
		// TODO Auto-generated method stub
		Edge tepEdge=new Edge();
		tepEdge.start=oneVertex;
		ListNode temp=graList[oneVertex].head;
		while(temp.next!=null) {
			tepEdge.end=temp.next.element.vertex;
			tepEdge.weight=temp.next.element.weight;
		}
		return tepEdge;
	}

	@Override
	Edge NextEdge(Edge oneEdge) {
		// TODO Auto-generated method stub
		Edge tepEdge=new Edge();
		tepEdge.start=oneEdge.start;
		ListNode temp=graList[tepEdge.start].head;
		while(temp.next!=null&&temp.next.element.vertex<=oneEdge.end)
			temp=temp.next;
		if(temp.next!=null) {
			tepEdge.end=temp.next.element.vertex;
			tepEdge.weight=temp.next.element.weight;
		}
		return tepEdge;
	}

	@Override
	void setEdge(int start, int end, int weight) {
		// TODO Auto-generated method stub
		ListNode temp=graList[start].head;
		while(temp.next!=null&&temp.next.element.vertexend) {
			if(temp.next.element.vertex==end) {
			   ListNode node=new ListNode(end,weight,null);
			   node.next=temp.next.next;
			   temp.next=node;
			   return;
			}
		}
	}

	@Override
	void delEdge(int start, int end) {
		// TODO Auto-generated method stub
		
		ListNode temp=graList[start].head;
		while(temp.next!=null&&temp.next.element.vertex

4.2.3 十字链表和邻接多重表

十字链表:有向图

邻接多重表:无向图

十字链表和邻接多重表

 

4.3 图的遍历

4.3.1 深度优先(DFS)

时间复杂度依赖存储结构        邻近矩阵:n2    邻接表:e +n   

深度优先遍历得到的遍历序列可能不唯一

在搜索过程中,由于顶点v访问与其相邻且未被访问的顶点u时经过的边(v,u)称为前向边。深度优先搜索过程中的所有前向边和顶点组成为子图G是原图的一个生成树,也称为深度优先搜索生成树

递归实现

        public void DFS(int v) {
		Mark[v]=1;
		visit(v);
		for(Edge e=FirstEdge(v);isEdge(e);e=NextEdge(e)) {
			if(Mark[endVertex(e)]==0) {
				DFS(Mark[endVertex(e)]);
			}
		}
	}
	
	public void DFSTraverse()
	{
		for(int i=0;i

非递归实现

	    public void DFSNoReverse() {
		int i,v,u;
		Stack s=new Stack<>();
		for(i=0;i

4.3.2 广度优先 (BFS)

	public void BFS() {
		
		Queue s=new ArrayDeque<>();
		for(int i=0;i

4.4 最小生成树

对于带权无向图,代价最小的生成树称为最小生成树。通常使用构造方法实现最小生成树

3.1  普里姆(Prim)算法

时间复杂度O(n*n).与边无关,适用边数比较稠密的图

	/**
	 * 从s顶点出发得到最小生成树
	 * @param G
	 * @param s
	 * @return
	 */
	
    Edge[] Prim(Graph G,int s) {
    	int i,j;
    	Edge[] MST;
    	int []nearest;      //表示生成树中点到i点的最小权值
    	int []neighbor;      //生成树中与i点最近的点编号
    	
    	int n=G.VerticesNum();
    	nearest=new int[n];
    	neighbor=new int[n];
    	MST=new Edge[n-1];
    	for(i=0;i=0) {
    			Edge e=new Edge(neighbor[v],v,nearest[v]);
    			MST[i]=e;
    			neighbor[v]=-1;
    			
    		}
    		for(Edge e=G.FirstEdge(v);G.isEdge(e);e=G.NextEdge(e)) {
    			int u=e.end;
    			if(neighbor[u]!=-1&&nearest[u]>e.weight) {
    				neighbor[u]=v;
    				nearest[u]=e.weight;
    			}
    		}
    	}
    	return MST;
    }

3.2 克鲁斯卡尔(Kruskal)算法

时间复杂O(eloge).主要取决于边数。适用构造稀疏图的最小生成树

使用最小生成树

	class UFSets{
		private int n;       //等价类中元素个数
		private int[]root;   //表示元素i所在等价类的代表元素符号
		private int[]next;   //表示在等价类i中,i的后面的元素编号
		private int[]length; //length[i]表示i所代表的等价类的元素个数
		public UFSets(int size) {
			n=size;
			for(int i=0;i MinHeap=new ArrayList<>(G.EdgesNum());
		Edge edge=new Edge();
		
		for(int i=0;i

四、最短路径  一般针对带权有向图

4.1  Dijkstra算法---->单源最短路径

 D[i]表示当前所找到的从s到每个顶点的最短特殊长度

时间复杂度O(n*n).使用最小堆O(n*logn)

Path数组表示到达各个顶点的前驱结点

	void Dijstra(Graph G,int s,int D[],int Path[]) {
		int n=G.EdgesNum();
		int i,j;
		for(int i=0;i(D[s]+e.weight)) {
				D[endVertex]=D[s]+e.weight;
				Path[endVertex]=s;
			}
		}
		
		for(i=0;iD[j]) {
					min=D[j];
					k=j;
				}
			}
			
			G.Mark[k]=1;
			for(Edge e=G.FirstEdge(k);G.isEdge(e);e=G.NextEdge(e)) {
				int endVertex=e.end;
				if(G.Mark[endVertex]==0&&D[endVertex]>(D[k]+e.weight)) {
					D[endVertex]=D[k]+e.weight;
					Path[endVertex]=k;
				}
			}
			
		}
	}

4.2  Floyd算法--->顶点对之间的最短路径

动态规划

时间复杂度O(n*n*n)

adj(k)矩阵。元素adjk[i,j]描述从Vi到Vj两点之间且中间顶点编号不大于k的最短路径长度

Pathk[i][j]表示从Vi到Vj两点之间且中间顶点编号不大于k的最短路径中vj的前驱结点编号

	void Floyd(Graph G,int [][]Adj,int[][]Path) {
		int i,j,v;
		int n=G.VerticesNum();
		for(i=0;iAdj[i][v]+Adj[v][j]) {
							Adj[i][j]=Adj[i][v]+Adj[v][j];
							Path[i][j]=v;
						}
					}
				}
			}
		}
	}

五、拓扑排序

  拓扑排序是对有向图中顶点进行的一种排序,根据排序结果可以判断有向图中是否存在环。

通过深度优先遍历可以确定图中有没有环

循环找到入度为0的结点。加入拓扑排序。更新其它结点的入度信息

如果vi是vj的前驱结点,在序列中vi必在vj之前,这样的排序为拓扑排序。拓扑排序可能不唯一。

 六、关键路径

若在带权的有向无环图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向无环图称为AOE网。

AOE-网还有一个特点就是:只有一个起点(入度为0的顶点)和一个终点(出度为0的顶点),并且AOE-网有两个待研究的问题:

  1. 完成整个工程需要的时间
  2. 哪些活动是影响工程进度的关键
  • 关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)
  • 关键活动:关键路径上的边

假设起点是vo,则我们称从v0到vi的最长路径的长度为vi的最早发生时间,同时,vi的最早发生时间也是所有以vi为尾的弧所表示的活动的最早开始时间,使用e(i)表示活动ai最早发生时间,除此之外,我们还定义了一个活动最迟发生时间,使用l(i)表示,不推迟工期的最晚开工时间。我们把e(i)=l(i)的活动ai称为关键活动,因此,这个条件就是我们求一个AOE-网的关键路径的关键所在了。

所以,我们现在要求的就是每弧所对应的e(i)和l(i),求这两个变量的公式是:

e(i)=ve(j)
l(i)=vl(k)-dut()

变量的介绍:

首先我们假设活动a(i)是弧上的活动,j为弧尾顶点,k为弧头(有箭头的一边),
ve(j)代表的是弧尾j的最早发生时间,
vl(k)代表的是弧头k的最迟发生时间
dut()代表该活动要持续的时间,既是弧的权值

首先我们假设活动a(i)是弧上的活动,j为弧尾顶点,k为弧头(有箭头的一边),
ve(j)代表的是弧尾j的最早发生时间,
vl(k)代表的是弧头k的最迟发生时间
dut()代表该活动要持续的时间,既是弧的权值

好了,先在我们知道了求e(i)和l(i)就必须先知道各个顶点的ve和vl了,所以下面我们就来求每个顶点ve和vl。其中,我们要知道ve和vl是要分开来求的。

先求ve,从ve(0)=0开始往前推(其实就是从起点开始往后,求各个顶点最早发生时间),公式如下:

ve(j)=Max{ve{i}+dut()};
属于T,j=1,2.....n-1,
其中T是所有以第j个顶点为头的弧的集合。n为顶点的个数

下面我们继续求:各个顶点的vl,vl是从vl(n-1)=ve(n-1)往后推进(其实就是从终点开始往前求各个顶点的最迟发生时间,其中终点的ve和vl是相等的)

vl(i)=Min{vl(j)-dut()}
属于S,i=n-2,n-3.....0
其中,S是所有以第i个顶点为尾的弧的集合

3、求关键路径的步骤

    输入顶点数和边数,已经各个弧的信息建立图
    从源点v1出发,令ve[0]=0;按照拓扑序列往前求各个顶点的ve。如果得到的拓扑序列个数小于网的顶点数n,说明我们建立的图有环,无关键路径,直接结束程序
    从终点vn出发,令vl[n-1]=ve[n-1],按逆拓扑序列,往后求其他顶点vl值
    根据各个顶点的ve和vl求每个弧的e(i)和l(i),如果满足e(i)=l(i),说明是关键活动。
 


五、查找

 

 为了提高在大规模数据中查找的效率,往往对数据的存储进行特殊处理。最常见的方法是建立索引和预排序

查找分为静态查找和动态查找。静态查找是在查找中不更改数据集中的元素。而动态查找指在查找不成功的时候要将查找的元素添加到数据集中,主要包括二叉搜索树、平衡二叉搜索树、B-树、B+树。

根据关键字的比较次数度量查找的效率

查找成功时的平均查找长度:如果要找的记录datai的概率为Pi,并且查找到datai需要经过Ci次比较

ASL=\sum_{i=1}^{n}PiCi

5.1 静态查找

5.1.1顺序查找

平均查找长度:\frac{n+1}{2}

如果查找的关键字不在,要比较n+1次才能确定失败

插入的时间复杂度O(1),平均查找长度O(n)

public static int DirectResearch(List list,int val) {
    	for(int i=0;i

5.1.2 折半查找

折半查找法的查找过程可以用二叉树描述,树中每个结点表示算法中参与比较的数据元素编号,这个二叉树被称为判定树

ASL=\frac{n+1}{n}log2(n+1)-1

折半查找只适用于顺序存储的有序表,而且向有序表中新增或删除数据操作比较复杂。

public static int BinaryResearch(List list,int val) {
    	int left=0;
    	int right=list.size()-1;
    	int mid;
    	while(left<=right) {
    		mid=(left+right)/2;
    		if(list.get(mid)==val) {
    			return mid;
    		}
    		if(list.get(mid)val) {
    			right=mid-1;
    		}
    	}
    	return -1;
    }

5.1.3 分块查找

分块查找分为两个阶段。第一阶段,根据索引表确定查找的记录所在的数据块。由于索引表按照关键字有序,因此可以用折半查找法。第二阶段,在已确定的数据块中用顺序查找法进行查找。

假如有n个记录,分成m块,每块m个记录

ASL=log_{2}(\frac{n}{m}+1)+\frac{m}{2}

package com;
import java.util.*;
/**
 * @describe 分块查找
 * @author Roy wang
 *
 */
public class BlockSearch {
    private int[] index;  //建立索引
    private ArrayList[] list;
    
    /**
     * 初始化索引
     * @param args
     */
    
    public BlockSearch(int[] index) {
    	if(null!=index&&index.length!=0) {
    		this.index=index;
    		this.list=new ArrayList[index.length];
    		for(int i=0;ivalue) {
    			end=mid-1;
    		}else {
    			start=mid+1;
    		}
    	}
    	return start;
    }
    
    public boolean search(int data) {
    	int i=binarySearch(data);
    	for(int j=0;j

5.2 动态查找

在大规模数据查找中,大量信息存储在外存磁盘中,选择正确的数据结构可以显著降低查找磁盘中数据的时间。B-树和B+树就是两种常见的高效外存数据结构。

B-树的详解

b树和b+树的区别。插入、删除什么的大同小异

5.3 散列

思想:在记录的关键字和存储位置之间建立一个确定的函数关系,使得每个关键字与结构中一个唯一的存储位置相对应。在查找时,首先对记录的关键字进行函数运算,把函数值当作该记录的存储位置。按照散列方法构造出来的表或结构称为散列表

散列方法的核心:由散列函数确定关键字与散列地址之间的对应关系,通过这种对应关系进行存储和查找。

由散列函数得到的散列地址相同的现象称为冲突,发生冲突的两个关键字称为散列函数的同义词。

在散列方法中,需要考虑两个问题:

1)构造使关键字均匀分布的散列函数,避免冲突

2)设计冲突解决方法,处理冲突。

影响散列函数的效率因素:散列函数是否均匀、处理冲突的方法以及装填因子。

5.3.1散列函数

  • 散列函数定义域必须包括需要存储的所有关键字
  • 散列函数计算出来的地址应能均匀分布在整个地址空间中
  • 散列函数应是简单的,能在较短的时间内计算出结果

1)直接定址法  Hash(key)=a*key+b;

2)数字分析法  Hash(key)=key%p

3)除留余数法

4)平方取中法

5)基数地址法

6)折叠法

5.3.2冲突解决方法

散列地址不同的结点争夺同一个后继散列地址的现象称为一次聚集或堆积。

二次探查法可能难以包括散列表的所有存储位置。避免了一次聚集。但是仍然避免不了冲突的聚集。因为对散列到相同地址的关键字,采用的是同样的猴急探查序列。这成为冲突的二次聚集。

1)开放定址法,也称闭散列法(线性探查法、二次探查法、伪随机探查法、双散列法)

  • 线性探查法(Hash(key)+1)%m;(Hash(key)+2)%m;(Hash(key)+3)%m;(Hash(key)+4)%m...(Hash(key)+m-1)%m;
  • 二次探查法 d=Hash(key)   d,(d+1^2)%m,(d-1^2)%m,(d+2^2)%m,(d-2^2)%m,(d+3^2)%m,(d-3^2)%m......
  • 伪随机探查法
  • 双散列法.避免二次冲突  Hi=(HashKey(key)+i*ReHash(key)%m, i=0,1,2,3,...m-1

2)链接法,也称开散列法、拉链法.散列表中的每个地址都是一个链表的表头,并关联着一个链表结构。不会出现冲突的聚集现象。

如果将整个散列表存储在磁盘中,拉链法就不合适。应为一个同义词链表中的元素可能存储在不同的磁盘中

3)桶定址法。桶定址法的基本思想是把记录分为若干个存储桶。每个存储桶包括一个或多个存储位置。如果桶满了,可以用开放定址法处理

5.3.3 散列查找例子

  使用除留取余法散列函数。使用双散列探测法解决冲突

package com;
import java.util.*;
/**
 * 没有使用泛型,使用int型变量作为关键字类型
 * @author Roy wang
 *
 */
public class hashtable {
	/**
	 * 冲突解决
	 * @return
	 * @param k,i
	 */
	private int currentsize;
	private int maxsize;
	public Integer[] list;
    public int probe(int k,int i) {
    	return k%maxsize+i;
    }
    
    int hash(int k) {
    	return k%maxsize;
    }
    
    public hashtable(int size) {
    	currentsize=0;
    	maxsize=size;
    	list=new Integer[maxsize];
    	
    }
    
    public boolean hashInsert(int T) {
    	int home=0;
    	int i=0;
    	int pos=home=hash(T);
    	while(list[i]!=null) {
    		if(list[i]==T) {
    			return false;
    		}
    		i++;
    		pos=(home+probe(T,i))%maxsize;
    		if(pos==home) {
    			return false;
    		}
    	}
    	list[i]=T;
    	return true;
    }
    
    
    public boolean hashSearch(int item) {
    	int home=hash(item);
    	int i=0;
    	int pos=home;
    	while(list[i]!=null) {
    		if(list[i]==item) {
    			return true;
    		}
    		i++;
    		pos=(home+probe(item,i))%maxsize;
    	}
    	return false;
    }
    
    public boolean hashDelete(int item) {
    	int home=hash(item);
    	int i=0;
    	int pos=home;
    	while(list[i]!=null) {
    		if(list[i]==item) {
    			list[i]=null;
    			return true;
    		}
    		i++;
    		pos=(home+probe(item,i))%maxsize;
    	}
    	return false;

    }
    
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		hashtable Hash=new hashtable(10);
		Hash.hashInsert(5);
		Hash.hashInsert(8);
		Hash.hashInsert(13);
		System.out.println(Hash.hashSearch(13));
	}

}

六、排序

6.1 排序的基本概念

  • 用来排序依据的属性称为关键字域,简称关键字。排序就是根据关键字的大小将无序的多条记录,调整为有序的序列。
  • 当关键字可以重复出现时,建设Ki=Kj,且在排序前的序列R中,Ri领先与Rj,若在排序后的序列中仍领先,则称排序算法是稳定的。
  • 可以将排序算法分为内部和外部排序。内部排序指的是待排序记录存放在计算机随机存储器中进行的排序过程。外部排序指的是待排序记录的数量很大,一次一次无法容纳全部记录,在排序过程中需要对外存进行访问的拍戏。
  • 一比较次数和移动次数来度量排序算法的时间复杂度。

6.2 插入排序

6.2.1 直接插入排序

由n-1趟排序组成。第p趟排序保证从第0个位置到第p个位置上的元素为有序状态。第p+1趟排序是将第p+2个元素插入到前面p+1个元素的有序表中.

public static void InsertionSort(int[]a) {
    	for(int i=1;i=0&&temp<=a[k];k--) {
       				 a[k+1]=a[k];
       			 }
       		 a[k+1]=temp;
       		 }
       	
       	 }
    }

6.2.2 折半插入排序

public void BinaryInsertionSort(int Data[],int n)
{
    int left,mid,right,p;
    for(int p=1;ptemp){
                right=mid-1;
            }else { 
                left=mid+1
            }
        }
        for(int i=p-1;i>=left;i++){
            Data[i+1]=Data[i];
        }
        Data[left]=temp;
    }
}

6.2.3 希尔排序

思想:先将待排序数据序列划分成若干子序列分别进行直接插入排序;待整个序列中的数据基本有序后,再对全部数据进行一次直接插入排序。对于子序列的排序可以采用任意的排序算法。

public static void ShellSort(int[]a) {
    	int increasement=a.length;
        do {
       	 int k;
       	 increasement=increasement/3+1;
       	 for(int i=0;i=0&&a[k]>temp;k-=increasement) {
       					 a[k+increasement]=a[k];
       				 }
       				 a[k+increasement]=temp;
       			 }
       		 }
       	 }
        }while(increasement>1);
    }

6.3 交换排序

6.3.1 冒泡排序

冒泡排序通过不断比较相邻元素的大小,然后决定是否对这两个元素进行交换操作,从而达到排序的目的。

 public static void Bubble(int[]a) {
	    for(int i=0;ia[j+1]) {
	   			 int temp;
	   			 temp=a[j];
	   			 a[j]=a[j+1];
	   			 a[j+1]=temp;
	   		 }
	   	 }
	    }
    }

6.3.2 快速排序

基于分治法思想提出的一种排序算法。由三步组成:分割、分治、合并。

 public static void QuickSort(int[] a,int start,int end) {
    	int i=start;
    	int j=end;
    	
    	if(start>=end) {
    		return;
    	}
    	int temp=a[start];
    	while(i=temp) {
    			j--;
    		}
    		if(i=a[left]) {
    			left++;
    		}
    		while(left<=right&&a[start]

6.4 选择排序

选择排序要经过N-1趟选择过程

6.4.1 简单选择排序

public static void Select(int[]a) {
    	int flag;
    	 for(int i=0;i=a[j]) {
        			 temp=j;
        		 }
        	 }
        	 flag=a[i];
        	 a[i]=a[temp];
        	 a[temp]=flag;
        	 
        	 
         }
    }

6.4.2 堆排序

其实就是最小堆删除顶部。easy to control.

6.5 归并排序

若一个序列中只有一个元素,归并操作不执行任何操作。否则,归并排序算法按照递归步骤进行:

  1. 把序列划分为长度基本相等的子序列
  2. 对每个子序列进行归并排序
  3. 把排好序的子序列合并为最后的结果

由于需要申请额外的空间,归并排序的效果往往没有快速排序好

public static void Merge(int[]a,int start,int mid,int end) {
    	List set1=new ArrayList<>();
    	List set2=new ArrayList<>();
    	for(int i=start;i<=mid;i++) {
    		set1.add(a[i]);
    	}
    	for(int j=mid+1;j<=end;j++) {
    		set2.add(a[j]);
    	}
    	
    	int i=0,j=0;
    	int k;
    	for(k=start;k<=end;k++) {
    		if(i==mid-start+1||j==end-mid) {
    			break;
    		}
    		if(set1.get(i)

6.6 比较排序算法的时间复杂度下界

基于比较的排序算法的时间复杂度的下界为nlogn.具有nlogn时间复杂度的算法在渐进意义来说是最优的算法

6.7 基数排序

借助多关键字排序的思想对单逻辑关键字进行排序的方法

  • 高位优先法(MSDF)

    按照优先级最高的关键字进行排序,然后按照次优先级关键字进行排序

  • 低位优先法(LSDF)

按照优先级最低的关键字进行排序,依次重复,直至按照最高优先级的关键字排序,序列就会变成有序序列。

基数排序方法就是将带排序的数据元素的单逻辑关键字拆分成若干个关键字。下面算法是低位优先

package com;

import java.lang.reflect.Array;
import java.util.Arrays;

public class good {
	public static final int RADIX=10;
	
	public static TubNode[] Distribute(int Data[],int n,int ith){
		TubNode[] tube=new TubNode[RADIX];
		for(int i=0;i

数据结构、算法与应用_第16张图片

你可能感兴趣的:(数据结构,算法)