:
数据结构与算法基础
线性表
1.顺序表
顺序的存储结构,元素在内存中以顺序存储。内存中占用连续的一个区域。
顺序表的删除
把要删除的元素后面每个元素向前移动一位顺序表的插入
把要插入的位置后面的(包括自己)所有元素向后移动一位,再把要插入的元素放入该位置。
2.链表
离散的存储结构,各个点的存储空间是离散的,通过指针联系起来,从而成为一个整体的链表。
单链表
从第一个元素开始指向下一个元素,最后一个元素指向NULL循环链表
最后一个元素指向头双链表
两个指针域,从两个方向连接
链表的操作
- 单链表的结点删除
前驱指向后继
Node* a1=(Node*)malloc(sizeof(Node));
Node* a2=(Node*)malloc(sizeof(Node));
Node* a3=(Node*)malloc(sizeof(Node));
a1->next=a2;
a2->next=a3;
a3->next=NULL;
cout<<a1->next<<endl;//未删除时
//删除a2这个结点
//关键步骤:将前一个元素的结点的next指向下一个节点
a1->next=a2->next;
//将删除的结点的内存释放
free(a2);
cout<<a1->next<<endl;
- 单链表的结点插入
①将新的结点指向后一个元素
②前一个结点指向要插入的结点
(顺序不能颠倒,否则下一个结点的地址会找不到!)
Node* a1=(Node*)malloc(sizeof(Node));
Node* a2=(Node*)malloc(sizeof(Node));
a1->next=a2;
a2->next=NULL;
//cout<<a1->next<<' '<<a2<<endl;
//插入x这个结点至a1与a2中间
Node* x=(Node*)malloc(sizeof(Node));
x->next=a1->next;//第一步
a1->next=x;
//cout<<a1->next<<' '<<x<<endl;
- 双链表的结点插入和删除
也是参照单链表的方法,做两次操作而已。但是都要先进行完第一步,再进行第二步。
3.顺序表与链表的比较
- 空间性能
项目 | 顺序存储 | 链式存储 |
---|---|---|
存储密度 | =1,更优 | <1 |
容量分配 | 事先确定 | 动态改变,更优 |
- 时间性能
项目 | 顺序存储 | 链式存储 |
---|---|---|
查找运算 | O(n/2) | O(n/2) |
读运算 | O(1),更优 | O([n+1]/2),最好1,最坏n |
插入运算 | O(n/2),最好0,最坏n | O(1),更优 |
删除运算 | O([n-1]/2) | O(1),更优 |
4.栈
先进后出,只能对栈顶进行操作。可以用顺序表和链表实现。
5.队列
先进先出,只能从队尾插入,对头读取。
循环队列
头指针:head 尾指针:tail
如果没有任何元素,head=tail,如果有元素入队,tail向后移一位
如果在最后一个位置也插入了元素,那么tail又会回到head的位置。
为了避免队列空和队列满是一个状态,将最后一个元素的位置舍弃不用。
树和二叉树
1.基本概念
结点的度:与下一层有几个结点相关联,它的度就是多少
树的度:整个树中度数最大的结点的度是多少,树的度就是多少
叶子结点:度为0的结点
分支结点:除了叶子结点的所有结点都是分支结点(下一层有分支)
内部结点:分支结点中除了根结点的所有结点都是内部结点(中间层的结点)
父结点
子节点
兄弟结点
层次
公式:所有结点的度之和+1=结点总个数
2.树的遍历
前序遍历:先访问根节点,再依次访问子结点(访问完了一个子结点在访问后一个子结点)
1 2 5 6 7 3 4 8 9 10
后序遍历:先访问子结点,再访问根结点
5 6 7 2 3 9 10 8 4 1
层次遍历:一层一层地访问
1 2 3 4 5 6 7 8 9 10
3.二叉树
每个结点最多只能有两个子结点,分为左子结点和右子结点。
满二叉树:二叉树的每层都是满的(完整金字塔形状)
完全二叉树:对于n层的二叉树,其n-1层是满二叉树,第n层的结点从左到右连续排列
4.二叉树的遍历
与树的遍历是一样的,就是多了一种中序遍历
- 中序遍历:先访问左子结点,再访问根节点,再访问右子结点
5.查找二叉树(二叉排序树)
空树或满足以下递归条件:
查找树的左右子树各是一颗查找树
若左子树非空,则左子树上的各个结点的值均小于根节点的值
- 若右子树非空,则左子树上的各个结点的值均大于于根节点的值
基本操作
查找:比较当前结点的值与键值,若键值小,则进入左子结点,若键值大,则进入右子结点
插入结点:
- 如果相同键值的结点已经在查找二叉树中,则不再插入
- 如果查找二叉树为空树,则以新结点为查找二叉树
- 比较插入结点的键值与插入后的父节点的键值,就能确定新结点是父节点的左子结点还是右子结点,并插入
- 删除操作
- 若删除的结点p是叶子结点,则直接删除
- 若p只有一个子结点,则将这个子结点与待删除的结点的父节点直接连接,然后删除节点p
- 若p有两个子结点,在左子树上,用中序遍历找到关键值最大的结点s,用s的值代替p的值,然后删除结点s,结点s必须满足上面两种情况之一
6.最优二叉树(哈夫曼树)
基本概念
- 树的路径长度:到达每个叶子结点所需要的长度之和
- 权:人为定义的每个结点的值
- 带权路径长度:路径长度*该结点的权值
- 树的带权路径长度(树的代价):每个叶子结点的带权路径长度之和
构造哈夫曼树
①把每个权值作为根节点,构造成树
②选择两颗根节点最小的树作为子树合成一颗新的树,根节点的值为两个根节点值的和
③重复②,直到只剩一棵树为止
7.线索二叉树
- 表示
[Lbit][Lchild][Data][Rchild][Rbit]
标志域规定:
Lbit=0,Lchild是通常的指针
Lbit=1,Lchild是线索(指向前驱)
Rbit=0,Rchild是通常的指针
Rbit=1,Rchild是线索(指向后继)
将二叉树转化为线索二叉树
对于每个空余的左右指针,都用线索替代,左指针指向前驱,右指针指向后继
前序:A B D E H C F G I
中序:D B H E A F C G I
后序:D H E B F I G C A
8.平衡二叉树
对某个数列构造排序二叉树,可以构造出多颗形式不同的排序二叉树
- 定义:树中任一结点的左、右子树的深度相差不超过1
平衡树调整
- LL型平衡旋转(单向右旋平衡处理)
- RR型平衡旋转(单向左旋平衡处理)
- LR型平衡旋转(双向旋转,先左后右)
- RL型平衡旋转(双向旋转,先右后左)
图
1.基本概念
图的构成:
图由两个集合:V和E所构成,V是非空点集,E是边集,图 G=(V,E)无向图和有向图:
边是单向的是有向图,双向的就是无向图顶点的度
无向图:有几条边相连度就为几
有向图:分为入度和出度子图
完全图
无向图中每对顶点都有一条边相连,有向图中每对顶点都有两条有向边相互连接路径和回路
连通图
有向图中,任意两点都有路径到达
无向图中,没有孤立点的图强连通
有向图中,任意两点作为起点和终点都有路径到达则为强连通。如果只能确保单向连通,则是弱连通。连通分量
图的一个子图是连通图,那么这个子图就是一个连通分量网络
每一条边都有一个权值
2.图的存储
(此部分图片来自刘伟老师)
邻接矩阵
邻接表
又叫邻接链表
3.图的遍历
深度优先(DFS)
- 首先访问一个未访问的节点V
- 依次从V出发搜索V的每个邻接点W
- 若W未访问过,则从该点出发继续深度优先遍历
#include <iostream>
#include <cstring>
#define mem(a,b) memset(a,b,sizeof(a))
using namespace std;
const int maxn=100;
struct EDG
{
int u;
int v;
int w;
//初始化列表
EDG(int uu=0,int vv=0,int ww=0):u(uu),v(vv),w(ww){}
}e[maxn];
int first[maxn],nxt[maxn*2];
int vis[maxn];
int len=0;
void mk_edg(int u,int v,int w)//加入u到v权值为w的边
{
e[++len]=EDG(u,v,w);
nxt[len]=first[u];
first[u]=len;
e[++len]=EDG(v,u,w);
nxt[len]=first[v];
first[v]=len;
}
//图的深度优先遍历(递归写法)
void DFS(int v)
{
vis[v]=1;
cout<<v<<' ';
for(int i=first[v];i!=-1;i=nxt[i])
{
if(!vis[e[i].v])
{
DFS(e[i].v);
}
}
}
int main()
{
mem(first,-1),mem(nxt,-1),mem(vis,0);
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
int u,v,w;
cin>>u>>v>>w;
mk_edg(u,v,w);
mk_edg(v,u,w);
}
for(int i=0;i<n;i++)
{
if(!vis[i])
DFS(i);
}
return 0;
}
广度优先(BFS)
- 首先访问一个未访问的顶点V
- 然后访问与顶点V邻接的全部未访问顶点W、X、Y……
- 然后再依次访问W、X、Y邻接的未访问顶点
4.最小生成树
(此部分代码来自刘伟老师)
Prim算法
思想:
(1) 任意选定一点s,设集合S={s}
(2) 从不在集合S的点中选出一个点j使得其与S内的某点i的距离最短,则(i,j)就是生成树上的一条边,同时将j点加入S
(3) 转到(2)继续进行,直至所有点都己加入S集合
#include<iostream>
using namespace std;
#define MAXN 2001
#define INF 1000000
int n, m;
int G[MAXN][MAXN]; //存储图
void init(){
for(int i = 0 ; i < n ; i++){
for(int j = 0 ; j < n ; j++)
G[i][j] = INF; //初始化图中两点间距离为无穷大
}
}
void prim(){
int closeset[n], //记录不在S中的顶点在S中的最近邻接点
lowcost[n], //记录不在S中的顶点到S的最短距离,即到最近邻接点的权值
used[n]; //标记顶点是否被访问,访问过的顶点标记为1
for (int i = 0; i < n; i++)
{
//初始化,S中只有第1个点(0)
lowcost[i] = G[0][i]; //获取其他顶点到第1个点(0)的距离,不直接相邻的顶点距离为无穷大
closeset[i] = 0; //初始情况下所有点的最近邻接点都为第1个点(0)
used[i] = 0; //初始情况下所有点都没有被访问过
}
used[0] = 1; //访问第1个点(0),将第1个点加到S中
//每一次循环找出一个到S距离最近的顶点
for (int i = 1; i < n; i++)
{
int j = 0;
//每一次循环计算所有没有使用的顶点到当前S的距离,得到在没有使用的顶点中到S的最短距离以及顶点号
for (int k = 0; k < n; k++)
if ((!used[k]) && (lowcost[k] < lowcost[j])) j = k; //如果顶点k没有被使用,且到S的距离小于j到S的距离,将k赋给j
printf("%d %d %d\n",closeset[j] + 1, j + 1, lowcost[j]); //输出S中与j最近邻点,j,以及它们之间的距离
used[j] = 1; //将j增加到S中
//每一次循环用于在j加入S后,重新计算不在S中的顶点到S的距离
//主要是修改与j相邻的边到S的距离,修改lowcost和closeset
for (int k = 0; k < n; k++)
{
if ((!used[k]) && (G[j][k] < lowcost[k])) //松弛操作,如果k没有被使用,且k到j的距离比原来k到S的距离小
{
lowcost[k] = G[j][k]; //将k到j的距离作为新的k到S之间的距离
closeset[k] = j; //将j作为k在S中的最近邻点
}
}
}
}
int main(){
int a , b , w;
scanf("%d%d" , &n , &m);
init();
for(int i = 0 ; i < m ; i++){
scanf("%d%d%d" , &a , &b , &w);
if(G[a-1][b-1] > w)
G[a-1][b-1] = G[b-1][a-1] = w; //无向图赋权值
}
prim();
system("pause");
return 0;
}
Kruskal算法
思想:
(1) 将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边
(2) 最终得到的结果就是最小生成树
#include <iostream>
#include <algorithm>
using namespace std;
/* 定义边(x,y),权为w */
struct edge
{
int x, y;
int w;
};
const int MAX = 26;
edge e[MAX * MAX];
int rank[MAX];/* rank[x]表示x的秩 */
int father[MAX];/* father[x]表示x的父节点 */
int sum; /*存储最小生成树的总权重 */
/* 比较函数,按权值非降序排序 */
bool cmp(const edge a, const edge b)
{
return a.w < b.w;
}
/* 初始化集合 */
void make_set(int x)
{
father[x] = x;
rank[x] = 0;
}
/* 查找x元素所在的集合,回溯时压缩路径 */
int find_set(int x)
{
if (x != father[x])
{
father[x] = find_set(father[x]);
}
return father[x];
}
/* 合并x,y所在的集合 */
int union_set(int x, int y, int w)
{
if (x == y) return 0;
if (rank[x] > rank[y])
{
father[y] = x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]++;
}
father[x] = y;
}
sum += w; //记录权重
return 1;
}
int main()
{
int i, j, k, m, n, t;
char ch;
while(cin >> m && m != 0)
{
k = 0;
for (i = 0; i < m; i++) make_set(i); //初始化集合,m为顶点个数
//对后m-1进行逐行处理
for (i = 0; i < m - 1; i++)
{
cin >> ch >> n; //获取字符(顶点)
for (j = 0; j < n; j++)
{
cin >> ch >> e[k].w; //获取权重
e[k].x = i;
e[k].y = ch - 'A';
k++;
}
}
sort(e, e + k, cmp); //STL中的函数,直接对数组进行排序
sum = 0;
for (i = 0; i < k; i++)
{
int result = union_set(find_set(e[i].x), find_set(e[i].y), e[i].w);
if(result) cout<< e[i].x + 1<< "," << e[i].y + 1 <<endl;
}
cout << sum << endl;
}
system("pause");
return 0;
}
5.拓扑排序
AOV网
拓扑排序
(1) 将所有入度为0的点加入队列
(2) 每次取出队首顶点
(3) 删除其连出的边,检查是否有新的入度为0的顶点,有则加入队列
(4) 重复(2)直到队列为空
6.关键路径
- AOE网
在AOV网中把边加上权值就变成了AOE网
概念:
- 顶点 j 事件的最早发生时间,即从源点到顶点 j 的最长路径长度,记作Ve( j );
- 活动ai的最早发生时间:Ve( j )是以顶点为 j 为起点的出边所表示的活动ai的最早开始时间,记作e( i )
- 顶点 j 事件的最迟发生时间:即在不推迟整个工程完成的前提下,事件 j 允许最迟的发生时间,记作Vl( j );
- 活动ai的最迟发生时间:Vl( j ) - (ai所需的时间),就是活动ai的最迟开始时间,其中j是活动ai的终点,记作l(i);
排序算法
1.插入排序
直接插入排序:
- 每一步把当前的数插入到已经有序的序列中
Shell排序:也称缩小增量排序
- 根据步长d,把相距间隔为d的元素分到一组,在内部进行直接插入排序,然后步长减半,重复这一步操作,直到步长d=1为止
2.选择排序
简单选择排序:
- 每一步查找剩余序列中最小的元素,然后将该元素放到已序序列的末尾
堆排序:
- 还没搞明白
3.交换排序
冒泡排序:
- 从后往前每一步对比相邻的两个元素,如果后面的元素小,则交换
快速排序:
- 运用分治思想。每次选择第一个元素作为基准,将比它小的元素放前面,比它大的放后面,接着在这两个子区间继续进行这一操作。
4.归并排序
- 首先把元素两个一组分组,使每个组内都有序,接下来在把每两组合并,重复这一操作直到只剩一组。
5.基数排序
- 根据元素的每一位来排序,高位的优先级比低位的高
哈希表
Hash表示一种十分实用的查找技术,具有极高的查找效率
1.Hash函数的构造
没有特定的要求,所以方法很多,只要能尽量避免冲突,就叫好的Hash函数,要根据实际情况来构造合理的Hash函数
直接定址法
H(key)=key 或 H(key) = a*key+b除余法
以关键码除以表元素总数后得到的余数为地址基数转换法
将关键码看作是某个基数制上的整数,然后将其转换为另一基数制上的数平方取中法
取关键码的平方值,根据表长度取中间的几位数作为散列函数值折叠法
将关键码分成多段,左边的段向右折,右边的段向左折,然后叠加移位法
将关键码分为多段,左边的段右移,右边的段左移,然后叠加随机数法
选择一个随机函数,取关键码的随机函数值
2.冲突处理方法
开放地址法
- 线性探查法:冲突后直接向下线性地址找一个新的空间存放
- 双散列函数法:用两个散列函数来解决冲突
拉链法
将散列表的每个结点增加一个指针字段,用于链接同义词的字表
查找算法
1.顺序查找
从一端开始逐个对比当前结点和关键字是否相等
2.二分查找
(要求待查序列为有序表)
每次对比中点和关键字是否相等,若相等则找到。若关键字大,则在右边的区间继续这一操作,否则在左边的区间继续这一操作
3.分块查找
用索引表记录块的最大关键字和起始地址,然后查找的时候只要找到关键字所在的块,然后在对应的块中查找就可以了