一、绪论
TARGET
掌握数据结构中涉及的基本概念
掌握算法的时间、空间复杂度及其简易分析方法
1、复杂现实问题
涉及线性表、树、图之类的数据结构
2、研究内容::研究非数值计算的程序设 计问题中计算机的操作对象以及它们之间的关系和操作
3、基本概念
数据:能输入到计算机中去的描述客观事物的符号
包括数值型数据和非数值型数据
数据元素:数据的基本单位,也称节点或记录
数据项:有独立含义的数据最小单位,也称域
数据对象:相同特性数据元素的集合,是数据的一个子集
数据结构:相互之间存在的一种或多种特定关系的数据元素的集合
4、数据结构的内涵
数据逻辑结构:与计算机无关,包括集合、线性结构、树形结构、图形结构
数据存储结构:
顺序结构:使用相对位置(ps:可以看成相对于基址偏移)来表示数据元素间的逻辑关系(随机存储)
链式结构:用指针来表示数据元素间的逻辑关系
数据的运算
a、逻辑结构和存储结构相同,但运算不同,则数据结构不同,如栈和队列
问题:栈和队列是两种数据结构,他们的不同体现在哪里?
b、数据结构本身的运算:增、删、查、改、排序
5、数据类型和抽象数据类型
数据类型:即变量所具有的的数据种类,可以看做计算机已经实现的数据结构
抽象数据结构:用户自定义,由基本数据类型组成,包括一组相关的操作,类似c语言中的结构体
ADT = (D,S,P)
6、算法与算法分析
算法定义:求解某类问题所使用的一系列清晰的操作序列
特性:
- 输入 0或多个输入项
- 输出 至少一个输出项
- 有穷性 算法步骤和执行时间都是有限的
- 确定性 每一步都有明确的意义
- 可行性 能达到语气目标
算法的评价方法
正确性、可读性、健壮性、高效性
高效性:时间复杂度、空间复杂度
时间复杂度
算法在计算机上执行的时间,两种度量方法
-
事后统计
-
事前分析估计
取决于:算法选用的策略、问题规模,用O()表示
求解方法:时间复杂度由最深层嵌套语句的重复执行次数决定
ps:有的情况下,算法中基本操作重复执行的次数随问题的输入实例不同而不同
空间复杂度
算法所需存储空间的度量,记作: S(n)=O(f(n))
算法执行要占据的空间 包括
- 算法本身要占据的空间,输入/输出,指令,常数,变量等
- 算法要使用的辅助空间
二、线性表
线性结构:
只有一个开始节点和一个终端节点
最多只有一个直接前驱和后继
反映的逻辑关系是一对一
TARGET
掌插顺序表
的定义、查找、插入和删除
掌插链表
的定义、创建、查找、插入和删除
1、线性表:
n(n>=0)个数据元素的有限序列
ps:n=0时称为空表
同一线性表中的元素必定具有相同特性
2、线性表的顺序表示
(又称顺序存储结构或顺序映像)和实现
定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的 存储结构。即:逻辑上相邻,物理上也相邻
方法:用一组地址连续的存储单元依次存储线性表的元素,可通过 数组V[n]来实现
实现:
初始化:
两种方式:参数用引用、参数用指针
代码部分
插入:
算法描述:
(1)判断揑入位置i 是否合法;
(2)判断顺序表的存储空间是否已满;
(3)将第n至第i 位的元素依次向后移动
一个位置,空出第i个位置;
(4)将要揑入的新元素e放入第i个位置;
(5)表长加1,返回OK。
代码
时间复杂度
最差:O(n)
最好:O(1)
平均:O(n/2)
删除:
算法描述:
算法描述:
(1)判断删除位置i 是否合法;
(2)将第i+1至第n 位的元素依次向前移
动一个位置;
(3)表长减1,返回OK。
代码
最差:移动n-1次
最好:不用移动
平均:移动(n-1)/ 2
查找:
随机存取,时间复杂度为O(1)
销毁:
if(L.elem) delete[]L.elem;
清空:
L.length=0
求长度
return L.length
判读是否为空
if (L.length == 0) return true;
else return 0;
特点:
逻辑结构与存储结构一致
访问每个元素花的时间相等
优点:
存储密度大(结点本身所占存储量/结点结构所占存储量)
可以实现随机存取
缺点:
在插入、删除某一元素时需要移动大量元素
当元素数量大且变化多时,浪费存储空间
属于静态存储模式,数据元素不能自由扩充
3、线性表的链式表示
又称为非顺序映像或链式映像
定义:结点在存储器中的位置是任意的,即逻辑上相邻的数据元素 在物理上不一定相邻。
方法:指针
相关术语:
单链表或线性链表、双链表、循环链表
头指针、首元结点、头结点
思考
:在链表中设置头结点有什么好处?
将非空表、空表、首元结点的操作统一
typedef struct LNode{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为LNode类型的指针
LNode *p <<——>> LinkList p
初始化:
ps:注意区分有无头结点
代码
代码后续补充,未完待续……
双链表、循环链表
头指针、首元结点、头结点
思考
:在链表中设置头结点有什么好处?
将非空表、空表、首元结点的操作统一
typedef struct LNode{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
// *LinkList为LNode类型的指针
LNode *p <<——>> LinkList p
初始化:
ps:注意区分有无头结点
代码
Status InitList_L (LinkList &L){
L=new LNode;
L->next=NULL;
return OK;
}
插入
头插法
p->data = v;
p->next = L->next; //p的指针域指向L的指针域,即p指向空,
L->next = p; //但是此时只有p这个结点,还需要头结点指向p
//此时头结点指向p,而p指针域指向最后,
//可以认为p是尾结点,它的指针域自然要指向空了。
尾插法
p->data = v; //输入元素值
p->next = NULL; r->next = p; //插入到表尾
r = p; //r指向新的尾结点
//插入的结点p是尾结点,所以要使r指向p,才能让再次插入的
删除
Status ListDelete_L(LinkList &L,int i){
p=L;j=0;
while(p->next &&j<i-1){p=p->next; ++j; }
if(!(p->next)||j>i-1) return ERROR;
q=p->next; //保存被删结点的地址以备释放
p->next=q->next;//改变删除结点前驱结点指针
delete q; //释放删除结点的空间
return OK;
}//ListDelete_L
查找、取值
p=L->next; j=1;
while(p &&p->data!=e) {p=p->next; j++;}
if(p) return j;
else return 0;
p=L->next;j=1; //初始化
while(p&&j<i) {//直到p指向第i个或p为空
p=p->next; ++j;
}
if(!p || j>i)return ERROR; //第i个元素不存在
e=p->data; //取第i个元素
return OK;
销毁
while(L) {
p=L;
L=L->next;
delete p;
}
清空
while(p) //没到表尾
{
q=p->next;
delete p;
p=q;
}
L->next=NULL; //头结点指针域为空
返回表长、判断表是否为空
特点
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物 理上丌一定相邻
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找每个结点所花费的时间不等
优点
数据元素的个数可以自由扩充
插入、删除等操作不必移动数据,只需修改链接指针
缺点
存储密度小
存取效率不高,必须采用顺序存取
循环链表
特点:从循环链表中的任何一个结点的位置都可以找到其他所有结点,而单链表做不到;
问题:没有明显的尾端,如何避免死循环?
将判断条件改成遇到头结点结束
循环条件:p!=NULL -> p!=L
p->next!=NULL -> p->next!=L
ps:给出了尾指针,可以找到尾结点和首元结点,但是注意有个头结点
首元结点: rear->next->next
终端节点: rear
两个链表合并
p = Ta->next;
Ta->next = Tb->next->next;
delete Tb->next;
Tb->next = p;
return Tb;
约瑟夫问题
代码
双向链表
略
4、线性表的应用
线性表的合并
算法描述:
依次取出Lb 中的每个元素
在La中查找该元素
如果找不到,则将其揑入La的最后
有序表的合并
算法描述:
创建一个空表Lc
依次从 La 或 Lb 中“摘取”元
素值较小的结点插入到 Lc 表的
最后,直至其中一个表变空为止
继续将 La 或 Lb 其中一个表的
剩余结点揑入在 Lc 表的最后
有序链表的合并
要求:结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间;表中允许有重复的数据
算法描述:
(1)Lc指向La
(2) 依次从 La 或 Lb 中“摘取”元素值较小的结点揑入到 Lc 表的最后,直至其中一个表变空为止
(3) 继续将 La 或 Lb 其中一个表的剩余结点插入在Lc 表的最后
(4) 释放 Lb 表的表头结点
思考
:如何排除相同的元素
通过增加判断条件,单独考虑相等,只增加一个值,指针同时后移
思考
:将两个非递减的有序链表合并为一个非递增的有序链表,如何实现?
用头插法,即可以逆序
三、栈与队列
TARGET
掌插栈和队列的特点,并能在应用问题中正确选用
熟练掌插栈的两种存储结构的基本操作实现算法,特别应注意栈满和栈空的条件
理解递归算法执行过程中栈的状态变化过程
熟练掌插循环队列和链队列的基本操作实现算法,特别注意队满和队空的条件
掌插进制转换、表达式求值等问题的方法实现
1、栈和队列的定义
栈:
定义:只能在表的一端进行插入和删除运算的线性表
逻辑结构:一对一
存储结构:顺序栈、链栈
实现方式:根据存储方式的不同而不同,包含入栈、出栈、取栈顶元素、建栈、判断栈空、栈满
队列:
定义:只能在表的一端(队尾)进行插入,在另一端(队头)进行删除运算的线性表
逻辑结构:一对一
存储结构:顺序队列、链队列
实现方式:根据顺序队或链队的不同而不同
入队、出队、建队列、判断队列满、队列空等
2、栈的表示和操作
顺序栈
初始化
元素进栈
元素出栈
思考:在一个程序中需要同时使用具有相同数据类型的两个栈,如何高效存储这两个栈?(从节约空间角度)
一种可取的方法是充分利用顺序栈单向延伸的特性,使用一个数组来存储两个栈,让每个栈的栈底为该数组的始端,另一个栈底为该数组的末端。每个栈从各自的端点向中间延伸。
链栈
如何改造链表,实现栈的链式存储?
a、将链表头作为栈顶(前揑法),方便操作
b、删除头结点
3、栈与递归
递归定义:若一个函数、过程或者数据结构定义的内部又直接 (或间接)的出现定义本身,则称它是递归的。
特点
a、后调用,先返回
应用在递归定义的数学函数
具有递归特性的数据结构
可递归求解的问题如汉诺塔问题
递归算法复杂度分析
时间复杂度:与递归树的节点数成正比
空间复杂度:与递归树的高度成正比
优缺点
优点:结构清晰,程序易读
缺点:使用递归时间开销大,每次调用时保存状态信息,入栈;返回时要出栈,恢复状态信息
4、队列的表示和操作
顺序队列
问题1:假溢出
即front != 0 但 rear = MAXSIZE在入队
解决:循环队列,用模运算实现
入队:
base[rear]=x;
rear=(rear+1)%MAXQSIZE ;
出队:
x=base[front];
front=(front+1)%MAXQSIZE;
问题2:队空、队满情况一样
解决:
1.另外设一个标志以区别队空、队满
2.少用一个元素空间:
队空:front==rear
队满:(rear+1)%==front
初始化
Status InitQueue (SqQueue &Q){
Q.base =new QElemType[MAXQSIZE]
if(!Q.base) exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
入队列
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;
Q.base[Q.rear]=e;
Q.rear=(Q.rear+1)%MAXQSIZE;
return OK;
出队列
if(Q.front==Q.rear) return ERROR;
e=Q.base[Q.front];
Q.front=(Q.front+1)%MAXQSIZE;
return OK;
链队
5、具体问题
十进制与其他进制的转换
括号匹配的检验
表达式的求值:需要设置OPND(操作数或运算结果)栈和OPTR(运算符)栈两个栈
舞伴问题:用队列实现
四、串、数组、广义表
1、串的定义
定义:零个或多个字符组成的有限序列
串的存储结构
顺序存储
链式存储:存储密度比较低
2、串的运算
模式匹配算法:确定主串中所含子串第一次出现的位置(定位)
包括BF算法和KMP算法
BF算法
int Index(SString S,SString T,int pos)
{
i = pos;
j = 1;
while(i <= S.length && j <= T.length)
{
if(S[i] == T[j])
{
i++;
j++;
}
else
{
i = i - j + 2;
j = 1;
}
}
if(j > T.length)
return i - T.length ;
else
return false;
}
时间复杂度:O(m*n)
KMP算法
利用已经部分匹配的结果加快模式 串的滑动速度
主串S的指针i不必回退
可提速到O(n+m)
算法策略:出现字符不等时,不回退主指针i,利用已得到的“部分匹配”结果将模式向右滑动尽可能远的一段距离。
void get_next(SString T, int &next[]){ i = 1; next[1] = 0; j = 0; while(j == 0 || i <= T.length ) { if(T[i] == T[j]) { i++; j++; next[i] = j; } else j = next[j]; }
void Index(SString S, SString T, int pos){ int i = pos; j = 0; while(i <= S.length && j <= T.length) { if(S[i] == T[j]) { i++; j++; } else j = next[i]; } if(j > T.length) return i - T.length; else return false;}
3、具体问题
病毒感染检测
算法步骤(实训题参考)]① 从文件中读取待检测的任务数num;② 循环num次,执行以下操作: 从文件中分别读取一对病毒DNA序列和人的DNA序列; 设置标志性变量flag,用来标识是否匹配成功,初始为0,表示未匹配; 病毒DNA序列的长度是m,将其复制扩大为2m; 循环m次,重复执行以下操作: 依次取得每个长度为m的病毒DNA环状字符串; 将此字符串作为模式串,将人的DNA序列作为主串,调用BF算法进行模式匹配,将匹配结果返回赋值给flag; 若flag非0,表示匹配成功,中止循环,表明该人感染了对应的病毒。 退出循环时,判断flag的值,若flag非0,输出“YES”,否则,输出“NO”
4、数组
二维数组与顺序存储
行向量(列序)
列向量(行序)
矩阵的压缩存储
压缩存储:若多个数据元素的值都相同,则只分配一个元素值的存储空间 ,且零元素不占存储空间。
一些特殊的矩阵能压缩:如对称矩阵,三角矩阵,对角矩阵
5、广义表
1、定义:
n (>= 0 )个表元素组成的有限序列,记作LS = (a0 , a1 , a2 , …, an-1 )
表元素可以是表 (称为子表),可以是数据元素(称为原子)
**广义表不线性表的区别 **
广义表是线性表的扩展,扩展后不一定是线性表
2、基本运算
GetHead(L):非空广义表的第一个元素,可以是一个单元素,也 可以是一个子表
GetTail(L):非空广义表除去表头元素以外其它元素所构成的表。 表尾一定是一个表
五、树和二叉树
TARGET
掌握二叉树的前、中、后序遍历方法
掌握哈夫曼树的实现方法、构造哈夫曼编码的方法
1、树和二叉树的定义
树的定义:
n(n≥0)个结点的有限集, n = 0为空树; n > 0为非空树
对于非空树T:
有且仅有一个称之为根的结点;
除根结点以外如果有其余结点,则可分为m(m>0)个互不相交的 有限集T1 , T2 , …, Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)
基本术语
结点、根、叶子、双亲、孩子、兄弟、堂兄弟、祖先、子孙
结点的度、树的度、结点的层次、树的深度(高度)、有序树、无序树、森林
二叉树的定义
n(n≥0)个结点的有限集, n = 0为空树, n > 0为非空树
对于非空树T:
有且仅有一个称之为根的结点;
除根结点以外如果有其余结点,则可分为两个互不相交的子集T1和 T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树
ps:所有的树都能转为唯一对应的的二叉树
特点
结点的度小于等于2
左右子树不一样,不能颠倒
2、二叉树的性质和存储结构
性质:
性质1: 在二叉树的第i层上至多有 2i-1 个结点
性质2: 深度为k的二叉树至多有 2k-1个结点
性质3: 对于任何一棵二叉树,若2度的结点数有n2个,则叶 子数n0必定为 n2+1(即n0=n2+1)
满二叉树::一棵深度为k 且有2k -1个结点的二叉树
完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都不深度为k 的满二叉树中编号从1至n的结点一一对应
特点:叧有最后一层叶子不满,且全部集中在左边
性质4: 具有n个结点的完全二叉树的深度必为 log2n+1
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i(1 ≤ i ≤ n) 的结点,其左孩子编号必为2i (如果有) ,其右孩子编号 必为2i+1(如果有);其父结点的编号必为i/2 (如果有) 。
思考:二叉树有无顺序存储结构?
有顺序存储结构,按照完全二叉树的节点层次编号,一次存放二叉树中的数据元素
**题目:**在n个结点的二叉链表中,有 n+1 个空指针域
在n个结点的三叉链表中,有 n+2 个空指针域
计算方法:总指针数(2*n或3*n) 减去 边的数量((n-1) 或2*(n-1))
3、遍历二叉树与线索二叉树
递归
先序遍历
递归算法: 若二叉树为空,则空操作 否则 访问根结点 (D) 先序遍历左子树 (L) 先序遍历右子树 (R)
中序遍历
递归算法描述: 若二叉树为空,则空操作 否则 中序遍历左子树 (L) 访问根结点 (D) 中序遍历右子树 (R)
后序遍历
递归算法描述: 若二叉树为空,则空操作 否则 后序遍历左子树 (L) 后序遍历右子树 (R) 访问根结点 (D)
时间效率:O(n) 空间效率:O(k)(k为树高),最差O(n)
ps:代数表达式与中序遍历一致
非递归
如何实现中序遍历的非递归实现分析算法?
使用栈数据结构
遇到结点就入栈,遍历它的左子树当左子树遍历结束,栈顶弹出结点访问继续遍历它的右子树
结论:若二叉树中各结点的值均不相同,则由二叉树的前序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树
线索二叉树
定义:
利用空链域(n+1个空链域)
1)若结点有左子树,则lchild指向其左孩子; 否则, lchild指向其直接前驱(即线索);
2)若结点有右子树,则rchild指向其右孩子; 否则, rchild指向其直接后继(即线索) 。
为了避免混淆,增加两个标志域 lchild LTag data RTag rchild
标志位为1,则说明指向线索 RTag = 1 或 LTag = 1
分类
先序线索二叉树
中序线索二叉树
后续线索二叉树
4、树与森林
数的存储结构
父节点表示法:找父节点容易,找孩子难
孩子表示法:找孩子容易,找父节点难
孩子兄弟表示法:一般树转换成二叉树的方法
树与二叉树的转换
树转换为二叉树
(1)加线。在所有兄弟结点之间加一条连线。
(2)去线。树中的每个结点,只保留它与第一个孩子结点的连线,删除它与其它孩子结点之间的连线。
(3)层次调整。以树的根节点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。(注意第一个孩子是结点的左孩子,兄弟转换过来的孩子是结点的右孩子)
森林转换为二叉树
(1)把每棵树转换为二叉树。
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
5、哈夫曼树及其应用
**前缀编码(**简称前缀码):所有的代码字都不是另一个字符代码字的前缀。
目的:高频字符编码短 低频字符编码长
哈夫曼编码的压缩率:(定长编码 - 哈夫曼编码长度)/定长编码
基本概念
路径、路径长度、树的路径长度、权、结点的带权路径长度、数的权路径长度、哈夫曼树
哈夫曼树
哈夫曼树中只有度为0和度为2的结点
二叉树的性质3(P118):n0=n2+1
哈夫曼树总结点数:2n-1
树的构造
哈夫曼编码
六、图
1、定义与概念
图的定义:
稀疏图、稠密图、邻接点、度
简单路径、简单回路
子图(边和点都属于另一图就是子图)
连通图(强连通图)
2、存储方式:
顺序存储方式(邻接矩阵)、链式存储方式(邻接表、十字链表、邻接多重表)
邻接矩阵表示法
特点
a、无向图
无向图的邻接矩阵是对称的
顶点i 的度=第 i 行 (列) 中 1 的个数
特别地:完全图的邻接矩阵中,对角元素为0,其余1
b、有向图
特点
顶点的出度=第i行元素之和;顶点的入度=第i列元素之和
顶点的度=第i行元素之和+第i列元素之和
优缺点
优点:容易实现查找顶点、判断是否邻接、计算度操作
缺点: 不便于增加和删除顶点;
n个顶点需要n*n个单元存储边;空间效率为O(n2),对稀疏图 而言尤其浪费空间
邻接表表示法
表示:对每个顶点vi 建立一个单链表,把与vi相邻接的顶点放在这个链 表中,包括表头结点和边表
ps:表头结点另外用顺序存储方式存储
ps:顶点的度=出边表中的结点数OD( Vi ) +邻接点域为Vi的个数ID(Vi )
十字链表
3、图的遍历
深度优先:模拟树的先序遍历过程
广度优先:模拟树的层次遍历过程
• 在访问了起始点v之后,依次访问 v的邻接点• 然后再依次访问这些顶点中未被访问过的邻接点• 直到所有顶点都被访问过为止
效率:
邻接表:O(n+m)
邻接矩阵:O(n2)
4、图的应用
最小生成树
连通网的一棵权重最小的生成树,其中树的权重定义为 树中所有边的权重总和
1、穷举法
2、Prim法(加点法)
思想:任意选择一个结点作为序列中的初始子树。每一次迭代时,把不在树中的最近结点添加到树中。当网的所有结点都包含在所构造的树中后,算法停止
3、Kruskal法(加边法)
最短路径
(一)单源最短路径—Dijkstra(迪杰斯特拉)算法
与prim算法唯一的不同是,计算到源点的距离,而prim算法计算的是到树的距离
时间复杂度:邻接矩阵O(n2) 邻接表O(m+nlogn)
(二)所有顶点间的最短路径—Floyd(弗洛伊德)算法
拓扑排序
① AOV网(Activity On Vertices):用顶点表示活动的网络
算法:重复选择没有直接前驱的顶点
输入AOV网络;2. 在AOV网络中选一个没有直接前驱的顶点, 输出;3. 从图中删去该顶点, 同时删去所有它发出的有向边;4. 重复以上 2、3 步, 直到: 全部顶点均已输出,拓扑排序完成; 或存在错误,AOV网络中必定存在有向环
② AOE网(Activity On Edges):用边表示活动的网络
关键路径
关键路径:路径长度最长 的路径
关键活动:关键路径上的活动,即这些活动的时间延迟或提前会影响整 个工程工期的延迟或提前