软考:软件设计师 — 13.数据结构

十三. 数据结构

数据结构部分也可参考文章:Java数据结构知识点 — 5种常见数据结构 

1. 线性结构

(1)线性表

顺序表

线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。数组的内存是连续、静态分配的,即在使用数组之前需要分配固定大小的空间。

  • 读操作:直接找到对应位置,时间复杂度为 O(1)。
  • 查询操作:最少查 1 个,最多查 n 个,平均查 \frac{n+1}{2} 个。
  • 插入操作:最少后移 0 个(插在末尾),最多后移 n 个(插在开头),平均后移 \frac{n+1}{2} 个。
  • 删除操作:最少前移 0 个(删末尾),最多前移 n-1 个(删开头),平均前移 \frac{n-1}{2} 个。

链表

线性表的链式存储是用通过指针链接起来的结点来存储数据元素。链表的内存是不连续的,前一个元素存储地址的下一个地址中存储的不一定是下一个元素。链表通过一个指向下一个元素地址的引用将链表中的所有元素串起来。

结点的基本结构:

链表的每个结点实际上是一个结构体变量,有若干成员组成,包括数据部分和指针变量。

struct node{
   int data;          // 结点的数据域
   struct node *link; // 结点的指针域
}

链表的基本结构:

  • 尾结点:最后一个有效结点。
  • 首结点:第一个有效结点。
  • 头结点:第一个有效结点之前的那个结点,存放链表首地址。
  • 头指针:指向头结点的指针变量。
  • 尾指针:指向尾结点的指针变量。 

 链表的特点:

  • n 个结点离散分布,彼此通过指针相联系。
  • 除头结点和尾结点外,每个结点只有一个前驱结点和一个后继结点。头结点没有前驱结点,尾结点没有后继结点。
  • 头结点并不存放有效数据,只存放链表首地址。其头结点的数据类型和首结点类型一样。
  • 加头结点的目的是方便对链表的操作,比如在链表头部进行结点的删除、插入。

链表的分类:

根据链表中指针域的设置方式,链表可分为:单链表、循环链表、双向链表。

单链表的插入与删除操作:

p 所指结点后插入新元素结点(s 所指结点)

① s -> link = p -> link;

② p -> link = s; 

删除 p 所指结点的后继结点

① q = p -> link;

② p -> link = p -> link -> link;

③ free(q);

删除 p 所指结点

①  p -> data = p -> link -> data;

② q = p -> link;

③ p -> link = p -> link -> link;

④ free(q);

这种方式比较灵活,是将 p 结点的数值域用下一个结点的数值域覆盖,这样就不存在 p 结点了,然后删除下一个结点即可,也相当于变相删除了 p 结点。

双向链表的插入与删除操作:

插入 q 所指向的结点

① s -> front = p -> front;

② p -> front -> link = s;

③ s -> link = p;

④ p -> front = s;

删除结点 p 指向的结点

① p -> front -> link = p -> link;

② p -> link -> front = p -> front;

③ free(p); 

顺序存储与链式存储对比

性能类别具体项目顺序存储链式存储
空间性能存储密度=1,更优<1
容量分配事先确定动态改变,更优
时间性能查找O(n)O(n)
O(1),更优O(n),最好情况为 1,最坏情况为 n
插入O(n),最好情况为 0,最坏情况为 nO(1),更优
删除O(n)O(1),更优

例题:

设有一个包含 n 个元素的有序线性表。在等概率情况下删除其中一个元素,若采用顺序存储结构,则平均需要移动()个元素;若采用单链表存储,则平均需要移动()个元素。

A.1  B.(n-1)/2  C.longn  D.n

A.0  B.1  C.(n-1)/2  D.n/2

解析:

删除元素,顺序表平均前移 \frac{n-1}{2} 个元素,链式表不需要移动元素,只需删除结点指针即可,因此选 BA。

(2)队列与栈

栈按照 “后进先出” 的原则操作。在栈顶进行入栈和出栈。

队列

队列按照 “先进先出” 的原则操作。在队尾入队,队头出队。常与循环单链表结合使用,不需要遍历。

一种比较特殊的队列是循环队列 

  • 队空条件:head = tail
  • 队满条件:(tail + 1)% size = head
  • 队列长度:(tail - head + size)% size

tail 指向最后一个元素的后一个位置,因此实际是空余一个位置。

例题1:

对于一个长度为 n(n>1) 且元素互异的序列,令其所有元素依次通过一个初始为空的栈后,再通过一个初始为空的队列。假设队列和栈的容量都足够大,且只要栈非空就可以进行出栈操作,只要队列非空就可以进行出队操作,以下叙述中,正确的是()。

A.出队序列和出栈序列一定互为逆序。

B.出队序列和出栈序列一定相同。

C.入栈序列和入队序列一定相同。

D.入栈序列和入队序列一定互为逆序。

解析1:

由题干可知,出栈序列和入队序列一定是相同的,因为出栈后必入队,只要栈和队列都非空,而入队和出队次序相同,队列先进先出,所以出队序列和出栈序列相同。B 正确,A 错误。而入栈序列是未知的,因为可能存在某个元素 a 先进栈然后出栈,然后又有两个元素 b c 依次进栈,此时出栈的顺序就变成了 c b,所以入队序列也就不相同也不互逆,因此 CD 错误。

例题2:

队列的特点是先进先出,若用循环单链表表示队列,则()。

A.入队列和出队列操作都不需要遍历链表。

B.入队列和出队列操作都需要遍历链表。

C.入队列操作需要遍历链表而出队列不需要。

D.入队列操作不需要遍历链表而出队列需要。

解析2:

入队在队尾,出队在对头,所以不需要遍历链表。选项 A 正确。

例题3:

若元素以 a,b,c,d,e 的顺序进入一个初始为空的栈中,每个元素进栈、出栈各 1 次,要求出栈的第一个元素为 d,则合法的出栈序列共有()种。

A.4  B.5  C.6  D.24

解析3:

出栈的第一个元素要求为 d,那么在 d 前面一定是 c,b,a,因为栈是后进先出,e 还没有进栈。d 第一个出栈,然后看剩余的元素 c b a 和 e 的排列方式,e 可以在 c 之前,也可以在 c b 之间,或 b a 之间,或 a 之后,因此出栈序列共 4 种。选 A。

例题4:

双端队列是指在队列的两个端口都可以加入和删除元素,如下图所示。现在要求元素进队和出队都必须在同一端口,即从 A 端进队的元素必须从 A 端出、从 B 端进队的元素必须从 B 端出,则对于 4 个元素的序列 a、b、c、d,若要求前两个元素(a、b)从 A 端口按次序全部进入队列,后两个元素(c、d)从 B 端口按次序全部进入队列,则不可能得到的出队序列是()。

A.d、a、b、c  B.d、c、b、a  C.b、a、d、c  D.b、d、c、a

解析4:

可以想象成有一条线将队列从中间隔开,一端是 A,一端是 B,而每端是栈,a、b 元素进入 A 端后,b 元素不出来,a 元素也无法出来,同理,B 端也是,d 元素不出来,c 元素也出不来。所以只有 A 选项错误,d 元素先出,然后要么下一个元素要么是 c 出来,要么是 b 出来,而 a 无法出来。 

(3)串

串是仅有字符构成的有限序列,是一种线性表。一般记为 S = 'a_{1}a_{2}\cdots a_{n}',其中,S 是串名,单引号括起来的字符序列是串值。

串的几个基本概念:

  • 空串:长度为零,不包含任何字符。
  • 空格串:由一个或多个空格组成的串。虽然空格是一个空白字符,但它也是一个字符,在计算串长度时要将其计算在内。
  • 字串:由串中任意长度的连续字符构成的序列称为子串(2 的 n 次方,n 为单个字符个数)。含有子串的串称为主串。子串在主串中的位置是指子串首次出现时,该子串的第一个字符在主串中的位置。空串是任意串的子串。
  • 子序列:一个串的子序列是将这个串中的一些字符提取出来得到一个新串,并且不改变它们的相对位置关系。也就是说,假设一个串是 abbc,那么 ab、ac都是它的子序列。
  • 串比较:两个串比较大小时以字符的 ASCII 码值作为依据。实质上,比较操作从两个的第一个字符开始进行,字符的码值大者所在的串为大;若其中一个串先结束,则以串长较大者为大。
  • 串相等:指两个串长度相等且对应序号的字符也相同。

串的基本操作:

  • 赋值操作 StrAssign(s,t):将串 s 的值赋给串 t。
  • 连接操作 Concat(s,t):将串 t 接续在串 s 的尾部,形成一个新的串。
  • 求串长 StrLength(s):返回串 s 的长度。
  • 串比较 StrCompare(s,t):比较两个串的大小。返回值 -1、0 和 1 分别表示 s<t、s=t 和 s>t 三种情况。
  • 求子串 SubString(s,start,len):返回串 S 中从 start 开始的、长度为 len 的字符序列。

串的存储:

  • 顺序存储:用一组地址连续的存储单元来存储串值的字符序列。
  • 链式存储:用链表存储串中的字符,,每个结点中可以存储一个字符,也可以存储多个。

串的模式匹配:

子串的定位操作通常称为串的模式匹配。(子串也称为模式串)

  • 朴素的模式匹配算法(布鲁特-福斯算法):其基本思想是从主串的第一个字符起与模式串的第一个字符比较,若相等,则继续逐一对字符进行后续的比较,否则从主串第二个字符起与模式串的第一个字符重新比较,直到模式串中每个字符依次与主串中一个连续的字符序列相等时为止,此时称为匹配成功。如果不能在主串中找到与模式串相同的子串,则匹配失败。
  • 改进的模式匹配算法(KMP算法):其改进之处在于,每当匹配过程中出现相比较的字符不相等时,不需要回退到主串的字符位置指针,而是利用已经得到的部分匹配结果将模式串向右滑动尽可能远的距离,再继续进行比较。

在 KMP 算法中,依据模式串的 next 函数值实现子串的滑动。若令 next[j] = k,则 next[j] 表示当模式串中的 p_{j} 与主串中相应字符不相等时,令模式串的 p_{next[j]]} 与主串的相应字符进行比较。(j = next[j])

next 函数定义如下:

需要掌握 next 函数值的求取,并且可能出题老师给出的 next 函数定义不同,但本质计算过程相同。其中,第二条是指有多个 k 值时,选择最大的那个。

例题1:

在字符串的 KMP 模式匹配算法中,需先求解模式串的 next 函数值,其定义如上图所示,j 表示模式串中字符的序号(从 1 开始)。若模式串 p 为 'abaac',则其 next 函数值为()。

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

解析1:

j12345
模式串abaac
next[j]01122

当 j = 1 时,next[j] = 0;当 j = 2 时,即 1<k<2,不存在整数 k ,因此是其它情况,next[j] = 1;当 j = 3 时,即 1<k<3,因此 k = 2,此时需要判断表达式 'p1p2…pk-1' = 'pj-k+1 pj-k+2…pj-1' 是否满足条件,即 p1p2…pk-1 = p1 = a,pj-k+1pj-k+2…pj-1 = p2 = b,a ≠ b,所以不满足条件,是其它情况,next[j] = 1;当 j = 4 时,即 1<k<4,因此 k = 2 或 3,当 k = 2 时,表达式为 p1 = p3,为真(a = a),当 k = 3 时,表达式为 p1p2 = p2p3,为假(ab ≠ ba),所以当 j = 4 时,k = 2;同理,继续推断 j = 5,即 1<k<5,k = 2 或 3 或 4,当 k = 2 时,表达式为 p1 = p4,满足条件,当 k = 3 时,表达式为 p1p2 = p3p4,为假,不满足条件,当 k = 4 时,表达式为 p1p2p3 = p2p3p4,为假,不满足条件,因此 next[j] 的值为 2。所以 next 函数值为 01122,选 B。

例题2:

设 S 是一个长度为 n 的非空字符串,其中的字符各不相同,则其互异的非平凡子串(非空且不同于 S 本身)个数为()。

A.2n-1  B.n^{2}  C.n(n+1)/2  D.(n+2)(n-1)/2

解析2:

可以通过举例判断,单个字符 a 的非平凡子串个数是 0,非空且不同于本身,也就是 n = 1,可以直接判断出只有 D 项,在 n = 1 时为 0,因此选 D。

2. 数组与矩阵

(1)数组

数组是定长线性表在维数上的扩展,即线性表中的元素又是一个线性表。n 维数组是一种 "同构" 的数据结构,其每个数据元素类型相同、结构一致。数组一般不做插入删除运算,一旦定义了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动,因此数组适合于采用顺序存储结构。

数组类型存储地址计算
一维数组 a[n]a[i] 的存储地址为:a + i*len
二维数组 a[m][n]

a[i][j] 的存储地址(按行存储)为:a + (i*n + j)*len

a[i][j] 的存储地址(按列存储)为:a + (j*m + i)*len

例如,已知 5 行 5 列的二维数组 a 中的各元素占两个字节,求元素 a[2][3] 按行优先存储的存储地址。

根据公式, a + (i*n + j)*len 可得,a + (5*2+3)*2B = a + 26B。如果按列存储,则 a + (5*3+2)*2B = a + 34B。但需要注意数组行列开始的下标,从 0 开始还是从 1 开始,如果不做说明,则默认从 0 开始。

例题:

二维数组 a[1..N, 1..N] 可以按行存储或列存储。对于数组元素 a[i][j](1<=i,j<=N),当()时,在按行和按列两种存储方式下,其偏移量相同。

A.i≠j  B.i=j  C.i>j  D.i<j

解析:

数组开始下标是 1,根据公式可得,按行存储:a + ((i-1)*N + (j-1))*len;按列存储:a + ((j-1)*N + (i-1))*len,由两个公式可知,当 i=j 时,偏移量相同。因此选 B。

(2)矩阵

稀疏矩阵

稀疏矩阵示意图要点
上三角矩阵\begin{pmatrix} x& x& x& x& x\\ x& x& x& x& \\ x& x& x& & \\ x& x& & & \\ x& & & & \end{pmatrix}\begin{pmatrix} x& x& x& x& x\\ & x& x& x& x\\ & & x& x& x\\ & & & x& x\\ & & & & x \end{pmatrix}在矩阵中下标分别为 i 和 j 的元素,对应的一维数组的下标计算公式为:(2n-i+1)×i/2+j
下三角矩阵\begin{pmatrix} & & & & x\\ & & & x& x\\ & & x& x& x\\ & x& x& x& x\\ x& x& x & x & x \end{pmatrix}\begin{pmatrix} x& & & & \\ x& x& & & \\ x& x& x& & \\ x& x& x& x& \\ x& x& x& x & x \end{pmatrix}

在矩阵中下标分别为 i 和 j 的元素,对应的一维数组的下标计算公式为:(i+1)×i/2+j

公式具体计算过程的推导不再介绍,可参考视频。 

这里默认一维数组的下标从 0 开始,所以公式最后不需要加 1。

特殊矩阵

特殊矩阵示意图要点
对角矩阵\begin{pmatrix} x& & & & \\ & x& & & \\ & & x& & \\ & & & x& \\ & & & &x \end{pmatrix}\begin{pmatrix} x & x & & & \\ x & x & x & & \\ & x& x & x & \\ & & x& x &x \\ & & & x& x \end{pmatrix}矩阵中的非零元素都集中在以主对角线为中心的带状区域中。

A_{n,n}  = \begin{vmatrix} A_{1,1} & A_{1,2} & 0& 0 & 0\\ A_{2,1}& A_{2,2} & A_{2,3} & 0 & 0\\ 0 & A_{3,2}& A_{3,3} & A_{3,4} & 0\\ 0& ...& ... & ... &... \\ 0& 0& 0 & A_{n,n-1} &A_{n,n} \end{vmatrix} 

A[i][j],上三角元素 A[i][j](i<=j),下三角元素 A[i][j](i>=j),上下三角元素根据对角线对称,即 i 和 j 互换。

例题1:

设某 n 阶三对角矩阵 A_{n\times n} 的示意图上上述图所示。若将该三对角矩阵的非零元素按行存储在一维数组 B[k](1≤k≤3*n-2)中,则 k 与 i、j 的对应关系是()。

A.k=2i+j-2  B.k=2i-j+2  C.k=3i+j-1  D.k=3i-j+2

解析1:

该类题型可通过举例带入选项演算得到答案,同样需要注意数组下标的起始位置,是 0 还是 1。题干中已经说明 k 从 1 开始,A_{1,1} 对应 B[1],即 i = 1,j = 1 时,k = 1,带入选项发现只有 A 项正确。正常情况下需要举例 2~3 个。往后举例的话就是,A_{1,2} 对应 B[2],A_{2,1} 对应 B[3],以此类推。

例题2:

对于一个 n 阶的对称矩阵 A,将其下三角区域(含主对角线)的元素按行存储在一维数组 S 中,设元素 A[i][j] 存放在 S[k] 中,且 S[1] = A[0][0],则 k 与 i,j(i≤j)的对应关系是()。

A.k=i(i+1)/2+j-1  B.k=j(j+1)/2+i+1  C.k=i(i+1)/2+j+1  D.k=j(j+1)/2+i-1

解析2:

注意题干中的描述,将下三角区域的元素按行存储在一维数组中,但问的是 k 与 i,j(i≤j)的对应关系,即上三角区域。根据下三角区域元素的计算公式 (i+1)*i/2+j,但公式的计算是从下标为 0 开始,题干中一维数组下标是从 1 开始,因此需要加一,即 (i+1)*i/2+j+1。注意,此时是下三角区域,转换成上三角区域,将 i 和 j 互换即可,所以 (j+1)*j/2+i+1,因此选 B。

或者举例带入选项计算,下三角矩阵第一个元素是 A_{0,0},即 i = j = 0,对应 k = 1,带入选项可排除AD 项,只有 BC 项为 1。第二个元素是 A_{1,0},但注意,题目问的是上三角矩阵,所以对应的上三角矩阵元素为 A_{0,1},即 i = 0,j = 1,对应 k = 2,BC 都为 2,继续举例;第三个元素为 A_{1,1},i = j = 1,对应 k 为 3,带入 BC 均为 3,继续举例;第四个元素为 A_{2,0},对应的上三角矩阵元素为 A_{0,2},i = 0,j = 2,对应 k = 4,带入 BC,只有 B 项正确。

3. 树

(1)树与二叉树的特性

树是 n(n≥0) 个结点的有限集合,当 n = 0 时称为空树。在任一非空树(n>0)中,有且仅有一个称为根的结点,其余结点可分为 m(m≥0) 个互不相交的有限子集 T_{1}T_{2}\cdots T_{m},其中,每个 T_{i} 又都是一棵树,并且称为根结点的子树。

树的基本概念:

  • 双亲、孩子和兄弟。结点的子树的根称为该结点的孩子;相应地,该结点称为其子结点的双亲。具有相同双亲的结点互为兄弟。
  • 结点的度。一个结点的子树的个数记为该结点的度。
  • 叶子结点。终端结点,度为 0 的结点。
  • 内部结点。度不为 0 的结点,也称为分支结点或非终端结点。除根结点外,分支结点也称为内部结点。
  • 结点的层次。根为第一层,根的孩子为第二层,依此类推,若某结点在第 i 层,则其孩子结点在第 i+1 层。
  • 树的高度。一棵树的最大层数记为树的高度或深度。

二叉树的定义:

二叉树是 n(n≥0) 个结点的有限集合,或者是空树,或者是由一个根节点及两棵不相交的且分别称为左、右子树的二叉树所组成。

二叉树的重要特性:

  • 在二叉树的第 i 层上最多有 2^{i-1} 个结点(i≥1)。
  • 深度为 k 的二叉树最多有 2^{k}-1 个结点(k≥1)。
  • 对任何一棵二叉树,如果其叶子结点树为 n_{0},度为 2 的节点数为 n_{2},则 n_{0} = n_{2} +1。(其中,n_{0} 表示度为 0,n_{2} 表示度为 2)
  • 如果对一棵有 n 个结点的完全二叉树的结点按层序编号(从第 1 层到 \left \lfloor log2n\right \rfloor+1 层,每层从左到右),则对任一结点 i(1≤i≤n),有:
  1. 如果 i = 1,则结点 i 无父节点,是二叉树的根;如果 i > 1,则父节点是  \left \lfloor i/2\right \rfloor(向下取整);
  2. 如果 2i > n,则结点 i 为叶子结点,无左子结点;否则其左子结点是结点 2i;
  3. 如果 2i+1 > n,则结点 i 无右子结点,否则其右子结点时结点 2i+1。

二叉树的存储:

顺序存储

链式存储

其中,n_{0} 表示度为 0 的叶子结点,在链式存储中也代表有 2 个空指针,同理,n_{1} 有 1 个空指针,n_{2} 没有空指针。并且满足二叉树的特性:n_{0} = n_{2} + 1。

例题1:

具有三个结点的二叉树有()种形态。

A.2  B.3  C.5  D.7

解析1:

可以画图,共有 5 种形态。也可以利用公式:A[N] = \sum_{M=0}^{N-1}(A[M]\times A[N-M-1]),其中,N 代表结点的数量,然后进行从 M = 0 到 M = N-1 的累加运算。如本题中,A[0] = 1,结点数为 0 时只有 1 种形态(空树);A[1] = 1,结点数为 1 时也只有一种形态;当 N = 2 时,A[2] = \sum_{M=0}^{2-1}(A[0]\times A[1])+(A[1]\times A[0]) = 2;当 N = 3时,A[3] = \sum_{M=0}^{3-1}(A[0]\times A[2])+(A[1]\times A[1])+(A[2]\times A[0]) = 5,所以三个结点的二叉树有 5 种形态。

例题2:

对下面的二叉树进行顺序存储(用数组 MEM 表示),已知结点 A、B、C 在 MEM 中对应元素的下标分别为 1、2、3,那么结点 D、E、F 对应的数组元素下标为()。

A.4、5、6  B.4、7、10  C.6、7、8  D.6、7、14

解析2:

按顺序找到对应结点的对应位置即可,选 D。

例题3:

设某二叉树采用二叉链表表示(即结点的两个指针分别指示左、右孩子)。当该二叉树包含 k 个结点时,其二叉链表结点中必有()个空的孩子指针。

A.k-1  B.k  C.k+1  D.2k

解析3:

度为 0 的结点 n_{0} 有两个空指针,度为 1 的结点 n_{1} 有一个空指针,度为 2 的结点 n_{2} 没有空指针,且 k = n_{0} + n_{1} + n_{2};该二叉树中的空指针数量为:2n_{0} + n_{1},并且由于 n_{0} = n_{2} + 1,那么 2n_{0} + n_{1} = n_{0} + n_{2} + 1 + n_{1} = k+1,因此选 C。或者直接举例验证,k = 1 时,有两个空指针,k = 2 时有 3 个空指针,因此选 C。

例题4:

若一棵二叉树的高度(层数)为 h,则该二叉树()。

A.有 2h 个结点

B.有 2h-1 个结点

C.最少有 2h-1 个结点

D.最多有 2h-1 个结点

解析4:

直接举例验证,假设 h = 2,最少有两个结点,最多可以有三个结点,因此选 D。

(2)二叉树遍历

遍历是按某种策略访问树中的每个结点,且仅访问一次的过程。

遍历策略:

  • 层次遍历:按结点顺序访问
  • 前序遍历:根 — 左 — 右
  • 中序遍历:左 — 根 — 右
  • 后序遍历:左 — 右 — 根

以上图二叉树为例,其层次遍历结果为:1 2 3 4 5 6 7 8

前序遍历:1 L1 R1 — 1 2 4 R2 3 6 — 1 2 4 5 L2 3 6 — 1 2 4 5 7 8 3 6

中序遍历:L1 1 R1 — 4 2 R2 1 3 6 — 4 2 L2 5 1 3 6 — 4 2 7 8 5 1 3 6

后序遍历:L1 R1 1 — 4 R2 2 6 3 1 — 4 L2 5 2 6 3 1 — 4 8 7 5 2 6 3 1

构造二叉树:

由前序序列 ABHFDECG 和 中序序列 HBEDFAGC 构造二叉树。

根据前序序列可知,该二叉树根节点为 A,根据中序序列可知,左子树的结点有 HBEDF,右子树结点有 GC。再继续分析,根据前序序列中左子树结点 BHFDE 可知,左子树根节点为 B,根据中序序列中左子树结点 HBEDF 可知,根结点 B 的左子树为 H,右子树有结点 EDF。依此类推,右子树 EDF 的根节点根据前序序列可知是 F,而根据中序序列可知 ED 均在 F 的左侧,那么 D 为 E 的根节点,E 在 D 左侧,二叉树根节点 A 的左子树分析完毕,现在看右子树,根据前序序列,右子树根节点为 C,根据中序序列,G 为 C 的左子树,因此整棵二叉树为:

例题:

某二叉树的先序遍历序列为 ABCDEF,中序遍历序列为 BADCFE,则该二叉树的高度(层数)为()。

A.3  B.4  C.5  D.6

解析:

根据先序和中序序列构造二叉树,结果为:

则该二叉树层数为 4,选 B。

(3)二叉排序树

基于结点的值,左结点小于根,右结点大于根。

如一个序列 {89,48,112,20,56,51},其二叉排序树为:

89 作为根节点;48 比 89 小,作为其左结点;112 比 89 大,作为其右结点;20 比 89 小,放在其左子树中,比 48 小,再作为 48 的左结点;56 比 89 小,放入左子树,比 48 大,作为 48 的右结点;51 比 89 小,放入左子树,比 48 大,放入 48 的右子树,比 56 小,作为 56 的左结点。

所以,给出的序列不同,那么产生的二叉排序树可能也会不相同。 

插入结点

  • 若该键值结点已存在,则不再插入,如:48。
  • 若查找二叉树为空树,则以新结点为查找二叉树。
  • 将要插入结点键值与插入后父结点键值比较,就能确定新结点是父结点的左子结点,还是右子结点。

删除结点

  • 若待删除结点是叶子结点,则直接删除。
  • 若待删除结点只有一个子结点,则将这个子结点与待删除结点的父结点直接连接,如:56。
  • 若待删除的结点 p 有两个子结点,则在其左子树上,用中序遍历寻找关键值最大的结点 s,用结点 s 的值代替结点 p 的值,然后删除结点 s,结点 s 必属于上述两种情况之一,如:89。

上图中,四种遍历策略结果分别为:

  • 层次遍历:89 48 112 20 56 51
  • 前序遍历:89 48 20 56 51 112
  • 中序遍历:20 48 51 56 89 112
  • 后序遍历:20 51 56 48 112 89

因此可以得出结论,只有中序遍历的结果是有序的

例题1:

设有二叉排序树(二叉查找树)如下如所示,建立该二叉树的关键码序列不可能是()。

A.23 31 17 19 11 27 13 90 61

B.23 17 19 31 27 90 61 11 13

C.23 17 27 29 31 13 11 90 61

D.23 31 90 61 27 17 19 11 13

解析1:

以 A 项为例。23 为根节点;31 比 23 大,作为 23 的右结点;17 比 23 小,作为 23 的左结点;19比 23 小,放入其左子树,比 17 大,作为 17 的右结点;11 比 23 小,放入其左子树,比 17 小,作为 17 的左结点;27 比 23 大,放入右子树,比 31 小,作为 31 的左结点;13 比 23 小,放入其左子树,比 17 小,放入 17 的左子树,比 11 大,作为 11 的右结点;90 比 23 大,放入其右子树,比 31 大,作为 31 的右结点;61 比 23 大,放入其右子树,比 31 大,放入 31 的右子树,比 90 小,作为 90 的左结点。因此 A 项产生的二叉排序树和图中相同。同理判断 BCD,只有 C 项无法得到图中二叉树,因此选 C。

总结一下可知,只有 C 项的前三个结点不在同一路径分支上,因此产生的二叉树一定和图中不同。

例题2:

以下关于二叉排序树(或二叉查找树、二叉搜索树)的叙述中,正确的是()。

A.对二叉排序树进行先序、中序和后序遍历,都得到结点关键字的有序序列。

B.含有 n 个结点的二叉排序树高度为 \left \lfloor log2n\right \rfloor+1

C.从根到任意一个叶子结点的路径上,结点的关键字呈现有序排列的特点。

D.从左到右排列同层次的结点,其关键字呈现有序排列的特点。

解析2:

只有中序遍历得到有序序列,A 项错误;含有 n 个结点的二叉树高度最小为 \left \lfloor log2n\right \rfloor+1,最大为 n(单支树),所以 B 项错误; C 项可以直接从例题 1 的图中找到反例,显然错误;D 项正确,因为左结点一定比右结点小,同层次上排列是有序的。

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

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

  • 路径:从树的一个结点到另一个结点直接的通路。
  • 路径长度:路径上的分支数目。
  • 树的路径长度:从根到每一个叶子结点之间的路径长度之和。
  • 权:结点的值。
  • 结点的带权路径长度:从该结点到根之间的路径长度与该结点权值的乘积。
  • 树的带权路径长度(树的代价):树中所有叶子结点的带权路径长度之和。

树的带权路径长度公式:WPL = \sum_{k=1}^{n}w_{k}l_{k}。其中,n 为带权叶子结点数,w_{k} 为叶子结点的权值;l_{k} 为叶子结点到根的路径长度。

哈夫曼树是指权值为 w_{1},w_{2},…,w_{k} 的 n 个叶子结点的二叉树中带权路径长度最小的二叉树。

构造哈夫曼树

假设有一组权值 5,29,7,8,14,23,3,11

由于哈夫曼树是让带权路径长度最小,所以在原则上让权值大的叶子结点尽量离根节点近,权值小的叶子结点远离根节点。

首先选出两个最小权值的结点 3 和 5,它俩形成新结点 8,然后将 3 和 5 从这组权值中剔除,换成 8;然后在权值 8,29,7,8,14,23,11 中选出两个最小权值结点,即 7 和 8,形成结点 15,然后再剔除结点 7 和 8;继续选择最小结点进行构造,即 8 和 11,形成结点 19;继续剔除和选择,15 和 14,形成结点 29;继续剔除和选择,23 和 19,形成结点 42;继续剔除和选择,29 和 29,形成结点 58,最后 58 和 42 形成结点 100。如下图所示,构造的哈夫曼树的形状有多种,但树的带权路径长度相同。 

哈夫曼编码

若对每个字符编制相同长度的二进制码,则称为等长(定长)编码。

如,字符集 {a,b,c,d,e},以三位二进制编码,则分别对应 {000,001,010,011,100}。

如果设计长度不等的编码,我们希望将用的多的字符编码少,用的少的编码可以相对多一些。

以上图哈夫曼树为例,假设给出的权值分别对应字符 a,b,c,d,e,f,g,h,将较大边标记为 1,较小边标记为 0,即左 1 右 0(这个原则可以反过来,只要整体统一就可以),即:

那么字符集对应的编码分别为:

a:11111  b:10  c:1110  d:000  e:110  f:01  g:11110  h:001 

例题1:

已知某文档包含 5 个字符,每个字符出现的频率如下表所示。采用哈夫曼编码对该文档压缩存储,则单词 “cade” 的编码为(),文档的压缩比为()。

字符

a

bcde
频率(%)4010201614

A.1110110101  B.1100111101  C.1110110100  D.1100111100

A.20%  B.25%  C.27%  D.30%

解析1:

首先需要根据字符频率(权值)构建哈夫曼树,如下图:

题干中询问单词 cade 的编码,并且所有选项都是 1 开头,因此该哈夫曼树编码为左 0 右 1。所以,a 的编码为 0,b 的编码为 101,c 的编码为 111,d 的编码为 110,e 的编码为 101。所以单词 cade 的编码为 1110110101,选 A。

文档的压缩比,首先需要找到字符的定长编码,由于共有 5 个字符,因此二进制编码为 2^{3},因为 2 的平方为 4,长度不够,所以需要 3 次方,即定长编码为 3。变长编码为 40%*1 + (10+14+16+20)*3 = 2.2。压缩比为 (3-2.2)/3 ≈ 0.27,即 27%,选 C。

例题2:

以下关于哈夫曼树的叙述中,错误的是()。

A.权值越大的叶子离根结点越近

B.哈夫曼树中不存在只有一个子树的结点

C.哈夫曼树中的结点总数一定为奇数

D.权值相同的结点到树根的路径长度一定相同

解析2:

根据构造哈夫曼树的原则可知,尽量让权值大的叶子结点离根近,因此 A 项正确;由于哈夫曼树的构造是两两结合构造,因此不存在只有一个子树的结点,B 项正确;由于哈夫曼树中的结点的度要么为 0 要么为 2,并且根据二叉树特点,n_{0} = n_{2} + 1,那么结点总数为 n_{0} + n_{2} = 2n_{2} + 1,一定为奇数,因此 C 项正确;权值相同的结点到树根的路径长度可能不同,与树的形状也有关系,D 项错误。

(5)其它特殊的二叉树

平衡二叉树

任意结点的左右子树深度相差不超过1,即左侧高度 - 右侧高度,也称为平衡因子。平衡因子只能等于 -1、0 或 1。超过了这个值就不是平衡二叉树了。

之所以提出平衡二叉树,是因为二叉树在建立的过程中可能会形成单支树,也可能会形成完全二叉树,相比较而言,我们更希望使用完全二叉树或满二叉树,因为更稳定。基于此,提出了平衡二叉树的概念。

线索二叉树

引入线索二叉树是为了保存在遍历的动态过程中得到的信息。为了保存结点在任一序列中的前驱和后继信息,考虑在每个结点中增加两个指针域来存放遍历时得到的前驱和后继信息,方便访问。

例如,一棵二叉树如下图所示:

其前序遍历为:A B D E H C F G I

其中, A B C 三个结点的左右两个指针均指向其孩子,而 D H E F G I 均只有一个或没有孩子,可以拿来做线索指针。如结点 D,左孩子指针指向前驱 B,右孩子指针指向后继 E;结点 E 左孩子指针指向结点 H,而右孩子指针为空,做线索指针,同样指向 H;同理,结点 F,左孩子指针指向前驱 C,右孩子结点指向后继 G;G 的左孩子指针指向前驱 F,右孩子指针指向 I;I 结点左孩子指针指向前驱 G,右孩子指针指向空 Null。

中序和后序遍历同样如此,只是遍历顺序不同。

后续会持续更新整理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Phoenixxxxxxxxxxxxx

感谢支持!

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

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

打赏作者

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

抵扣说明:

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

余额充值