您的位置:首页 > 理论基础 > 数据结构算法

[备战软考]数据结构与算法基础

2017-09-09 16:15 405 查看

数据结构与算法基础

数据结构与算法基础
线性表
顺序表

链表
链表的操作

顺序表与链表的比较



队列
循环队列

树和二叉树
基本概念

树的遍历

二叉树

二叉树的遍历

查找二叉树二叉排序树
基本操作

最优二叉树哈夫曼树
基本概念

构造哈夫曼树

线索二叉树
将二叉树转化为线索二叉树

平衡二叉树
平衡树调整


基本概念

图的存储
邻接矩阵

邻接表

图的遍历
深度优先DFS

广度优先BFS

最小生成树
Prim算法

Kruskal算法

拓扑排序

关键路径

排序算法
插入排序

选择排序

交换排序

归并排序

基数排序

哈希表
Hash函数的构造

冲突处理方法
开放地址法

拉链法

查找算法
顺序查找

二分查找

分块查找

线性表

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,最坏nO(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
, //记录不在S中的顶点在S中的最近邻接点
lowcost
, //记录不在S中的顶点到S的最短距离,即到最近邻接点的权值
used
; //标记顶点是否被访问,访问过的顶点标记为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)=keyH(key) = a*key+b

除余法

以关键码除以表元素总数后得到的余数为地址

基数转换法

将关键码看作是某个基数制上的整数,然后将其转换为另一基数制上的数

平方取中法

取关键码的平方值,根据表长度取中间的几位数作为散列函数值

折叠法

将关键码分成多段,左边的段向右折,右边的段向左折,然后叠加

移位法

将关键码分为多段,左边的段右移,右边的段左移,然后叠加

随机数法

选择一个随机函数,取关键码的随机函数值

2.冲突处理方法

开放地址法

线性探查法:冲突后直接向下线性地址找一个新的空间存放

双散列函数法:用两个散列函数来解决冲突

拉链法

将散列表的每个结点增加一个指针字段,用于链接同义词的字表

查找算法

1.顺序查找

从一端开始逐个对比当前结点和关键字是否相等

2.二分查找

要求待查序列为有序表

每次对比中点和关键字是否相等,若相等则找到。若关键字大,则在右边的区间继续这一操作,否则在左边的区间继续这一操作

3.分块查找

用索引表记录块的最大关键字和起始地址,然后查找的时候只要找到关键字所在的块,然后在对应的块中查找就可以了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构 算法 软考