经典数据结构和算法回顾
作者:网络转载 发布时间:[ 2015/8/3 10:10:39 ] 推荐标签:数据结构 算法
邻接矩阵
对于一个又n个节点的图,邻接矩阵以一个n*n的二维数组a来描述图,对于不同的图,比如,有向图和无向图,带权图和无权图,a[i,j]表示的含义有所不同,但都是描述边的。
邻接表
邻接表组合使用数组和链表描述图,其中数组的每一个元素代表一个节点i,i由两部分组成,一部分代表节点的数据,另一部分为一个指向一链表,这个链表里存放着能从节点i出发能走到的所有节点。对于有向图和无向图会有不同的表示。邻接表一般要比领接矩阵更省空间,但它带来了求入度不便等问题。
十字链表
结合使用邻接表与逆邻接表,这种方式只能描述有向图。首先它也有一个数组,每个数据元素代表一个节点i,i由三部分组成,i在邻接表的基础上增加了一个指针,这个指针指向第一个以i为弧尾的及节点。这很好的解决了求入度的问题。
邻接多重表
邻接多重表主要,它主要用来表描述无向图,在邻接表或十字链表中,数组元素的指针域指向的链表元素其实代表了边,如果用邻接表来存无向图,会使得一条边对应的两个节点分别位于两条链中,当我需要删除一条边时,总是需要找到另一个表示这条边的边节点,再删除。所以有了邻接多重表,邻接多重表是只用一个边界点表示边,但是将它链接到两链表中(对,没有错,一个节点,同时存在于两个链表中)
下面是上面四种描述的代码表示
#ifndef __GRAPH_DEFINE_H__
#define __GRAPH_DEFINE_H__
#define INT_MAX 9999
#define INFINITY INT_MAX // 大值
#define MAX_VERTEX_NUM 20 // 大顶点个数
#define VEX_FORM "%c" // 顶点输入格式
#define INFO_FORM "%d" // 边信息输入格式
typedef int InfoType; // 弧相关信息类型
typedef char VextexType; // 顶点数据类型
typedef int VRType; // 顶点关系类型,对无权图,用0或者1表示是否相邻。对带权图,则是权值类型。
typedef enum { DG,DN,UDG,UDN} GraphKind;// 图类型 {有向图,有向网,无向图,无向网}
//////////////////////////////////////////////////////////////////////////
// 邻接矩阵存储结构: 可存储任意类型图
//////////////////////////////////////////////////////////////////////////
typedef struct{
VRType Adj;
InfoType Info; // 该弧相关信息
}ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
typedef struct{
char cVexs[MAX_VERTEX_NUM]; // 顶点向量
AdjMatrix arcs; // 邻接矩阵
int iVexNum,iArcNum; // 图中当前顶点数和弧数
GraphKind kind; // 图的种类标志
}MGraph;
//////////////////////////////////////////////////////////////////////////
// 邻接表存储结构: 可存储任意类型的图
//////////////////////////////////////////////////////////////////////////
typedef struct ArcNode{
int iAdjvex; // 该弧所指向的顶点位置
struct ArcNode *nextarc; // 指向下一条弧的指针
InfoType Info; // 该弧相关信息
}ArcNode;
typedef struct VNode{
VextexType cData; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧的指针
}VNode,AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;
int iVexnum,iArcnum; // 图的当前顶点个数和弧数
GraphKind Kind; // 图的种类标志
}ALGraph;
//////////////////////////////////////////////////////////////////////////
// 十字链表存储结构: 只存储有向图
//////////////////////////////////////////////////////////////////////////
typedef struct ArcBox{
int iTailVex,iHeadVex; // 该弧的尾和头顶点的位置
struct ArcBox *hLink,*tLink; // 分别为弧头相同和弧尾相同的链域
InfoType Info; // 该弧相关信息
}ArcBox;
typedef struct VexNode{
VextexType data;
ArcBox *firstIn,*firstOut; // 分别指向了该顶点的第一条入弧和出弧
}VexNode;
typedef struct OLGraph{
VexNode xlist[MAX_VERTEX_NUM]; // 表头向量
int iVexNum,iArcNum; // 有向图当前顶点数和弧数
}OLGraph;
//////////////////////////////////////////////////////////////////////////
// 邻接多重表: 存储无向图
//////////////////////////////////////////////////////////////////////////
typedef enum {unvisited,visited}VisitIf;
typedef struct EBox{
VisitIf mark; // 访问标记
int iIVex,iJVex; // 边依附的两个顶点的位置
struct EBox *iLink,*jLink; // 分别指向依附这两个顶点的下一条边
InfoType Info; // 该边信息指针
}EBox;
typedef struct VexBox{
VextexType data;
EBox *firstEdge; // 指向第一条依附该顶点的边
}VexBox;
typedef struct {
VexBox adjmulist[MAX_VERTEX_NUM];
int iVexNum,iEdgeNum; // 无向图当前顶点数和边数
}
#endif//__GRAPH_DEFINE_H__
图相关操作
对图的操作有创建,增删查等等,其中查是遍历,遍历分为深度优先搜索和广度优先搜索。
深度优先搜索类似于树的钟旭遍历,即遇到未范围的节点,马上访问,修改标记数组,然后沿着这个节点继续访问,直到访问完,然后在回朔,转向未访问的分支。直到节点被访问完,如果是连通图,只要访问进行一次深度搜索,如果是非连通的,要搜索多次。
广度优先搜索像金字塔从上向下的一层一层的搜索,广度优先搜索除了需要用标记数组记录状态以外,还需要用队列来将发现而未访问的节点记录下来。用队列是为了保证遍历顺序。
下面是一些图相关的操作算法
#include <stdio.h>
#include "GraphDefine.h"
#include "Define.h"
#include <malloc.h>
//////////////////////////////////////////////////////////////////////////
// 邻接矩阵图的相关操作
//////////////////////////////////////////////////////////////////////////
Status CreateDG(MGraph &G); // 构造有向图
Status CreateDN(MGraph &G); // 构造有向网
Status CreateUDG(MGraph &G); // 构造无向图
Status CreateUDN(MGraph &G); // 构造无向网
int LocateVex(const MGraph &G,const VextexType &v); // 获得顶点v在图中的位置
Status CreateGraph(MGraph &G,VextexType v){
// 采用数组(邻接矩阵)表示法,构造图G
int iType;
scanf("%d%*c",&iType);
G.kind = (GraphKind)iType;
switch(G.kind) {
case DG: return CreateDG(G); // 构造有向图
case DN: return CreateDN(G); // 构造有向网
case UDG:return CreateUDG(G); // 构造无向图
case UDN:return CreateUDN(G); // 构造无向网
default:
return ERROR;
}
}
Status CreateUDN(MGraph &G){
// 采用数组(邻接矩阵)表示法,构造网G
int i,j,k;
int IncInfo;
scanf("%d %d %d",&G.iVexNum,&G.iArcNum,&IncInfo);
for(i=0;i<G.iVexNum;++i) // 构造顶点向量
scanf("%c%*c",&G.cVexs[i]);
for(i=0;i<G.iVexNum;i++){
for(j=0;j<G.iVexNum;j++){ // 初始化邻接矩阵
G.arcs[i][j].Adj = INFINITY;
G.arcs[i][j].Info =NULL;
}
}
VextexType v1,v2;
int w;
for(k=0;k<G.iArcNum;k++){ // 构造邻接矩阵
scanf("%c %c %d%*c",&v1,&v2,&w);
i = LocateVex(G,v1),j = LocateVex(G,v2); // 确定v1,v2在G中的位置
G.arcs[i][j].Adj = w; // 弧<v1,v2>的权值
if(IncInfo) scanf(INFO_FORM,G.arcs[i][j].Info);
G.arcs[j][i] = G.arcs[i][j]; // 置<v1,v2>对称弧<v2,v1>
}
return OK;
}
//////////////////////////////////////////////////////////////////////////
// 十字链表图的相关操作
//////////////////////////////////////////////////////////////////////////
int LocateVex(const OLGraph &G,const VextexType &v); // 获得顶点v在图中的位置
Status CreateDG(OLGraph &G){
// 采用十字链表存储表示,构造有向图 G(G.kind = DG)
InfoType IncInfo;
scanf("%d %d %d",G.iVexNum,G.iArcNum,&IncInfo);
int i;
for(i=0;i<G.iVexNum;i++){ // 构造表头向量
scanf(VEX_FORM "%*c",G.xlist[i].data); // 输入顶点值
G.xlist[i].firstIn = G.xlist[i].firstOut = NULL;// 初始化指针
}
int k,j;
VextexType v1,v2;
ArcBox * p;
for(k=0;k<G.iArcNum;k++){ // 输入各弧并构造十字链表
scanf(VEX_FORM VEX_FORM,&v1,&v2); // 输入一条弧的始点和终点
i = LocateVex(G,v1),j = LocateVex(G,v2); // 确定V1和 V2在G中的位置
p = (ArcBox*)malloc(sizeof(ArcBox)); // 假设有足够的空间
// 对弧节点赋值
p->iTailVex = v1,p->iHeadVex = v2;
p->hLink = G.xlist[j].firstIn,p->tLink = G.xlist[i].firstOut;
p->Info = NULL;
G.xlist[j].firstIn = G.xlist[i].firstOut = p; // 完成在入弧与出弧的链头的插入
if(IncInfo) scanf(INFO_FORM,&p->Info); //若含有相关信息,则输入
}
return OK;
}
//////////////////////////////////////////////////////////////////////////
// 深度优先搜索
//////////////////////////////////////////////////////////////////////////
bool visited[MAX_VERTEX_NUM];
Status(*VisitFunc)(int v);
int FirstAdjVex(MGraph,int);
int NextAdjVex(MGraph,int);
void DFSTeaverse(MGraph G,Status (*Visit)(int v)){
int v;
VisitFunc = Visit;
for(v=0;v<G.iVexNum;v++) visited[v] = false;
for(v=0;v<G.iVexNum;v++)
if(!visited[v]) DFS(G,v);
}
void DFS(MGraph G,int v){
visited[v] = true;
VisitFunc(v);
int w;
for(w = FirstAdjVex(G,v); w>0 ; w = NextAdjVex(G,v))
if(!visited[w]) DFS(G,w);
}
//////////////////////////////////////////////////////////////////////////
// 广度优先搜索
//////////////////////////////////////////////////////////////////////////
// 队列相关函数
void InitQueue(int []);
void EnQueue(int []);
bool QueueEmpty(int []);
void DFSTeaverse(MGraph G,Status (*Visit)(int v)){
int v;
for (v=0;v<G.iVexNum;v++)
{
visited[v] = false;
}
int Q[MAX_VERTEX_NUM];
InitQueue(Q);
for(v=0;v<G.iVexNum;v++){
if(!visited[v]){
visited[v] = true;
Visit(v);
EnQueue(Q);
while(!QueueEmpty(Q)){
int u,w;
DeQueue(Q,u);
for( w = FirstAdjVex(G,u); w>=0 ; w = NextAdjVex(G,u))
if(!visited[w]){
visited[w] = true;
Visit(w);
EnQueue(Q,w);
}
}
}
}
}
与图相关的还有很多算法,比如求小生成树的prim算法和kruskal算法
prim算法初始化一个s集合,始终挑选与s集合相连小的边连接的节点加到集合中,然后更新剩余节点到s的距离,直到所有的点添加进了s集合,prim算法代码如下
#include <stdio.h>
#include <string.h>
#define MAXN 1024
#define INF 0xeFFFFFFF
int e[MAXN][MAXN];
int low[MAXN];
bool inSet[MAXN];
int n;
int prim(){
int res=0,i,j,k;
inSet[0] = true;
memset(inSet,0,sizeof(inSet));
for(i=0;i<n;i++){
//现在所有点到S的距离是到0的距离
low[i] = e[0][i];
}
for(i=1;i<n;i++){
int minv = INF;
int p = 0;
//找出与集合s相连的小的边
for(j=0;j<n;j++){
if(!inSet[j] && low[j] < minv){
minv = low[j],p = j;
}
}
if(minv == INF) return -1;//非连通图
//将顶点j加到S里面
inSet[p] = true;
//将短路径加到结果里
res += low[p];
//更新low数组
for(k=0;k<n;k++){
if(!inSet && low[p] > e[p][k]){
low[p] = e[p][k];
}
}
}
return res;
}
int main(void){
int i;
scanf("%d",&n);
for(i=0;i<n-1;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
e[x][y] = e[y][x] = w;
}
int res = prim();
printf("%d ",res);
return 0;
}
Kruskal算法不断选取小的边i,只要biani加进来不构成回路,则加入到边的集合e中来,直到加入的边能连接所有的顶点,则结束算法
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXN 1024
#define INF 0xeFFFFFFF
//定义边的结构
typedef struct Ele{
int x,y; //边的端点
int w; //边的权重
bool inSet;
}Ele;
Ele * eles[MAXN];//对于n个顶点,多有n*(n-1)条边
int m;//m条边
int pa[MAXN];
int r[MAXN];
void make_set(){
int i;
for(i=0;i<n;i++){
pa[i] = i;
r[i] = 1;
}
}
int find_set(int x){
if(x != pa[x]){
pa[x] = find_set(x);
}
return pa[x];
}
bool unin_set(int x,int y){
if(x==y) return;
int xp = find_set(x);
int yp = find_set(y);
if(xp == yp) return false;//构成回路,不能合并
if(r[xp]>r[yp]){
pa[yp] = xp;//zhi小的放在zhi大的下面
}
else{
pa[xp] = yp;
if(r[xp] == r[yp]){
r[yp]++;
}
}
return true;
}
void sort(){
int i,j;
for(i=0;i<n-2;i++){
int p = i;
for(j = i+1;j<n-1;j++){
if(eles[p].w > eles[j].w){
p = j;
}
}
if(p!=i){
Ele * tmp = eles[i];eles[i] = eles[p];eles[p] = tmp;
}
}
}
/*
int cmp(void * a,void * b){
return (Ele*)a->w - (Ele*)b->w;
}
*/
int klske(){
//将边由小到大排序
//qsort(eles,sizeof(eles),sizeof(eles[0]),cmp)
sort();
int res;
for(int i=0;i<m;i++){
if(unin_set(find_set(eles[i].x),find_set(eles[i].y))){
printf("%d %d %d ",else[i].x,eles[i].y,eles[i].w);
}
}
return res;
}
int main(void){
int i;
scanf("%d",&m);
//eles = (Ele*)malloc(n*sizeof(Ele));
for(i=0;i<m-1;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
eles[i] = (Ele*)malloc(sizeof(Ele));
eles[i]->x = x;
eles[i]->y = y;
eles[i]->w = w;
eles[i]->inSet = false;
}
int res = klske(k);
printf("%d ",res);
for(i=0;i<m-1;i++){
free(eles[i]);
}
return 0;
}
上面主要涉及的是一些数据结构,以及这些数据结构基本的算法,下面进入算法部分
查找算法
树表查找
线索二叉树
线索二叉树要求任何几节点的左子树比该节点的值小,右子树的值比该节点大。二叉排序树,主要涉及的是插入和搜索
#include <stdio.h>
#include <malloc.h>
typedef struct bsTree{
int m_iDate;
struct bsTree * m_lChild/*左孩子指针*/,* m_rChild/*右孩子指针*/;
} * BsTree,BsNode ;
BsTree insert(BsTree bs,int x){
BsNode * p = bs;
BsNode * note = p;
BsNode * ct = NULL;
while(p){
if(x == p->m_iDate){
return p;
}
else{
// 记录上一个节点
note = p;
if(x < p->m_iDate) p = p->m_lChild;
else p = p->m_rChild;
}
}
ct = (BsNode * )malloc(sizeof(BsNode));
ct->m_iDate = x;
ct->m_rChild = NULL;
ct->m_lChild = NULL;
if(!bs){
bs = ct;
}else if(x < note->m_iDate){
note->m_lChild = ct;
}else note->m_rChild = ct;
return bs;
}
BsNode * search(BsTree bs , int x){
if(!bs || bs->m_iDate == x){
return bs;
}else if(x < bs->m_iDate){
return search(bs->m_lChild,x);
}else{
return search(bs->m_rChild,x);
}
}
有序表查找
二分查找
int binarySearch(int arr[],int l,int r,int key){
while(l<r){
int mid = (l+r) >> 1;
if(arr[mid] > key){
r = mid -1;
}else if(arr[mid] < key){
l = mid + 1;
}else{
return mid;
}
}
return -l-1; // 返回可插入位置
}
排序算法
冒泡排序
以升序为例,冒泡排序每次扫描数组,将逆序修正,每次恰好将大的元素过五关斩六将推到后,再在剩余的元素重复操作
void mpSort(int a[],int n){
int i,j,swaped=1;
for(i=0;i<n-1 && swaped;i++){
swaped = 0;
for(j=0;j<n-i;j++){
if(a[j] > a[j+1]){
swap(a[j],a[j+1]);
swaped = 1;
}
}
}
}
可见,冒泡排序的平均时间复杂度为O(n^2)
选择排序
以升序为例,每次扫描数组,找到小的元素直接挪到第一个来,再在剩余的数组中重复这样的操作
void selectSort(int a[],int n){
int i,j;
for(i=0;i<n-1;i++){
int p =i;
for(j=i;j<n;j++){
if(a[p] > a[j]){
p = j;
}
}
if(p != i){
swap(p,i);
}
}
}
选择排序的平均时间复杂度也是O(n^2)
直接插入排序
直接插入排序不断在前面已经有序的序列中插入元素,并将元素向后挪。再重取一个元素重复这个操作
#define MAXSIZE 20
typedef int KeyType;
typedef struct {
KeyType key;
}RedType;
typedef struct{
RedType r[MAXSIZE+1];
int length;
}SqList;
//---------------------直接插入排序---------------------
void InsertSort(SqList & L){
int i,j;
for (i=2;i<=L.length;i++)
{
L.r[0] = L.r[i];
j = i -1;
while (L.r[0].key < L.r[j].key)
{
L.r[j+1] = L.r[j];
j--;
}
L.r[j+1] = L.r[0];
}
}
插入排序的平均时间复杂度也是O(n^2)
堆排序
堆排序也是一种插入排序,不过是向二叉堆中插入元素,而且以堆排序中的方式存储二叉堆,则二叉堆必定是一棵完全二叉树,堆排序设计的主要操作是插入和删除之后的堆调整,使得堆保持为大底堆或者小底堆的状态。也是根节点始终保持为小或大,每次删除元素,将根节点元素与后元素交换,然后将堆的大小减1,然后进行堆调整,如此反复执行这样的删除操作。很显然,大底堆后会得到递增序列,小底堆会得到递减序列。
/**
* 堆调整
* 在节点数为n的堆中从i节点开始进行堆调整
*/
void heapAjust(int arr[] ,int i,int n){
// 从i节点开始调整堆成为小底堆
int tmp = arr[i],min;
// 左孩子,对于节点i,它的左孩子是2i+1,右孩子是2i+2;
for(;(min = 2*i + 1)<n;i = min){
if(min+1 != n && arr[min] > arr[min+1]) min ++;
if(arr[i] > arr[min]){
// 将小元素放置到堆顶
arr[i] = arr[min];
}
else break;
}
arr[i] = tmp;
}
void heapSort(int arr[],int n){
// 建堆
int i;
for(i=n>>1;i>=0;i--){
heapAjust(arr,i,n);
}
// 取堆顶,调整堆
for(i = n-1;i>0;i--){
// 每次取堆顶小的元素与后的元素交换,后会得到递减序列
int tmp = arr[0];arr[0] = arr[i],arr[i] = tmp;
// 删除一个元素后需要从根元素起重新调整堆
heapAjust(arr,0,i);
}
}
排序的平均时间复杂度为O(NLogN)
快速排序
说到快排,想起了大一上学期肖老师(教我C语言的老师)与我讨论的问题,当时懵懂无知,后才才知道那是快排。
快排的思想也很简单,以升序为例,在序列中选一标杆,一般讲第一个元素作为标杆,然后将序列中比标杆小的元素放到标杆左边,将比标杆大的放到标杆右边。然后分别在左右两边重复这样的操作。
void ksort(int a[],int l,int r){
// 长度小于2有序
if(r - l < 2) return;
int start = l,end = r;
while(l < r){
// 向右去找到第一个比标杆大的数
while(++l < end && a[l] <= a[start]);
// 向左去找第一个比标杆小的数
while(--r > start && a[r] >= a[start]);
if(l < r) swap(a[l],a[r]); // 前面找到的两个数相对于标杆逆序 ,需交换过来 。l==r 不需要交换,
}
swap(a[start],a[r]);// 将标杆挪到正确的位置.
// 对标杆左右两边重复算法,注意,这个l已经跑到r后面去了
ksort(a,start,r);
ksort(a,l,end);
}
快速排序的平均时间复杂度为O(NLogN)
合并排序
合并排序采用分治法的思想对数组进行分治,对半分开,分别对左右两边进行排序,然后将排序后的结果进行合并。按照这样的思想,递归做是方便的。
int a[N],c[N];
void mergeSort(l,r){
int mid,i,j,tmp;
if(r - 1 > l){
mid = (l+r) >> 1;
// 分别左右两天排序
mergeSort(l,mid);
mergeSort(mid,r);
// 合并排序后的数组
tmp = l;
for(i=l,j=mid;i<mid && j<r;){
if(a[i] > a[j]) c[tmp++] = a[j++];
else c[tmp++] = a[i++];
}
// 把剩余的接上
if(j < r) {
for(;j<r;j++) c[tmp++] = a[j];
}else{
for(;i<mid;i++) c[tmp++] = a[i];
}
// 将c数组覆盖到a里
for(i=l;i<r;i++){
a[i] = c[i];
}
}
}
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。

sales@spasvo.com