数据结构 =(D,S,Op)
带结构的数据元素的集合
D为数据结构集合;S为D上的关系;Op为定义在D上的运算。
逻辑结构:数据元素之间的逻辑关系,与计算机无关。
逻辑结构=(D,S)
数据的逻辑结构分为 集合、线性、树、图
一种抽象数据类型包括 数据描述和操作声明
数据项是数据的最小单位
数据元素是数据的基本单位
.
.
.的优先级高于
∗
*
∗和
−
>
->
−>
[ ]的优先级高于
∗
*
∗
!
=
!=
!=和
=
=
==
==优先级高于位操作
算术运算高于位运算
逗号在所有运算符中优先级最低
函数()优先级高于
∗
*
∗
分类
- 集合结构:数据元素除属于同一集合外,无其他的关系。——成员关系
- 线性结构:数据元素之间存在一对一的关系。——长幼关系
- 树型结构:数据元素之间存在一对多的关系。——管理关系
- 图形结构:数据元素之间存在多对关系。——朋友关系
操作集合
- 构造函数、析构函数
- 询问类操作:判空、求长度...
- 查找类操作:查找某个元素的位置、遍历...
- 添加和删除类操作:添加一个数据元素、删除一个数据元素、置空...
指数据的逻辑结构在计算机存储器种的映像表示。
- 顺序存储:数据元素依次放在连续的存储单元中。
- 链式存储:在存储结点中增加若干指针域,记录后继或者相关结点的地址(指针)。
- 索引存储:将数据元素分为若干子表,子表的开始位置存放在索引表中。
- 散列存储:根据数据元素的关键字值,由散列函数计算出存储地址。
概念:建立在数据结构基础上的、求解问题的一系列确切的步骤。
必须满足的五个特征:
- 有穷性:对任何合法输入执行有穷步后能结束。
- 确定性:每条指令必须有确切的含义。
- 可行性:算法的每一条指令均能执行。
- 输入:有零个或多个输入。
- 输出:有一个或多个输出。
评价算法性能的标准
正确性、可读性、健壮性、高效率与低存储量需求,时空复杂度。
算法的事前估计
空间复杂度
S(n)= θ \theta θ(f(n))表示随着问题规模n的增大,算法运行所需存储量的增长率与f(n)的增长率相同。
原地算法
当某个算法空间复杂度为O(1)时, 称该算法为原地算法。
存储密度
d=数据本身存储量/实际所占存储量
时间复杂度
算法中各语句的频度之和T(n)。
频度——语句的执行次数。
n——问题的规模,一般为数据的输入量。
注意:复杂语句应该换算成简单语句再计算语句频度!
运行时间
程序步:语法上或语义上有意义的一段指令序列、执行时间与实例特性无关
例如:注释和声明语句的程序步数为0,表达式的程序步数为1。
渐进时间复杂度
当问题的规模n趋于无穷大时,T(n)的数量级记为:T(n)= θ \theta θ(f(n))
实际上,现实中使用渐进时间复杂度作为时间复杂度,用以描述算法的时间特性
1. O(1) - 常数时间复杂度,表示算法的运行时间与输入规模无关,是最高效的。 2. O(log n) - 对数时间复杂度,通常出现在二分查找等分治算法中。 3. O(n) - 线性时间复杂度,运行时间与输入规模成线性关系。 4. O(n log n) - 线性对数时间复杂度,常见于快速排序和归并排序等分治算法。 5. O(n^2) - 平方时间复杂度,常见于简单的嵌套循环算法。 6. O(2^n) - 指数时间复杂度,通常表示指数级的增长,效率非常低下。
平均时间复杂度
对算法在所有情况下的时间复杂度,按照等概率情况,取算术平均值(数学期望)。
分摊时间复杂度
将M个操作的总工作量按照平均分摊到每个操作上。
第二章 线性表
由n个数据元素组成的有序序列。LInear_list=(D,S)
其中:D={ai | ai
∈
\in
∈D0 , i=1,… , n;n
≥
\geq
≥ 0}
R={N} N={<ai , ai+1>| ai, ai+1 D0 , i=1,2, , n }
D0为某个数据对象
或者简记为:(a1, …, ai , …an ) n0
(n为表长。 当n=0,称为空表)
特点:在数据元素的非空有限集中
数据元素间是线性关系,数据元素在表中的位置只取决于其序号
存在唯一的一个被称为第一个的元素
创建表时要留出冗余的空间以应对特殊情况,具体多少要经过估算。
动态内存分配
malloc,free
常见错误:
未检查内存是否成功分配
使用malloc后立刻用一个指针指向它,随后操作新指针,不去动原指针,便于free
顺序表的优点
1.逻辑相邻,物理相邻
2.可随机存取任一元素
3.存储空间使用紧凑
缺点
插入、删除操作需要移动大量的元素
预先分配空间需按最大空间分配,利用不充分
表容量扩充困难
在线性表的散列存储中,处理冲突的常用方法有开放定址法和链接法
单链表
:此链表的每个节点中只包含一个指针域。逻辑上相邻的两个数据元素物理位置不要求紧邻。
头节点的作用:插入和删除第一个数据元素时不必对头指针进行特殊处理。
节点类型的定义:
typedef struct node
{
Elemtype data;
struct node *next;
}Node
链表中间插入元素:
p->next=s,s->next=p->next
头插法 O(n)
尾插法 O(
n
2
n^2
n2)
单循环链表
循环链表的类型定义与单链表相同
第三章1 栈
后缀表达式
后缀表达式
后缀表达式(也称为逆波兰表达式)是一种数学表达式的表示方法,其中操作符在操作数之后。相比于常见的中缀表达式(例如,1 + 2 * 3),后缀表达式的计算更直接,不需要括号或优先级规则。
例如,中缀表达式 “3 + 4 * 2 / (1 - 5)” 可以转换为后缀表达式 “3 4 2 * 1 5 - / +”。要计算后缀表达式,可以使用栈来执行操作。
下面是一个用栈计算后缀表达式的简单示例:
给定后缀表达式: “3 4 2 * 1 5 - / +”
1. 从左到右遍历后缀表达式的每个元素。
2. 如果当前元素是操作数,则将其压入栈。
3. 如果当前元素是操作符,则从栈中弹出相应数量的操作数,执行该操作符的运算,并将结果压入栈中。
4. 最终,栈中的唯一元素就是后缀表达式的计算结果。
针对上述示例后缀表达式的计算过程如下:
- 遇到 “3”:将其压入栈:[3]
- 遇到 “4”:将其压入栈:[3, 4]
- 遇到 “2”:将其压入栈:[3, 4, 2]
- 遇到 " * ":弹出 “2” 和 “4”,计算 (4 * 2 = 8),将结果 (8) 压入栈:[3, 8]
- 遇到 “1”:将其压入栈:[3, 8, 1]
- 遇到 “5”:将其压入栈:[3, 8, 1, 5]
- 遇到 “-”:弹出 “5” 和 “1”,计算 (1 - 5 = -4),将结果 (-4) 压入栈:[3, 8, -4]
- 遇到 “/”:弹出 (-4) 和 (8),计算 (8 / (-4) = -2),将结果 (-2) 压入栈:[3, -2]
- 遇到 “+”:弹出 (-2) 和 (3),计算 (3 + (-2) = 1),将结果 (1) 压入栈:[1]
最终栈中的唯一元素 (1) 就是后缀表达式 “3 4 2 * 1 5 - / +” 的计算结果。
递归
⊙
\odot
⊙ 递归算法的实现原理
利用栈,栈中每个元素称为工作记录,分成三个部分:返回地址 实在参数表(变参和值参) 局部变量
发生调用时,保护现场,即当前工作记录入栈,然后转入被调用的过程
一个调用结束时,恢复现场,即若栈不空,则退栈,从退出的返回地址处继续执行下去
⊙
\odot
⊙ 递归算法的用途
求解递归定义的数学函数
在以递归方式定义的数据结构上的运算/操作
可用递归方式描述的解决过程
⊙
\odot
⊙ 递归转为非递归的方法
递归——从顶到底 迭代——从底到顶
⊙ \odot ⊙
第三章2 队列
队列是一种特殊的线性表,限定插入和删除操作分别在表的两端进行。具有先进先出(FIFO)的特点。
⊙ \odot ⊙ 定义在队列结构上的基本运算
- 构造空队列操作
- 判队空否函数
- 元素入队操作
- 元素出队操作
- 取队头元素函数
- 队列置空函数
- 求队中元素个数函数
⊙
\odot
⊙ 进队时队尾指针先加一 rear = rear + 1 ,再将新元素按rear指示位置加入
出队时队头指针先加一 front = front + 1 ,再将front指示的元素取出
队满时再进队将溢出出错;队空时再出队将队空处理
循环队列
将顺序队列的存储区假象为一个环状空间。
⊙
\odot
⊙ 类型定义
一维数组(队列空间)+头指针+尾指针
为解决判断队列的满与空问题:牺牲一个存储单元
⊙
\odot
⊙ 出队: front = (front + 1) % maxsize
⊙
\odot
⊙ 入队: rear = (rear + 1) % maxsize
⊙
\odot
⊙ 队列初始化:front = rear = 0
⊙
\odot
⊙ 队空条件:front == rear
⊙
\odot
⊙ 队满条件:(rear + 1) % maxsize == front
⊙
\odot
⊙ 当前元素个数n:n = (rear - front + maxsize) % maxsize
双端队列:限定插入和删除操作在线性表的两端进行。
用链接方式存储的队列,在进行插入运算时头、尾指针可能都要修改
第四章 字符串
⊙
\odot
⊙ C语言:以\0结尾的字符数组
⊙
\odot
⊙ C++:在C语言的基础上,新增了std::string类
⊙
\odot
⊙ 串是特殊的线性表,数据元素是单个字符
串长:串中字符的个数n。
子串和主串:串中任意个连续的字符组成的子序列称为该串的子串。包含子串的串称为主串。
串相等:两个串长度相等,且对应位置的字符都相等。
空串和空格串:空串不包含任何字符,表示为
ϕ
\phi
ϕ;空格串由一个或多个空格组成
线性表的操作通常以“数据元素”为操作对象;串的操作主要以“串的整体”为操作对象。
串的模式匹配
[定义] 在主串中寻找子串在主串中的位置在模式匹配中,子串称为模式串,主串称为目标串。
KMP算法
在比较过程中,主串的下标i只增不减,不回溯,可以使算法的复杂度提高到O(m+n)
int KMP(char const* t, char const* p, int const* next)
{
int i = 1, j = 1;
int n = strlen(t), m = strlen(p);
while((i<=n)&&(j<=m)){
if((t[i]==p[j]) || j==0){
i++;
j++;
}
else{
j = next[j];
//j = nextval[j];
}
}
if(j == m+1) return (i-m);
else return 0;
}
void get_next(int* next, char const* p)
{
int j = 1, k = 0, next[1]= 0;
while(j < strlen(p)){
if((k == 0)||(p[k]==p[j])) next[++j] = ++k;
else k = next[k];
}
}
void get_nextval(int* nextval, char const* p) //改进
{
int j = 1, k = 0, nextval[1]= 0;
while(j < strlen(p)){
if(k==0||p[j] == p[k]){
if(p[++j] != p[++k]) nextval[j] = k;
else nextval[j] = nextval[k];
}
else k = nextval[k];
}
}
void get_next(int *next, char const *p){
int j = 1 , k = 0 ,next[1] = 0;
while(j < strlen(p)){
if(k == 0 || p[k] == p[j]){
if(p[++j]!=p[++k])
next[j] = k;
else next[j] = next[k];
}
else k = next[k];
}
}
第五章 数组和广义表
⊙
\odot
⊙ 任何数组A都可以看作一个线性表
A = (a1,a2,a3,…,ai,…an)
⊙
\odot
⊙ 数组的特点
- 数组中各元素都具有统一的类型
- 可以认为,d维数组的非边界元素具有d个直接前趋和d个直接后继
- 数组维数确定后,数据元素个数和元素之间的关系不再发生改变,适合于顺序存储
- 每组有定义的下标都存在一个与其相对应的值
⊙ \odot ⊙ C语言中任何一维数组均可作为函数的实参。但无法向函数传递普通的多维数组。这是因为我们需要知道每一维的长度,为地址运算提供正确的单位长度。因此必须提供除最左边一维外的所有维的长度,把实参限制在除最左边一维外的所有维都必须与形参匹配的数组。
矩阵的压缩存储
⊙
\odot
⊙ 对称矩阵
[特点]在n
∗
*
∗n的矩阵a中,满足如下性质:aij=aji (1
<
=
<=
<= i, j
<
=
<=
<= n)
[存储方法]只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
k =i(i-1)/2+j 当i
≥
\geq
≥ j
j(j-1)/2+i 当i
<
<
< j
⊙
\odot
⊙ 三角矩阵
[特点]对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c
[存储方法]重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间: sa [ [ [ 1… n(n+1)/2+1 ] ] ]
⊙
\odot
⊙ 带状矩阵
[特点]在n
∗
*
∗n的方阵中,非零元素集中在主对角线及其两侧共L(奇数)条对角线的带状区域内 — L对角矩阵。
[存储方法]只存储带状区内的元素。
1) 以对角线的顺序存储,共n
∗
*
∗L个元素。
2)只存储带状区内的元素。除首行和末行,按每行 L个元素,共(n-2)L+(L+1)个元素: sa
[
[
[ 1…(n-1)L+1
]
]
]
⊙
\odot
⊙ 稀疏矩阵
[特点]大多数元素为零。
[存储方法]记录每一非零元素(i, j, aij )——节省空间,但丧失随机存取功能
⊙
\odot
⊙ 三元组表
采用三元组
(
r
o
w
,
c
o
l
,
d
a
t
a
)
(row, col, data)
(row,col,data)(或称为ijv format)的形式来存储矩阵中非零元素的信息 ;
三个数组
r
o
w
row
row 、
c
o
l
col
col 和
d
a
t
a
data
data 分别保存非零元素的行下标、列下标与值(一般长度相同 );
故 coo
[
r
o
w
[
k
]
[row[k]
[row[k]]
[
c
o
l
[
k
]
]
[col[k]]
[col[k]]
=
=
=
d
a
t
a
[
k
]
data[k]
data[k] ,即矩阵的第
r
o
w
[
k
]
row[k]
row[k] 行、第
c
o
l
[
k
]
col[k]
col[k] 列的值为
d
a
t
a
[
k
]
data[k]
data[k]
⊙
\odot
⊙ 优点:1.转换成其它存储格式很快捷简便(tobsr()、tocsr()、to_csc()、to_dia()、to_dok()、to_lil())
2.能与CSR / CSC格式的快速转换
3.允许重复的索引(例如在1行1列处存了值2.0,又在1行1列处存了值3.0,则转换成其它矩阵时就是2.0+3.0=5.0)
⊙
\odot
⊙ 缺点: 1.不支持切片和算术运算操作
2.如果稀疏矩阵仅包含非0元素的对角线,则对角存储格式(DIA)可以减少非0元素定位的信息量
3.这种存储格式对有限元素或者有限差分离散化的矩阵尤其有效
广义表
⊙
\odot
⊙ 概念:广义表是由零个或多个原子或者子表组成的有限序列。可以记作:LS=(d1, d2, … , dn )
原子:逻辑上不能再分解的元素。
子表:作为广义表中元素的广义表。
⊙ \odot ⊙ 逻辑定义 Lists = (D,R)
⊙ \odot ⊙ 与线性表的关系: 广义表中的元素全部为原子时即为线性表。线性表是广义表的特例,广义表是线性表的推广。
⊙
\odot
⊙ 表的长度 表中的元素(第一层)个数。
⊙
\odot
⊙ 表的深度 表中元素的最深嵌套层数。
⊙
\odot
⊙ 表头 表中的第一个元素。
⊙
\odot
⊙ 表尾 除第一个元素外,剩余元素构成的广义表。
广义表结构的分类
- 纯表:与树型结构对应的广义表。
- 再入表:允许结点共享的广义表。
- 递归表:允许递归的广义表。
递归表 ⊃ \supset ⊃再入表 ⊃ \supset ⊃纯表 ⊃ \supset ⊃线性表
广义链表的存储结构
- 类型定义 方法一——头尾链表形式
typedef enum {ATOM, LIST} ElemTag;
/*ATOM=0,原子;LIST=1,子表 */
typedef struct GLNode
{
ElemTag tag;
/* 标志位tag用来区别原子结点和表结点 */
union
{
AtomType data;
/* 原子结点的值域data */
struct { struct GLNode * hp, *tp; } ptr;
/* 表结点的指针域ptr, 包括表头和表尾指针域 */
};
/* 原子结点的值域atom和表结点的指针域htp的联合体域 */
}*GList;
⊙ \odot ⊙ 头尾链表存储方法中:(1)除空表的表头指针为空外,任何非空表的表头指针均指向一个表结点,且该结点的hp域指示表的表头,tp域指示表的表尾;(2)容易分清列表中的原子和子表所在的层次;(3)最高层的表结点数即为列表的长度。
- 方法二——扩展的线性链表形式
typedef enum {ATOM, LIST} ElemTag;
/* ATOM=0, 表示原子; LIST=1, 表示子表 */
typedef struct GLNode
{
ElemTag tag;
union
{
AtomType data;
struct GLNode * hp;
};
/* 原子结点的值域atom和表结点的表头指针域hp的联合体域 */
struct GLNode * tp;
} *GList;
⊙
\odot
⊙ 任何一个非空广义表,都可以分解成表头Head,表尾Tail两个部分
例如: D = ( E, F ) = ((a, (b, c)),F )
Head( D ) = E Tail( D ) = ( F )
Head( E ) = a Tail( E ) = ( ( b, c) )
Head( (( b, c)) ) = ( b, c) Tail( (( b, c)) ) = ( )
Head( ( b, c) ) = b Tail( ( b, c) ) = ( c )
Head( ( c ) ) = c Tail( ( c ) ) = ( )
第六章 树与二叉树
树是一类重要的非线性数据结构,是以分支关系定义的层次结构。
⊙
\odot
⊙ 两种特殊的二叉树:
满二叉树 每一层上的结点数都是最大结点数。
完全二叉树 只有最下面两层结点的度可小于2,而最下一层的叶结点集中在左边若干位置上。
⊙ \odot ⊙ 特点:除根节点外,每个结点都仅有一个前趋结点。
⊙ \odot ⊙ 性质
- 具有n个结点的完全二叉树的深度为 ⌊ l o g 2 n ⌋ \lfloor log_2n \rfloor ⌊log2n⌋+1。
- 一棵具有n个结点的完全二叉树(又称顺序二叉树),对其结点按层从上至下(每层从左至右)进行1至n的编号,则对任一结点i(1
≤
\leq
≤i
≤
\leq
≤n)有:
(1)若i>1,则i的双亲是 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋;若i=1,则i是根,无双亲。
(2)若2i ≤ \leq ≤n,则i的左孩子是 2 i 2i 2i;否则, i无左孩子。
(3)若2i+1 ≤ \leq ≤n,则i的右孩子是 2 i + 1 2i+1 2i+1;否则, i无右孩子。
⊙ \odot ⊙ 基本术语
- 结点的度:结点拥有的子树数目
- 叶子结点:度为0的结点
- 分支结点:度不为0的结点
- 树的度:树的各结点度的最大值
- 内部结点:除根结点之外的分支结点
- 双亲与孩子(父与子)结点:结点的子树的根称为该结点的孩子,
对一棵由算术表达式组成的二叉语法树进行后序遍历得到的结点序列是该算术表达式的后缀表达式
⊙
\odot
⊙ 二叉树的遍历
遍历的目的 非线性结构线性结构
遍历的概念 指按某条搜索路线走遍二叉树的每个结点,使得树中每个结点都被访问一次,且仅被访问一次。
典型的遍历方法
先(根)序遍历 DLR
中(根)序遍历 LDR
后(根)序遍历 LRD
层序遍历
先序遍历算法
void Preorder (BiTree T)
{
if (T)
{ Printdata(T->data);
Preorder(T->lc);
Preorder(T->rc);
}
}
改进算法
void Preorder2 (BiTree T)
{
Printdata(T->data);
if (T->lc)
Preorder2(T->lc);
if (T->rc)
Preorder2(T->rc);
}
消除尾递归的递归算法
void Preorder3 (BiTree T)
{
while (T)
{
Printdata(T->data);
Preorder3(T->lc);
T = T->rc;
}
非递归算法
void Preorder4 (BiTreeT)
{ inistack(s);
p=bt;
push(s,NULL);
while (p)
{ Printdata(T->data);
if (p->rc)
push(s, p->rc);
if ( p->lc)
p=p->lc;
else
p=pop(s);
}
}
中序遍历算法
void Inorder (BiTree T)
{
if (T)
{
Inorder(T->lc);
Printdata(T->data);
Inorder(T->rc);
}
}
后序遍历算法
void Postorder(BiTree T)
{
if (T)
{
Postorder(T->lc);
Postorder(T->rc);
Printdata(T->data);
}
}
⊙ \odot ⊙ 按层次遍历算法
void Levelorder (BiTree T);
{ iniqueue( q);
if (T) enqueue( q, T);
while (!empty( q))
{ x= dequeue( q);
Printdata(x->data);
if (x->lc) enqueue( q, x->lc );
if (x->rc) enqueue( q, x->rc );
}
}
若先序序列与后序序列相同,则或为空树,或为只有根结点的二叉树
若中序序列与后序序列相同,则或为空树,或为任一结点至多只有左子树的二叉树.
若先序序列与中序序列相同,则或为空树,或为任一结点至多只有右子树的二叉树.
若中序序列与层次遍历序列相同,则或为空树,或为任一结点至多只有右子树的二叉树
⊙ \odot ⊙ 在二叉树中查找结点值为x的结点
void pre_find(BiTree bt, ElemType x, BiTree &q)//用q返回
{ //F是全局bool型变量,初值为FALSE
if (bt && F==FALSE)
{
if (bt->data==x)
{ q=bt; F=TRUE;}
else
{ pre_find(bt->lc, x, q);
pre_find(bt->rc, x, q)
}
}
}
⊙ \odot ⊙ 求二叉树中每个结点所处的层次
void pre_level(BiTree p, int level)
{
if (p)
{
write(p->data, level);//实现时可用printf代替
pre_level(p->lc; level+1);
pre_level(p->rc; level+1);
}
}
⊙ \odot ⊙ 求二叉树的高度
void pre_height(BiTree p, int level)
{ if (p)
{ if (h<level) h=level;
pre_height(p->lc, level+1);
pre_height(p->rc,level+1);
}
}
void post_height(BiTree bt,int &h)
{ if (bt==NULL) h=0
else
{ post_height(bt->lc, h1);
post_height(bt->rc,h2);
h=1+max(h1,h2);
}
}
⊙ \odot ⊙ 复制一颗二叉树
void pre_copy(BiTree bt;BiTree &q)
{
if(bt)
{
new(q);
q->data = bt->data;
pre_copy(bt->lc,q->lc);
pre_copy(bt->rc,q->rc);
}
else
q=NULL;
}
⊙ \odot ⊙ 二叉树类型定义
typedef enum PointerTag{
Link,Thread
}
typedef struct BiThrNode{
TElemType data;
struct BiThrNode *lc,*rc;
PointerTag lTag,rTag;
}BiThrNode,BiThrTree;
哈夫曼树(最优二叉树)
带权路径长度WPL最小的二叉树。
⊙
\odot
⊙ 建立方法
使权大的结点靠近根。
(1)将给定权值从小到大排序成{w1,w2,…,
w
m
w_m
wm},生成一个森林F={T1,T2,…,
T
m
T_m
Tm},其中
T
i
T_i
Ti是一个带权
W
i
W_i
Wi的根结点,它的左右子树均空。
(2)把F中根的权值最小的两棵二叉树T1和T2合并成一棵新的二叉树T: T的左子树是T1 ,右子树是T2 ,T的根的权值是T1、T2树根结点权值之和。
(3)将T按权值大小加入F中,同时从F中删去T1和T2
(4)重复(2)和(3),直到F中只含一棵树为止,该树即为所求。
⊙ \odot ⊙ 哈夫曼树的存储结构
- 初始有n个叶子结点,则构造出的哈夫曼树(属于严格二叉树)共有2n-1个结点。
- 用大小为2n-1的向量存储哈夫曼树—顺序存储。
⊙
\odot
⊙ 哈夫曼树实现的算法步骤
1)初始化ht
[
2
n
−
1
]
[2n-1]
[2n−1]
根节点为0或者为1
weight = 0,lc = rc = parent = 0
2)输入初始n个叶子结点:置ht
[
0..
n
−
1
]
[0..n-1]
[0..n−1]的weight值
3)进行以下n-1次合并,依次产生ht
[
i
]
[i]
[i],i=n…2n-2
3.1)在ht
[
0..
i
−
1
]
[0..i-1]
[0..i−1]中选两个未被选过的weight最小的两个结点ht
[
s
1
]
[s1]
[s1]和ht
[
s
2
]
[s2]
[s2] (从parent = 0 的结点中选)
3.2)修改ht
[
s
1
]
[s1]
[s1]和ht
[
s
2
]
[s2]
[s2]的parent值: parent=i
3.3)置ht
[
i
]
[i]
[i]:weight=ht
[
s
1
]
[s1]
[s1].weight + ht
[
s
2
]
[s2]
[s2].weight ,lc=s1, rc=s2
⊙ \odot ⊙ 哈夫曼树的应用
- 最佳判定树
- 哈夫曼编码:用于通信和数据传送中字符的二进制编码,可以使电文编码总长度最短。
根据码字出现频次来决定出现顺序
森林与二叉树转换
⊙
\odot
⊙ 树转换为二叉树
1)在兄弟之间加一根线
2)对每一结点,去掉它与孩子的连线(最左子除外)
3)以根为轴心将整棵树顺时针转450
特点:无右子树,左支是孩子,右支是兄弟
⊙
\odot
⊙ 森林转换为二叉树
1)先将森林里的每一棵树转换成一棵二叉树
2)从最后一棵树开始,把后一棵树的根作为前一棵树的根的右子树
⊙ \odot ⊙ 树的遍历
- 先序遍历:先访问树的根结点,然后依次先根遍历根的每棵子树(二叉树先序)
- 后序遍历:先依次后根遍历根的每棵子树,然后访问树的根结点(二叉树中序)
⊙ \odot ⊙ 森林的遍历
- 先序遍历:访问第一棵树的根结点;先序遍历第一棵树的根的子树森林;先序遍历除第一棵树外
剩余的树构成的森林(二叉树先序) - 中序遍历:中序遍历第一棵树的根的子树森林;访问第一棵树的根结点;中序遍历除第一棵树外
剩余的树构成的森林(二叉树中序)
树的存储方法
- 多重链表表示法
结点结构: d a t a data data| c h i l d 1 child_1 child1| c h i l d 2 child_2 child2| c h i l d 3 child_3 child3| . . . ... ...| c h i l d m child_m childm - 双亲表示法
用一维数组存储树的信息
data存储信息,parent存储结点的父结点 - 孩子链表表示法
数组+单链表
data+link - 孩子兄弟表示法(二叉链表表示法)
结点结构: f i r s t c h i l d firstchild firstchild| d a t a data data| n e x t b r o t h e r nextbrother nextbrother
第七章图
⊙
\odot
⊙ 基本概念:非线性结构,数据元素之间呈现多对多的关系
定义:Graph(V,E)
V:顶点(数据元素)的有穷非空集合。
E:边的有穷集合。
⊙
\odot
⊙ 图的相关术语
顶点 数据元素所构成的结点。
无向图 边的顶点偶对是无序的。
(
v
i
,
v
j
)
(v_i, v_j)
(vi,vj)和
(
v
j
,
v
i
)
(v_j, v_i)
(vj,vi)代表同一条边(
i
≠
j
i\neq j
i=j)。
有向图 边的顶点偶对是有序的。有向边
<
v
i
,
v
j
>
< v_i, v_j >
<vi,vj>也称为弧。
v
i
v_i
vi是弧尾/初始点;
v
j
v_j
vj是弧头/终端点。
(无向)完全图 每个顶点与其余顶点都有边的无向图。顶点数为n时,边数
e
=
n
(
n
−
1
)
/
2
e=n(n-1)/2
e=n(n−1)/2
有向完全图 每个顶点与其余顶点都有弧的有向图。顶点数为n时,弧数
e
=
n
(
n
−
1
)
e=n(n-1)
e=n(n−1)
思考:一无向图的顶点个数为n,则该图最多有
(
n
(
n
−
1
)
/
2
)
(n(n-1)/2)
(n(n−1)/2)条边
稀疏图 有很少边或弧的图。
稠密图 有较多边或弧的图。
权 图中的边或弧具有一定的大小的概念。
网 边/弧带权的图。
邻接 有边/弧相连的两个顶点之间的关系。
存在
(
v
i
,
v
j
)
(v_i, v_j)
(vi,vj),则称
v
i
v_i
vi和
v
j
v_j
vj互为邻接点;
存在
<
v
i
,
v
j
>
<v_i, v_j>
<vi,vj>,则称
v
i
v_i
vi邻接到
v
j
v_j
vj,
v
j
v_j
vj邻接于
v
i
v_i
vi
关联(依附) 边/弧与顶点之间的关系。
存在
(
v
i
,
v
j
)
/
<
v
i
,
v
j
>
(v_i, v_j)/ <v_i, v_j>
(vi,vj)/<vi,vj>, 则称该边/弧关联于
v
i
v_i
vi和
v
j
v_j
vj
子图 对于图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)和
G
’
=
(
V
’
,
E
’
)
G’=(V’,E’)
G’=(V’,E’),如果
V
’
⊆
V
V’ \subseteq V
V’⊆V,
E
’
⊆
E
E’ \subseteq E
E’⊆E,且E’关联的顶点都在V’中,则称G’是G的子图。
生成子图 由图的全部顶点和部分边组成的子图称为原图的生成子图。
顶点的度 与该顶点相关联的边的数目,记为D(v)
入度ID(v):有向图中,以该顶点为弧头的弧数目。
出度OD(v):有向图中,以该顶点为弧尾的弧数目。
路径 接续的边构成的顶点序列。
路径长度 路径上边或弧的数目/权值之和。
回路(环) 第一个顶点和最后一个顶点相同的路径。
简单路径 除路径起点和终点可以相同外,其余顶点均不相同的路径。
简单回路(简单环) 除路径起点和终点相同外,其余顶点均不相同的路径。
思考:n个顶点的无向完全图中,两个顶点之间简单路径数目为多少?
1 + A(n-2,1) + A(n-2,2) + … + A(n-2,n-2)
连通图 无向图中,任何一对顶点间都存在路径。
强连通图 有向图中,任何一对顶点间都存在路径。
生成树 包含图中全部顶点的极小连通子图。在生成树中添加一条边后,形成回路或环。
图的存储结构
- 有向图的加权邻接矩阵
优点:判断任意两点之间是否有边方便,仅耗费 O(1) 时间。
缺点:即使 < < n 2 << n^2 <<n2 条边,也需内存 n 2 / 2 n^2/2 n2/2单元,太多; 仅读入数据耗费 O ( n 2 ) O( n^2 ) O(n2)时间,太长。
- 邻接表
设有向图或无向图具有 n 个顶点,则用 顶点表、边表表示该有向图或无向图。
顶点表:用数组的形式存放所有的顶点。
边表(出边表):从某个顶点出发的边组成的单链表。
优点:内存消耗 = 顶点数 + 边数
缺点:确定 i --> j 是否有边,最坏需耗费 O(n) 时间。无向图同一条边表示两次边表空间浪费一倍。有向图中寻找进入某结点的边,非常困难。
- 关联矩阵
行为顶点,列为边,有向图从负到正,无向图都为1
两种遍历方式
深度优先遍历(树的先根遍历的推广)
每个顶点只能被访问一次,因为一个顶点可以和其它的任何顶点相邻接,为了避免对一个顶点的重复访问,必须对访问过的顶点加以标记。顶点的邻接顶点的次序是任意的,因此深度优先搜索的序列可能有多种。
⊙ \odot ⊙ 遍历过程
- 选中第一个被访问的顶点。
- 对顶点作已访问过的标志。
- 依次从顶点的未被访问过的邻接顶点出发,进行深度优先遍历。转向2。
- 如果还有顶点未被访问,则选中一个起始顶点,转向2。
- 所有的顶点都被访问到,则结束。
数据结构:
主——图的存储结构
辅——数组visit[1…n]用于标识结点是否已被访问过
⊙ \odot ⊙ 算法实现
- 访问顶点v,并记录v已被访问
- 依次从v的未访问的邻接点出发,深度优先搜索图G。
void DFSTraverse(Graph G, Status(*VisitFunc)(int v)){
VisitFunc = Visit;
for(v = 0; v < G.vexnum; ++v) visited[v] = false;
for(v = 0; v < G.vexnum; ++v)
if(!Visited[v]) DFS(G,v);
}
void DFS(Graph G, int v){
Visited[v] = true;
VisitFunc(v);
for(w = FirstAdjVex(G,v); w ; w = NexAdjVex(G, v, w))
if(!Visited[w]) DFS(G, w);
}
⊙
\odot
⊙ 算法时间复杂度分析
主要操作:查找每个顶点的所有邻接点
- 邻接矩阵: O ( n 2 ) O(n^2) O(n2)
- 邻接表:无向图 O ( n + 2 e ) O(n+2e) O(n+2e) 有向图 O ( n + e ) O(n+e) O(n+e)
广度优先遍历(树的按层次遍历的推广)
⊙ \odot ⊙ 遍历过程
- 选中第一个被访问的结点 V V V
- 对结点 V V V 作已访问过的标志。
- 依次从结点 V V V 的未被访问过的第一个、第二个、第三个……第 M M M个邻接结点 W 1 、 W 2 、 W 3... W m W1 、W2、W3... W_m W1、W2、W3...Wm ,且进行标记。
- 依次访问结点 W 1 、 W 2 、 W 3... W m W1 、W2、W3...Wm W1、W2、W3...Wm的邻接结点,且进行标记。
- 如果还有结点未被访问,则选中一个起始结点,也标记为 V V V,转向2。
- 所有的结点都被访问到,则结束。
⊙ \odot ⊙ 算法实现
void BFSTraverse( Graph G, Status ( * VisitFunc) (int v)){
VisitFunc = Visit;
for( v = 0; v < G.vexnum; ++v ) visited[v] = FALSE;
InitQueue(Q);
for( v = 0; v < G.vexnum; ++v )
if (!Visited[v]){
Visited[v] = TRUE;
Visit(v);
EnQueue(Q,v);
while(!EmptyQueue(Q)){
DeQueue(Q,u);
for ( w = FirstAdjVex(G, u) ; w ; w = NextAdjVex(G, u, w) )
if (!Visited[w] ){
Visited[v] = TRUE;
Visit(v);
EnQueue(Q, w);
}
}
}
⊙
\odot
⊙ 算法时间复杂度分析
同[[#深度优先遍历(树的先根遍历的推广)]]
堆
在计算机科学中,堆(Heap)是一种特殊的数据结构,用于动态分配内存。堆的主要特点是允许在运行时动态地分配和释放内存空间。与栈不同,堆内存的分配和释放不是由编译器自动管理的,而是由程序员显式地控制。
以下是堆的一些关键特点和用途:
-
动态内存分配: 堆允许程序在运行时动态地分配内存,而不需要在编译时指定内存大小。这使得程序能够灵活地使用内存,根据实际需求动态调整内存空间的大小。
-
手动管理: 堆内存的分配和释放是由程序员手动管理的。程序员负责在使用完内存后显式释放它,以防止内存泄漏(未释放的内存)和提高程序性能。
-
堆的实现: 堆通常是通过动态存储分配函数(如C语言中的
malloc
和free
函数)来实现的。这些函数允许程序员请求一定数量的内存,并在不再需要时将其释放。 -
不同于堆栈: 堆和栈是两种不同的内存分配区域。栈上的内存是由编译器自动分配和释放的,而堆上的内存需要程序员显式控制。
-
常见用途: 堆常用于存储动态数据结构,如链表、树和图等。它也是实现动态数组和字符串的关键部分。
-
堆中的数据结构: 在堆中,通常会使用一些数据结构来组织和管理内存块,例如空闲块链表、二叉堆等。
需要注意的是,堆不同于操作系统中的堆栈。操作系统中的堆栈是用于存储函数调用和局部变量的一种机制,而不涉及动态内存分配。
在使用堆时,程序员需要小心管理内存,确保及时释放不再使用的内存,以免出现内存泄漏和程序性能下降的问题。
第八章 查找
- 静态查找表
最先访问的结点应是概率最大的结点
每次访问结点应使两边尚未被访问结点的被访概率之和尽可能相等
- 次优查找树
PH值近似为最小
比静态最优查找树易于构造,时间开销少
- 索引表
折半或者顺序查找索引表,确定所在块
在已确定的块中顺序查找/折半查找
- 二叉排序树
二叉排序树或者是空树,或者是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;
- 其左右子树本身又各是一棵二叉排序树
中序遍历一棵二叉排序树,将得到一个以关键字递增排列的有序序列
- 平衡二叉树 AVL树
平衡二叉树或者是空树,或者是满足如下性质的二叉排序树:
- 它的左、右子树的高度之差的绝对值不超过1;
- 其左右子树本身又各是一棵平衡二叉树。
二叉树上的平衡因子:该节点左子树高度减去右子树的高度
当每个结点的平衡因子都为-1、0、1时,为平衡二叉树
第九章 排序
- 排序是根据记录关键字的值的递增(递减)的关系将文件记录的次序重新排列。
- 排序的分类
- 根据排序时文件记录的存放位置
内部排序:排序过程中将全部记录放在内存中处理。
外部排序:排序过程中需在内外存之间交换信息。- 根据排序前后相同关键字记录的相对次序
稳定排序:设文件中任意两个记录的关键字值相同,即Ki=Kj(i!=j),若排序之前记录Ri领先于记录Rj ,排序后这种关系不变(对所有输入实例而言)。
不稳定排序:只要有一个实例使排序算法不满足稳定性要求。- 根据文件的存储结构划分排序的种类
连续顺序文件排序
链表排序
地址排序: 待排记录顺序存储,排序时只对辅助
表(关键字+指针)的表目进行物理重排。- 根据排序的方法
插入排序 交换排序 选择排序 归并排序 基数排序- 根据排序算法所需的辅助空间
就地排序: O(1) 非就地排序: O(n)或与n有关
插入排序
- 直接插入排序(增量法)
void InsertSort (SqList &L)
{
for ( i = 2; i <= L.length ; ++i )
{
r[0] = r[i]; j = i-1;
while (r[0].key <r[j].key)
{
r[j+1] = r[j];
j--;
}
r[j+1] = r[0];
}
- 改进1:折半插入排序
void BInsertSort (SqList &L)
{
for ( i = 2; i <= n; ++i )
{
L.r[0] = L.r[i]; low = 1 ; high = i-1 ;
while ( low <= high )
{
m = ( low + high ) / 2 ;
if (L.r[0].key < L.r[m].key ) ) high = m -1 ;
else low = m + 1;
}
for ( j=i-1; j>=high+1; - - j )
r[j+1] = r[j];
r[high+1] = r[0];
}
}
冒泡排序
- 算法思想:将两个相邻记录的关键字进行比较,若为逆序则交换两者位置,小者往上浮,大者往下沉。
void BubbleSort2(int a[],int n) //相邻两趟向相反方向起泡的冒泡排序算法
{
change=1;low=0;high=n-1; //冒泡的上下界
while(low<high && change)
{
change=0; //设不发生交换
for(i=low;i<high;i++) //从上向下起泡
if(a[i]>a[i+1]){
a[i]<-->a[i+1];change=1;
}
//有交换,修改标志change
high--; //修改上界
for(i=high;i>low;i--) //从下向上起泡
if(a[i]<a[i-1]){
a[i]<-->a[i-1];change=1;
}
low++; //修改下界
}
}
快速排序(分划交换排序/分治法)
- 算法原理:
1)分解:将原问题分解为若干子问题
2)求解:递归地解各子问题,若子问题的规模足够小,则直接求解
3)组合:将各子问题的解组合成原问题的解
Void QuickSort ( SqList &L )
{ QSort ( L, 1, L.length ); } // QuickSort
Void QSort ( SqList &L,int low, int high )
{ if ( low < high )
{ pivotloc = Partition(L, low, high ) ;
Qsort (L, low, pivotloc-1) ;
Qsort (L, pivotloc+1, high )
}
} // QSort
int Partition ( SqList &L,int low, int high )
{ L.r[0] = L.r[low]; pivotkey = L.r[low].key;
while ( low < high )
{ while ( low < high && L.r[high].key >= pivotkey ) --high;
L.r[low] = L.r[high];
while ( low < high && L.r[low].key <= pivotkey ) ++low;
L.r[high] = L.r[low];
}
L.r[low]=L.r[0]; return low;
} // Partition
- 性能分析
最坏情况(原始数据正/逆序排列)
Cmax=n(n-1)/2 Mmax <= Cmax O(n2)
最好情况:每次划分的结果是基准的左、右两个无序子区间的长度大致相等
Cmin <= O(nlgn) Mmin <= Cmin O(nlog2n)
选择排序
int SelectMinKey (SqList L; int i)
{
k = i;
for (j = i + 1; j<=n; j--)
if (L .r [ j ] .key < L .r [ k ].key) k = j;
return k;
}
void SelectSort ( SqList &L )
{ for ( i =1; i < L.length; ++i )
{ j = SelectMinKey ( L, i );
if ( i != j ) r[i] <--> r[j];
}
} // SelectSort
- 性能分析:平均时间复杂度O(n2) 辅助空间复杂度O(1)
堆排序
归并排序
void mergesort ( SqList &r ) {
len = 1;
While (len < n)
{ s1 = 1; q = 1;
While (s1 + len <= n)
{ s2= s1 +len; t1 = s2 - 1; t2 = s2 + len -1;}
If (t2 > n) t2 = n;
merge ( r, t, s1, t1, s2, t2, q); s1 = q;
}
for (i = 1; i <= q – 1) r [ i ] = t [ i ];
len = len*2;
}
}
void merge (SqList r, &t ; int s1, t1, s2, t2, int &q) {
while (( s1 <= t1 ) && ( s2 <= t2 ) )
{ If (r [ s1 ] <= r [ s2 ])
{ t [ q ] = r [ s1 ]; s1 = s1 + 1; }
else
{ t [ q ] = r [ s2 ]; s2 = s2 + 1; }
q = q + 1;
}
while (s1 <= t1}
{ t [ q ] = r [ s1 ]; s1 = s1 + 1; q = q + 1; }
while ( s2 <= t2)
{ t [ q ] = r [ s2 ]; s2 = s2 + 1; q = q + 1 }
}
基数排序
先按规则将元素分为多类,再按照不同规则排序
例:将多个三位数进行排序,第一趟对个位排序,第二趟对十位排序,第三趟对百位排序
排序比较
![[排序方法比较.png]]
- 按平均时间排序方法分为四类
O(n2)、O(nlgn)、O(n^(1+ θ \theta θ))、O(n) - 快速排序是目前基于比较的内部排序中最好的方法
- 关键字随机分布时,快速排序的平均时间最短,堆排序次之,但后者所需的辅助空间少
- 当n较小时如(n<50),可采用直接插入或简单选择排序,前者是稳定排序,但后者通常记录移动次数少于前者
- 当n较大时,应采用时间复杂度为O(nlgn)的排序方法(主要为快速排序和堆排序)或者基数排序的方法,但后者对关键字的结构有一定要求
- 当n较大时,为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构(如插入排序、归并排序、基数排序)
- 快速排序和堆排序难于在链表上实现,可以采用地址排序的方法,之后再按辅助表的次序重排各记录
- 文件初态基本按正序排列时,应选用直接插入、冒泡或随机的快速排序
- 选择排序方法应综合考虑各种因素