软件设计师——03 数据结构(上)

1 线性结构

1.1 线性表

1.1.1 线性表的定义

  • 线性表是由 n n n 个元素组成的有限序列,具有以下特点:
    • 存在唯一一个被称为"第一个"的元素;

    • 存在唯一一个被称为"最后一个"的元素;

    • 除第一个元素外,集合中的每个元素均只有一个直接前驱;

    • 除最后一个元素外,集合中的每个元素均只有一个直接后继;

1.1.2 线性表的存储结构

  • 线性表的存储结构分为顺序存储链式存储

  • 顺序存储

    • 概念:用一组地址连续 的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素物理上也相邻

    • 优点:可以随机存取表中元素,读取、查找比较方便;

    • 缺点:必须按最大可能长度预分配存储空间,存储空间的利用率低,表的容量难以扩容,是一种静态存储结构;插入与删除需要移动大量元素 ,平均移动次数为 n / 2 n/2 n/2;

  • 链式存储

    • 概念:存储各元素的节点的地址并不要求是连续的,逻辑上相邻的数据元素,物理上不一定相邻。元素的结构包含数据域(存储数据)和指针域(存储下一个元素的地址);

    • 优点:插入与删除不需要移动元素 ,较为方便,只需要修改指针即可;存储空间不需要提前确定,可以动态改变,是一种动态存储结构;

    • 缺点:不能对数据元素进行随机访问;需要存储指针,有空间浪费存在。

1.1.3 单链表的插入和删除操作

  • 插入操作

    • 操作目的:将值为 x x x 的新节点 s s s 插入到单链表的第 i i i 个节点的位置上;

    • 操作步骤:先在单链表中找到第 i − 1 i - 1 i−1 个节点(设为 p p p),再在其后插入新节点;

    • 操作语句:

      c 复制代码
      s->next = p->next; // s 的指针指向 p 原本指向的 b 节点
      p->next = s; // p 的指针指向 s
  • 删除操作

    • 操作目的:删除单链表中的某个节点(以删除 p p p 节点后的节点为例);

    • 操作语句:

      c 复制代码
      p->next = p->next->next; // p 直接指向 b 的下一个节点(跳过 b)

1.1.4 练习

  • 对于线性表,相比顺序存储,采用链表存储的缺点是()。

    • A.数据元素之间的关系需要占用存储空间,导致存储密度不高
    • B.表中节点必须占用地址连续的存储单元,存储密度不高
    • C.插入新元素时需要遍历整个链表,运算的时间效率不高
    • D.删除元素时需要遍历整个链表,运算的时间效率不高

    A

    链表存储的缺点为数据元素之间的关系(也就是指针域)需要占用存储空间,导致存储密度不高。

1.2 栈和队列

1.2.1 栈

  • 是限定仅在表尾进行插入或删除操作的线性表 ,表尾称为栈顶 ,表头称为栈底,是一种**先进后出(LIFO)**的线性结构;

    有一个记录栈顶元素的栈顶指针;

  • 应用:括号匹配、迷宫求解和汉诺塔等场景;

1.2.2 队列

  • 队列是只允许在表的一端进行插入,在另一端进行删除操作的线性表 ,允许插入的一端称为队尾 ,允许删除的一端称为队头,是一种**先进先出(FIFO)**的线性结构;

  • 应用:操作系统中的作业排队等场景;

1.2.3 循环队列

  • 循环队列 是一种顺序表示的队列,用一组地址连续的存储单元依次存放从队头到队尾的元素。由于队列中队头和队尾的位置是动态变化的,要附设两个指针 frontrear,分别指示队头元素队尾元素在数组中的位置;

  • 具体操作如下(设循环队列Q的容量为M):

    容量为M,以下图为例,虽然从 0 起算,但是实际上M=5;

    • 初始状态 :初始队列为空,Q.frontQ.rear都等于0

    • 元素入队 :修改队尾指针 Q.rear = (Q.rear + 1) % M

      下图与此处操作不匹配,不要对照理解。下面给出步骤解释:

      • 第一个元素入队,(0 + 1) % 6 = 1,则第一个元素被放在位置 1,同时Q.rear指向 1;
      • 第二个元素入队,(1 + 1) % 6 = 2,则第二个元素被放在位置 2,同时Q.rear指向 2;
      • 第三个元素入队,(2 + 1) % 6 = 3,则第三个元素被放在位置 3,同时Q.rear指向 3;
      • 第四个元素入队,(3 + 1) % 6 = 4,则第四个元素被放在位置 4,同时Q.rear指向 4;
      • 第五个元素入队,(4 + 1) % 6 = 5,则第五个元素被放在位置 5,同时Q.rear指向 5;
      • 此时Q.rear的下一个位置就是Q.front,队列已满;
    • 元素出队 :修改队头指针 Q.front = (Q.front + 1) % M

      下图与此处操作不匹配,不要对照理解。下面给出步骤解释:

      • 第一个元素出队,(0 + 1) % 6 = 1,Q.front 指向 1,被放在位置 1 的元素出队;
      • 第二个元素出队,(1 + 1) % 6 = 2,Q.front 指向 2,被放在位置 2 的元素出队;
      • 第三个元素出队,(2 + 1) % 6 = 3,Q.front 指向 3,被放在位置 3 的元素出队;
      • 第四个元素出队,(3 + 1) % 6 = 4,Q.front 指向 4,被放在位置 4 的元素出队;
      • 第五个元素出队,(4 + 1) % 6 = 5,Q.front 指向 5,被放在位置 5 的元素出队;
      • 此时Q.front == Q.rear,队列已空;
    • 队空判断Q.front == Q.rear

    • 队满判断 :循环队列约定以"队尾指针所指位置的下一个位置是队头指针 "来表示"队列满"的情况,即 Q.front == (Q.rear + 1) % M

    • 队长计算(Q.rear - Q.front + M) % M

1.2.4 练习

  • 设栈初始时为空,对于入栈序列1,2,3,...,n,这些元素经过栈之后得到出栈序列 P 1 , P 2 , P 3 , . . . , P n P_1,P_2,P_3,...,P_n P1,P2,P3,...,Pn,若 P 3 = 4 P_3 = 4 P3=4,则 P 1 , P 2 P_1,P_2 P1,P2不可能的取值为()。

    • A.6,5
    • B.2,3
    • C.3,1
    • D.3,5

    C

    栈是限定仅在表尾进行插入或删除操作的线性表,表尾称为栈顶,表头称为栈底,是一种先进后出(LIFO)的线性结构。

  • 利用栈对算术表达式 10 ∗ ( 40 − 30 / 5 ) + 20 10*(40 - 30/5)+20 10∗(40−30/5)+20求值时,存放操作数的栈(初始为空)的容量至少为(),才能满足暂存该表达式中的运算数或运算结果的要求。

    • A.2
    • B.3
    • C.4
    • D.5

    C

    解析:在计算算术表达式的值时,通常需要使用两个栈:一个运算符栈 和一个操作数栈

    对于给定的表达式 10 ∗ ( 40 − 30 / 5 ) + 20 10*(40 - 30/5)+20 10∗(40−30/5)+20,可以按照以下步骤进行计算:

    • 将表达式转换为后缀(逆波兰)表达式: 10 40 30 5 / − ∗ 20 + 10\ 40\ 30\ 5\ /\ -\ *\ 20\ + 10 40 30 5 / − ∗ 20 +
    • 从左到右扫描表达式,遇到数字就将其压入操作数栈中,遇到运算符就从操作数栈中弹出相应的操作数进行计算,计算结果再压入操作数栈中。在这个过程中,需要使用一个运算符栈来保存运算符及其优先级。
    • 在扫描整个表达式后,操作数栈中所剩余的元素即为表达式的计算结果。
    • 因此,在扫描后缀表达式时,遇到 10 10 10、 40 40 40、 30 30 30、 5 5 5等数字都压入操作数栈,而当遇到/时才进行出栈操作。遇到第一个符号之前操作数栈中已有 4 个元素,之后进行运算再将结果重新压入操作数栈,不断计算得到最终结果。因此操作数栈的容量至少为 个元素,之后进行运算再将结果重新压入操作数栈,不断计算得到最终结果。因此操作数栈的容量至少为 个元素,之后进行运算再将结果重新压入操作数栈,不断计算得到最终结果。因此操作数栈的容量至少为 4 才能满足运算结果的要求。

1.3 串

1.3.1 基本概念

  • 是仅由字符构成的有限序列,是一种线性表,一般记为 s = ′ a 1 a 2 ⋯ a n ′ s = 'a_1a_2\cdots a_n' s=′a1a2⋯an′,其中, s s s 是串的名称,用单引号括起来的字符序列是串值。串中字符的个数 n n n 称为串的长度;

  • 串的基本概念包括:

    • 空串:长度为 0 的串称为空串,空串不包含任何字符;

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

    • 子串 :由串中任意长度的连续字符构成的序列称为子串。含有子串的串称为主串。空串是任意串的子串。

1.3.2 串的模式匹配算法

  • 串的模式匹配算法用于子串的定位操作,即查找子串在主串中第一次出现的位置;
1.3.2.1 BF算法
  • 布鲁特-福斯算法(BF算法 ):这是基本的模式匹配算法,其基本思想是从主串的第一个字符起与模式串的第 1 1 1 个字符比较,若相等,则继续逐个字符进行后续的比较;否则从主串的第 2 2 2 个字符起与模式串的第 1 1 1 个字符重新比较,直至模式串中每个字符依次和主串中的一个连续字符序列相等为止,此时称为匹配成功,否则称为匹配失败;

  • 如下图:

    • 当模式串的第 6 个字符与主串的第 6 个字符不匹配的时候,就要回溯两个串的指针;

    • 即重新从主串的第 2 个字符开始,与模式串的第 1 个字符开始重新匹配;

1.3.2.2 KMP算法
  • 4.2.2_1_KMP算法(新版)_哔哩哔哩_bilibili
  • KMP算法 是对基本匹配算法的改进,其改进之处在于:每当匹配过程中出现相比较的字符不等时,不再需要回溯主串的字符位置指针,而是利用已经得到"部分匹配"结果,将模式串向右"滑动"尽可能远的距离,再继续比较即可
来看一个场景
  1. 如果匹配到第 6 个字符才发现匹配失败,可以发现,主串第 1~5 个字符的内容是已知的,且和模式串是保持一致的;

  2. 根据 BF 算法的思想,上图主串中以第 1 个字符开始的子串匹配失败,那么要匹配主串的第 2 个字符开始的主串。此时我们已经知道该主串中的子串的前 4 个字符的信息,可以发现,第 1 个字符就已经不匹配了(如下图)。那么是不是可以直接跳过这个子串?

  3. 再来看主串中以第 3 个字符开始的子串,可以发现,第 2 个字符就不匹配了,所以再次跳过;

  4. 再来看主串中以第 4 个字符开始的子串,主串的第 4 和第 5 个字符是与模式串匹配的,而第 6 个字符与模式串的第 3 个字符是否匹配就不得而知了;

  5. 所以对于这种情况,只需要从下图 i 和 j 指针所指示的位置开始匹配即可;

再来捋一遍思路
  • 对于模式串 T 来说,将其与主串 S 进行字符匹配,当发现某一个字符(比如第 6 个字符)发生失配的时候,那么这个字符之前的主串信息,是可以确定的,且一定和主串保持一致;

  • 那么就没有必要检查主串中以第 2 和第 3 个位置开始的子串**(为什么?),而是可以直接对比以第 4 个位置开始的子串。而对于该子串来说,也没有必要对比其与模式串的第 1 个元素和第 2 个元素 (为什么?)**。现在不能确定的是:模式串的第 3 个元素与主串中失配的那个位置的元素能否匹配?所以:对于模式串abaabc,当第 6 个元素匹配失败的时候,可以令主串指针 i 不变(依然指向那个失配的元素),模式串指针 j=3;

    原因1:见上面的步骤二和步骤三;

    原因2:见上面的步骤四;

  • 这个过程是不是就是跳过了主串中两次子串的对比,且对于当前以第 4 个元素开始的子串,也跳过了该子串中前两个元素的对比。这就是 KMP 算法的思想,利用模式串匹配过程中隐藏的一些信息。

注意
  • 上面得出的结论,并不依赖于主串,只和模式串有关,和要匹配主串中的哪一个位置并没有关系,下面验证一下;

  • 从主串中以第 5 个字符开始的子串进行匹配,发现又是第 6 个位置的字符失配;

  • 根据结论,令主串指针 i 不变,模式串指针 j = 3,从模式串的第 3 个位置开始匹配;

如果是第 5 个元素匹配失败
  • 以上都是模式串的第 6 个元素与主串的第 6 个元素发生失配的情况,如果是第 5 个元素呢?

  • 假设此时第 5 个元素失配,那么主串中第 1~4 个位置的元素是可以知道其信息的;

  • 主串中以第 2 个字符开始的子串,第 1 个字符不匹配,跳过;

  • 主串中以第 3 个字符开始的子串,第 2 个字符不匹配,跳过;

  • 再看主串中以第 4 个字符开始的子串,该子串中仅已知的第 1 个字符与模式串中的第 1 个字符匹配成功。而该子串后续的内容是否与模式串匹配就不得而知了,所以就可以从该子串的第 2 个位置开始匹配。那么对于当第 5 个元素匹配失败的时候,可以令主串指针 i 不变(依然指向那个失配的元素),模式串指针 j = 2;

其它情况
  • 对于当第 4 个元素匹配失败的时候,可以令主串指针 i 不变(依然指向那个失配的元素),模式串指针 j = 2;

  • 对于当第 3 个元素匹配失败的时候,可以令主串指针 i 不变(依然指向那个失配的元素),模式串指针 j = 1;

  • 对于当第 2 个元素匹配失败的时候,可以令主串指针 i 不变(依然指向那个失配的元素),模式串指针 j = 1;

  • 对于当第 1 个元素匹配失败的时候,直接匹配下一个相邻子串;

    从代码的角度怎么处理这种情况呢?让 j = 0,二者再同时++

结论
  • 下面完整讲解一遍流程;

  • 主串中以第 1~5 个字符开始的子串,将其与模式串的第 1~5 个字符依次对应匹配,均相等。当匹配第 6 个字符的时候,发现失配。按照 BF 算法(即下图中的朴素模式匹配算法),需要将主串的指针回溯到第 2 个字符,然后将主串中以第 2 个字符开始的子串,与模式串重新匹配;

  • 但是如果使用 KMP 算法,让主串的指针 i 不变,模式串指针 j = 3。让主串中的子串的第 3 个元素与模式串的第 3 个元素开始依次匹配;

  • 可以发现,均匹配成功,算法结束。与 BF 算法对比,KMP 算法极大地优化了字符匹配的速度;

    可以尝试着将主串中的第 5 个元素的值由 b 改成 c,再推导一遍过程。

next 数组
  • 以上我们在字符匹配开始前,得到的结论,可以用一个数组来表示,即next 数组

  • next 数组:当模式串第 j 个字符与主串失配时,模式串中与主串继续匹配的位置;

KMP 算法的过程
  • 根据模式串,求出 next 数组,利用 next 数组进行匹配(主串指针不回溯);

1.3.2.3 求 next 数组
  • next 数组的作用:当模式串的第 j 个字符失配时,从模式串的第 next[j] 个字符继续往后匹配(即指针 j 应该指向哪);

  • 对于模式串google,求 next[1]:

  • 对于模式串google,求 next[2]:

  • 对于模式串google,求 next[3]:

  • 对于模式串google,求 next[4]:

  • 求后续的 next[5] 和 next[6] 也是同样的方式。

1.3.3 练习

  • 在字符串的KMP算法中,需先求解模式串的next函数值,其定义如下式所示,j表示模式串中字符的序号(从1开始)。若模式串p为"abaac",则其next函数值为()。
    n e x t [ j ] = { 0 , j = 1 m a x { k ∣ 1 < k < j , p 1 p 2 p k − 1 = p j − k + 1 p j − k + 2 p j − 1 } , 当集合不为空时 1 , 其他情况 next[j]= \begin{cases} 0, & j = 1 \\ max\{k|1 < k < j, p_1p_2p_{k - 1} = p_{j - k + 1}p_{j - k + 2}p_{j - 1}\}, & \text{当集合不为空时} \\ 1, & \text{其他情况} \end{cases} next[j]=⎩ ⎨ ⎧0,max{k∣1<k<j,p1p2pk−1=pj−k+1pj−k+2pj−1},1,j=1当集合不为空时其他情况

    • A.01234
    • B.01122
    • C.01211
    • D.01111

    B

    next[1] 无脑写0;

    next[2] 无脑写1;

    求 next[3]:

    c 复制代码
          i
    a b | × × × // 主串
    a b | a a c // 模式串
      a | b a a c // 划线后将模式串右移
        | a b a a c
          j // 1

    求 next[4]:

    c 复制代码
            i
    a b a | × × // 主串
    a b a | a c // 模式串
      a b | a a c // 划线后将模式串右移
        a | b a a c j // 2

    求 next[5]:

    c 复制代码
              i
    a b a a | × // 主串
    a b a a | c // 模式串
      a b a | a c // 划线后将模式串右移
        a b | a a c j
          a | b a a c j // 2

2 数组、矩阵和广义表

2.1 数组

数组类型 存储地址计算
一维数组a[n] a[i]的存储地址为a+i*len
二维数组a[m][n] a[i][j]的存储地址为 a + ( i ∗ n + j ) ∗ l e n a+(i*n+j)*len a+(i∗n+j)∗len(按行存储); a[i][j]的存储地址为 a + ( j ∗ m + i ) ∗ l e n a+(j*m+i)*len a+(j∗m+i)∗len(按列存储)
  • 数组存储地址计算的基本公式 :数组元素的存储地址计算基于基地址 (数组起始存储地址,用 a 表示)、元素下标每个元素占用的字节数 (用 len 表示)。

2.1.1 一维数组

  • 一维数组的存储地址计算 :对于一维数组 a[n],数组元素 a[i]i 为下标,从 0 开始)的存储地址计算公式为:
    地址 = a + i × len \text{地址} = a + i \times \text{len} 地址=a+i×len

    • 因为一维数组元素是连续存储 的,第 i 个元素相对于起始地址 a,偏移了 i 个元素的长度,每个元素占 len 字节,所以总偏移量是 i × len,加上基地址 a 就是 a[i] 的存储地址。

2.1.2 二维数组的存储地址计算

  • 二维数组有按行存储 (行优先)和按列存储(列优先)两种方式,对应不同的计算公式;

  • 按行存储 :对于二维数组 a[m][n]m 行,n 列),数组元素 a[i][j]i 为行下标,j 为列下标,均从 0 开始)的存储地址计算公式为
    地址 = a + ( i × n + j ) × len \text{地址} = a + (i \times n + j) \times \text{len} 地址=a+(i×n+j)×len

    • 按行存储时,先存第 0 行,再存第 1 行......第 i 行之前已经存了 i 行,每行有 n 个元素,所以前 i 行总共有 i × n 个元素;
    • i 行中,a[i][j] 是第 j 个元素,因此相对于起始地址 a,总偏移量是 (i × n + j) 个元素的长度,乘以 len 后加上基地址 a 得到存储地址;
  • 按列存储 :对于二维数组 a[m][n],数组元素 a[i][j] 的存储地址计算公式为
    地址 = a + ( j × m + i ) × len \text{地址} = a + (j \times m + i) \times \text{len} 地址=a+(j×m+i)×len

    • 按列存储时,先存第 0 列,再存第 1 列......第 j 列之前已经存了 j 列,每列有 m 个元素,所以前 j 列总共有 j × m 个元素;
    • j 列中,a[i][j] 是第 i 个元素,因此相对于起始地址 a,总偏移量是 (j × m + i) 个元素的长度,乘以 len 后加上基地址 a 得到存储地址。

2.1.3 练习

  • 已知 6 行 8 列的二维数组 a,各元素占 2 个字节,求 a[2][5] 按行优先存储的存储地址?

    数组维度:m = 6(行),n = 8(列)

    元素下标:i = 2(行下标),j = 5(列下标)

    每个元素长度:len = 2 字节

    基地址:题目未给出,假设基地址为 a

    计算:
    地址 = a + ( i × n + j ) × len = a + ( 2 × 8 + 5 ) × 2 = a + ( 16 + 5 ) × 2 = a + 21 × 2 = a + 42 \begin{align*} \text{地址} &= a + (i \times n + j) \times \text{len} \\ &= a + (2 \times 8 + 5) \times 2 \\ &= a + (16 + 5) \times 2 \\ &= a + 21 \times 2 \\ &= a + 42 \end{align*} 地址=a+(i×n+j)×len=a+(2×8+5)×2=a+(16+5)×2=a+21×2=a+42

  • 对于二维数组 a [ 1.. N , 1.. N ] a[1..N,1..N] a[1..N,1..N]中的一个元素 a [ i , j ] ( 1 ≤ i , 1 ≤ j ) a[i,j](1≤i,1≤j) a[i,j](1≤i,1≤j),存储在 a [ i , j ] a[i,j] a[i,j]之前的元素个数()。

    • A.与按行存储和按列存储无关
    • B.在 i = j i = j i=j时与按行存储或按列存储方式无关
    • C.在按行存储方式下比按列存储方式下要多
    • D.在按行存储方式下比按列存储方式下要少

    B

    若按行存储:存储在 a [ i , j ] a[i,j] a[i,j]之前的元素个数有 i ∗ N + j i*N+j i∗N+j个

    若按列存储:存储在 a [ i , j ] a[i,j] a[i,j]之前的元素个数有 j ∗ N + i j*N+i j∗N+i个

    对于B选项,当 i = j 时, i ∗ N + j = j ∗ N + i i*N+j = j*N+i i∗N+j=j∗N+i

2.2 矩阵

2.2.1 特殊矩阵

2.2.1.1 对称矩阵
  • 定义 :对于 n n n阶矩阵 A A A,若其中的元素满足 a i j = a j i a_{ij} = a_{ji} aij=aji(其中 1 ≤ i , j ≤ n 1\leq i,j\leq n 1≤i,j≤n),则称该矩阵为 n n n阶对称矩阵。这意味着矩阵关于主对角线(左上到右下)对称,主对角线两侧对称位置上的元素相等;

  • 例:

    • 二阶对称矩阵

      1 2 2 3 \] \\begin{bmatrix} 1 \& 2 \\\\ 2 \& 3 \\end{bmatrix} \[1223

    • 三阶对称矩阵

      5 4 3 4 6 2 3 2 7 \] \\begin{bmatrix} 5 \& 4 \& 3 \\\\ 4 \& 6 \& 2 \\\\ 3 \& 2 \& 7 \\end{bmatrix} 543462327

      1 0 − 1 2 0 3 4 0 − 1 4 5 − 3 2 0 − 3 6 \] \\begin{bmatrix} 1 \& 0 \& -1 \& 2 \\\\ 0 \& 3 \& 4 \& 0 \\\\ -1 \& 4 \& 5 \& -3 \\\\ 2 \& 0 \& -3 \& 6 \\end{bmatrix} 10−120340−145−320−36

    • 当 i ≥ j i\geq j i≥j时, k = i ( i − 1 ) 2 + j − 1 k=\frac{i(i - 1)}{2}+j - 1 k=2i(i−1)+j−1;

    • 当 i < j i\lt j i<j时, k = j ( j − 1 ) 2 + i − 1 k=\frac{j(j - 1)}{2}+i - 1 k=2j(j−1)+i−1。通过这样的映射关系,能将二维的矩阵元素存储到一维数组中,节省存储空间;

    以上面的四阶对称矩阵为例,那么用于存储的一维数组长度为 n ( n + 1 ) / 2 = 4 × ( 4 + 1 ) / 2 = 10 n(n + 1)/2=4\times(4 + 1)/2 = 10 n(n+1)/2=4×(4+1)/2=10;

    例: a 21 a_{21} a21

    • i = 2 i = 2 i=2, j = 1 j = 1 j=1, i ≥ j i\geq j i≥j
    • 根据公式 k = i ( i − 1 ) 2 + j − 1 k=\frac{i(i - 1)}{2}+j - 1 k=2i(i−1)+j−1,将 i = 2 i = 2 i=2, j = 1 j = 1 j=1 代入可得: k = 2 × ( 2 − 1 ) 2 + 1 − 1 = 2 × 1 2 + 0 = 1 + 0 = 1 k=\frac{2\times(2 - 1)}{2}+1 - 1=\frac{2\times1}{2}+0 = 1 + 0=1 k=22×(2−1)+1−1=22×1+0=1+0=1
    • 这意味着矩阵元素 a 21 a_{21} a21 会被存储在一维数组 s a sa sa 的第 1 1 1 个位置(数组下标从 0 开始的话,就是 s a [ 1 ] sa[1] sa[1])

    例: a 12 a_{12} a12

    • i = 1 i = 1 i=1, j = 2 j = 2 j=2, i < j i\lt j i<j;

    • 根据公式 k = j ( j − 1 ) 2 + i − 1 k=\frac{j(j - 1)}{2}+i - 1 k=2j(j−1)+i−1,将 j = 2 j = 2 j=2, i = 1 i = 1 i=1 代入可得: k = 2 × ( 2 − 1 ) 2 + 1 − 1 = 2 × 1 2 + 0 = 1 + 0 = 1 k=\frac{2\times(2 - 1)}{2}+1 - 1=\frac{2\times1}{2}+0 = 1 + 0 = 1 k=22×(2−1)+1−1=22×1+0=1+0=1

    • 这表明矩阵元素 a 12 a_{12} a12 也会被存储在一维数组 s a sa sa 的第 1 1 1 个位置(即 s a [ 1 ] sa[1] sa[1])

    a i j a_{ij} aij 和 a j i a_{ji} aji( i ≠ j i\neq j i=j)在一维数组中共享同一个存储位置,从而节省了存储空间

2.2.1.2 三角矩阵
  • 定义 :矩阵的上三角(主对角线以上的部分,不包括对角线)或下三角(主对角线以下的部分,不包括对角线)中的元素均为常数 c c c或零的 n n n阶矩阵;

  • 上三角矩阵 :以上三角矩阵 A n × n A_{n\times n} An×n为例,其主对角线以下(不包括对角线)的元素都为 0 ,主对角线及其以上的元素可以是任意值。矩阵形式为:

    a 00 a 01 ⋯ a 0 , n − 2 a 0 , n − 1 0 a 11 ⋯ a 1 , n − 2 a 1 , n − 1 ⋯ ⋯ ⋯ ⋯ ⋯ 0 0 ⋯ a n − 2 , n − 2 a n − 2 , n − 1 0 0 ⋯ 0 a n − 1 , n − 1 \] \\begin{bmatrix}a_{00}\&a_{01}\&\\cdots\&a_{0,n - 2}\&a_{0,n - 1}\\\\0\&a_{11}\&\\cdots\&a_{1,n - 2}\&a_{1,n - 1}\\\\\\cdots\&\\cdots\&\\cdots\&\\cdots\&\\cdots\\\\0\&0\&\\cdots\&a_{n - 2,n - 2}\&a_{n - 2,n - 1}\\\\0\&0\&\\cdots\&0\&a_{n - 1,n - 1}\\end{bmatrix} a000⋯00a01a11⋯00⋯⋯⋯⋯⋯a0,n−2a1,n−2⋯an−2,n−20a0,n−1a1,n−1⋯an−2,n−1an−1,n−1

    a 00 0 ⋯ 0 0 a 10 a 11 ⋯ 0 0 ⋯ ⋯ ⋯ ⋯ ⋯ a n − 2 , 0 a n − 2 , 1 ⋯ a n − 2 , n − 2 0 a n − 1 , 0 a n − 1 , 1 ⋯ a n − 1 , n − 2 a n − 1 , n − 1 \] \\begin{bmatrix}a_{00}\&0\&\\cdots\&0\&0\\\\a_{10}\&a_{11}\&\\cdots\&0\&0\\\\\\cdots\&\\cdots\&\\cdots\&\\cdots\&\\cdots\\\\a_{n - 2,0}\&a_{n - 2,1}\&\\cdots\&a_{n - 2,n - 2}\&0\\\\a_{n - 1,0}\&a_{n - 1,1}\&\\cdots\&a_{n - 1,n - 2}\&a_{n - 1,n - 1}\\end{bmatrix} a00a10⋯an−2,0an−1,00a11⋯an−2,1an−1,1⋯⋯⋯⋯⋯00⋯an−2,n−2an−1,n−200⋯0an−1,n−1

  • 定义 :矩阵中的非零元素都集中在以主对角线为中心的带状区域内

  • 例:
    A n × n = [ a 00 a 01 □ □ □ a 10 a 11 a 12 □ □ ⋯ a 21 a 22 a 23 □ □ □ a 32 a L i a n − 2 , n − 1 □ □ ⋯ a n − 1 , n − 2 a n − 1 , n − 1 ] A_{n\times n}=\begin{bmatrix}a_{00}&a_{01}&\square&\square&\square\\a_{10}&a_{11}&a_{12}&\square&\square\\\cdots&a_{21}&a_{22}&a_{23}&\square\\\square&\square&a_{32}&a_{Li}&a_{n - 2,n - 1}\\\square&\square&\cdots&a_{n - 1,n - 2}&a_{n - 1,n - 1}\end{bmatrix} An×n= a00a10⋯□□a01a11a21□□□a12a22a32⋯□□a23aLian−1,n−2□□□an−2,n−1an−1,n−1

  • 可以看出,非零元素分布在主对角线附近的一个带状区域里,远离这个带状区域的元素大多为零。

2.2.2 稀疏矩阵

  • 在一个矩阵中,若非零元素的个数远远小于零元素的个数 ,并且非零元素的分布没有规律,这样的矩阵就称为稀疏矩阵 。一般来说,当非零元素的个数小于矩阵元素总个数的 5 % 5\% 5%时,就可以称该矩阵为稀疏矩阵;

  • 例:

    0 0 3 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 − 2 0 0 0 0 0 0 \] \\begin{bmatrix} 0 \& 0 \& 3 \& 0 \& 0 \& 0 \\\\ 0 \& 0 \& 0 \& 0 \& 0 \& 0 \\\\ 0 \& 1 \& 0 \& 0 \& 0 \& 0 \\\\ 0 \& 0 \& 0 \& 0 \& 0 \& -2 \\\\ 0 \& 0 \& 0 \& 0 \& 0 \& 0 \\end{bmatrix} 0000000100300000000000000000−20

    • 三元组:使用三个域来存储每个非零元素,这三个域分别是行(记录非零元素所在的行号)、列(记录非零元素所在的列号)、值(记录非零元素的具体数值)。通过这种方式,只需要存储所有非零元素的行、列、值信息,就能表示整个稀疏矩阵,极大地节省了存储空间;

      行号(row) 列号(col) 值(value)
      1 3 3
      3 2 1
      4 6 -2

    这里行号和列号从 1 1 1 开始计数(实际编程中也可以从 0 0 0 开始计数,取决于具体实现 );

    • 十字链表 :对于每个非零元素,会有 5 5 5个域,分别是行(非零元素的行号)、列(非零元素的列号)、值(非零元素的数值)、指向同行下一个非零元素的指针right、指向同列下一个非零元素的指针down。这种存储方式不仅存储了非零元素的基本信息,还通过指针建立了同行和同列非零元素之间的联系,方便在对稀疏矩阵进行诸如查找、插入、删除等操作时,快速定位到相关的非零元素;

    这里行号和列号从 1 1 1 开始计数(实际编程中也可以从 0 0 0 开始计数,取决于具体实现 )。

2.2.3 练习

  • 设某 n n n阶三对角矩阵 A n × n A_{n\times n} An×n的示意图如下所示。若将该三对角矩阵的非零元素按行存储在一维数组 B [ k ] ( 1 ≤ k ≤ 3 ∗ n − 2 ) B[k](1\leq k\leq3*n - 2) B[k](1≤k≤3∗n−2)中,则 k k k与 i i i, j 的对应关系是()。
    A n × n = [ a 11 a 12 ⋯ □ □ a 21 a 22 a 23 □ □ ⋯ a 32 a 33 a 34 □ □ □ a i , 3 a i i a n − 1 , n □ □ ⋯ a n , n − 1 a n , n ] A_{n\times n}=\begin{bmatrix} a_{11}&a_{12}&\cdots&\square&\square\\ a_{21}&a_{22}&a_{23}&\square&\square\\ \cdots&a_{32}&a_{33}&a_{34}&\square\\ \square&\square&a_{i,3}&a_{ii}&a_{n - 1,n}\\ \square&\square&\cdots&a_{n,n - 1}&a_{n,n} \end{bmatrix} An×n= a11a21⋯□□a12a22a32□□⋯a23a33ai,3⋯□□a34aiian,n−1□□□an−1,nan,n

    • A. k = 2 i + j − 2 k = 2i + j - 2 k=2i+j−2

      • B. k = 2 i − j + 2 k = 2i - j + 2 k=2i−j+2

      • C. k = 3 i + j − 1 k = 3i + j - 1 k=3i+j−1

      • D. k = 3 i − j + 2 k = 3i - j + 2 k=3i−j+2

    A

    特殊值代入法,取 k = 1 k = 1 k=1,得到第一个元素,对应矩阵中的 a 11 a_{11} a11,因此将 i = 1 i = 1 i=1, j = 1 j = 1 j=1代入所给答案选项中,只有选项A的值为 k = 1 k = 1 k=1,因此答案选A。

2.3 广义表

  • 广义表是线性表的推广,是由0个或多个单元素或子表组成的有限序列;

  • 广义表与线性表的区别:

    • 线性表的元素都是结构上不可分的单元素;
    • 而广义表的元素既可以是单元素,也可以是有结构的表,甚至可以包含其本身;
  • 广义表的一般形式
    L S = ( a 1 , a 2 , a 3 , ⋯   , a n ) LS = (a_1, a_2, a_3, \cdots, a_n) LS=(a1,a2,a3,⋯,an)

    • L S LS LS是表名;

    • a_i 是表元素,它可以是表(称为子表),也可以是数据元素(称为原子);

    • n n n是广义表的长度,即最外层包含的元素个数, n = 0 n = 0 n=0 时广义表称为空表;

    • 广义表的深度通过递归定义,是定义中所包含括号的重数(单边括号的个数),原子的深度为0,空表的深度为1;

  • head()tail()操作

    • head():取表头,即广义表的第一个表元素,这个元素可以是子表,也可以是单元素;

    • tail():取表尾,即广义表中除了第一个表元素之外的其他所有表元素构成的表,需要注意的是,非空广义表的表尾必定是一个表,即使表尾看起来是单元素。

3 树

3.1 树与二叉树的定义

3.1.1 树的定义

  • 树是一种非线性结构,树中的每个数据元素可拥有两个或两个以上的直接后继元素,用于描述层次结构关系。树是 n ( n ≥ 0 ) n(n \geq 0) n(n≥0) 个节点的有限集合:

    • 当 n = 0 n = 0 n=0 时,称为空树;

    • 对于任意一棵非空树( n > 0 n > 0 n>0),有且仅有一个根节点;

    • 其余节点可分为 m ( m ≥ 0 ) m(m \geq 0) m(m≥0) 个互不相交的有限子集 t 1 , t 2 , ⋯   , t m t_1,t_2,\cdots,t_m t1,t2,⋯,tm,每个 t i t_i ti 都是一棵树,且是根节点的子树;

    当节点数量 n = 0 n = 0 n=0 时是空树 ,而此树 n\>0 ,所以是非空树 ,且有且仅有一个根节点,即节点 A A A。

    除根节点 A A A 外,其余节点可分为 m ( m ≥ 0 ) m(m \geq 0) m(m≥0) 个互不相交的有限子集,每个子集都是一棵树,且是根节点的子树。在这棵树中,其余节点分为3个互不相交的有限子集:

    • 第一个子集是以 B 为根节点的子树,该子树包含节点 B 、 、 、 E 、 、 、 F
    • 第二个子集是以 C 为根节点的子树,该子树包含节点 C 、 、 、 G
    • 第三个子集是以 D 为根节点的子树,该子树包含节点 D 、 、 、 H

3.2.2 树的基本概念

  • 双亲、孩子和兄弟:节点子树的根是该节点的孩子节点;相应地,该节点是其孩子节点的双亲。具有相同双亲的节点互为兄弟;

    • B、C、D是A的孩子节点。对应的,A是B、C、D的双亲节点(父节点);
    • B、C、D的双亲节点都是A,所以他们互为兄弟节点;
  • 节点的度:一个节点拥有子树的个数就是该节点的度;

    • A的度为3,B的度为2,C的度为1,D的度为1;
  • 叶子节点:度为0的节点,也叫终端节点;

    • E、F、G、H都是叶子节点;
  • 节点的层次:A在第一层,B、C、D在第二层,E、F、G、H在第三层;

  • 树的深度:树的最大层数就是树的深度(或高度);

    • 上图中树的深度为3;
  • 有序/无序树:若树中各节点的子树从左到右有序排列且不可交换,就是有序树,否则为无序树。

3.2.3 二叉树的定义

  • 二叉树是 n n n 个节点的有限集合,它要么是空树,要么由一个根节点和两棵互不相交、分别称为左子树右子树 的二叉树组成。和树的区别在于,二叉树每个节点的度最大为2

3.2.4 补充

  • 满二叉树:在一棵二叉树中,若所有非叶子节点都同时有左孩子和右孩子 ,且所有叶子节点都在同一层上,即深度为 k k k 且含有 2\^k - 1 个节点的二叉树,称为满二叉树;

  • 完全二叉树:一棵深度为 k k k、有 n n n 个节点的二叉树,按从上至下、从左到右 的顺序对节点编号。若编号为 i(1 \\leq i \\leq n) 的节点,与满二叉树中编号为 i i i 的节点在二叉树中的位置相同,这棵二叉树就是完全二叉树;

    • 比如下图,对满二叉树和完全二叉树的每一个节点进行编号,完全二叉树与满二叉树对应编号的节点值是相同的;

3.2 二叉树的性质与存储结构

3.2.1 二叉树的性质

  • 第 i i i 层节点数 :在二叉树的第 i i i 层上最多有 2\^{i - 1} 个节点( i ≥ 1 i \geq 1 i≥1);

    • 第1层( i = 1 i = 1 i=1):最多有 2 1 − 1 = 1 2^{1 - 1}=1 21−1=1 个节点,上图中第1层只有节点 A A A;
    • 第2层( i = 2 i = 2 i=2):最多有 2 2 − 1 = 2 2^{2 - 1}=2 22−1=2 个节点,上图中第2层有 B B B、 C C C 两个节点;
    • 第3层( i = 3 ):最多有 2 3 − 1 = 4 2^{3 - 1}=4 23−1=4 个节点,上图中第3层有 D D D、 E E E、 F F F、 G G G 四个节点;
  • 深度为 k k k 的节点总数 :深度为 k k k 的二叉树最多有 2 k − 1 2^{k} - 1 2k−1 个节点( k ≥ 1 k \geq 1 k≥1);

    上图中二叉树深度 k = 3 k = 3 k=3,最多应有 2\^{3} - 1 = 7 个节点,而上图中确实也就有 A A A、 B B B、 C C C、 D D D、 E E E、 F F F、 G G G 共7个节点;

    • 如果刚好有最多个节点,也是一棵满二叉树;
  • 叶子节点与度为2的节点关系 :对于任何一棵二叉树,若其叶子节点数为 n 0 n_0 n0,度为2的节点数为 n 2 n_2 n2,则有 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1;

    • 上图中叶子节点是 D D D、 E E E、 F F F、 G G G,所以 n 0 = 4 n_0 = 4 n0=4;
    • 度为2的节点是 A A A、 B B B、 C C C,所以 n 2 = 3 n_2 = 3 n2=3;
    • 此时 4 = 3 + 1 4 = 3 + 1 4=3+1,符合 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1 的关系;
  • 完全二叉树的深度 :具有 n n n 个节点的完全二叉树的深度为 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 ⌊log2n⌋+1;

    上图中的树有 7 个节点,即 n = 7 n = 7 n=7,则 ⌊ log ⁡ 2 7 ⌋ + 1 = ⌊ 2.807 ⌋ + 1 = 2 + 1 = 3 \lfloor \log_2 7 \rfloor + 1 = \lfloor 2.807 \rfloor + 1 = 2 + 1 = 3 ⌊log27⌋+1=⌊2.807⌋+1=2+1=3,与深度为3一致;

  • 完全二叉树的层序编号性质

    • 若对有 n n n 个节点的完全二叉树的节点按层序编号(从第1层到 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 ⌊log2n⌋+1 层,每层从左到右),对于任一节点 i ( 1 ≤ i ≤ n ) i(1 \leq i \leq n) i(1≤i≤n):
      • 若 i = 1 i = 1 i=1,该节点是二叉树的根,无双亲;若 i > 1 i > 1 i>1,该节点的双亲是 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋;

        比如节点 B B B, i = 2 i = 2 i=2,双亲是 ⌊ 2 / 2 ⌋ = 1 \lfloor 2/2 \rfloor = 1 ⌊2/2⌋=1,即节点 A A A,符合;节点 C C C, i = 3 i = 3 i=3,双亲是 ⌊ 3 / 2 ⌋ = 1 \lfloor 3/2 \rfloor = 1 ⌊3/2⌋=1,即节点 A A A;

      • 若 2 i ≤ n 2i \leq n 2i≤n,该节点左子树的编号是 2 i 2i 2i;否则,无左子树;

        节点 A A A, i = 1 i = 1 i=1, 2 × 1 = 2 ≤ 7 2×1 = 2 \leq 7 2×1=2≤7,左子树编号是 2 2 2(即节点 B B B),符合;节点 B B B, i = 2 i = 2 i=2, 2 × 2 = 4 ≤ 7 2×2 = 4 \leq 7 2×2=4≤7,左子树编号是 4 (即节点 D D D);

      • 若 2 i + 1 ≤ n 2i + 1 \leq n 2i+1≤n,该节点右子树的编号是 2 i + 1 2i + 1 2i+1;否则,无右子树;

        节点 A A A, i = 1 i = 1 i=1, 2 × 1 + 1 = 3 ≤ 7 2×1 + 1 = 3 \leq 7 2×1+1=3≤7,右子树编号是 3 3 3(即节点 C C C),符合;节点 B B B, i = 2 i = 2 i=2, 2 × 2 + 1 = 5 ≤ 7 2×2 + 1 = 5 \leq 7 2×2+1=5≤7,右子树编号是 5 5 5(即节点 E E E);

  • 节点总数 N = 所有节点的度之和 + 1;

  • 节点总数 N = 分支总数 M + 1;

  • 练习:已知树 T T T的度为 4 4 4,且度为 4 4 4的节点数为 7 7 7个、度为 3 3 3的节点数 5 5 5个、度为 2 2 2的节点数为 8 8 8个、度为 1 1 1的节点数为 10 10 10个,那么 T T T的叶子节点个数为()。

    • A. 30 30 30
    • B. 35 35 35
    • C. 40 40 40
    • D. 49 49 49

    C

    设叶子节点(度为 0 0 0的节点)个数为 n 0 n_0 n0。根据树的性质:树中所有节点的度之和等于节点数减 1 1 1

    • 节点总数 N = n 0 + 10 + 8 + 5 + 7 = n 0 + 30 N = n_0 + 10 + 8 + 5 + 7 = n_0 + 30 N=n0+10+8+5+7=n0+30

    • 分支总数 M = 1 × 10 + 2 × 8 + 3 × 5 + 4 × 7 = 69 M=1\times10 + 2\times8 + 3\times5 + 4\times7=69 M=1×10+2×8+3×5+4×7=69
      N = M + 1 n 0 + 30 = 69 + 1 n 0 = 40 N = M + 1 \\ n_0 + 30 = 69 + 1 \\ n_0 = 40 N=M+1n0+30=69+1n0=40

3.2.2 二叉树的存储结构

  • 顺序存储:完全二叉树采用顺序存储时相比一般二叉树更节省空间。因为一般二叉树需要添加一些"虚节点"来保证存储结构的规律性,从而造成空间浪费,而完全二叉树结构规整,无需或只需少量"虚节点";

  • 链式存储 :通常用二叉链表 来存储二叉树节点。二叉链表中,每个节点除了存储自身数据外,还存储左孩子节点的指针右孩子节点的指针 ,即一个数据 + 两个指针。每个二叉链表节点对应存储一个二叉树节点,头指针指向根节点;

3.3 二叉树的遍历

  • 遍历是按某种策略访问树中的每个节点且仅访问一次的过程;

  • 二叉树的遍历形式有以下四种,以下图为例:

  • 前(先)序遍历

    • 遍历顺序:根左右,即先访问根节点,再访问左子树,最后访问右子树;

    • 示例:对于图中的二叉树,前序遍历结果为 a b d e g h c f abdeghcf abdeghcf;

  • 中序遍历

    • 遍历顺序:左根右,即先访问左子树,再访问根节点,最后访问右子树。

    • 示例:图中二叉树的中序遍历结果为 d b g h e a c f dbgheacf dbgheacf;

  • 后序遍历

    • 遍历顺序:左右根,即先访问左子树,再访问右子树,最后访问根节点。

    • 示例:图中二叉树的后序遍历结果为 d h g e b f c a dhgebfca dhgebfca;

  • 层次遍历

    • 遍历顺序:按层次,从上到下,从左到右,即从二叉树的第一层(根节点所在层)开始,依次向下,每层从左到右访问节点;

    • 示例:图中二叉树的层次遍历结果为 a b c d e f g h abcdefgh abcdefgh。

3.4 线索二叉树

  • 引入线索二叉树是为了保存二叉树遍历时节点的前驱节点和后继节点的信息。因为二叉树的链式存储只能获取到某节点的左孩子和右孩子节点,无法直接获取其遍历时的前驱和后继节点。因此可以在链式存储中再增加两个指针域,使其分别指向前驱和后继节点,但这样太浪费空间,可以考虑以下实现方法:

    • 若有 n n n个节点的二叉树使用二叉链表存储,必然有 n + 1 n + 1 n+1个空指针域。可以利用这些空指针域来存储节点的前驱和后继节点信息;

      在二叉链表中,每个节点有两个指针域lchild(指向左孩子)和 rchild(指向右孩子)。因此, n n n 个节点的二叉链表,总共有 2 n 2n 2n 个指针域;

      对于一棵二叉树,除根节点外,每个节点都有且仅有一个"父节点" ,而父节点通过 lchildrchild 指针指向该节点。因此:

      • 根节点没有父节点,不需要被其他指针指向;

      • 其余 n − 1 n - 1 n−1 个节点,每个都被一个指针(父节点的 lchildrchild)指向,即有 n − 1 n - 1 n−1 个非空的指针域;

      • 总指针域数量( 2 n 2n 2n)减去非空指针域数量( n − 1 n - 1 n−1),就是空指针域的数量:

      空指针域数量 = 2 n − ( n − 1 ) = n + 1 \text{空指针域数量} = 2n - (n - 1) = n + 1 空指针域数量=2n−(n−1)=n+1

    • 为了区分指针域存放的是孩子节点还是遍历节点(前驱或后继节点),需要增加两个标志( l t a g ltag ltag和 r t a g rtag rtag);

  • 若二叉树的二叉链表采用上述带有标志的节点结构,则成为线索链表。其中指向前驱、后继节点的指针称为线索,加上线索的二叉树称为线索二叉树

    • l t a g ltag ltag(左标志) :当 l t a g = 0 ltag = 0 ltag=0时, l c h i l d lchild lchild域指示节点的左孩子;当 l t a g = 1 ltag = 1 ltag=1时, l c h i l d lchild lchild域指示节点的遍历前驱;

    • r t a g rtag rtag(右标志) :当 r t a g = 0 rtag = 0 rtag=0时, r c h i l d rchild rchild域指示节点的右孩子;当 r t a g = 1 rtag = 1 rtag=1时, r c h i l d rchild rchild域指示节点的遍历后继。

3.5 最优二叉树(哈夫曼树)

  • 定义:最优二叉树也称为哈夫曼树,是一种带权路径长度最短的二叉树;

  • 相关概念:

    • 路径:树中一个节点到另一个节点之间的通路;

    • 路径长度:通路上的分支个数;

    • 树的路径长度:根节点到每一个叶子节点之间的路径长度之和;

    • 权:节点代表的值;

      有的会写在节点上,有的会写在节点旁边。比如下图中的1、2、4、5;

    • 节点的带权路径长度:该节点到根节点之间的路径长度与该节点权值的乘积;

    • 树的带权路径长度:树中所有叶子节点的带权路径长度之和,记为 W P L = ( W 1 ∗ L 1 + W 2 ∗ L 2 + ⋯ + W n ∗ L n ) WPL=(W_1*L_1 + W_2*L_2+\cdots+W_n*L_n) WPL=(W1∗L1+W2∗L2+⋯+Wn∗Ln),其中 W i W_i Wi 为权值, L i L_i Li 为路径长度。最优二叉树就是带权路径长度最短的树

  • 最优二叉树(哈夫曼树)的构建。假设 有 n n n 个权值,则构造出的哈夫曼树有 n n n 个叶子节点 。 n n n 个权值分别设为 W 1 、 W 2 ⋯ W n W_1、W_2\cdots W_n W1、W2⋯Wn,哈夫曼树的构造规则为:

    • 将 W 1 、 W 2 ⋯ W n W_1、W_2\cdots W_n W1、W2⋯Wn 看成是有 n n n 棵树的森林(每棵树仅有一个节点);

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

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

    • 重复步骤2、3,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树;

      例:有森林:1 2 4 5,将这四棵树合并为一棵二叉树

      第一次合并:
      3
      /
      1 2
      森林列表:3, 4, 5

      第二次合并:
      7
      /
      3 4
      /
      1 2
      森林列表:7, 5

      第三次合并:
      12
      /
      5 7
      /
      3 4
      /
      1 2

  • 哈夫曼编码又称霍夫曼编码(赫夫曼编码),是总字符编码长度最短的编码。先构造哈夫曼树,左分支表示字符"0",右分支表示字符"1",可得到字符的哈夫曼编码。需要注意的是,构造哈夫曼树时一般遵循权值"左小右大"的原则;

    复制代码
        12
      0/  \1
      5    7
         0/ \1
         3   4
       0/ \1
       1   2
    • 值为 5 的节点的哈夫曼编码就是0,值为 1 的节点的哈夫曼编码就是 1 0 0;
  • 练习:下表为某文件中字符的出现频率,采用霍夫曼编码对下列字符编码,则字符序列"bee"的编码为();编码"110001001101"对应的字符序列是()。

    字符 a b c d e f
    频率 45 13 12 16 9 5
    • A.10111011101

    • B.10111001100

    • C.001100100

    • D.110011011

    • A.bad

    • B.bee

    • C.face

    • D.bace

    A C

    霍夫曼编码又称哈夫曼编码(赫夫曼编码),也就是总字符编码长度最短的编码。先构造哈夫曼树,左分支表示字符"0",右分支表示字符"1",可得到字符的哈夫曼编码。注意:构造哈夫曼树时一般遵循权值"左小右大"的原则。

  • 练习:设有5个字符,根据其使用频率为其构造哈夫曼编码。以下编码方案中()是不可能的。

    • A.{ 111, 110, 101, 100, 0 }

    • B.{ 0000, 0001, 001, 01, 1 }

    • C.{ 11, 10, 01, 001, 000 }

    • D.{ 11, 10, 011, 010, 000 }

    D

    先构造哈夫曼树,左分支表示字符"0",右分支表示字符"1",可得到字符的哈夫曼编码。而选项D中构造的哈夫曼树存在度为1的节点,而哈夫曼树中只能有度为0和度为2的节点。不满足哈夫曼树的性质要求。并且该哈夫曼树应当是奇数个节点。

3.6 树和森林

3.6.1 树的存储结构

  • 双亲表示法:用一组连续的地址单元存储树的节点,每个节点附带一个指示器,指出其双亲节点所在数组元素的下标;

    • 特点是易找祖先节点,但找孩子节点麻烦;
  • 孩子表示法:给每个节点建立链表,存储该节点的所有孩子节点,再将每个链表的头指针放在同一个线性表中;

    • 特点是易找孩子节点,找祖先节点麻烦;
  • 孩子兄弟表示法:又称二叉链表表示法,每个节点有左右两个指针域,左指针指向该节点的第一个孩子,右指针指向该节点下一个兄弟节点。为树、森林、二叉树的转换提供了基础;

3.6.2 树和森林的遍历

  • 树的先根遍历:先访问根节点,再依次访问根节点的各个子树;
  • 树的后根遍历:先遍历根的各个子树,再访问根节点;
  • 森林的遍历:和树的遍历类似,分为先序遍历和后序遍历,对于多棵树,依次遍历即可。

3.6.3 树、森林和二叉树之间的相互转换

  • 转换原理:树的最左边节点作为二叉树的左子树 ,树的其他兄弟节点作为其左边兄弟节点的右子树。任何一棵与树对应的二叉树,其右子树必为空

3.7 二叉排序树

  • 二叉排序树,也称为二叉查找树或者二叉搜索树,其定义如下:

    • 要么为空树,要么具有以下性质:
      • 若左子树不为空,则左子树的所有结点值均小于根结点
      • 若右子树不为空,则右子树的所有结点值均大于根结点
  • 这是一个递归定义,对二叉排序树进行中序遍历,能得到一个递增的有序序列,所以它经常被用于查找算法。可以用"左小右大"的口诀来记忆其性质。

3.8 平衡二叉树

  • 平衡二叉树,又称AVL树,它或者是一棵空树,或者具有如下性质:

    • 它的左子树和右子树都是平衡二叉树;

    • 左子树和右子树的深度之差的绝对值不超过1;

  • 希望任何序列构成的二叉排序树都是平衡二叉树,这样其平均查找长度与 log ⁡ ( n ) \log(n) log(n)同量级。

3.9 练习

  • 设由三棵树构成的森林中,第一棵树、第二棵树和第三棵树的结点总数分别为n1、n2和n3。将该森林转换为一颗二叉树,那么该二叉树的右子树包含()个结点。

    • A.n1
    • B.n1 + n2
    • C.n3
    • D.n2 + n3

    D

    树、森林与二叉树的转换转换原理:树的最左边结点作为二叉树的左子树,树的其他兄弟结点作为其左边兄弟结点的右子树。任何一颗与树对应的二叉树,其右子树必为空。

  • 当二叉树的结点数目确定时,()的高度一定是最小的。

    • A.二叉排序树
    • B.完全二叉树
    • C.线索二叉树
    • D.最优二叉树

    B

    二叉排序树的性质是左子树结点均小于根结点的值,右子树结点均大于根结点的值,这样就可能产生单枝树,无法保证高度最小。

    完全二叉树每层都将结点尽可能地排满,如果有空结点则其一定在最后一层上,因此树的高度一定是最小的。

    线索二叉树增设指针去保存结点的前驱、后继关系,无法保证书树的高度最小。

    最优二叉树是带权路径长度最小的树,也跟树的高度没有必然的联系。

相关推荐
Chance_to_win3 小时前
数据结构之双向链表
数据结构·链表
乌萨奇也要立志学C++3 小时前
【洛谷】二叉树专题全解析:概念、存储、遍历与经典真题实战
数据结构·c++·算法
MOONICK5 小时前
数据结构——红黑树
数据结构
(●—●)橘子……5 小时前
记力扣2271.毯子覆盖的最多白色砖块数 练习理解
数据结构·笔记·python·学习·算法·leetcode
做运维的阿瑞5 小时前
Python 面向对象编程深度指南
开发语言·数据结构·后端·python
new coder7 小时前
[算法练习]第三天:定长滑动窗口
数据结构·算法
晨非辰7 小时前
《从数组到动态顺序表:数据结构与算法如何优化内存管理?》
c语言·数据结构·经验分享·笔记·其他·算法
筱砚.8 小时前
【数据结构——十字链表】
网络·数据结构·链表
坚持编程的菜鸟9 小时前
LeetCode每日一题——重复的子字符串
数据结构·算法·leetcode