数据结构与算法(一)数据结构基础

目录

一、绪论

1.1 什么是程序

程序 = 数据结构 + 算法

本篇文章只介绍 数据结构 基础内容。


二、算法

2.1 定义

算法 是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令标识一个或多个操作。

简而言之,算法是描述解决问题的方法。

2.2 特性

  • 输入、输出、有穷性、确定性和可行性。

好的算法,应该具有正确性、可读性、健壮性、高效率和低存储量的特征。

2.3 算法时间效率

  1. 可以忽略加法常数:O(2n + 3) = O(2n)

  2. 与最高次项相乘的常数可忽略:O(2n^2) = O(n^2)

  3. 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长更快:O(n^3) > O(n^2)

  4. 判断一个算法时间效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数:

    O(2n^2) = O(n^2 + 3n + 1)

    O(n^3) > O(n^2)

2.4 时间复杂度

时间复杂度:所消耗的时间即基本操作执行次数。

1)大 O 阶推导法:
  1. 用常数1取代运行时间中的所有加法常数。

  2. 在修改后的运行次数函数中,只保留最高阶项。

  3. 如果最高阶项存在且不是 1,则去除与这个项相乘的常数,得到的结果就是大 O 阶。

    O(2n+1) = O(2n)

    O(2n^2+1) = O(n^2)

2)举个例子:

计算下面程序的时间复杂度:

int i, j;
for (i = 0; i < n; i++) {
    for (j = i; j < n; j++) {
        System.out.println("hello");
    }
}

分析:

对于外循环,其时间复杂度为 O(n);

对于内循环,总执行次数为:

在这里插入图片描述

根据大 O 阶推导法,上述代码的时间复杂度为:

O ( n 2 ) O(n^{2}) O(n2)

3)常见的时间复杂度
执行次数函数非正式术语
12 12 12 O ( 1 ) O(1) O(1)常数阶
2 n + 3 2n+3 2n+3 O ( n ) O(n) O(n)线性阶
3 n 2 + 2 n + 1 3n^{2}+2n+1 3n2+2n+1 O ( n 2 ) O(n^{2}) O(n2)平方阶
5 l o g 2 n + 20 5log_{2}n+20 5log2n+20 O ( l o g n ) O(logn) O(logn)对数阶
2 n + 3 n l o g 2 n + 19 2n+3nlog_{2}n+19 2n+3nlog2n+19 O ( n l o g n ) O(nlogn) O(nlogn) n l o g n 阶 nlogn阶 nlogn
6 n 3 + 2 n 2 + 3 n + 4 6n^{3}+2n^{2}+3n+4 6n3+2n2+3n+4 O ( n 3 ) O(n^{3}) O(n3)立方阶
2 n 2^{n} 2n O ( 2 n ) O(2^{n}) O(2n)指数阶

常用的时间复杂度所耗费的时间从小到大依次是:

O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1) < O(logn) < O(n) < O(nlogn) < O(n^{2}) < O(n^{3}) < O(2^{n}) < O(n!) < O(n^{n}) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

2.5 空间复杂度

算法的 空间复杂度 是指算法所需的存储空间,即运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序运行所需要的内存有个预先估计。

算法的时间复杂度和空间复杂度是可以相互转化的。

1)计算方法
  • 没有层级结构的代码,空间复杂度为 O(1)
int a = 0;
int b = 0;
System.out.println(a + b);
  • 递归算法的空间复杂度 = 递归深度N * 每次递归所需要的辅助空间。

例如下面这段代码,通过递归实现,每次调用 fun 函数,都会创建 1 个变量 k。调用 n 次,空间复杂度为:O(n*1) = O(n)

对于单线程来说,递归有运行时的堆栈。空间复杂度求得是递归最深一次所耗费得空间个数,保证空间足够容纳它得所有递归过程。

public int fun(int n) {
    int k = 10;
    if (n == k) {
        return n;
    } else {
        return fun(n + 1);
    }
}
2)存储空间

存储空间包括两部分:

  • 静态空间:这部分空间的大小与输入/输出数据的个数多少、数值大小无关,主要包括:指令空间(代码空间)、数据空间(常量、简单变量)等所占空间。
  • 可变空间:这部分空间大小与算法有关,主要包括:动态分配的空间,以及递归栈所需空间等。

2.6 常见算法的时间复杂度、空间复杂度

排序法最差时间最好时间平均时间复杂度稳定性空间复杂度复杂性性能
冒泡排序 O ( n 2 ) O(n^{2}) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^{2}) O(n2)稳定 O ( 1 ) O(1) O(1)简单
快速排序 O ( n 2 ) O(n^{2}) O(n2) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)不稳定 O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)较复杂
插入排序 O ( n 2 ) O(n^{2}) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^{2}) O(n2)稳定 O ( 1 ) O(1) O(1)简单 n 小时较好 n小时较好 n小时较好
希尔排序依赖于->增量序列 O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)不稳定 O ( 1 ) O(1) O(1)较复杂 n 大时较好 n大时较好 n大时较好
选择排序 O ( n 2 ) O(n^{2}) O(n2) O ( n 2 ) O(n^{2}) O(n2) O ( n 2 ) O(n^{2}) O(n2)不稳定 O ( 1 ) O(1) O(1)简单大部分已排序时较好
堆排序 O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)不稳定 O ( 1 ) O(1) O(1)较复杂
二叉树排序 O ( n 2 ) O(n^{2}) O(n2) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)稳定 O ( n ) O(n) O(n) n 小时较好 n小时较好 n小时较好
归并排序 O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n) O ( n ∗ l o g 2 n ) O(n*log_{2}n) O(nlog2n)稳定 O ( n ) O(n) O(n)较复杂 n 大时较好 n大时较好 n大时较好
基数排序稳定较复杂

三、线性表(Linear list)

在这里插入图片描述

3.1 定义

线性表:零个或多个数据元素的有限序列。

3.2 顺序存储结构

顺序存储结构:用一段地址连续的存储单元依次存储线性表的数据元。

顺序存储示意图如下所示:

在这里插入图片描述

存储器中的每个存储单元都有自己的编号,这个编号称为 地址

1)存储位置公式:

每个数据元素,不管它是整型、实型还是字符型,都是需要占用一定的存储单元空间的。假设占用的是 c 个存储单元,那么对于线性表的第 i 个数据元素 ai 的存储位置都可以由 a1 推导算出:
L O C ( a i ) = L O C ( a 1 ) + ( i − 1 ) ∗ c LOC(a_{i}) = LOC(a_{1}) + (i - 1) * c LOC(ai)=LOC(a1)+(i1)c

存储位置如下所示:

在这里插入图片描述

通过存储位置公式,就可以随时算出线性表中任意位置的地址。不管是第一个还是最后一个,都是相同的时间,即:对于计算来说,线性表中每个位置的存入或者取出数据都是相等的时间,也就是一个常数时间。因此,线性表的存取操作时间复杂度为:O(1)

我们通常将存取操作具备常数性能(时间复杂度为 O(1))的存储结构称为随机存储结构

2)时间复杂度:
  • 对于查询操作,因为元素位置可以直接计算得到,时间复杂度为:O(1)
  • 对于插入和删除操作,因为需要移动其余元素,时间复杂度为:O(n)

总结: 线性表顺序存储结构比较 适用于存取操作较多,增删操作较少的场景

3.3 链式存储结构

一个或多个结点组合而成的数据结构称为链表

结点,一般由两部分内容构成:

  • 数据域:存储真实数据元素。
  • 指针域:存储下一个结点的地址(指针)。

在这里插入图片描述

1)头指针、头结点:
  • 一般把链表中的第一个结点称为 头指针,用于存储链表的第一个数据元素。
  • 为了方便对链表进行操作,在单链表的第一个结点(即头指针)前附设一个结点,称为头节点。

头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针,即指向头指针。

在这里插入图片描述

2)单链表

在线性表的顺序存储结构(即数组)中,其任意一个元素的存储位置可以通过计算得到,因此其数据读取的时间复杂度为 O(1)

3)单链表的时间复杂度
  • 对于查询操作,因为必须从第一个结点开始遍历,时间复杂度为:O(n)
  • 对于插入和删除操作,
    • 如果已知存储位置,时间复杂度为:O(1)
    • 如果未知存储为止,由于需要先遍历查找,时间复杂度为:O(n)

总结: 单链表 适用于频繁插入或删除数据的场景

3.4 循环链表

将单链表的终端结点的指针端由空指针改为指向头节点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称 循环链表(circular linked list)

循环链表不一定需要头结点。

单链表和循环链表的区别:

(主要差异在循环的判断条件上)

  • 单链表判断条件:尾结点是否指向空(p -> next = NULL)。
  • 循环链表判断条件:当前结点是否指向头结点(p -> next = head)。

3.5 双向链表

双向链表(double linked list)在单链表的每个结点中,再设置一个指向其前驱结点的指针域。


四、栈与队列

4.1 栈(Stack)

是限定尽在表尾(栈顶)进行插入和删除操作的线性表。所以栈又称为后进先出(Last In First Out)的线性表,简称 LIFO 结构。

内部实现原理:

  • 栈内部实现原理其实就是数组或链表的操作。之所以引入栈这个概念,是为了将程序设计问题模型化。
  • 用高层的模块指导特定行为(栈的后进先出特性),划分了不同关注层次,缩小思考范围,更加聚焦于我们致力于解决的问题核心,简化了程序设计的问题。
  • 我们把允许插入和删除的一端称为 栈顶,另一端称为 栈底
  • 不含任何数据元素的栈称为 空栈

在这里插入图片描述

4.2 顺序栈、链栈

栈,是特殊的线性表,具备后进先出(LIFO)的特性。

  • 使用线性表的顺序存储结构(即数组)实现的栈,称为 顺序栈
  • 使用单链表的链式存储结构(即链表)实现的栈,称为 链栈

顺序栈和链栈对比:

  • 相同点:顺序栈和链栈的时间复杂度均为 O(1)。
  • 不同点:
    • 顺序栈需要事先确定一个固定的长度(数组长度),可能存在空间浪费问题,但它的优势是查询方便。
    • 链栈要求每个元素都要配套一个指向下个结点的指针域,增大了内存开销,好处是栈的长度没有限制。因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,最好使用链栈;反之,如果它的变化在可控范围内,则建议使用顺序栈。

4.3 队列(Queue)

队列 是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表。和线性表一样,队列也存在顺序存储和链式存储两种存储方式。

  • 允许删除的一端称为 队头
  • 允许插入的一端称为 队尾

五、数组和广义表(Arrays and generalized tables)

5.1 数组

数组 是一种特殊的线性表存储结构。其特殊性在于表中的元素本身也是一种线性表,内存连续,根据下标在 O(1) 时间读/写任何元素。

数组的顺序存储:

  • 行优先顺序:每一行的第一个元素位于低地址,最后一个元素位于高地址。因此,访问同一行中的元素时,其地址是连续的。
  • 列优先顺序:每一列的第一个元素位于低地址,最后一个元素位于高地址。因此,访问同一列中的元素时,其地址是连续的。

数组中的任一元素可以在相同的时间存取,即顺序存储的数组是一个随机存取结构。

数组的优点是访问速度快,缺点是插入和删除元素效率较低。

5.2 广义表

广义表,又称列表,也是一种线性存储结构。同数组类似,广义表中既可以存储不可再分的元素,也可以存储广义表。

记作:LS=(a1, a2, ..., an)

  • LS 代表广义表的名称;
  • an 代表广义表存储的数据。
  • 广义表中的每个 ai 既可以代表单个元素,也可以代表另一个广义表。

广义表中存储的每个元素称为 原子,而存储的广义表称为 子表

广义表的存储结构:

  • 深度优先顺序:广义表的每个元素都会被递归地处理,直到到达叶子结点。
  • 广度优先顺序:广义表的每个元素都会被逐层处理,每一层的元素都按照其在该层中的顺序进行处理。

总结: 广义表的优点是具有较好的灵活性,可以存储任意类型的数据,缺点是访问速度较慢。


六、串(String)

6.1 定义

串(String) 是由零个或多个字符组成的有限序列,又叫字符串。

6.2 串的逻辑结构

串的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。因此,对于串的基本操作与线性表是由很大差别的。线性表更关注的是单个元素的操作,比如查、插入或删除一个元素,但串中更关注的是查找子串为止,得到指定位置字串,替换字串等操作。

6.3 串的存储结构

1)串的顺序存储结构

串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列。

一般是用定长数组来定义。由于是定长数组,因此就会存在一个预定义的最大串长度。一般可以将实际的串长度值保存在数组 0 下标位置,也可以放在数组最后一个下标位置。

也有些语言使用在串值后面加一个不计入串长度的结束标记符(比如 \0),来表示串值的终结,这样就无需使用数字进行记录。

2)串的链式存储结构

在这里插入图片描述

串的链式存储结构,与线性表相似。

由于串结构的特殊性(结构中的每个元素数据都是一个字符),如果也简单地将每个链结点存储一个字符,就会存在很大的空间浪费。因此,一个结点可以考虑存放多个字符。如果最后一个结点未被占满时,可以使用 # 或其他非串值字符补全。

串的链式存储结构除了在链接串与串时有一定的方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。


七、树(Tree)

7.1 定义

是 n 个结点的有限集合(n >= 0)。

  • 当 n = 0 时,称为 空树
  • 树,其实也是一种递归的实现,即树的定义之中还用到了树的概念。

7.2 特点

  1. 有且仅有一个特定的结点:根节点(Root)。
  2. 当 n > 1 时,其余节点可分为 m(m > 0)个互不相交的有限集 T1、T2、T3 … Tm。其中每一个集合本身又是一棵树,并且称为根的 子树(SubTree)

正确的树结构:

下图所示的结构就不符合树的定义,因为它们都有相交的子树。

在这里插入图片描述

7.3 线性结构、树结构对比

线性结构树结构
- 第一个数据元素:无前驱
- 随后一个数据元素:无后继
- 中间元素:一个前驱一个后继
- 根结点:无双亲,唯一
- 叶结点:无孩子,可以多个
- 中间结点:一个双亲多个孩子

7.4 二叉树

二叉树(Binary Tree):是一个包含 n 个结点的有限集合(n >= 0)。该集合或者为空集(称为空二叉树)或者由一个根节点和两棵互不相交的二叉树(左子树右子树)组成。

1)特点:
  • 每个结点最多只能有两棵树;
  • 左子树和右子树是有顺序的,次序不能任意颠倒;
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
2)五种基本形态:
  1. 空二叉树;
  2. 只有一个根结点;
  3. 根结点只有左子树;
  4. 根结点只有右子树;
  5. 根节点既有左子树,又有右子树。
3)二叉树的四种遍历方式

二叉树的遍历 是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问且仅被访问一次。

3.1)前序遍历
  • 规则是先访问根结点,然后前序遍历左子树,再前序遍历右子树。

总结: 根节点 -> 左子树 -> 右子树

如下图所示,遍历的顺序为:ABDGHCEIF。

3.2)中序遍历
  • 从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后再访问根节点,最后中序遍历右子树。

总结: 左子树 -> 根节点 -> 右子树

如下图所示,遍历的顺序为:GDHBAEICF

3.3)后序遍历
  • 从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点。

总结: 从左到右访问叶子结点 -> 根节点

如下图所示,遍历的顺序为:GHDBIEFCA

3.4)层序遍历
  • 从树的第一层(即根节点)开始访问,从上而下逐层遍历,在同一层中按从左到右的顺序对结点逐个访问。

总结: 第一层 -> 第二层(从左到右访问结点)-> …… -> 最后一层(从左到右访问结点)

如下图所示,遍历的顺序为:ABCDEFGHI

7.5 赫夫曼编码

树,森林看似复杂,其实它们都可以转化为简单的二叉树来处理。这样就使得面对树和森林的数据结构时,编码实现成为了可能。最基本的压缩编码方法:赫夫曼编码。

赫夫曼编码:给定 n 个权限作为 n 个叶子结点,构造一棵二叉树,若 树的带权路径长度 达到最小,则这棵树被称为哈夫曼树。

下图这棵树就是哈夫曼树:

哈夫曼树指的是树的带权路径长度达到最小,那什么是树的带权路径长度呢?我们需要先了解一下路径和路径长度的概念。

1)路径和路径长度

在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到 L 层结点的路径长度为 L - 1。

例如:

  • 100 和 80 的路径长度是 1;
  • 50 和 30 的路径长度是 2;
  • 20 和 10 的路径长度是 3。

了解了路径和路径长度的概念之后,我们还是不能理解什么是树的带权路径长度,下面我们来看一下带权路径长度的概念吧。

2)结点的权、带权路径长度

若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的

结点的 带权路径 长度为:从根节点到该结点之间的路径长度与该结点的权的乘积。

例如:

  • 结点 20 的路径长度是 3,它的带权路径长度 = 路径长度 * 权 = 3 * 20 = 60。

了解了权路径长度的概念之后,那么树的带权路径长度是指什么呢?

3)树的带权路径长度

树的带权路径长度 规定为所有叶子结点的带权路径长度之和,记为 WPL

例如:

  • 示例中,树的 WPL = 1 * 100 + 2 * 50 + 3 * 20 + 3 * 10 = 100 + 100 + 60 * 30 = 290

所以,赫夫曼树作为树的带权路径长度达到最小的树,就是指树上所有带权路径的总和最小。

我们再比较下面两棵树:

在这里插入图片描述

上面的两棵树都是以 {10, 20, 50, 100} 为叶子节点的树。

  • 左边的树 WPL = 2 * 10 + 2 * 20 + 2 * 50 + 2 * 100 = 360
  • 右边的树 WPL = 350

左边的树 WPL > 右边的树 WPL

也可以尝试计算除上面两种示例之外的情况,试过之后就会发现,实际上右边的树就是 {10, 20, 50, 100} 对应的哈夫曼树。


八、图(Graph)

8.1 定义

在线性表中:

  • 数据元素之间是被串起来的,仅有线性关系;

  • 每个元素只有一个直接前驱和一个直接后驱。

在树形结构中:

  • 数据元素之间有着明显的层次关系;
  • 并且每一层的数据元素可能和下一层中的多个元素相关,但只能和上一层中的一个元素相关。

是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的。途中任意两个数据元素之间都可能相关。

图是由顶点的有穷非空集合和顶点之间边的集合组成。

通常表示为:G(V, E)

  • G:表示一个图。
  • V:表示图 G 中顶点的集合。
  • E:表示图 G 中边的集合。

8.2 线性表、树、图之间的关系

1)数据元素名称区别
  • 线性表中,数据元素叫 元素
  • 树中,数据元素叫 结点
  • 图中,数据元素叫 顶点(Vertex)
2)可有无结点区别
  • 线性表中,可以没有数据元素,称为 空表
  • 树中,可以没有结点,称为 空树
  • 图中,不允许没有顶点,在定义中,若 V 是顶点的集合,即强调了顶点集合 V 有穷非空
3)内部之间的关系区别
  • 线性表中,相邻的数据元素之间具有线性关系;
  • 树中,相邻两层的结点之间具有层次关系;
  • 图中,任意两个顶点之间都可能存在关系,顶点之间的逻辑关系用边表示,边集可以是空的

8.3 图的五种类型

1)无向图

若顶点 Vi 到 Vj 之间的边没有方向,则称这条边为 无向边(Edge),用无序偶对 (Vi, Vj) 来表示。

如果图中任意两个顶点之间的边都是无向边,则称该图为 无向图

无向图顶点的边数叫做

下图所示即为无向图 G1:

结合 G(V, E) 表达式介绍:

  • 由于无向图是无方向的,连接顶点 A 与 D 的边可以表示成 (A, D),也可以写成 (D, A)
  • 对于上图种的无向图 G1 来说,G1 = (V1, {E1})
2)有向图

若从顶点 Vi 到 Vj 的边 有方向,则称这条边为 有向边,也成为 弧(Arc),用有序偶 <Vi, Vj> 来表示,Vi 称为 弧尾(Tail),Vj 称为 弧头(Head)

如果图种任意两个顶点之间的边都是有向边,则称该图为 有向图(Directed graphs)

有向图的顶点分为 入度 (箭头朝自己)和 出度 (箭头朝外)。

如下图所示即为一个有向图 G2:

结合 G(V, E) 表达式介绍:

  • 连接到顶点 A 到 D 的有向边就是 ,其中顶点 A 是 弧尾,顶点 D 是 弧头<A, D> 表示 。(注意:不能写成 <D, A> !)
  • 对于上图的有向图 G2,G2 = (V2, {E2})
  • 顶点集合:V2 = {A, B, C, D}
  • 弧集合:E2 = {<A, D>, <B, A>, <C, A>, <B, C>}
3)简单图

在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则成这样的图为 简单图

如下图所示的两个图就不属于简单图:

在这里插入图片描述

4)无向完全图

在无向图中,如果 任意两个顶点之间都存在边,则称该图为 无向完全图

如图所示即为一个无向完全图:

5)有向完全图

在有向图中,如果 任意两个顶点之间都存在方向互为相反的两条弧,则称该图为 有向完全图

如图所示即为一个有向完全图:

8.4 权

有些图的边或弧具有与它相关的数字,这种 与图的边或弧相关的数 叫做 权(Weight)

这些权可以表示从一个顶点到另一个顶点的距离或耗费,这种 带权的图 通常称为 网(Network)

上图就是一张带权的图,即标识中国四大城市的直线距离的网。此图中的权就是两地的距离。

8.5 环

图结构中,路径的长度是路径上的边或弧的数据。第一个顶点到最后一个顶点相同的路径 称为 回环环(Cycle)序列中顶点不重复出现的路径 称为 简单路径

除了一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称之为 简单回路简单环

举个例子:

在这里插入图片描述

如图所示,两个图粗线都构成环。

  • 左侧的环只有第一个顶点和最后一个顶点都是 B,其余顶点没有重复出现,因此其是一个简单环。
  • 而右侧的环,由于顶点 C 的重复,因此它就不是简单环了。

8.6 连通生成树、有向树、森林

  • 连通:图中两顶点间存在路径,则说明是连通的。
  • 简单路径:如果路径最终回到起始点则称为环,当中不重复的叫做简单路径。
  • 强连通图:若任意两顶点都是连通的,则该图就是连通图,有向则称为强连通图。
  • 强连通分量:图中能够连通的极大子图就是连通分量,有向则称为强连通分量。

在这里插入图片描述

  • 生成树:无向图中连通且 n 个顶点 n-1 条边叫生成树。
  • 有向树:有向图中一顶点入度为 0,其余顶点入度为 1 的叫有向树。
  • 森林:一个有向图由若干棵有向树构成森林。

8.7 图的两种遍历

图的遍历和树的遍历类似。我们希望 从图中某一顶点触发,遍历图中其余顶点。且使每一个顶点仅被访问一次,这一过程就叫做 图的遍历(Traversing Graph)

1)深度优先搜索(DFS)

深度优先搜索(Depth-First-Search),简称 DFS。最直观的例子就是 “走迷宫”。假设你站在迷宫的某个岔路口,要找到出口就需要先选择一个岔路口,走不通之后再回来重新选择一条路。这种走法就是一种深度优先搜索。

二叉树的前序、中序、后序遍历,本质上也可以认为是深度优先搜索,深度优先搜索是先序遍历的推广。

深度优先搜索的核心思想:

  • 首先以一个未被访问过的顶点作为 起始顶点,沿当前顶点的边走到 未访问过的顶点
  • 当没有未访问过的顶点时,则回到上一个顶点继续试探别的顶点,直至无路可走。
  • 若此时还有顶点未被访问过,选择其中一个,重复上述过程
1.1)连通图的 DFS:

知识回顾: 若任意两顶点都是连通的,则该图就是 连通图

  • 它从图中某个顶点 V 出发,访问此顶点;
  • 然后从 V 的未被访问的邻接点出发深度优先搜索图;
  • 直至图中所有和 V 有路径相通的顶点都被访问到。
1.2)非连通图的 DFS:

只需要对它的连通分量分别进行深度优先遍历。

知识回顾:

  • 图中两顶点间存在路径,则说明是 连通 的。
  • 图中能够连通的极大子图就是 连通分量
  • 在前一个顶点进行一次深度优先搜索后,
  • 若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为起始点。
  • 重复上述过程,直至图中所有顶点都被访问到为止。
1.3)无向图的 DFS

对上无向图进行深度优先遍历,从 A 开始
第1步: 访问 A
第2步: 访问 B (A的邻接点)。 在第1步访问A之后,接下来应该访问的是A的邻接点,即"B,D,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"D和F"的前面,因此,先访问B。
第3步: 访问 G (B的邻接点)。 和B相连只有"G"(A已经访问过了)
第4步: 访问 E (G的邻接点)。 在第3步访问了B的邻接点G之后,接下来应该访问G的邻接点,即"E和H"中一个(B已经被访问过,就不算在内)。而由于E在H之前,先访问E。
第5步: 访问 C (E的邻接点)。 和E相连只有"C"(G已经访问过了)。
第6步: 访问 D (C的邻接点)。
第7步: 访问 H。因为D没有未被访问的邻接点;因此,一直回溯到访问G的另一个邻接点H。
第8步: 访问(H的邻接点)F。
因此,访问顺序是: A -> B -> G -> E -> C -> D -> H -> F

1.4)有向图的 DFS

对上有向图进行深度优先遍历,从A开始
第1步: 访问 A
第2步: 访问 B (A的出度对应的字母)。 在第1步访问A之后,接下来应该访问的是A的出度对应字母,即"B,C,F"中的一个。但在本文的实现中,顶点ABCDEFGH是按照顺序存储,B在"C和F"的前面,因此,先访问B。
第3步: 访问 F (B的出度对应的字母)。 B的出度对应字母只有F。
第4步: 访问 H (F的出度对应的字母)。 F的出度对应字母只有H。
第5步: 访问 G (H的出度对应的字母)。
第6步: 访问 E (G的出度对应字母)。 在第5步访问G之后,接下来应该访问的是G的出度对应字母,即"B,C,E"中的一个。但在本文的实现中,顶点B已经访问了,由于C在E前面,所以先访问C。
第7步: 访问 D (C的出度对应的字母)。
第8步: 访问 D (C的出度对应字母)。 在第7步访问C之后,接下来应该访问的是C的出度对应字母,即"B,D"中的一个。但在本文的实现中,顶点B已经访问了,所以访问D。
第9步: 访问 E。D无出度,所以一直回溯到G对应的另一个出度E。
因此,访问顺序是:A -> B -> F -> H -> G -> C -> D -> E

2)广度优先搜索(BFS)

广度优先搜索(Breadth-First-Search),简称 BFS。

类似于树的层序遍历,广度优先搜索是按层来处理顶点,距离开始点最近的顶点首先被访问,而距离最远的顶点则最后被访问。

广度优先搜索的核心思想:

  • 首先选择一个顶点作为 起始顶点
  • 将起始顶点 放入队列 中;
  • 队列首部 取出一个顶点,修改标识为 已访问,并找出所有与之 邻接的顶点放入队列尾部
  • 重复上一步操作,直至 清空队列
2.1)无向图的 BFS
  • A 开始,有 4 个邻接点,“B,C,D,F”,这是第二层;
  • 再分别从 B、C、D、F 开始找它们的邻接点,为第三层。以此类推:

因此,访问顺序是:A -> B -> C -> D -> F -> G -> E -> H

2.2)有向图的 BFS
  • 从 A 开始,有 3 个邻接点,“B、C、F”,这是第二层;
  • 再分别从 B、C、F 开始找它们的邻接点,为第三层。以此类推:

因此,访问顺序是:A -> B -> C -> F -> D -> H -> G -> E

3)DFS 和 BFS 的异同

相同点:

  • 深度优先搜索与广度优先搜索算法 在时间复杂度上是一样的

不同点:

对顶点访问的顺序不同:

  • 深度优先搜索(DFS)更适合目标比较明确,以找到目标为主要目的的情况
  • 广度优先搜索(BFS)更适合在不断扩大遍历范围时找到相对最优解的情况

8.8 最小生成树的算法

把构造连通网的最小代价生成树称为 最小生成树

  • 构造连通网的最小代价,即 成本最小
  • 就是 n 个顶点,用 n-1 条边把一个连通图连接起来,并且使权值的和最小。
1)假设场景:

如图所示,假设 V0 到 V8 表示 9 个村庄,现在需要在这 9 个村庄架设通信网络,村庄之间的数组代表村庄之间的直线距离,求用最小成本完成这 9 个村庄的通信网络建设。

2)分析:
  • 这幅图只是 一个带权值的图,即网结构
  • 如果无向连通网是一个网图,那么它的所有生成树中必有一棵是 边的权值总和最小的生成树,即 最小生成树

找连通网的最小生成树,经典的算法有两种:普利姆算法(Prim)克鲁斯卡尔算法(Kruskal)

3)普利姆算法(Prim算法)
  • 从图中某一个顶点出发,寻找它相连的所有结点,比较这些结点的权值大小,然后连接权值最小的那个结点
  • 然后,寻找这两个结点相连的所有结点,找到权值最小的连接
  • 重复上一步,直到所有结点都连接上。

时间复杂度:

记顶点数为 v,边数为 e,邻接矩阵的时间复杂度为 O(v2),邻接表的时间复杂度为 O(elog2v)

4)克鲁斯卡尔算法(Kruskal算法)

Kruskal 算法可以称为 “加边法”,初始最小生成树边数为 0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

  1. 把图中的所有边按代价从小到大排序;
  2. 把图中的 n 个顶点看成独立的 n 棵树组成的森林;
  3. 按权值从小到大选择边,所选的边连接的两个顶点 ui、vi 应属于两颗不同的树,则成为最小生成树的一条边,并将这两棵树合并作为一棵树。
  4. 重复(3),直到所有顶点都在一棵树内或者有 n-1 条边为止。

在这里插入图片描述

时间复杂度:

记边数为 e,克鲁斯卡尔算法(Kruskal算法)的时间复杂度为 elog2e

补充: Kruskal 算法和 Prim 算法、Boruvka 算法一样,都是贪婪算法的应用。不同点在于 Kruskal 算法在图中存在同样权值的边时也有效。

8.9 AOV 网

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为 AOV网(Activity On Vertext Network)

AOV 网中的弧表示活动之间存在的某种制约关系,同时 AOV 网中不能存在回路。

8.10 拓扑序列

设 G=(V, E) 是一个具有 n 个顶点的有向图,V 中的顶点序列 V1,V2,…,Vn 满足若从顶点序列 Vi 到 Vj 有一条路径,则在顶点序列中顶点 Vi 必在顶点 Vj 之前,则我们将这样的顶点序列成为一个 拓扑序列

所谓 拓扑排序,其实就是对一个有向图构造拓扑序列的过程。

对 AOV 网进行拓扑排序的基本思路是:

  1. 从 AOV 网中选择一个入度为 0 的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧;
  2. 继续重复上步操作,直到输出全部顶点,或者 AOV 网中不存在入度为 0 的顶点为止。

8.11 AOE 网

在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间。这种有向图的边表示活动的网,我们称之为 AOE 网(Activity On Edge Network)

  • 我们把 AOE 网中没有入边的顶点称为 始点源点
  • 没有出边的顶点称为 终点汇点。由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE 网只有一个源点一个汇点。
  • 我们把路径上各个活动所持续的时间之和称为 路径长度
  • 从源点到汇点具有最大长度的路径叫 关键路径
  • 在关键路径上的活动叫 关键活动

九、查找(Search)

9.1 二叉排序树(二叉查找树)

1)定义

二叉排序树(Binary Sort Tree),又称为 二叉查找树,它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的 左子树不为空,则左子树上所有结点的值均 小于它的根结点 的值;
  • 若它的 右子树不为空,则右子树上所有节点的值均 大于它的根节点 的值;
  • 它的 左、右子树也分别为二叉排序树
2)目的

构造一棵二叉排序树的目的,其实并不是为了排序,而是 为了提高查找和插入删除关键字的速度

不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集。

而二叉排序树这种非线性的结构,也有利于插入和删除的实现。

3)存储方式

二叉排序树是以 链接 的方式存储。

  • 执行插入或删除操作时,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点。
  • 查找操作时,二叉排序树走的就是从根节点到要查询结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。
4)最少查找次数、最多查找次数
  • 最少查找次数:极端情况下,查找次数最少为 1 此,即根节点就是要找的结点。
  • 最多查找次数:最多也不会超过树的深度。也就是说,二叉排序树的查询性能取决于二叉排序树的形状。
5)二叉排序树的形状问题
  • 关于二叉排序树的形状,很可惜的是,它是不确定的。比如极端的右斜树或左斜树。
  • 如果要解决二叉排序树的形状问题,解决方案就是让二叉排序树左右子树是比较平衡的,即其深度与完全二叉树相同,均为向下取整的 log_2*n + 1
  • 时间复杂度:O(logn)
6)平衡二叉树

平衡二叉树 是一种二叉排序树,近似二分查找,其中 每一个结点的左子树和右子树的高度差至多等于 1

  • 将二叉树上结点的左子树深度减去右子树深度的值称为 平衡因子BF
  • 平衡二叉树上所有节点的平衡因子只可能是 -101
  • 只要二叉树上有一个结点的平衡因子的绝对值大于 1,则该二叉树就是不平衡的。
7)最小不平衡子树

距离插入结点最近的,且平衡因子的绝对值大于 1 的结点为根的子树,我们称为 最小不平衡子树

8)平衡二叉树实现原理
  • 平衡二叉树构建的基本思想就是在构建二叉树排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性。
  • 若是,则找出最小不平衡树,在保持二叉树特性的前提下,调整最小不平衡子树中各个结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

9.2 多路查找树

对于树来说,一个结点只能存储一个元素。那么在元素非常多的时候,就会使得:

  • 要么树的度非常大(结点拥有子树的个数最大值);
  • 要么树的高度非常大,甚至两者都必须足够大才行。

这就使得内存存取外存次数非常多,这显然成了时间效率上的平静,这迫使我们要打破每一个结点只存储一个元素的限制,为此引入了 多路查找树 的概念:

  • 其每一个结点的孩子树可以多于两个,且每一个结点处可以存储多个元素。
  • 由于它是查找树,因此所有元素之间存在某种特定的排序关系(即其是有序的)。

常见的多路查找树有四种:2-3树、2-3-4树、B树、B+树。

1)2-3树

2-3树:每个结点都具有两个孩子(我们称它为 2 结点)或三个孩子(我们称它为 3 结点)的树。

  • 一个 2 结点 包含:一个元素、两个孩子(或没有孩子);
    • 左子树数据元素小于该元素;
    • 右子树数据元素大于该元素。
  • 一个 3 结点 包含:一小一大两个元素、三个孩子(或没有孩子);
    • 左子树数据元素小于较小元素;
    • 右子树数据元素大于较大元素;
    • 中间子树包含介于两元素之间的元素。

在这里插入图片描述

2)2-3-4 树

2-3-4树 是 2-3 树的扩展,在其基础上增加一个 4 结点的使用。

  • 一个 4 结点 包含:小、中、大三个元素、四个孩子(或没有孩子)。
    • 左子树元素小于最小元素;
    • 第二子树元素大于最小元素,小于第二元素;
    • 第三子树元素大于第二元素,小于最大元素;
    • 右子树元素大于最大元素。
3)B 树(B-tree)

B 树 是一种平衡的多路查找树。2-3 树和 2-3-4 树都是 B 树的特例。

结点最大的孩子数目称为 B 树的 阶(order)

  • 2-3 树为 3阶B树
  • 2-3-4 树为 4阶B树
4)B+ 树

B+ 树 是一种应文件系统所需而出的一种 B 树的变形树。

  • 在 B 树中,每一个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上。
  • 在 B+ 树中,出现在分支结点中的元素会被当做它们在该分支结点位置的中序后继者(叶子结点)中再次列出。

9.3 散列表(哈希表)

散列表(Hash table,也叫哈希表) 是一种根据关键码值(Key value)而直接进行访问的数据结构。

  • 它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做 散列函数,存放记录的数组叫做 散列表
  • 关键字对应的记录存储位置称为 散列地址

散列函数设计原则:

  • 计算简单: 散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
  • 散列地址分布均匀: 防止散列冲突最好的办法就是尽量让散列地址均匀地分布在存储空间中,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。

构造散列函数的六种方法: 直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。

处理散列冲突的四种方法: 开放定址法、再散列函数法、链地址法、公共溢出区法。

整理完毕,完结撒花~ 🌻





参考地址:

1.数据结构——学习笔记——入门必看【建议收藏】,https://blog.csdn.net/liu17234050/article/details/104237990

2.常见排序算法及对应的时间复杂度和空间复杂度,https://blog.csdn.net/gane_cheng/article/details/52652705

3.连通分量,https://www.ultipa.cn/document/ultipa-graph-analytics-algorithms/connected-component/v4.0

4.最小生成树(Kruskal算法),https://blog.csdn.net/qq_36932169/article/details/81236147

5.B+树看这一篇就够了(B+树查找、插入、删除全上),https://zhuanlan.zhihu.com/p/149287061

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不愿放下技术的小赵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值