辅导书本无非王道数据结构和天勤,如果你把这两本书看了好几遍,可以看一下数据结构1800,这本书非常不错,它长下面这样子,时间充裕的话就看,紧张的话就直接刷真题吧。
由于这本书题量过大,建议只看链表,树,图这三个章节,这三个章节侧重选择题和编码题,一些填空题可以忽略,吉大不考这类题型,如果你时间充裕那上面的请自动忽略。还有最重要的一点就是真题,这个务必要过几遍,达到看到题就会做的水准,差不多就130+的水平,吉大的题目不难,很多都是原题,还有以下的一些题型。
顺便一提,今年树和图的算法大题一个都没考。。。。。全是链表还有千年没命过题的文件操作,只能说吉大命题太诡异了。大家务必要重温一下文件操作,很多东西要复习全面,基于去年没考树图代码题,今年考的可能性很大,请大家务必注意,链表大题也不要丢下,也可能杀个回马枪,总之,复习要全面吧。
以下的代码仅供参考,所以部分代码没有备注,每个人的代码习惯不一样,所以大家重点注意的是命题风格以及题型。
以下的所有题型来源于1800题,王道,天勤以及历年真题。
这里我只写几种常见的类型,还有更多请把王道课后习题多做几遍
//链表的插入排序
void insert(Linklist &L){
Lnode *p = L->next; //如果你看了王道或者天勤应该知道我定义的结构体
Lnode *pre;
Lnode *r = p->next;
p->next = NULL;
p=r;
while(p!=NULL){
r = p->next; //记录下一节点
pre = L;
while(pre->next != NULL && pre->next->data < p->data)
pre = pre->next; //找到插入位置
p->next = pre->next; // 这行和下面一行都是插入操作
pre->next = p;
p = r; //向前推进一个节点
}
}
1,改进的层序遍历(非递归实现计算树的深度以及宽度)
void level(BTNode *p){
int front,rear,level = 0,length = 0;
int count = 0,width = 1,cursor; //这里width为宽度,level为深度
BTNode *q;
if(p != NULL){
rear = (reat+1)%maxsize;
que[rear] = p;
cursor = rear;
while(front != rear){
front = (front+1)%maxsize;
count ++;
q = que[front];
if(q->lchild != NULL){
rear = (rear+1)%maxsize;
que[rear] = q->lchild;
}
if(q->rchild != NULL){
rear = (rear+1)%maxsize;
que[rear] = q->rchild;
}
if(front == cursor){ //是否扫描完一层
cursor = rear; //将下一层的最后一个节点赋值给扫描器
level ++; //层数+1
width = count > width ? count : width; //取最大宽度
count = 0; //重新计算下一层
}
}
}
}
首先,需要我们掌握图的结构体
typedef struct{
char data;
char info;
} vertexType;
typedef struct {
int edges[maxsize][maxsize]
int vnums,enums;
vertexType vex[maxsize]
}mGraph
基于邻接矩阵的非递归深度优选遍历
void DFS_Nonrecursion(mGraph *G, int visit[], int stackVT){
int stack[maxsize];
int top = -1;
int i,j,curVT;
stack[++top] = stackVT;
while(top != -1){
curVT = stack[top--];
cout<<G->vex[curVT].data;
for( i = G->vnums-1; i>0; i--){
if(G->edges[curVT][i] == 1 && visit[i] == 0){
stack[++top] = i;
visit[i] = 1;
}
}
}
}
基于邻接矩阵的广度优先遍历
void BFS(mGraph *G, int visit[], int startVT){
int i,queue[maxsize],front=0,rear = 0,vertex;
queue[++rear] = startVT;
while(front != rear){
front = (front+1)%maxsize;
vertex = queue[front];
cout<<G->vex[vertex].data;
for(i = 0; i<G->nums;i++){
if(G->edges[vertex][i] == 1 && visit[i] == 0){
rear = (rear+1) % maxsize;
queue[rear] = i;
visit[i] = 1;
}
}
}
}
基于邻接矩阵的深度优先遍历(递归)
void DFS(mGraph *G, int visit[], int startVT){
int j;
cout<<G->vex[startVT].data;
for(j = 0; j<G->vnums; j++){
if(G->edges[starVT][j] == 1 && visit[j] == 0){
visit[j] = 1;
DFS(G,visit,j);
}
}
}
判断图是否有回路(邻接矩阵,有向图)
int flag = 0;
void IsCircle(int n, int v){
if(flag == 1)return; //回路
for(int i = 0; i<n; i++){
if(visit[i] == 0 && a[v][i] == 1){
visit[i] = 1;
IsCircle(n,i);
visit[i] = 0;
}
else if(a[v][i] == 1 && v!= i) flag = 1;
}
}
书上有拓扑排序的解决方案,邻接表的实现(有向图)天勤
下面是无向图回路的判断(连通图)
void DFS(AGraph *G, int v, int &flag){
ArcNode *p;
if(visit[v] == 1){
flag = 1;
return;
}
visit[v] = 1;
p = G->adjlist[v].firstArc;
while(p!=NULL){
DFS(G, p->adjvex,flag);
p = p->nextarc;
}
}
至此完毕
如果是非连通图
for(i = 0; i<G->n; i++)
if(visit[i] == 0)
DFS(G,i,flag);
类型 | 时间复杂度 | 空间复杂度 |
---|---|---|
邻接矩阵 | o(n^2) | o(n^2) |
邻接表 | o(n+e) | o(n) |
为了区分同一节点的两次出栈,设置flag。
flag = 1,表示第一次出栈,只遍历完左子树,该节点不能访问
flag = 0, 表示第二次出栈,遍历完右子树,可以访问
typedef struct element{
BTNode * ptr;
int flag;
}element;
void postorder(BTNode *bt){
int top = -1;
element *s[maxsize];
while(bt != NULL || top != -1){
while(bt != NULL){
top ++;
s[top].ptr = bt;
s[top].flag = 1;
bt = bt->lchild;
}
// 标记一
while(top != -1 && s[top].flag == 2){
//标记二
bt = s[top--].ptr;
visit[bt->data);
}
if(top!=-1){
s[top].flag = 2;
bt = s[top].ptr->rchild;
}
}
}
以上是后序非递归的模板
后序非递归模板可用于根节点到某一节点的路径(在标记一处写代码),也可以求公共祖先(标记二),也可以交换左右子树
求p,q节点的最近公共祖先
BTNode *CommonAncester(BTNode *bt, BTNode*p, BTNode *q){
if(bt == NULL || bt == p || bt == q ) return bt;
BTNode *left = CommonAncester(bt->lchild,p,q);
BTNode *right = CommonAncester(bt->rchild,p,q);
if((left == p && right == q) || (left ==q || right == p))
return bt;
return (left != NULL} ? left:right;
}
// 如果p,q 分别在左右子树中,则根节点就是最近的公共祖先,分别对左右子树进行递归,
// 如果某一子树返回为空,则证明返回上一层寻找。
n个节点的完全二叉树存放在数组A[n]中,建立一棵用二叉链表表示的二叉树根用tree指向。
BTNode *Create(ElementType a[], int i){
BTNode * tree;
if(i <= n){
tree = (BTNode) malloc(sizeof(BTNode));
tree->data = A[i];
if(2*i > n)
tree->lchild = NULL;
else
tree->lchild = Create(A, 2*i);
if(2*i+1>n)
tree->rchild = NULL;
else
tree->rchild = Create(A, 2*i+1);
}
return tree;
}
对上面那棵树进行前序非递归遍历
void preorder(ElemType bt[], int n){
int i = 1,top = 0,s[];
while(i<= n || top > 0){
while(i<n){
visit(bt[i]); //访问根节点
if(2*i+1 <= n)
s[++top] = 2*i+1; //柚子树入栈
i = 2*i; //不断访问左子树
}
if(top>0)
i = s[top--]; //开始访问柚子树
}
}
由二叉树的中序遍历和后序遍历建立二叉树
BiTree IntoPost(ElemType in[], post[], int l1, int h1, int l2, int h2){
//in和post是中序序列和后序序列,l1,h1是第一个和最后一个节点的下标
BiTree bt = (BiTree)malloc(sizeof(BiNode));
bt->data = post[h2]; //后序序列最后一个元素是根节点
for(int i = l1;i<=h1;i++){
if(in[i] == post[h2])break; //在中序序列中查找根节点
}
if(i == l1)
bt->lchild = NULL;
else
bt->lchild = IntoPost(in,post,l1,i-1,l2,l2+i-l1-1);
if(i == h1)
bt->rchild = NULL;
else
bt->rchild = IntoPost(in, post, i+1, h1, l2+i-l1,h2-1);
return bt;
}
已知满二叉树先序遍历序列,求其后序遍历序列
void change(char pre[], int L1, inr R1, char post[], int L2, int R2){
if(L1 <= R2){
post[R2] = pre[L1];
change(pre, L1+1,(L1+1+R1)/2, post, L2, (L2+R2-1)/2);
change(pre, (L1+1+R1)/2+1, R1, post, (L2+R2-1)/2+1, R2-1);
}
}
先序遍历的第一个元素为根节点,将除第一个元素之外的元素序列分为前后两个相等的序列,前一半
为左子树的节点,后一半为柚子树的节点。只需将根节点移至序列的末尾,再分别递归处理前一半,后一半。
求有向图中邻接表的入度
void indegree(Adjlist g){
for(i = 0; i<n; i++){
num = 0; //入度
for(j = 0;j<n;j++)
if(i!=j){
p = g[i].firstarc;
while(p){
if(p->adjvex == i)
num++;
p = p->next;
}
}
}
}
another way:
for(i =0;i<n;i++){
p = G->adjlist[i].firstarc;
while(p){
indegree[p->adjvex]++;
p = p->nextarc;
}
}
下面是出度的算法
int out;
void outdegree(AdjList g){
for(int i = 0; i< n; i++){
out = 0;
p = g[i].firstarc;
while(p){
out++;
p = p->nextarc;
}
cout<<"顶点"<<g[i].vexdata<<"的出度为"<<out;
}
}
邻接矩阵转邻接表
void AdjmatrixToAdjList(AdjMatrix gm, AdjList gl){
for(i = 0; i<n; i++){
cin>>gl[i].vertex;
gl[i].firstarc = NULL;
} //初始化
for(i = 0;i<n;i++){
for(j = 0;j<n;j++){
if(gm[i][j] == 1){
p = (ArcNode*)malloc(sizeof(ArcNode));
p->adjvex = j;
p->next = g[i].firstarc;
g[i].firstarc = p;
}
}
}
}
已知图邻接链表,设计算法生成相应的逆邻接表,时间复杂度为o(n+e)
void InvertAdjList(AdjList gin, AdjList gout){
for(int i = 0; i<n; i++){
gin[i].vertex = gout[i].vertex;
gin[i].firstarc = NULL;
for(int i = 0; i<n;i++){
p = gout[i].firstarc;
while(p!= NULL){
j = p->adjvex;
s= (ArcNode*)malloc(sizeof(ArcNode));
s->adjvex = i;
s->next = gin[j].firstarc;
gin[j].firstarc = s;
p = p->next;
}
}
}
}
二叉树的查找与删除(真题出现过)
bool DeleteBST(BiTree *T, int key){
if(T == NULL)
return false;
else if(key == T->data){
Delete(T);
return false;
}
else if(key < T->data)
return DeleteBST(T->lchild, key);
else
return DeleteBST(T->rchild, key);
}
void Delete(BiTree *T){
BiTree L;
if(T->lchild == NULL && T->rchild == NULL)
T = NULL;
else if(T->rchild != NULL && T->lchild == NULL)
T = T->rchild;
else if(T->rchild == NULL && T->lchild != NULL)
T = T->lchild;
else{
L = T->lchild;
while(L->rchild) L = L->rchild; //标注一
L->rchild = T->rchild; // 标注二
T = T->lchild; // 标注三
}
}
这里的标注一是寻找最右子树,标注二是T的柚子树接到左子树的最右孩子的柚子树,
标注三是T的左子树直接作为T父节点的子树
图的深度非递归遍历
void DFS(graph g, int v){
int i;
arcnode *p;
int visited[vnum],top = -1,stack[vnum];
for(i = 0; i<g.vexnum;i++){
visited[i] = 0;
}
visit(v);
top++;
stack[top] = v;
visited[v] = 1;
while(top > 0){
v = stack[top];
--top;
p = g.adjlist[v].firstarc;
while(p!=NULL && visited[p->adjvex] == 1)
p = p->nextarc;
if(p == NULL) --top;
else{
v = p->adjvex;
visit(v);
visited[v] = 1;
top++;
stack[top] = v;
}
}
}
打印vi到vj所有长度为L的简单路径。这类问题有模板,基本上解题思路大同小异,如果理解了这个问题。
int visited[Vnum],A[Vnum];
void dfs(graph *g, int vi, int vj, int L, int d){
int v,i;
arcnode *p;
visited[vi] = 1;
d++;
A[d] = vi;
if(vi == vj && d == L){
cout<<"一条路径"; // dfs模板
for(i = 0; i<=d; i++)
cout<<A[i];
return; // 退出这层递归
}
p = g->adjlist[vi].firstarc;
while(p != NULL){
v = p->adjlist;
if(visited[v] == 0)
dfs(g,v,vj,L,d);
p = p->nextarc;
}
visited[vi] = 0;
--d; //擦除访问标记,使顶点可以重用
}
hash 函数的构造方法
处理冲突的方法
开放定址法包括 线性探查法(容易堆积),平方探查法(不能探查表上的所有单元)
a = 装填因子 = {关键字个数 n} / {表的长度 m} a越大,发生冲突的可能性越大,反之则反
散列表的平均查找长度依赖于a,不直接依赖于n 或 m
关于一些方法的总结:
数组的去重,可以先排序后用插入排序的思想,还可以用一个数组来计算元素的个数,计数排序的思想。
用dfs来拓扑排序
void DFS(Graph G, int v){
visited[v] = 1;
visit(v);
p = G->adjlist[v].firstarc;
while(p){
if(visited[p->adjvex] == 0)
DFS(G, p->adjvex);
p = p->nextarc;
}
time = time+1;
finishTime[v] = time;
}
for(v = 0;v<G.n;v++)
if(!visited[v])
DFS(G,v);
finishTime数组中结束的时间从小到大排列,既为TOPSort
总结图时间复杂度:
类型 | 时间复杂度 | 空间复杂度 |
---|---|---|
广+表 | o(n+e) | o(n) |
深+表 | o(n+e) | o(n) |
广+矩阵 | o(n^2) | o(n) |
深+矩阵 | o(o^2) | o(n) |
非递归的快排:
void quickSort_Nonrecursion(int a[], int n){
int stack[maxsize];
int top = -1;
stack[++top] = n-1;
while(top >= 0){
int high = stack[top--];
int low = stack[top--];
int m = partition(a,low,high);
if(m+1 < high){
stack[++top] = m+1;
stack[++top] = high;
}
if(low < middle -1){
stack[++top] = low;
stack[++top] = middle-1;
}
}
}
partion函数在王道292页
prim函数和kruskal函数都是针对无向图
prim
时间复杂度o(v^2),不依赖|E|,因此适用于稠密图
kruskal
时间复杂度o(log(E)),其实它的时间复杂度由排序算法决定的,而排序规模是由边数e决定的,因此适用于稀疏图
Dijkstra
见天勤,内容太多了。。。。
Floyd
它的核心思想,对每个中间点进行更新一次距离,中间点为两个顶点的必经点。
还有一些需要注意的是归并排序的比较次数,这个笔记有点多,就不一一写了。需要的可以私聊我拍笔记照片给你。
还有Dijkstra图的画法,这个今年考到了,下面给个例子。
顶点 | 第一趟 | 第二趟 |
---|---|---|
a | (a,b)2 | … |
b | 无穷 | … |
c | … | (a,b,d)5 |
集合 | a,b | {a,b,c} |
这个好像王道还是天勤上有一个例子,和上面的差不多。
传送门 吉大高级程序设计笔记