数据结构期末复习
一、基本概念
- 什么是算法
- 算法的基本特性有哪些
- 理解性掌握算法的度量方法,会求时间复杂度、空间复杂度
- 理解性掌握数据结构中的基本概念和术语,如数据结构、数据的逻辑结构、数据的物理结构等
- 数据的逻辑结构和物理结构又分为哪些结构,这些结构的特点,元素与元素之间的关系等,逻辑结构里面有集合、线性结构、树状
1.数据结构的基本概念
1.1数据结构
数据:能输入到计算机中并能被计算机程序识别和处理的符号
数据元素:数据的基本单位,在计算机中通常作为一个整体进行考虑和处理
数据项:构成数据元素的最小单位
数据结构:相互之间存在一定关系的数据元素的集合。分为逻辑结构和存储结构。
-
逻辑结构:数据元素以及数据元素之间的逻辑关系,在形式上可以定义为一个二元组:(D,R),其中,D是数据元素的有限集合,R是D上的关系集合。
- 集合:数据元素之间没有关系
- 线性结构:数据元素之间是一对一的关系
- 树结构:数据元素之间是一对多的层次关系
- 图结构:数据元素之间是多对多的任意关系
-
存储结构(物理结构):数据及其逻辑结构在计算机中的表示。存储结构除了存储数据元素之外必须隐式或显式地存储数据元素之间的逻辑关系。通常有两种存储结构:顺序存储结构和链式存储结构
- 顺序存储结构:用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置来表示
- 链式存储结构:用一组任意的存储单元来存储元素,数据元素之间的逻辑关系用指针来表示
2.算法的基本概念
2.1 什么是算法
算法:对特定问题求解步骤的一种描述,是指令的有限序列
2.2 算法的特性
算法的基本特性:
- 有穷性:总是在执行有穷步之后结束,且每一步都在有穷时间内完成
- 确定性:每一条指令必须有确切的含义,相同的输入得到相同的输出
- 可行性:操作步骤可以通过已经实现的基本操作执行有限次来实现
好算法特性:
- 正确性
- 健壮性
- 可理解性
- 抽象分级
- 高效性
算法的描述方法:
- 自然语言
- 流程图
- 程序设计语言
- 伪代码
3.算法分析
3.1算法的时间复杂度
常见时间复杂度:
O(
l
o
g
2
n
{log_2{n}}
log2n)<O(n)<O(n
l
o
g
2
n
{log_2{n}}
log2n)<O(n2)<O(n3)<…<O(2n)<O(n!)
算法运行时间计算:
-
for循环
假设循环体的时间复杂度为O(n)循环次数为m,则这个循环的时间复杂度为O(nm);
-
嵌套的for循环
由内外各个循环的复杂度为O(a)、O(b)、O©…则这个嵌套循环时间复杂度为O(abc…)
-
顺序语句
各个语句运行时间求和(或取较大值)
n=1000; k=0; for a in range(n): k+=1 for a in range(n): for b in range(n): k+=1
-
if/else语句
总的时间复杂度最大的路径的时间复杂度
3.2算法的空间复杂度
:算法在执行过程中需要的辅助空间数量,也就是除算法本身和输入输出数据所占用的空间外,算法临时开辟的存储空间
二、线性表
- 理解性掌握线性表的特点及线性表的存储,会求线性表元素的存储地址
- 重点掌握单链表的插入、删除和遍历的算法(c++)及算法思想(自然语言),会灵活运用
- 掌握单链表的构造算法及算法思想、头插法和尾插法
- 掌握顺序表的插入、删除、查找算法
- 理解顺序表和链表的比较,时间性能和空间性能
2.1 线性表
线性表的特点:
- 数据元素个数的有限性
- 数据元素类型的相同性
- 数据元素类型的抽象性
- 相邻数据元素的序偶关系
2.2 线性表的顺序存储结构——顺序表
基本思想:用一段地址连续的存储单元依次存储线性表的数据元素
2.2.1 顺序表的特点
- 随机存取
顺序表的优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 随机存取
顺序表的缺点:
- 表的容量难以确定,表的容量难以扩充
- 插入和删除操作需要移动大量元素
2.2.2 元素的存储地址
第i个元素的存储地址:
Loc(ai) = Loc(a1) + (i-1)*c
2.2.3 顺序表的实现
const int MaxSize=100;
template<typename DataType>
class SeqList{
public:
SeqList();
SeqList(DataType a[],int n);
~SeqList();
int Length;
DataType Get(int i);//按位查找
int Locate(DataType x);//按值查找
void Insert(int i,DataType x);
DataType Delete(int i);
int Empty();
void PrintList();
private:
DataType data[MaxSize];
int length;
};
-
插入
template<class DataType> //注:这里的i是从1开始算,但是下标从0开始算 void SeqList<DataType>::Insert(int i, DataType x){ if(length == MaxSize) throw "上溢"; if(i<1 || i>length+1) throw "插入位置错误"; else{ for(int k = length; k>=i; k--){ data[k] = data[k-1]; } data[i-1] = x; length++;//别漏了! } }
-
删除
template<class DataType> DataType SeqList<DataType>::Delete(int i){ DataType x; if( length == 0) throw "下溢"; if( i<1 || i>length ) throw "删除位置错误"; else{ x = data[i-1]; for(int k = i-1; k<length-1; k++){ data[k] = data[k+1]; } length--; return x; } }
-
查找
按值查找
template <class DataType> int SeqList<DataType>::Locate(DataType x){ for(int i = 0; i<length; i++){ if(data[i] == x) return i+1; } return -1; }
按位查找
template <class DataType> DataType SeqList<DataType>::Get(int i){ if( i<1 || i>length ) throw "查找位置错误"; else return data[i-1]; }
2.3 线性表的链接存储结构
2.3.1 单链表
:用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续甚至可以零散分布在内存中的任意位置。
存储特点:
- 逻辑次序和物理次序不一定相同
- 元素之间的逻辑关系用指针表示
2.3.2 单链表的实现
template<class DataType>
struct Node{
DataType data;
Node<DataType> *next;
}
template<typename DataType>
class LinkList{
public:
LinkList();
LinkList(DataType a[],int n);
~LinkList();
int Length();
DataType Get(int i);
int Locate(DataType x);
void Insert(int i,DataType x);
DataType Delete(int i);
int Empty();
void PrintList();
private:
Node<DataType> *first;
}
-
建立
头插法
template<class DataType> LinkList<DataType>::LinkList(DataType a[],int n){ Node<DataType> *p = nullptr; first = new Node<DataType>; first -> next = nullptr; for(int i=0;i<n;i++){ p = new Node<DataType>; p->data = a[i]; p->next = first -> next; first -> next = p; } }
尾插法
template<class DataType> LinkList<DataType>::LinkList(DataType a[],int n){ first = new Node<DataType>; Node<DataType> *p = first; Node<DataType> *s = nullptr; for(int i=0;i<n;i++){ s = new Node<DataType>; s -> data = a[i]; p -> next = s; p = p->next; } s -> next = nullptr; }
-
插入
template<class DataType> void LinkList<DataType>::Insert(int i, DataType x){ int count = 0; if( i< 1) throw "插入位置错误"; Node<DataType> *p = first; while(p != nullptr && count < i-1){ p = p->next; count++; } if(p == nullptr){ throw "插入位置错误"; }else{ Node<DataType> *s = new Node<DataType>; s -> data = x; s -> next = p -> next; p -> next = s; } }
-
删除
template<class DataType> DataType LinkList<DataType>::Delete(int i){ Node<DataType> *p = first, *s = nullptr; DataType x; int count = 0; while(p!=nullptr && count < i-1){ p = p -> next; count++; } if(p == nullptr || p -> next == nullptr){ throw "删除位置错误"; }else{ s = p -> next; x = s -> data; p -> next = s -> next; delete s; return x; } }
-
遍历
void LinkList<DataType>::PrintList(){ Node<DataTyep> *p = first -> next; while(p != nullptr){ cout<<p->data<<"\t"; p = p -> next; } cout<<endl; }
2.4 顺序表与链表的比较
2.4.1 存储分配方式
- 顺序表:采用顺序存储结构——静态存储分配,即用一段地址连续的存储单元依次存储线性表的数据元素,数据元素之间的逻辑关系通过**存储位置(下标)**来表示
- 链表:采用链式存储结构——动态存储分配,即用一组任意的存储的单元存放线性表的元素,用指针来反映数据元素之间的逻辑关系
2.4.2 空间性能比较
- 结点的存储密度比较
- 顺序表:只存储数据元素
- 链表:指针的结构性开销
- 结构的存储密度比较
- 顺序表:预分配存储空间
- 链表:链表中的元素个数没有限制
2.4.3 时间性能比较
- 按位查找
- 顺序表:O(1),随机存取
- 链表:O(n),顺序存取
- 插入和删除
- 顺序表:O(n),平均移动表长一半的元素
- 链表:O(1),不用移动元素,合适位置的指针
2.4.4 结论
- 从空间上讲,若线性表中元素个数变化较大或者未知,最好使用链表实现;如果用户事先知道线性表的大致长度,使用顺序表的空间效率会更高
- 从时间上讲,若线性表频繁查找却很少进行插入和删除操作,或其操作和元素在表中的位置密切相关时,宜采用顺序表作为存储结构;若线性表需频繁插入和删除时,则宜采用链表做存储结构
三、栈和队列
- 深刻领会栈的定义、特点,栈在计算机中的存储,理解栈顶、栈底的含义,掌握运用栈的特点,会写进栈出栈序列
- 掌握队列的定义、特点,会运用队列的特点写进队出队序列,了解队列的典型应用
- 什么是循环队列,理解性掌握判断队列满和队列空的条件,会灵活运用
- 掌握在链队列中入队和出队的算法
3.1 栈
3.1.1 定义
栈:限定仅在表的一端进行插入和删除操作的线性表
栈顶:允许插入和删除的一端为栈顶
栈底:另一端
特点:
- 后进先出
3.1.2 顺序栈
3.1.2.1 顺序栈的实现
const int StackSize = 10;
template<typename DataType>
class SeqStack{
public:
SeqStack();
~SeqStack();
void Push(DataType x);
DataType Pop();
DataType GetTop();
int Empty();
private:
DataType data[StackSize];
int top;
};
-
进栈
template<class DataType> void SeqStack<DataType>::Push(DataType x){ if(top == StackSize-1) throw "上溢"; else{ data[++top] = x; } }
-
出栈
template<class DataType> DataType SeqStack<DataType>::Pop(){ if(top < 0) throw "下溢"; else{ return data[top--]; } }
3.2 队列
3.2.1 定义
队列:只允许在一端进行插入操作,在另有但进行删除操作的线性表
队尾:允许插入的一端
队头:允许删除的一端
特点:
- 先进先出
3.2.2 顺序队列
-
进队
SeqQueue[++rear] = x ;
-
出队
x = SeqQueue[++front];
3.2.3 循环队列
3.2.3.1 循环队列的实现
const int QueueSize = 100;
template<class T>
class CirQueue{
public:
CirQueue();
~CirQueue();
void EnQueue(T x);
T DeQueue();
T GetHead();
int Empty();
private:
T data[QueueSize];
int front,rear;
};
-
进队
template<class DataType> void CirQueue<DataType>::EnQueue(T x){ if((rear+1) % QueueSize == front) throw "队满"; rear = (rear+1) % QueueSize; data[rear] = x; }
-
出队
template<class DataType> T CirQueue<DataType>::DeQueue(){ DataType x; if(front == rear) throw "队空"; front = (front+1) % QueueSize; x = data[front]; return x; }
3.2.4 链队列
template <typename DataType>
class LinkQueue
{
public:
LinkQueue( );
~LinkQueue( );
void EnQueue(DataType x);
DataType DeQueue( );
DataType GetQueue( );
int Empty( );
private:
Node<DataType> *front, *rear;
};
-
进队
template<class DataType> void LinkQueue<DataType>::EnQueue(DataType x){ Node<DataType> *p = new Node<DataType>; p -> data = x; p -> next = nullptr; rear -> next = p; rear = p; }
-
出队
template<class DataType> DataType LinkQueue<DataType>::DeQueue( ){ DataType x; Node<DataType> *p = nullptr; if(rear == front) throw "下溢"; p = fornt -> next; x = p -> data; front -> next = p->next; //!!!当出队钱队列长度为1时 if(p->next = nullptr) rear = front; delete p; return x; }
四、字符串多维数组
- 了解KMP算法的运行过程,理解性掌握如何求KMP算法的next[j],next[j]在KMP算法中有什么作用,会灵活运用
- 理解性掌握二维数组按行优先存储的寻址方法,及按列优先存储的寻址方法
- 理解性掌握对称矩阵的压缩存储及三角矩阵的压缩存储
4.1 KMP算法
4.2 二维数组
按行优先存储
按列优先存储
4.3 压缩存储
对称矩阵压缩
注意!这里的i和j是从1开始的(一开始没看见,算半天不一样…)
三角矩阵压缩
五、树和二叉树
- 树的定义、树的基本概念,如结点、结点的度、叶子结点、有序树、无序树等,会灵活运用,例如求度为1、2、叶子结点数等
- 熟练掌握二叉树的性质,会灵活运用
- 掌握二叉树的遍历方法,先序中序后序,会写遍历序列
- 重点掌握已知二叉树的中序后序遍历如何确定一颗二叉树
- 深刻理解并重点掌握二叉树的先序、中序、后序遍历的算法和算法思想,会灵活运用,比如求叶子,二叉树的高度,左右子树的交换,求结点的双亲,结点个数
- 掌握树和二叉树的转换,树如何转换成二叉树,二叉树如何还原为树
- 掌握森林与二叉树的转换,森林如何转换为二叉树,二叉树如何还原为森林
- 理解性掌握哈夫曼树的构造过程,会灵活运用;解性掌握哈夫曼编码的原则,了解哈夫曼树的特点,会灵活运用
5.1 树的定义和基本术语
5.1.1 树的定义
结点:在树中通常讲数据元素称为结点
树:是n(n≥0)个结点的有限集合。
5.1.2 树的基本术语
结点的度:某结点所拥有的子树的个数
树的度:树中各结点度的最大值
叶子结点(终端结点):度为0的结点
分支结点(非终端结点):度不为0的结点
结点的层数:根结点的层数为 1;对其余结点,若某结点在第 k 层,则其孩子结点在第 k+1 层
树的深度(高度):树中所有结点的最大层数
树的宽度:树中每一层结点个数的最大值
有序树、无序树:如果一棵树中结点的各子树从左到右是有次序的,称这棵树为有序树;反之,称为无序树。
求度为k的结点个数
遍历每一个结点,如果该结点具有k个孩子,则cnt++
求叶子结点个数
如果一个结点的左右都儿子为空,则为叶子结点,cnt++
5.2 树的存储
双亲表示法
孩子表示法
孩子兄弟表示法
5.3 二叉树
5.3.1 二叉树的定义
二叉树: n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
二叉树的特点:
- 每个结点最多有两颗子树
- 二叉树的左右子树不能随意颠倒,如果某结点只有一棵子树,一定要指明它是左子树还是右子树
特殊的二叉树:
-
斜树
-
左斜树:所有结点都只有左子树的二叉树
右斜树:所有结点都只有右子树的二叉树
-
特点:
- 每一层只有一个结点
- 结点个数与其深度相同
-
-
满二叉树
:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树
- 特点:
- 叶子只能出现在最下一层
- 只有度为 0 和度为 2 的结点
- 特点:
-
完全二叉树
:在满二叉树中,从最后一个结点开始,连续去掉任意个结点得到的二叉树
- 特点:
- 叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左侧连续的位置
- 完全二叉树中如果有度为 1 的结点,只可能有一个,且该结点只有左孩子
- 深度为 k 的完全二叉树在 k-1 层上一定是满二叉树
- 特点:
【答案】B
【解析】当深度为1时
5.3.2 二叉树的性质
⭐对于一颗具有n个结点的树,其所有结点的度之和为n-1
5.3.3 二叉树的遍历
- 前序遍历
template<class DataType>
void BiTree<DataType>::PreOrder(BiNode<DataType> *bt){
if(bt == nullptr)
return;
cout<<bt->data<<"\t";
PreOrder(bt->lchild);
PreOrder(bt->rchild);
}
- 中序遍历
template<class DataType>
void BiTree<DataType>::InOrder(BiNode<DataType> *bt){
if( bt == nullptr)
return;
InOrder(bt->lchild);
cout<<bt->data<<"\t";
InOder(bt->rchild);
}
- 后序遍历
template<class DataType>
void BiTree<DataType>::PostOrder(BiNode<DataType> *bt){
if(bt==nullptr)
return;
PostOrder(bt->lchild);
PostOrder(bt->rchild);
cout<<bt->data<<"\t";
}
- 层序遍历
template<class DataType>
void BiTree<DataType>::LevelOrder(){
BiNode t[MaxSize];
int front = -1, rear = -1;
//!!别漏了!当树为空时的判断!
if(root=nullptr)
return;
t[++rear] = root;
while(front != rear){
BiNode *p = t[++front];
cout<<p->data<<"\t";
if(p->lchild != nullptr)
t[++rear] = p->lchild;
if(p->rchild != nullptr)
t[++rear] = p->rchild;
}
}
求叶子,二叉树的高度,左右子树的交换,求结点的双亲,结点个数
5.3.4 树、森林与二叉树的转换
森林:m(m≥0)棵互不相交的树的集合。
森林的遍历:
- 前序遍历:前序遍历森林中的每一棵树
- 后序遍历:后序遍历森林中的每一棵树
树转换为二叉树
- 加线:数中所有相邻兄弟几点之间加一条连线
- 去线:对树中的每个结点只保留它与第一个孩子结点间的连线,删除与其他孩子结点之间的连线
- 层次调整:按照二叉树结点之间的关系进行层次调整
树的前序遍历序列等于对应二叉树的前序遍历序列
树的后序遍历序列等于对应二叉树的中序遍历序列
森林转换为二叉树
- 将森林中的每棵树转换为二叉树
- 将每棵树的根结点视为兄弟,在所有根结点之间加上连线
- 按照二叉树结点之间的关系进行层次调整
二叉树转换为树或森林
- 加线:若结点x是其双亲的左孩子,则把结点x的右孩子,右孩子的右孩子、…,都与结点y用线连起来
- 去线:删除原二叉树中所有的双亲结点与右孩子的连线
- 层次调整
5.3.5 哈夫曼编码
相关概念:
- 叶子结点的权值:对叶子结点赋予的一个有意义的数值量
- 带权路径长度:从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和
- 最优二叉树(哈夫曼树):带权路径长度最小的二叉树
- 特点:
- 权值越大的叶子结点越靠近根结点
- 只有度为 0 和度为 2 的结点,不存在度为 1 的结点
- 特点:
5.3.5.1 构造哈夫曼树
struct ElemType{
int weight;//权值
int parent,lchild,rchild;
}
HuffmanTree(int w[], int n){
huffTree = new ElemType[2*n-1];
//1.初始化
for(int i=0;i<2*n-1;i++){
huffTree[i].parent = -1;
huffTree[i].lchild = -1;
huffTree[i].rchild = -1;
}
//2.赋权值
for(int i=0;i<n;i++){
huffTree[i].weight = w[i];
}
//3.合并
int i1,i2;
for(int i=n;i<2*n-1;i++){
Select(i,&i1,&i2);
huffTree[i].weight = huffTree[i1].weight+huddTree[i2].weight;
huffTree[i1].parent = i;
huffTree[i2].parent = i;
huffTree[i].lchild = i1;
huffTree[i].rchild = i2;
}
}
5.3.5.2 哈夫曼编码
六、图
- 图的定义,深刻领会图的基本概念,如有向图、无向图、有向完全图、无向完全图、连通图、连通分量、强连通图等基本概念,会灵活运用,如求顶点数、边数等
- 掌握度、入度、出度的计算方法,会根据存储结构求有向图的入度和出度
- 图的深度优先遍历和广度优先遍历方法,了解深度优先遍历算法和广度优先遍历算法的时间复杂度,掌握图的广度优先遍历算法
- 掌握图的两种存储结构,邻接矩阵和邻接表,重点掌握图的邻接矩阵和邻接表的表示方法
- 重点掌握Prim算法及Kru算法如何求最小生成树,会求最小生成树的权值,会灵活运用Prim算法和K
- 掌握拓扑排序的思想及拓扑排序的用途,重点掌握给定一个图如何进行拓扑排序,会写出拓扑排序的序列
- 会计算关键路径及关键路径的长度
- 采用邻接矩阵表示法构造一个无向图的算法,采用邻接表表示法构造一个无向图的算法
6.1 图的定义和基本术语
6.1.1 图的定义
图:由顶点的有穷非空集合和顶点之间边的集合组成
无向图:图中任意两个顶点之间的边都是无向边
有向图:图中任意两个顶点之间的边都是有向边
带权图(网图):边上带权的图
稠密图:边数很多的图
稀疏图:边数很少的图
6.1.2 图的基本术语
- 邻接、依附
-
顶点的度、入度
-
完全图
-
路径、路径长度、回路
路径长度:
-
非带权图——路径上边的个数
-
带权图——路径上边的权值之和
回路(环):第一个顶点和最后一个顶点相同的路径
-
-
简单路径、简单回路
简单路径:序列中顶点不重复出现的路径
简单回路(简单环):除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路
-
子图
-
连通图、连通分量
连通图:在无向图中,如果任意两个顶点都是连通的,则称该无向图是连通图
-
强连通图、强连通分量
求顶点数、边数
求有向图的入度和出度
根据存储结构
6.2 图的存储
6.2.1 邻接矩阵
空间复杂度:O(n^2)
6.2.1.1 无权图的邻接矩阵
特点:
- 对称矩阵
求顶点v的度:
- 第 v 行(或第 v 列)非零元素的个数
求顶点i的邻接点:
- 扫描第i行
6.2.1.2 带权图的邻接矩阵
求顶点v的入度:
- 第v列非零元素的个数
求顶点v的出度:
- 第v行非零元素的个数
1.图的邻接矩阵采用数组方式进行存储,因此属于顺序存储结构。(×)
【解析】图没有顺序存储结构
2.无向图的邻接矩阵一定是对称的,有向图的邻接矩阵一定是不对称的。(×)
【解析】当顶点间存在方向相反的弧
6.2.1.3 邻接矩阵的建立
const int MaxSize = 10;
template <typename DataType>
class MGraph
{
public:
MGraph(DataType a[ ], int n, int e);
~MGraph( );
void DFTraverse(int v);
void BFTraverse(int v);
private:
DataType vertex[MaxSize];
int edge[MaxSize][MaxSize];
int vertexNum, edgeNum;
};
template<class DataType>
MGraph<DataType>::MGraph(DtatType a[],int n, int e){
int v1, v2, edge;
vertexNum = n;
edgeNum = e;
for(int i=0;i<n;i++)
vertex[i] = a[i];
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
edge[i][j] = 0;
for(int i=0;i<e;i++){
cin>>v1>>v2>>edge;
edge[v1][v2] = 1;
edge[v2][v1] = 1;
}
}
6.2.2 邻接表
空间复杂度:O(n+e)
求顶点v的度
-
顶点v边表中的结点个数
p = adjlist[v].firstEdge; count = 0; while (p != nullptr) count++; p = p->next;
求顶点v的所有邻接顶点
-
遍历顶点v边表中的所有结点
p = adjlist[v].firstEdge; while (p != nullptr) { j = p->adjvex; //j是v的邻接点 p = p->next; }
求顶点v的出度
- 顶点v的出边表中的结点个数
求顶点v的入度
- 所有出边表中数据域为v的结点个数
存储结构定义:
struct EdgeNode
{
int adjvex;
EdgeNode *next;
};
template <typename DataType>
struct VertexNode
{
DataType vertex;
EdgeNode *firstEdge;
};
6.2.2.1 邻接表的建立
const int MaxSize = 10;
template <typename DataType>
class ALGraph
{
public:
ALGraph(DataType a[ ], int n, int e);
~ALGraph( );
void DFTraverse(int v);
void BFTraverse(int v);
private:
VertexNode<DataType> adjlist[MaxSize];
int vertexNum, edgeNum;
};
tempalte<class DataType>
ALGraph<DataType>::ALGraph(DataType a[ ], int n, int e){
int v1, v2, edge;
vertexNum = n;
edgeNum = e;
for(int i=0;i<n;i++){
adjlist[i].vertex = a[i];
adjlist[i].firstEdge = nullptr;
}
for(int i=0;i<e;i++){
cin>>v1>>v2>>edge;
EdgeNode *p = new EdgeNode;
p->adjvex = v2;
//从边表表头插入
p->next = adjlist[v1].firstEdge;
adjlist[v1].firstEdge = p;
}
}
6.2.3 邻接矩阵与邻接表的比较
-
空间性能比较
- 邻接矩阵:O(n^2)
- 邻接表:O(n+e)
-
时间性能比较
查找某顶点的所有邻接点
-
邻接矩阵:O(n)
-
邻接表:O(e/n)
-
-
唯一性比较
- 邻接矩阵:唯一
- 邻接表:不唯一
-
对应关系
- 邻接表中顶点i的边表对应邻接矩阵的第i行
6.3 图的遍历
6.3.1 图的深度优先遍历
基本思想:
邻接矩阵中
时间复杂度:O(n^2)
visited[MaxSize];
template<class DataType>
void MGrapg<DataType>::DFTraverse(int v){
cout<<vertex[v]<<"\t";
//记得修改访问状态!!!
visited[v] = 1;
for(int i=0; i<vertexNum; i++){
if(edge[v][i] == 1 && visited[i] == 0)
DFTraverse(i);
}
}
邻接表中
时间复杂度:
int visited[MaxSize];
template <class DataType>
void ALGraph<DataType>::DFTraverse(int v){
EdgeNode *p = nullptr;
cout<<adjlist[v].vertex<<"\t";
visited[v]=1;
p = adjlist[v].firstEdge;
while(p!=nullptr){
if(visited[p->adjvex] == 0)
DFTraverse(p->adjvex);
p = p->next;
}
}
6.3.2 图的广度优先遍历
基本思想:
邻接矩阵中
时间复杂度:O(n^2)
visited[MaxSize];
template<class DataType>
void BFTraverse(int v){
int Q[vertexNum];
int front = -1, rear = -1;
int k;
cout<<vertex[v]<<"\t";
Q[++rear] = v;
visited[v]=1;
while(front!=rear){
k=Q[++front];
for(int i=0;i<vertexNum;i++){
if(edge[k][i]==1 && visited[i]==0){
cout<<vertex[i]<<"\t";
Q[++rear] = i;
visited[i] = 1;
}
}
}
}
邻接表中
visited[MaxSize];
template <class DataType>
void ALGraph<DataType>::BFTraverse(int v){
int Q[MaxSize];
int front = -1, rear = -1;
int k, j;
EdgeNode *p = nullptr;
cout<<adjlist[v].vertex<<"\t";
visited[v] = 1;
Q[++rear] = v;
while(rear!=front){
k=Q[++front];
p=adjlist[k].firstEdge;
while(p!=nullptr){
j = p->adjvex;
if(visited[j]==0){
cout<<adjlist[j].vertex<<"\t";
visited[j]=1;
Q[++rear] = j;
}
p=p->next;
}
}
}
6.4 最小生成树
- 重点掌握Prim算法及Kruskal算法如何求最小生成树,会求最小生成树的权值,会灵活运用Prim算法和Kruskal
生成树:连通图的生成树是包含全部顶点的一个极小连通子图
- 含有n-1条边
- 多则形成回路,少则不连通
生成树的代价:在无向连通网中,生成树上各边的权值之和
最小生成树:在无向连通网中,代价最小的生成树
6.4.1 Prim算法(加点法)
时间复杂度:O(n^2)
- 采用邻接矩阵存储
void Prim(int v){
int lowcost[MaxSize],adjvex[MaxSize];
int k;
for(int i=0;i<vertexNum;i++){
lowcost[i] = edge[v][i];
adjex[i] = v;
}
lowcost[v]=0;
for(int i=1;i<vertexNum;i++){
k=minEdge(lowcost, vertexNum);
cout<<j<<adjvex[j]<<lowcost[j]<<endl;
lowcost[k]=0;
for(int j=0;j<vertexNum;j++){
if(edge[k][j]<lowcost[j]){
lowcost[j] = edge[k][j];
adjvex[j] = k;
}
}
}
}
6.4.2 Kruskal算法(加边法)
时间复杂度:O(elog2e)+O(elog2n) = O(elog2e)
O(elog2e)用于对边集数组进行排序
- 采用边集数组表示法
struct EdgeType{
int from, to, weight;
}
const int MaxVertex = 10;
const int MaxEdge = 100;
template<class DataType>
class EdgeGraph{
public:
EdgeGraph(DataType a[], int n, int e);
~EdgeGraph();
void Kruskal();
private:
int FindRoot(int parent[], int v);
DataType vertex[MaxVertex];
EdgeType edge[MaxEdge];
int vertexNum, edgeNum;
};
int FindRoot(int parent[], int v){
int t = v;
while(parent[t]>-1)
t = parent[t];
return t;
}
void EdgeGraph<DataType>::Kruskal(){
int parent[vertexNum];
int vex1,vex2,num = 0;
for(int i=0; i<vertexNum; i++)
parent[i] = -1;
for(int i=0,num=0; num<vertexNum-1; i++){
vex1 = FindRoot(parent, edge[i].from);
vex2 = FindRoot(parent, edge[i].to);
if(vex1!=vex2){
cout<<"("<<edge[i].from<<","<<edge[i].to<<")"<<edge[i].weight<<endl;
parent[vex2] = vex1;
num++;
}
}
}
6.5 AOV网与拓扑排序
- [] 掌握拓扑排序的思想及拓扑排序的用途,重点掌握给定一个图如何进行拓扑排序,会写出拓扑排序的序列
基本思想
算法:拓扑排序TopSort
输入:AOV网 G=(V,E)
输出:拓扑序列
1. 重复下述操作,直到输出全部顶点,或AOV网中不存在没有前驱的顶点
1.1 从AOV网中选择一个没有前驱的顶点并且输出;
1.2 从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧;
1.在一个有向图的拓扑序列中,若顶点a在顶点b之前,则图中必有一条弧<a,b>(×)
2.若一个有向图的邻接矩阵中对角线以下元素均为零,则该图的拓扑序列必定存在(√)
3.拓扑排序算法可以用栈或者队列保存入度为0的顶点(√)
6.6 AOE网与关键路径
- 会计算关键路径及关键路径的长度
关键路径:AOE网中从源点到终点的最长路径
关键活动:关键路径上的活动
基本思想
设带权有向图 G=(V,E)含有 n 个顶点 e 条边,设置 4 个一维数组:
(1)事件的最早发生时间 ve[n]
(2)事件的最迟发生时间 vl[n]
(3)活动的最早开始时间 ae[e]
(4)活动的最晚开始时间 al[e]
算法:关键路径算法
输入:带权有向图 G=(V,E)
输出:关键活动
1. 计算各个事件的最早发生时间和最晚发生时间ve[n]和vl[n];
2. 计算各个活动的最早开始时间和最晚开始时间ae[e]和al[e];
3. 计算各个活动的时间余量,时间余量为 0 即为关键活动 ;
七、查找技术
- 重点掌握顺序查找及折半查找(递归和非递归)的算法和算法思想,算法的时间复杂度,了解顺序查找、折半查找的存储结构,会灵活运用
- 什么是二叉排序树,重点掌握如何在二叉排序树中插入或删除一个元素,如何构造一颗二叉排序树
- 理解性掌握平衡二叉树的构造方法,平衡二叉树首先是一颗二叉排序树,怎么进行构造平衡二叉树
- 什么是散列表,什么是散列函数,散列函数的设计方法,重点掌握散列表处理冲突的方法,会灵活运用,包括开散列表、闭散列表怎样处理冲突
- 掌握开散列表的查找算法及算法思想,会灵活运用
7.1 线性表的查找技术
const int MaxSize = 100;
class LineSearch{
public:
LineSearch(int a[], int n);
~LineSearch();
int SeqSearch(int k);
int BinSearch1(int k);
int BinSearch2(int low, int high, int k);
private:
int data[MaxSize];
int length;
}
7.1.1 顺序查找
时间复杂度:
int SeqSearch(int k){
int i = length;
data[0] = k;
while(data[i]!=k)
i--;
return i;
}
7.1.2 折半查找
时间复杂度:
递归算法
int BinSearch2(int low, int high, int k){
if(high<low)
return 0;
else{
int mid = (low+high)>>1;
if(data[mid] == k)
return mid;
else if(data[mid] > k)
BinSearch2(low,mid-1,k);
else
BinSearch2(mid+1,high,k);
}
}
非递归算法
int BinSearch1(int k){
int low = 1, high = length,mid;
while(low<=high){
mid=(low+high)>>1;
if(k<data[mid]){
high = mid-1;
}else if(k>data[mid]){
low = mid+1;
}else{
return mid;
}
}
//别漏了当找不到要查询的值的情况!!
return 0;
}
7.2 树表的查找技术
7.2.1 二叉排序树
- 什么是二叉排序树,重点掌握如何在二叉排序树中插入或删除一个元素,如何构造一颗二叉排序树
7.2.1.1 什么是二叉排序树
二叉排序树(二叉查找树):或者是一棵空的二叉树,或者是具有下列性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于根结点的值
(2)若它的右子树不空,则右子树上所有结点的值均大于根结点的值
(3)它的左右子树也都是二叉排序树
二叉排序树的存储:二叉链表
时间复杂度:
7.2.1.2 二叉排序树的类定义
class BiSortTree
{
public:
BiSortTree(int a[ ], int n);
~ BiSortTree( ) {Release(root);}
BiNode<int> *InsertBST(int x) {return InsertBST(root, x);}
void DeleteBST(BiNode<int> *p, BiNode<int> *f );
BiNode<int> *SearchBST(int k) {return SearchBST(root, k);}
private:
BiNode<int> *InsertBST(BiNode<int> *bt , int x);
BiNode<int> *SearchBST(BiNode<int> *bt, int k);
void Release(BiNode<DataType> *bt);
BiNode<int> *root;
};
7.2.1.3 二叉排序树的查找
BiNode<int> *SearchBST(BiNode<int> *bt, int k){
//当是空树时!!
if(bt == nullptr)
return nullptr;
if(k==bt->data){
return bt;
}else if(k>bt->data){
return SearchBST(bt->rchild, k);
}else{
return SearchBST(bt->lchild, k);
}
}
7.2.1.4 二叉排序树的插入
BiNode<int> *InsertBST(BiNode<int> *bt , int x){
if(bt==nullptr){
BiNode *p = new BiNode<int>;
p->data = x;
p->rchild=nullptr;
p->lchild=nullptr;
bt=p;
return bt;
}
else if(x<bt->data)
bt->lchild = InsertBST(bt->lchild, x);
else
bt->rchild = InsertBST(bt->rchild, x);
return bt;
}
7.2.1.5 二叉排序树的构造
BiSortTree(int a[ ], int n){
root = nullptr;
for(int i=0 ;i<n; i++)
root = InsertBST(root,a[i]);
}
7.2.1.6 二叉排序树的删除
void DeleteBST(BiNode<int> *p, BiNode<int> *f ){
//删除的是叶子结点
if(p->lchild==nullptr && p->rchild==nullptr){
f->lchild = nullptr;
delete p;
return;
}
//如果被删除结点只有左子树或只有右子树
if(p->rchild==nullptr){
f->lchild = p->lchild;
delete p;
return;
}
if(p->lchild==nullptr){
f->lchild = p->rchild;
delete p;
return;
}
//如果被删除的结点有左右子树
//操作:以其左子树中的最大值结点(或以右子树中的最小值)替换之,然后再删除该结点
BiNode *par = p, *s=p->rchild;
while(s->lchild !=nullptr){
par=s;
s=s->lchild;
}
p->data=s->data;
//如果p的右孩子就是最小值
if(par==p)
p->rchild=s->rchild;
else
par->lchild=s->rchild;
delete s;
}
7.2.2 平衡二叉树
- 理解性掌握平衡二叉树的构造方法,平衡二叉树首先是一颗二叉排序树,怎么进行构造平衡二叉树
平衡因子:该结点的左子树的深度减去右子树的深度
平衡二叉树:或者是一棵空的二叉排序树,或者是具有下列性质的二叉排序树:
(1)根结点的左子树和右子树的深度最多相差 1;
(2)根结点的左子树和右子树也都是平衡二叉树;
最小不平衡子树:以距离插入结点最近的、且平衡因子的绝对值大于 1 的结点为根的子树
7.2.2.1 平衡二叉树的构造方法
7.2.2.2 平衡二叉树的平衡调整
- LL型
- RR型
- RL型
- LR型
7.3 散列表的查找技术
- 什么是散列表,什么是散列函数,散列函数的设计方法,重点掌握散列表处理冲突的方法,会灵活运用,包括开散列表、闭散列表怎样处理冲突
- 掌握开散列表的查找算法及算法思想,会灵活运用
7.3.1 散列表
散列表:采用散列技术存储查找集合的连续存储空间
散列函数:将关键码映射为散列表中适当存储位置的函数
7.3.2 散列函数的设计
7.3.2.1 直接定址法
散列函数是关键码的线性函数,即:
H(key) = a × key + b (a,b为常数)
适用于:事先知道关键码,关键码集合不是很大且连续性较好
7.3.2.2 平方取中法
对关键码平方后,按散列表大小,取中间的若干位作为散列地址
适用于:事先不知道关键码的分布且关键码的位数不是很大
7.3.2.3 除留余数法
散列函数:H(key)=key mod p
-
如何选取合适的 p,才能产生较少的同义词?
:小于等于表长(最好接近表长)的最小素数或不包含小于20质因子
适用于:最简单、最常用,不要求事先知道关键码的分布
7.3.3 开散列表
处理冲突的办法:用拉链法处理冲突得到的散列表
拉链法:
对于给定的关键码key执行下述操作:
(1)计算散列地址:j = H(key)
(2)将key对应的记录插入到同义词子表 j 中;
const int MaxSize = 100;
class HashTable2
{
public:
HashTable2( );
~HashTable2( );
int Insert(int k);
int Delete(int k);
Node<int> * Search(int k);
private:
int H( );
Node<int> * ht[MaxSize];
};
HashTable2 :: HashTable2( )
{
for (int i = 0; i < MaxSize; i++)
{
ht[i] = nullptr;
}
}
HashTable2 :: ~HashTable2( )
{
Node<int> *p = nullptr, *q = nullptr;
for (int i = 0; i < MaxSize; i++)
{
p = q = ht[i];
while (p != nullptr)
{
p = p->next;
delete q;
q = p;
}
}
}
构造
j = H(k);
Node<int> *p = ht[j];
while (p != nullptr)
{
if (p->data == k) break;
else p = p->next;
}
if (p == null) {
q = new Node<int>; q->data = k;
q->next = ht[j]; ht[j] = q;
}
查找
Node<int> * Search(int k){
int j=H(k);
Node<int> *p = ht[j];
while(p!=nullptr){
if(p->data==k)
return p;
else
p=p->next;
}
return nullptr;
}
7.3.4 闭散列表
处理冲突的办法:用开放定址法处理冲突得到的散列表
开放定址法:
对于给定的关键码key执行下述操作:
(1)计算散列地址:j = H(key)
(2)如果地址 j 的存储单元没有存储记录,则存储key对应的记录;
(3)如果在地址 j 发生冲突,则寻找一个空的散列地址,存储key对应的记录;
寻找空的散列地址的方法:
-
-
随机探测法
const int MaxSize = 100;
class HashTable1
{
public:
HashTable1( );
~HashTable1( );
int Insert(int k);
int Delete(int k);
int Search(int k);
private:
int H( );
int ht[MaxSize];
};
HashTable1 :: HashTable1( )
{
for (int i = 0; i < MaxSize; i++)
ht[i] = 0;
}
HashTable1 :: ~HashTable1( )
{
}
查找算法
int Search(int k){
int i, j=H(k);
i=j;
while(ht[i]!=0){
if(ht[i]==k)
return i;
else
i=(i+1)%MaxSize;
}
return -1;
}
7.3.5 开散列表与闭散列表的比较
八、排序技术
- 了解排序的基本概念
- 理解性掌握插入排序,包含直接插入排序、希尔排序;交换排序,包括起泡排序和快速排序;选择排序,包括简单排序和堆排序
- 重点掌握起泡排序、快速排序、希尔排序的排序方法及排序过程,会灵活运用
- 什么是堆,重点掌握堆排序的调整过程及堆排序的过程
- 掌握希尔排序的排序算法
- 掌握起泡排序的排序算法
- 掌握快速排序的排序算法
8.1 排序的基本概念
排序算法的稳定性:假定在待排序的记录序列中存在多个具有相同关键码的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法稳定,否则称为不稳定。
8.2 插入排序
8.2.1 直接插入排序
稳定性:稳定
void InsertSort(){
int i,j,temp;
//排序进行n-1趟
for(i=1;i<length;i++){
temp=data[i];
for(j=i-1;j>=0&&temp<data[j];j--)
data[j+1]=data[j];
data[j+1] = temp;
}
}
8.2.2 希尔排序
void ShellSort(){
int i,j,d, temp;
for(d=length/2;d>=1;d/=2){
for(i=d;i<length;i++){
temp=data[i];
for(j=i-d;j>=0&&temp<data[j];j-=d)
data[j+d] = data[j];
data[j+d]=temp;
}
}
}
8.3 交换排序
8.3.1 起泡排序
稳定性:稳定
void BubbleSort(){
int j, exchange, bound, temp;
//用exchange记录最后一次交换的位置
//减少重复的比较
exchange = length-1;
while(exchange!=0){
bound = exchange;
exchange = 0;
for(j=0;j<bound;j++)
if(data[j]>data[j+1]){
temp=data[j];
data[j]=data[j+1];
data[j+1]=temp;
exchange=j;
}
}
}
8.3.2 快速排序
int Partition(int first, int last){
int i=first,j=last;
int temp;
while(i<j){
while(i<j && data[j]>=data[i])
j--;
if(i<j){
temp = data[j];
data[j] = data[i];
data[i] = temp;
}
while(i<j && data[i]<=data[j])
i++;
if(i<j){
temp = data[j];
data[j] = data[i];
data[i] = temp;
}
}
return i;
}
void QuickSort(int first, int last){
if(first>=last)
return;
else{
int pivot = Partition(first, last);
QuickSort(first,pivot-1);
QuickSort(pivot+1,last);
}
}
8.4 选择排序
8.4.1 简单选择排序
稳定性:不稳定
void SelectSort(){
int k;
for(int i=0;i<length-1;i++){
k=i;
for(int j=i+1;j<length;j++){
if(data[j]<data[k])
k=j;
}
if(k!=i){
int temp = data[i];
data[i] = data[k];
data[k] = temp;
}
}
}
8.4.2 堆排序
稳定性:不稳定
void Sift(int k, int last){
int i=k,j = 2*k+1;
while(j<=last){
if(j<last && data[j]<data[j+1])
j=j+1;
if(data[i]>data[j])
break;
else{
int temp = data[k];
data[k] = data[j];
data[j] = temp;
i=j;
j=2*i+1;
}
}
}
void HeapSort(){
for(int i=ceil(length/2)-1;i>=0;i--){
Sift(i,length-1);
}
for(int i=1;i<length;i++){
int temp = data[0];
data[0] = data[length-i];
data[length-i] = temp;
Sift(0,length-i-1);
}
}