7.01数据结构与算法基础

7.02数组
在数组存储地址的计算中,“len”指的是数组中每个元素所占用的字节数(即元素数据类型的大小)。

答: 由题知:
- 按行优先,a[0]占满,a[1]占满。5行5列,那么a[0]+a[1]前俩行就占了10位。
- a[2] [3]在第三行占3位。那么a[2] [3]共占10+3=13位。
- 因为每个元素占俩字节(len=2),
- 所以按行优先的存储地址为:a+(2*5+3) *2=a+13 *2=a+26
7.03稀疏矩阵


方法一:代入法进行排除:
答:可以用代入法解决问题,排除错误的,剩下的就是正确的。
表示数组M第一个元素,即M[1]。那么依次排列,
就是M第二个元素M[2],
就是M[3]。
- (1) A[i,j],当i=0,j=0时:答案A为M[1],正确。B为M[0],错误。C为M[0],错误。D为M[1]正确。所以排除BC。
- (2) 当i=1,j=0时:答案A为M2。D为M[1]错误。排除D
- (3) 所以答案为A
方法二:根据下三角矩阵的行优先压缩存储规则,元素A[i,j]在数组M中的位置需要计算其所在行之前所有行的元素个数之和,加上当前行的位置。由于数组M的下标从1开始,公式需进行调整。
关键推导步骤:
- 前i行的元素总数:第k行(0≤k≤i−1)包含k+1个元素。前i行的元素总数为 1 + 2 + 3 + ... + i,即由等差数列求和公式得出为 i(i+1)/2。
- 当前行内的位置:在第i行中,元素A[i,j]是第j+1个元素(因j从0开始),因此在M中的位置需额外加j。
- 数组起始调整:因数组M从1开始,最终位置为前i行总数加上j+1(而非j),即 i(i+1)/2 + j + 1。
选项验证:
- 选项A:M[i(i+1)/2 + j + 1] 满足上述推导。
- 其他选项均不符合实际存储结构(如选项D未正确计算前i行总数,且未考虑数组起始)。
7.04数据结构的定义
1、数据结构的概念
数据结构(英语:data structure)是计算机中存储、组织数据的方式。
数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。
不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B 树高度适用于数据库的封装。
2、数据逻辑结构

从大的方向讲,图包含树,树又包含线性结构。
(1)线性结构:
是一个有序数据元素的集合。 其中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。
常用的线性结构有:线性表,栈,队列,双队列,数组,串。
(2)非线性结构:
各个数据元素不再保持在一个线性序列中,每个数据元素可能与零个或者多个其他数据元素发生联系。根据关系的不同,可分为层次结构和群结构。
常见的非线性结构有:二维数组,多维数组,广义表,树(二叉树等),图。(其中多维数组是由多个一维数组组成的,所以不再是线性结构)
(3)线性结构特点:
- 1.集合中必存在唯一的一个"第一个元素";
- 2.集合中必存在唯一的一个"最后的元素";
- 3.除最后元素之外,其它数据元素均有唯一的"后继";
- 4.除第一元素之外,其它数据元素均有唯一的"前驱"。
数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。
如(a1,a2,a3,.....,an),a1为第一个元素,an为最后一个元素,此集合即为一个线性结构的集合。
(4)非线性结构的特点:
相对应于线性结构,非线性结构的逻辑特征是一个结点元素可能对应多个直接前驱和多个后继。
7.05线性表—顺序表与链表

顺序表是连续的空间


1、 单链表删除结点:
(1) 如图所示,删除a2,q指向a2。P指向a1。
(2) P—>next=q—>next,即删除掉a2。
2、 单链表插入结点:
(1) 如图所示,s指向x,插入s结点。
(2) P指向a1,p—>next就是a2。
(3) 另s—>next=p—>next,p—>next=s。
(4) 则成功插入s结点。
3、 双向链表删除结点:
假设链表初始状态:
A <-> B <-> C <-> D
(A 是头结点,D 是尾结点)
删除结点 C:
- 定位到结点 C。
- 更新 C 的前驱 B:
B->next = C->next(即 B->next = D)。- 更新 C 的后继 D:
D->prev = C->prev(即 D->prev = B)。- 释放 C 的内存。
删除后链表:
A <-> B <-> D
4、 双向链表插入结点:
假设链表初始状态:
A <-> B <-> D
(A 是头结点,D 是尾结点)
在 B 后插入新结点 C:
- 创建新结点 C。
- 设置 C 的前驱:C->prev = B。
- 设置 C 的后继:C->next = B->next(即 C->next = D)。
- 更新 D 的前驱:D->prev = C。
- 更新 B 的后继:B->next = C。
插入后链表:
A <-> B <-> C <-> D
7.06线性表—顺序存储与链式存储
1、顺序存储与链式存储对比

(1) 顺序存储适合读运算。
(2) 链式存储适合插入和删除运算。
7.07线性表—队列与栈(重要 )
循环对列是把队头和队尾连接在一块。

答:(1)abc,bac,cba,bca,acb

答:注:题中因为第一个输出都是e4,所以可以不考虑e4,只考虑其他3个。
A:e1,e2,e3,e4依次从左端进入,则输出序列就为A。
B:e1进,不出;e2从左端进入,输出;e3从右端进入,不出,排在e1后面;然后e1输出,e2输出。
C:e1进,不出;e2从右端输入,不出,排在e1后面;e3进,输出;然后输出e1,e2。
D:e1进,不出;但e2无论从左端还是从右端进入,都与e1挨着。故选项D,不符合要求。
详细步骤:
理解数据结构 输出受限的双端队列允许元素从两端插入,但只能从一端(如队首)删除。元素按
e1→e2→e3→e4的顺序依次插入,但每次插入时可选择放在队列的头部或尾部。逐项分析选项
选项A (e4, e3, e2, e1) 每次插入均选择头部插入:
e1→[e1]
e2→[e2, e1]
e3→[e3, e2, e1]
e4→[e4, e3, e2, e1]输出顺序为e4, e3, e2, e1,可行。选项B (e4, e2, e1, e3) 插入策略:
e1→[e1]
e2→ 头部插入 →[e2, e1]
e3→ 尾部插入 →[e2, e1, e3]
e4→ 头部插入 →[e4, e2, e1, e3]输出顺序为e4, e2, e1, e3,可行。选项C (e4, e3, e1, e2) 插入策略:
e1→[e1]
e2→ 尾部插入 →[e1, e2]
e3→ 头部插入 →[e3, e1, e2]
e4→ 头部插入 →[e4, e3, e1, e2]输出顺序为e4, e3, e1, e2,可行。选项D (e4, e2, e3, e1) 尝试构造队列
[e4, e2, e3, e1]:
插入
e1→[e1]插入
e2需放在头部或尾部,但后续无法通过合法插入操作使队列变为[e2, e3, e1]。无论选择哪种插入方式,均无法在插入
e4前形成[e2, e3, e1]的队列结构。 因此,无法生成选项D的输出序列。结论 选项D的输出序列无法通过合法插入操作实现,故为正确答案。
7.08广义表
注:
- 表头:就是指第一个元素。
- 表尾:除表头之外的所有的元素。
| 特性 | 深度 | 长度 |
| 定义 | 括号的最大嵌套层数 | 最外层元素的个数 |
| 关注点 | 纵向复杂性(嵌套层次) | 横向扩展性(元素数量) |
| 核心规则 | ✅ 原子元素(如 a, 5)无括号 → 深度为 0 ✅ 空表 () → 仅有一层括号 → 深度为 1 ✅ 非空表的深度 = 1 + 所有子表中最大的深度 💡 关键:逐层拆解括号,统计最大嵌套次数。 | ✅ 长度统计范围:仅统计最外层括号内的元素数量。 ✅ 元素类型无关:无论是原子元素(如 a, 5)还是子表(如 (b, c)),每个元素均计为1个。 ✅ 空表特殊处理:空表 () 的长度为 0(因为括号内无元素)。 |
| 示例 | (a, (b, c)) 的深度=2 | (a, (b, c)) 的长度=2 |
常见误区
| (长度)错误理解 | 真相 |
| "所有元素的总个数" | ❌ 仅统计最外层元素,子表内的元素不计入长度 |
| "子表会影响长度" | ❌ 子表作为整体计为1个元素,无论其内部多复杂 |
| "空表长度为1" | ❌ 空表 () 长度为0(括号内无元素) |
| (深度)错误理解 | 真相 |
| "元素个数越多,深度越大" | ❌ 深度与元素个数无关,只与括号嵌套有关 |
| "只要存在子表,深度至少为2" | ❌ 若子表是空表 (),则深度仍为1 |
| "所有子表深度相同" | ❌ 不同子表可有不同的嵌套深度 |


例2:
- 因为b在表尾中,所以要取tail(LS1),注:tail(LS1)=((b,c),(d,e))
- head(tail(LS1))就相当于在一个新的元素中取表头:(b,c)
- head(head(tail(LS1)))就是取到b
7.09树与二叉树—树与二叉树的基本概念

(1) 结点:1、2、3、4、5…….都是结点
(2) 结点的度:指结点所拥有的孩子结点的数量(直接子节点的数量)。
例:1结点拥有2个孩子结点,那么度为2。
3的结点的度为1。
7是叶子结点,没有孩子结点,那么度就位0。
(3) 树的度:所有结点最高的度就是树的度【树中所有节点的度的最大值】。例如:在这棵树中最高的度为2度,则树的度就为2。
(4) 叶子结点:没有孩子结点的,在树的末端,就是叶子结点。例如:4、5、7、8
(5) 分支结点:有相应的分支,拥有子节点,度不为0的节点。例如:1、2、3、6
如果根节点有至少一个子节点(左或右) → 是分支节点。
如果根节点没有子节点(即整个树仅有根节点)→ 不是分支节点(此时根节点同时也是叶子节点)。
(6) 内部结点:不是根节点与叶子结点,夹在中间的结点。例如:2、3、6
【但是有的说,根节点不一定属于分支节点或内部节点,需看其是否有子节点】
(7) 根结点:最顶层的结点。例如:1
(8) 兄弟结点:同一个层次的。例如:4、5、6
7.10 树与二叉树—满二叉树与完全二叉树(重要)

(1)满二叉树:所有非叶子节点都有两个子节点,且所有叶子节点都在同一层。
(2)完全二叉树:除最后一层外,每一层都被完全填充,且最后一层的节点从左到右连续排列(中间无空缺)。
(3)非完全二叉树:不满足完全二叉树的条件,即:【某一层节点未完全填充】或 【最后一层节点未从左到右连续排列】
关键区别
满二叉树 vs 完全二叉树:
满二叉树要求所有叶子节点在同一层,完全二叉树允许最后一层不满但需连续。
满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
完全二叉树 vs 非完全二叉树:
完全二叉树的最后一层节点必须从左到右连续,非完全二叉树存在空缺。
应用场景:
满二叉树:理论模型(如哈夫曼树)。
完全二叉树:堆(Heap)、数组存储(如二叉堆)。
非完全二叉树:普通二叉搜索树(BST)、表达式树。
(4)树的深度,就是树的层次。例如:以上树的深度就位3。

背景知识
完全二叉树的层序编号规则:
- 根节点编号为 1,位于第 1 层;
- 第 k 层的节点从左到右依次编号,范围为 [2^{k-1}, 2^k - 1](若该层未满,编号连续但不一定填满)。
结论1:父节点规律
- 若 i=1,节点 i 是根,无父节点;
- 若 i>1,父节点为 ⌊i/2⌋(向下取整)。
举例(n=7的完全二叉树):
完全二叉树结构如下(层序编号):
层1: 1 (根) 层2: 2, 3 层3: 4, 5, 6, 7
- 节点 2 的父节点:⌊2/2⌋ = 1(正确,节点 1 是其父);
- 节点 3 的父节点:⌊3/2⌋ = 1(正确,节点 1 是其父);
- 节点 4 的父节点:⌊4/2⌋ = 2(正确,节点 2 是其父)。
结论2:左子节点规律
- 若 2i > n,节点 i 无左子节点(叶子节点);
- 否则,左子节点为 2i。
举例(n=7的完全二叉树):
- 节点 1 的左子节点:2×1=2 ≤7(存在,节点 2);
- 节点 2 的左子节点:2×2=4 ≤7(存在,节点 4);
- 节点 3 的左子节点:2×3=6 ≤7(存在,节点 6);
- 节点 4 的左子节点:2×4=8 >7(不存在,节点 4 是叶子)。
结论3:右子节点规律
- 若 2i+1 > n,节点 i 无右子节点;
- 否则,右子节点为 2i+1。
举例(n=7的完全二-binary tree):
- 节点 1 的右子节点:2×1+1=3 ≤7(存在,节点 3);
- 节点 2 的右子节点:2×2+1=5 ≤7(存在,节点 5);
- 节点 3 的右子节点:2×3+1=7 ≤7(存在,节点 7);
- 节点 4 的右子节点:2×4+1=9 >7(不存在,节点 4 是叶子)。
补充例子:n=5的完全二叉树
结构如下:
层1: 1 层2: 2, 3 层3: 4, 5
- 节点 3 的父节点:⌊3/2⌋ = 1(正确);
- 节点 3 的左子节点:2×3=6 >5(不存在);
- 节点 3 的右子节点:2×3+1=7 >5(不存在);
- 节点 4 的父节点:⌊4/2⌋ = 2(正确);
- 节点 4 的左子节点:8 >5(不存在)。
- 节点2的左子节点:2x2 = 4 ≤ 5【4为2的左子节点】
- 节点2的右子节点:2x2+1 = 5 ≤ 5【5为2的右子节点】
总结
完全二叉树的层序编号规律本质是二叉树结构的数学表达:
- 父节点公式 ⌊i/2⌋ 反映了“子节点编号是父节点的2倍”的关系;
- 左右子节点公式 2i 和 2i+1 则是对应“左子节点在前、右子节点在后”的层序规则。
这些规律使得完全二叉树可以用数组高效存储(无需指针),也是堆(Heap)数据结构的基础。
7.11 树与二叉树—二叉树遍历

可以分成两大组:
1、 前序、中序、后序遍历:具有相似性,均基于递归实现,区别就在于根节点什么时候被访问。【递归遍历】
(1) 前序遍历:(根左右)先访问根节点,递归遍历左子树,递归遍历右子树。
结果为:1、2、4、5、7、8、3、6
(2) 中序遍历:(左根右)递归遍历左子树,访问当前节点(根),递归遍历右子树。
结果为:4、2、7、8、5、1、3、6
(3) 后序遍历:(左右根)递归遍历左子树,递归遍历右子树,访问当前节点(根)。
结果为:4、8、7、5、2、6、3、1
2、 层次遍历:按层次依次进行遍历:从根节点开始,逐层向下访问;同一层的节点从左到右依次访问
结果为:1、2、3、4、5、6、7、8
7.12 树与二叉树—反向构造二叉树
要反向构造二叉树(即根据遍历序列重建原始二叉树),核心技巧是利用遍历序列的特性:前序/后序确定根节点,中序划分左右子树。其中,前序+中序或后序+中序是最常用的组合,因为它们能唯一确定二叉树(仅一种序列无法唯一确定)。


详解举例:
一、关键原理
- 前序遍历:第一个节点是根节点(Root)。
- 中序遍历:根节点左侧是左子树,右侧是右子树(Left Subtree → Root → Right Subtree)。
- 后序遍历:最后一个节点是根节点(Left Subtree → Right Subtree → Root)。
因此,前序+中序或后序+中序的组合,可通过“根节点定位+子树划分”递归重建二叉树。
二、具体步骤(以前序+中序为例)
假设给定:
- 前序序列:[1, 2, 4, 5, 3, 6, 7](根→左→右)
- 中序序列:[4, 2, 5, 1, 6, 3, 7](左→根→右)
步骤1:确定根节点
前序序列的第一个元素是根节点,即 1。
步骤2:在中序序列中划分左右子树
在中序序列中找到根节点 1 的位置,其左侧是左子树([4, 2, 5]),右侧是右子树([6, 3, 7])。
步骤3:递归处理左右子树
- 左子树:
前序子序列:[2, 4, 5](去掉根节点 1 后的剩余部分)
中序子序列:[4, 2, 5]
重复步骤1-2:前序第一个元素 2 是左子树的根,中序中 2 左侧是 4(左子树的左子树),右侧是 5(左子树的右子树)。- 右子树:
前序子序列:[3, 6, 7]
中序子序列:[6, 3, 7]
重复步骤1-2:前序第一个元素 3 是右子树的根,中序中 3 左侧是 6(右子树的左子树),右侧是 7(右子树的右子树)。步骤4:组装二叉树
最终重建的二叉树结构如下:
1 / \ 2 3 / \ / \ 4 5 6 7
三、后序+中序的示例
假设给定:
- 后序序列:[4, 5, 2, 6, 7, 3, 1](左→右→根)
- 中序序列:[4, 2, 5, 1, 6, 3, 7](左→根→右)
步骤1:确定根节点
后序序列的最后一个元素是根节点,即 1。
步骤2:在中序序列中划分左右子树
中序序列中 1 左侧是左子树([4, 2, 5]),右侧是右子树([6, 3, 7])。
步骤3:递归处理左右子树
- 左子树:
后序子序列:[4, 5, 2](去掉根节点 1 后的剩余部分)
中序子序列:[4, 2, 5]
后序最后一个元素 2 是左子树的根,中序中 2 左侧是 4,右侧是 5。- 右子树:
后序子序列:[6, 7, 3]
中序子序列:[6, 3, 7]
后序最后一个元素 3 是右子树的根,中序中 3 左侧是 6,右侧是 7。结果
与上述前序+中序重建的树一致。
四、注意事项
- 仅一种序列无法唯一确定二叉树:
例如,前序序列 [1, 2, 3] 可能对应多种二叉树(如 1→2→3 或 1→(2→3)),因为没有中序序列划分左右子树。- 输入有效性:
若中序序列中找不到前序/后序的根节点,说明输入序列无效(如遍历序列不匹配)。- 递归终止条件:
当子序列长度为0时,停止递归(表示空子树)。
7.13 树与二叉树—树转二叉树

转化过程:将第一个孩子结点变为左子树结点(2变为左子树结点),其余的2的兄弟结点都为右子树结点(3,4为右子树结点)。按照同样的规则依次向下排列。
7.14 树与二叉树—查找二叉树(排序二叉树)
二叉排序树是一种左子节点小于根节点、右子节点大于根节点的二叉树,支持高效的插入、删除和查找操作。
其核心性质是:对于任意节点 p,其左子树的所有节点值都小于 p 的值,右子树的所有节点值都大于 p的值。

一、插入节点操作
核心原则:保持BST性质(左子节点 < 父节点 < 右子节点),插入后树仍为BST。 步骤:
空树处理:若树为空,新节点直接作为根节点。
去重检查:若树中已存在相同键值的节点,不插入(避免重复)。
定位插入位置:从根节点开始,比较键值:
若新节点键值 < 当前节点键值,进入左子树;
若新节点键值 > 当前节点键值,进入右子树;
重复上述步骤,直到找到空位置(左/右子节点为空),将新节点插入该位置。
二、删除节点操作
核心原则:删除后保持BST性质,分三种情况处理:
叶子节点(无子节点):直接删除,不影响其他节点。
单子节点(仅有一个子节点):用该子节点代替被删除节点的位置(即子节点成为被删除节点的父节点的子节点)。
双子节点(有两个子节点):
找到左子树的最大值(中序前驱)或右子树的最小值(中序后继);
用该值替换被删除节点的值;
删除原替代节点(此时替代节点必为叶子节点或单子节点,转化为前两种情况)。
关键逻辑总结
插入:通过比较键值定位空位置,确保左<父<右;
删除:分三种情况简化操作,双子节点需用替代节点保持性质。 所有操作均严格遵循BST的“左小右大”规则,确保树的结构始终有效。
举例:
案例1:删除单子节点(40)
初始BST结构:
50 / \ 30 70 \ 40 \ 45删除步骤:
节点40有右子节点45(单子节点);
将45提升为30的右子节点(取代40的位置)。
删除后BST结构:
50 / \ 30 70 \ 45
案例2:删除双子节点(根节点60)
1. 构造初始BST
假设初始树结构如下(键值满足左<父<右):
60 (根节点,待删除,有双子节点40和80) / \ 40 80 / \ \ 30 50 90 \ 55 (左子树最大值,因50的右子树为55)2. 删除步骤(用左子树最大值替代)
步骤1:找到左子树最大值 左子树(40)的最大值为55(50的右子树,无更大值)。
步骤2:用55替换60 将根节点60的值改为55,树结构暂时变为:
55 / \ 40 80 / \ \ 30 50 90 \ 55 (原55节点保留,需删除)
步骤3:删除原55节点 原55节点是50的右子节点(叶子节点),直接删除。最终树结构为:
55 / \ 40 80 / \ \ 30 50 903. 验证BST性质
左子树:40的左子节点30(30<40)、右子节点50(50>40),符合左<父<右;
右子树:80的右子节点90(90>80),符合左<父<右;
根节点55:左子树最大值50(50<55)、右子树80(55<80),整体满足BST规则。
7.15 树与二叉树—最优二叉树(哈夫曼树)

1、 最优二叉树(哈夫曼树):
在权为(wl,w2,…,wn)的n个叶子所构成的所有二叉树中,带权路径长度最小(即代价最小)的二叉树称为最优二叉树或哈夫曼树。
2、 路径长度:
在树中从一个结点到另一个结点所经历的分支构成了这两个结点间的路径上的分支数称为它的路径长度。
3、 树的路径长度:
树的路径长度是从树根到树中每一结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
4、树的带权路径长度(Weighted Path Length of Tree,简记为WPL)亦称为树的代价:是叶子结点的带权路径长度之和。(只考虑叶子结点)
(1)结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数。
例如:结点2的权就为2,结点4的权就为4。
(2) 结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积。
例如:结点2的带权路径长度:2 * 2=4;结点4的带权路径长度:3 * 4=12
要构造哈夫曼树(最优二叉树),核心是通过贪心算法让带权路径长度(WPL)最小化。以下是口诀+具体步骤+举例的完整指南:
一、哈夫曼树的核心口诀
小权合并,循环往复,直到根现 (每次选权重最小的两个节点合并,重复此过程,直到只剩一个根节点。)
二、构造步骤(以用户提供的权值为例)
给定权值:
5, 29, 7, 8, 14, 23, 3, 11(共8个节点) 目标:构造带权路径长度最小的二叉树【哈夫曼树】。步骤 1:初始权值排序
首先将 8 个初始权值按从小到大排序,方便每次选取最小的两个节点:
初始权值集合:{5,29,7,8,14,23,3,11}
排序后集合:S0 = {3,5,7,8,11,14,23,29}步骤 2:第 1 次合并(选最小的 2 个节点:3 和 5)
- 合并节点:3(叶子)、5(叶子)
- 新节点权值:3 + 5 = 8
- 移除原节点 3、5,加入新节点 8
- 新集合(排序后):
S1 = {7,8,8,11,14,23,29}步骤 3:第 2 次合并(选最小的 2 个节点:7 和 8)
- 合并节点:7(叶子)、8(第 1 次合并的新节点)
- 新节点权值:7 + 8 = 15
- 移除原节点 7、8,加入新节点 15
- 新集合(排序后):
S2 = {8,11,14,15,23,29}步骤 4:第 3 次合并(选最小的 2 个节点:8 和 11)
- 合并节点:8(初始叶子)、11(初始叶子)
- 新节点权值:8 + 11 = 19
- 移除原节点 8、11,加入新节点 19
- 新集合(排序后):
S3 = {14,15,19,23,29}步骤 5:第 4 次合并(选最小的 2 个节点:14 和 15)
- 合并节点:14(初始叶子)、15(第 2 次合并的新节点)
- 新节点权值:14 + 15 = 29
- 移除原节点 14、15,加入新节点 29
- 新集合(排序后):
S4 = {19,23,29,29}步骤 6:第 5 次合并(选最小的 2 个节点:19 和 23)
- 合并节点:19(第 3 次合并的新节点)、23(初始叶子)
- 新节点权值:19 + 23 = 42
- 移除原节点 19、23,加入新节点 42
- 新集合(排序后):
S5 = {29,29,42}步骤 7:第 6 次合并(选最小的 2 个节点:29 和 29)
- 合并节点:29(第 4 次合并的新节点)、29(初始叶子)
- 新节点权值:29 + 29 = 58
- 移除原节点 29、29,加入新节点 58
- 新集合(排序后):
S6 = {42,58}步骤 8:第 7 次合并(最后 2 个节点:42 和 58)
- 合并节点:42(第 5 次合并的新节点)、58(第 6 次合并的新节点)
- 新节点权值:42 + 58 = 100(此为根节点)
- 移除原节点 42、58,集合仅剩根节点 100
- 哈夫曼树构造完成!
100(根) / \ 42 58 / \ / \ 19 23 29 29 / \ / \ 8 11 15 14 / \ 7 8 / \ 3 5带权路径长度(WPL)是所有叶子节点的权值 × 该节点到根节点的路径长度(边的数量) 之和,是衡量哈夫曼树 “最优性” 的核心指标。
步骤 1:确定每个叶子节点的路径长度(根到叶子的边数)
- 3:路径「100→58→29→15→8→3」→ 路径长度 = 5
- 5:路径「100→58→29→15→8→5」→ 路径长度 = 5
- 7:路径「100→58→29→15→7」→ 路径长度 = 4
- 8(初始叶子):路径「100→42→19→8」→ 路径长度 = 3
- 11:路径「100→42→19→11」→ 路径长度 = 3
- 14:路径「100→58→29→14」→ 路径长度 = 3
- 23:路径「100→42→23」→ 路径长度 = 2
- 29(初始叶子):路径「100→58→29」→ 路径长度 = 2
步骤 2:计算 WPL
WPL=(3×5)+(5×5)+(7×4)+(8×3)+(11×3)+(14×3)+(23×2)+(29×2)=15+25+28+24+33+42+46+58=271
最终结论
- 最小带权路径长度(WPL):271
- 关键说明:哈夫曼树的构造结果可能因相同权值合并顺序不同而结构不同,但最小WPL值唯一(此处为271)。
四、哈夫曼树的应用
哈夫曼树主要用于数据压缩(如Huffman编码),通过将高频字符用短编码表示,低频字符用长编码表示,大幅减少数据传输量。
总结
口诀:小权合并,循环往复,直到根现;
关键:贪心选择最小权值合并,保证总路径长度最小;
验证:计算WPL确认最优性。
通过以上步骤,可快速构造任意权值的哈夫曼树,并理解其最优性的本质。
7.16 树与二叉树—线索二叉树

1、 概念:
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
2、 为什么要有线索二叉树:
线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。
3、 例:
以前序线索二叉树为例:
(1) 先把前序二叉树进行前序排列(根左右):ABDEHCFGI
(2) 所以D的前序为B,D的后序为E。H的前序为E,H的后序为C。后面同理可证。【只有当前节点的左子节点或者右子节点为空的时候,当前节点才可以被线索化】

详细介绍如下:
一、背景介绍
线索化二叉树就可以将这些浪费的指针域空间给利用起来
如上图所示,是一个二叉树。可以看到,每一个节点都有三个元素:左子指针域、右子指针域、值域。
对于存在左右子树的节点,其左右指针域指向的分别是各自的左右子节点;
而对于未存在左子树,或者未存在右子树,或者左右子树均未存在的节点,该节点的左子指针域、右子指针域、左右指针域就会指向为空,此时就会存在指针域空间浪费的情况。
可以更加快速和便捷地查找到某个节点的前驱和后继节点。
二、对二叉树进行线索化的逻辑
二叉树的线索化,实际上就是将某个节点的未利用到的指针域空间指向某种遍历次序(前序、中序、后序)下的前驱或者后继节点。如下图所示,实线指向的就是子节点,虚线指向的就是前驱结点或者后序节点:
要注意的是,前驱或者后继的指向是跟遍历次序有关的,某一个节点的前驱和后继在不同的遍历次序下是不一样的。
比如,在前序、中序以及后序遍历的次序下,某个节点的前驱或者后继的指向都是不一样的。
线索化的目的其实就是为了能够快速找到某一个节点的前驱和后继节点。
与一般的二叉树相比,线索化的二叉树无非就是将空余的指针域给利用了起来,然后将这些空余的指针域指向某种遍历次序下的前驱或者后继。
并不是所有的二叉树节点都可以线索化的,只有当前节点的左子节点或者右子节点为空的时候,当前节点才可以被线索化。
若某节点的左子树为空,则该节点的左子指针域指向前驱节点;
若某节点的右子树为空,则该节点的右子指针域指向后继节点。
如上图所示,是中序遍历次序下的二叉树中各个前驱后继指针的指向
实际上,我们在对二叉树进行线索化的时候,肯定是需要进行一次遍历的,分析遍历到的每一个节点,如果有无用的指针域,那么就为其设置前驱或者后继。
7.17 树与二叉树—平衡二叉树

平衡因子(Balance Factor, BF):
每个节点的左子树高度减去右子树高度的差值。
平衡二叉树要求:所有节点的BF绝对值 ≤ 1(即 −1,0,1−1,0,1)。
失衡节点: 插入/删除后,BF绝对值 > 1 的节点,需通过旋转操作恢复平衡。
平衡二叉树最少节点递推公式
设 N(h) 表示高度为 h的平衡二叉树的最少节点数。
基础情况:
N(0)=0(高度为0,是空节点)。
N(1)=1(高度为1,根节点)。
N(2)=2(高度为2,根节点和一个子节点)。
对于h≥2,递推关系为:N(h)=N(h−1)+N(h−2)+1
这是因为为了最小化节点数,同时满足平衡条件,
一个子树的高度应为h−1,另一个子树的高度应为h−2
(高度差不超过1),根节点加上这两个子树的节点数即为总节点数。
7.18 图—图的基本概念与存储
1、图的基本概念

2、图的存储—邻接矩阵

3、图的存储—邻接表

例如:第一个v1到v2为6,v1到v4,为1,v1到v6,为50
7.19 图—图的遍历
- DFS 像“探险家”: 选一条路,走到尽头再回头换条路。适合需要穷举所有可能或深入探索的场景。
- BFS 像“扫雷”: 从中心点开始,一圈一圈地向外排查。适合需要找“最近”或“最短”的场景。

1. 深度优先搜索 (DFS - Depth-First Search)
核心思想: 一条路走到黑,不撞南墙不回头。
从一个起始顶点出发,沿着一条路径尽可能深地探索下去,直到无法再前进(到达了没有未访问邻居的顶点)。
然后,回溯到上一个顶点,尝试探索它的另一条未访问的路径。
重复这个过程,直到所有从起始顶点可达的顶点都被访问过。
数据结构: 栈 (Stack)。无论是使用递归(隐式调用栈)还是显式使用栈数据结构,其“后进先出”的特性完美契合了“深入探索”和“回溯”的行为。
过程比喻:
想象你在迷宫里。你选择一个方向一直走,遇到岔路口就选一个新方向继续深入。当你走进死胡同时,你原路返回到上一个岔路口,再尝试另一个没走过的方向。直到你探索完所有能到达的地方。
2. 广度优先搜索 (BFS - Breadth-First Search)
核心思想: 层层推进,由近及远。
从一个起始顶点出发,先访问它的所有直接邻居(第一层)。
然后,再访问这些邻居的所有未被访问过的邻居(第二层)。
接着访问第三层、第四层……像水波一样一圈一圈地向外扩散,直到所有从起始顶点可达的顶点都被访问过。
数据结构: 队列 (Queue)。其“先进先出”的特性确保了先被发现的顶点(离起点近)会先被处理,从而实现“层序”遍历。
过程比喻:
想象你在迷宫里。你先标记起点,然后查看起点所有相邻的房间(第一层),把它们都标记为待探索。然后你依次进入这些房间,每个进入后,再查看它们所有相邻的、还没被标记过的房间(第二层),加入待探索列表。你总是先探索离起点最近的一圈房间,再探索更远的一圈。
通过邻接表+可视化图的组合,展示了:
图的存储结构(邻接表)如何支持遍历算法;
遍历过程中,“访问邻居”的操作是如何基于邻接表执行的;
不同遍历算法(DFS/BFS)因规则不同(栈vs队列),会产生截然不同的访问顺序。
图的遍历(DFS/BFS)的本质是从一个顶点出发,按照某种规则访问所有可达的顶点。而邻接表的作用是:告诉算法“当前顶点有哪些邻居可以访问”。
遍历的核心逻辑:基于邻接表访问邻居

以深度优先搜索(DFS)为例(假设从
v0开始):假设邻接表中邻居按从小到大的顺序排列(即每个顶点的邻接表是升序的),DFS的具体步骤如下:
访问
v0,标记为已访问;
查看
v0的邻接表(4, 3, 1),选择第一个邻居v4;
访问
v4,标记为已访问;
查看
v4的邻接表(6, 1, 0),选择第一个未访问的邻居v6;
访问
v6,标记为已访问;
查看
v6的邻接表(7, 4, 3),选择第一个未访问的邻居v7;
访问
v7,标记为已访问;
v7的邻接表只有6(已访问),回溯到v6,选择下一个未访问的邻居v4(已访问)、v3;
访问
v3,标记为已访问;
v3的邻接表是6, 0(均访问过),回溯到v6→v4→v0,选择v0的下一个邻居v3(已访问)、v1;
访问
v1,标记为已访问;
v1的邻接表是4, 2, 0,v4和v0已访问,选择v2(未访问)。
访问
v2,标记为已访问
v2的邻接表:[5, 1](升序),v1已访问,选择v5(未访问)。
访问
v5,标记为已访问
v5的邻接表:[2](仅v2,已访问),回溯结束。
最终DFS的访问顺序可能是:
v0 → v4 → v6 → v7 → v3 → v1 → v2 → v5(具体顺序取决于邻接表中邻居的选择顺序,比如是否按从小到大选)。
对于广度优先搜索(BFS),逻辑类似,但使用队列代替栈:
初始化:
队列:
[v0](将起始节点v0入队)
visited:[true, false, false, false, false, false, false, false](v0已访问)第1次循环(处理
v0):
出队
v0,访问v0。检查v0的邻居[4, 3, 1],均为未访问节点,依次入队并标记:
队列:
[v4, v3, v1]
visited:[true, true, false, true, true, false, false, false](v4, v3, v1已访问)第2次循环(处理
v4):
出队
v4,访问v4。检查v4的邻居[6, 1, 0]:1和0 已访问,仅v6未访问,入队并标记:
队列:
[v3, v1, v6]
visited:[true, true, false, true, true, false, true, false](v6已访问)第3次循环(处理
v3):
出队
v3,访问v3。检查
v3的邻居[6, 0]:6和0均已访问,无新节点入队。队列:
[v1, v6]第4次循环(处理
v1):
出队
v1,访问v1。检查v1的邻居[4, 2, 0]:4和0已访问,仅v2未访问,入队并标记:
队列:
[v6, v2]
visited:[true, true, true, true, true, false, true, false](v2已访问)第5次循环(处理
v6):
出队
v6,访问v6。检查v6的邻居 [7, 4, 3]:4 和 3已访问,仅v7未访问,入队并标记:
队列:
[v2, v7]
visited:[true, true, true, true, true, false, true, true](v7已访问)第6次循环(处理
v2):
出队
v2,访问v2。检查v2的邻居[5, 1]:1已访问,仅v5未访问,入队并标记:
队列:
[v7, v5]
visited:[true, true, true, true, true, true, true, true](v5已访问)第7次循环(处理
v7):
出队
v7,访问v7。检查
v7的邻居[6]:6已访问,无新节点入队。队列:
[v5]第8次循环(处理
v5):
出队
v5,访问v5。检查
v5的邻居[2]:2已访问,无新节点入队。队列:
[](空)BFS 最终访问顺序:v0 → v4 → v3 → v1 → v6 → v2 → v7 → v5
关键说明
层次性:BFS严格按“距离起始节点的层数”访问节点。例如:
第1层:
v0的邻居v4, v3, v1;第2层:
v4的邻居v6、v1的邻居v2;第3层:
v6的邻居v7、v2的邻居v5。最短路径:在无权图中,BFS找到的从
v0到任意节点的路径,一定是边数最少的路径(如v0→v1→v2→v5是v0到v5的最短路径,边数为3)。完整性:由于图是连通的(所有节点可通过边连接),BFS会访问所有8个节点,无遗漏。
7.20 图—拓扑排序

7.21 图—图的最小生成树(普利姆算法与克鲁斯卡尔算法)
注:图和树的区别:图形成了环路,而树没有环路。
Prim算法和Kruskal算法介绍 - LeftBody - 博客园
1、 普利姆算法(Prim)
选择一个节点开始,从最小的数【最近的路径】出发:比如A进入集合U,剩下的集合的A-U包括剩下的节点,然后寻找从集合U到集合A-U最近的路径。
【与图中边数无关,因此适合于稠密图】


2、克鲁斯卡尔算法(Kruskal)
从边出发【简单来说每次都选择最短的路径作为边,连接俩个节点】:首先n个顶点分别视为n个连通分量,然后选择一条权重最小的边,如果边的两端分属于两个连通分量,就把这个边加入集合E,否则舍去这条边而选择下一条代价最小的边,依次类推,直到所有节点都在同一个连通分量上。
【与图中边数有关,因此适合求稀疏图的最小生成树】


7.22 算法基础—算法特性

7.23算法基础—算法的时间复杂度与空间复杂度
- 时间复杂度:衡量算法执行所需基本操作次数的增长趋势(如比较、赋值、算术运算等),用大O符号表示(如 O(n)、O(n2)、其中o(1)是常量 )。
- 空间复杂度:衡量算法执行所需的额外内存空间的增长趋势(不包括输入数据本身的大小),同样用大O符号表示。

public class LoopExample {
public static void main(String[] args) {
int n = 5; // 输入规模(可调整)
printMultiplicationTable(n); // 调用函数打印乘法表
}
// 打印 n×n 乘法表的函数
public static void printMultiplicationTable(int n) {
for (int i = 1; i <= n; i++) { // 外层循环:执行 n 次
for (int j = 1; j <= n; j++) { // 内层循环:每次执行 n 次
System.out.println(i + " × " + j + " = " + (i * j)); // 基本操作(打印)
}
}
}
}
1. 时间复杂度分析
时间复杂度衡量算法执行基本操作次数的增长趋势(如打印语句、赋值操作等)。
- 外层循环 for (int i = 1; i <= n; i++) 执行 n 次;
- 内层循环 for (int j = 1; j <= n; j++) 每次执行 n 次;
- 因此,总操作次数 = 外层次数 × 内层次数 = n×n=n²。
结论:时间复杂度为 O(n²)(读作“大O n平方”)。
- 含义:当输入规模 n 增大时,操作次数随 n²增长(如 n=10 时操作 100 次,n=100时操作 10000 次)。
2. 空间复杂度分析
空间复杂度衡量算法执行所需的额外内存空间(不包括输入数据本身)。
- 该函数中,仅使用了循环变量 i 和 j,占用的内存空间是固定大小的(无论 n 多大,只需存储两个整数);
- 没有使用额外的数组、列表或其他动态数据结构。
结论:空间复杂度为 O(1)(读作“大O 1”)。
- 含义:额外内存空间不随输入规模 n 变化,始终保持常数级别(如仅占用几十字节)。
总结:时间与空间的权衡
复杂度类型
分析依据
结果
含义
时间复杂度
双重循环的总操作次数
O(n²)
操作次数随 n²增长,效率较低(适用于小规模 n)。
空间复杂度
额外使用的内存空间
O(1)
内存占用固定,不随 n变化,非常节省空间。
7.24 查找—顺序查找与二分查找
1、顺序查找(效率不高)

注:n是指比对了n次,也就是第n个元素。上表中平均查找长度为(8+1)/2=4.5
2、 二分查找
前置条件:从大到小或从小到大,是有序的
注:若中间位置mid=(low+high)/2=小数的话,直接取整数,例6.5=6
第一次中间位置为6,那么第二次1-5,中间位置为3



7.25查找—散列表
散列表(哈希表)的核心挑战是冲突解决——当两个键通过哈希函数映射到同一位置时,如何处理。线性探测法和伪随机数法都属于开放寻址法(Open Addressing),即所有元素都存储在哈希表本身的数组中,通过探查其他位置来解决冲突。以下是两者的详细解析:
| 维度 | 线性探测法 | 伪随机数法 |
| 探查序列 | 线性递增(h(k)+1,h(k)+2,…h(k)+1,h(k)+2,…) | 伪随机数(h(k)+r1,h(k)+r2,…h(k)+r1,h(k)+r2,…) |
| 聚集问题 | 易产生一次聚集(连续槽位占用) | 减少一次聚集,但仍可能二次聚集 |
| 实现复杂度 | 简单(仅需加减运算) | 较复杂(需伪随机数生成器) |
| 性能(高负载) | 急剧下降(探查次数多) | 优于线性探测法(探查更分散) |
| 缓存友好性 | 极佳(数组连续) | 极佳(数组连续) |
总结
线性探测法:适合小型、低负载的哈希表,实现简单但易聚集;
当发生冲突时,线性地依次探查下一个槽位,直到找到空位或确定无空位
线性探测法是“顺序找”:冲突后往下一个位置放(比如位置1满了,就放位置2,再满放位置3……)。
伪随机数法:适合大型、高负载的哈希表,性能更好但实现较复杂。
而伪随机数法是“随机找”:冲突后不按顺序,而是用“随机数”选下一个位置(比如位置1满了,随机选位置3放;下次冲突再随机选位置5放……)。

按此规则,如果要存储3和8,各自对5求余,都得到余数3,这时就涉及到了两种方法:线性探测法和伪随机数法。
线性探测法(顺序找):三号空间被占,则将8放在三号后面的空间,即四号空间。12在二号空间,此时17本应在二号空间,被占向后移,也被占,以此类推,被存到五号空间。这又使得整体效率降低了。
伪随机数法(随机找):当发生地址冲突时候,地址增量为伪随机数序列。
例如:伪随机数生成器产生的序列为 r1=3,r2=5,r3=7,…
若插入key=8时,发现位置3已经被3占了,那么采用伪随机数 r1=3,探查位置 3+3=6,将8放在6的位置,以此类推,直到找到空位
相比线性探测法,冲突后的探查位置更分散,减少了聚集。
7.26 排序

7.27 直接插入排序

7.28 希尔排序
是插入排序的一种,但比直接插入排序效率要高:简单来说就是分组进行【直接插入排序】

例如:
(1)d1=5,表示每隔5个数,是一个组。每个组内进行比较(57,28)、(68,96)......
(2)d2=3,每隔3个数为一组,每个组内进行比较
(3)d3=1,每隔1个数为一组,也就是说当dt=1的时候,就直接放在同一个组中进行直接插入排序
7.29 直接选择排序
- “从头到尾找最小”:从当前元素开始,往后找最小的;
- “找到之后放前头”:把最小的放到当前的位置;
- “重复直到排完序”:对剩下的元素重复上述步骤,直到所有元素都处理完。

一、直接选择排序的核心思想
直接选择排序的核心是 “找最小,放前面”:
- 遍历数组:从第一个元素开始,依次往后看;
- 找最小:在当前及后面的元素中,找出最小的那个;
- 交换:把这个最小的元素,放到当前的位置;
- 重复:对剩下的元素重复上述步骤,直到数组排好序。
二、举个例子(用数组 [64, 34, 25, 12, 22, 11, 90])
我们一步步来看排序过程:
- 第一步:找整个数组的最小元素(11),把它和第一个元素(64)交换,得到 [11, 34, 25, 12, 22, 64, 90];
- 第二步:找剩下元素(34,25,12,22,64,90)的最小元素(12),和第二个元素(34)交换,得到 [11, 12, 25, 34, 22, 64, 90];
- 第三步:找剩下元素(25,34,22,64,90)的最小元素(22),和第三个元素(25)交换,得到 [11, 12, 22, 34, 25, 64, 90];
- 第四步:找剩下元素(34,25,64,90)的最小元素(25),和第四个元素(34)交换,得到 [11, 12, 22, 25, 34, 64, 90];
- 第五步:找剩下元素(34,64,90)的最小元素(34),和第五个元素(34)交换(不用动);
- 第六步:找剩下元素(64,90)的最小元素(64),和第六个元素(64)交换(不用动);
- 第七步:最后一个元素(90)自然有序,排序完成!
7.30 堆排序
堆排序是一种基于堆数据结构的排序算法,利用堆的“堆序性质”(大顶堆或小顶堆)来实现排序。
- 大顶堆:每个父节点的值 ≥ 其左右孩子的值(根节点是最大值);
- 小顶堆:每个父节点的值 ≤ 其左右孩子的值(根节点是最小值)。





7.31 冒泡排序

7.32 快速排序
快速排序的核心是 “分而治之”:
- 选基准:从数组中选一个元素作为“基准”(比如第一个元素);
- 划分:把数组分成两部分——左边全是比基准小的,右边全是比基准大的;【注意:划分的时候要注意指针,与元素交换,两针相遇就停下】
- 递归:对左右两部分分别重复上述步骤,直到所有子数组只剩1个元素(自然有序)。

7.33 归并排序
归并排序属于分治算法,就像“把蛋糕切成小块,再按顺序拼回去”:
- 拆分:把数组不断分成两半,直到每个子数组只剩1个元素(1个元素天然有序);
- 合并:把两个有序的子数组合并成一个新的有序数组,重复直到所有子数组合并成一个完整数组。

第一步:拆分成小数组 图片第一行用红框标出了4个子数组:
[57,68]、[52,59]、[28,72]、[33,96]。这些是长度为2的子数组,说明在拆分阶段,数组被分成了多个“小块”。第二步:对小数组排序 这些长度为2的子数组已经是有序的(因为1个元素天然有序,2个元素只需比较一次):
[57,68]:57 < 68,已排序;
[52,59]:52 < 59,已排序;
[28,72]:28 < 72,已排序;
[33,96]:33 < 96,已排序。第三步:合并小数组成大数组【进行排序】 下一步是将这些长度为2的子数组合并成更大的有序数组(比如长度为4),再继续合并直到整个数组有序。
7.34 基数排序

- “按位排”:按关键字的每一位(个位、十位、百位……)依次排序;
- “从个到高”:通常从最低位(个位)开始,逐步到最高位;
- “每位排,收集好”:每次按某一位排序后,把元素重新收集起来,作为下一次排序的输入。
7.35 排序算法的时间与空间复杂度
使用场景: “有序用插入,逆序用快排;随机选归并,稳定靠归并;小数据插冒,大数据归基。”

文档说明:希赛教育王勇老师软件设计师教学课程,这里学习整理后进行分享



1万+

被折叠的 条评论
为什么被折叠?



