前言
主要是記錄下代碼,根據考綱把一些簡單算法記錄下,方便學習和回憶。代碼方面我盡量寫的規范點,如果大家覺的不妥請自動腦部 = v =(算了,我還是為所欲為吧,畢竟自己爽的東西)
參考書是2021年王道數據結構,我寫的是書上的補充,看時請配合書一起使用
請注意,我介紹的東西全部簡化處理過了,就是有很多的定義,概念和高級算法直接舍去。原因很簡單,因為我覺的它不會考,看了歷年真題,考試只會考一些基礎的東西,他不會讓你去改進算法,不會太難。
為了確保我寫的代碼的准確性,我都會在oj上找題目做練習,算法前我都附上oj鏈接並測試,考試的時候只要把思想說清楚,關鍵的函數寫出來問題就應該不大了。(為了應用題分數真的不容易)
我自己寫的代碼在這里https://github.com/taoyeh/DataStructure
簡易版本請看這
考試重點
就兩年真題,還能怎樣。考試側重點為:棧,二叉樹,排序>圖,隊列,查找>復雜度,額外考點
#include<bits/stdc++.h> //萬能頭文件
請注意,考試重點考的是算法,是思想,不仔細看你每一步語法錯還是對,請確保大抵方向對,還有C++它寫起來方便多了。
基礎
考試內容
- 計算機中算法的角色
- 算法復雜度分析
- 遞歸
計算機中算法的角色
算法的定義: 算法是解決特定問題求解決步驟的描述,再計算機中表現為指令的有限序列,並且每條指令表示一個或多個操作。其擁有五個重要特性:
1.有窮性2.確定性3.可行性4.輸入5.輸出
它的目標為:
1.正確性2.可讀性3.健壯性4.效率和低儲存量需求
算法復雜度分析
- 時間復雜度(考試重點)
- 空間復雜度
線性表
考試內容
- 基於順序存儲的定義和實現
- 基於鏈式存儲的定義和實現
- 線性表的應用
基於順序存儲的定義和實現
線性表的順序存儲又稱順序表,特點為邏輯地址和物理地址相同
定義
靜態分配
# define MaxSize 50
typedef struct
{
ElemType data[MaxSize];
int length;
}Sqlist
動態分配
# define InitSize 50
typedef struct
{
ElemType *data;
int length,MaxSize;
}Sqlist
申請空間
C言語版本 L.data=(ElemType*) malloc(sizeof(ElemType)*InitSize)
C++言語版本 L.data=new ElemType[InitSize]
我喜歡C++版本,我考場上我也寫C++
實現:插入操作,刪除操作和按值查找
插入操作:在順序表L的第\(i(1\leq i\leq L.length+1)\)個位置插入新元素e。
bool ListInsert(SqList &L,int i,ElemType e)
{
if(i<=1 || i>L.length+1) return false; //判斷是否合理
if(i>=MaxSize) return false; //判斷是否滿
for(int j=L.length;j>=i;j--)
L.data[j+1]=L.data[j];
L.length++;
return true;
}
刪除操作:刪除順序表L的第\(i(1\leq i\leq L.length+1)\)個位置的元素
bool ListInsert(SqList &L,int i,ElemType &e)
{
if(i<=1 || i>L.length+1) return false; //判斷是否合理
e=L.data[i-1] //判斷是否滿
for(int j=i;j<L.length;j++)
L.data[j-1]=L.data[j];
L.length--;
return true;
}
查找操作
基於鏈式存儲的定義和實現
物理地址不一定連續
定義
typedef struct LNode
{
Elemtype data;
struct LNode *next;
}LNode,*LinkList;
頭插法
LinkList List_HeadInsert(LinkList &L)
{
L=new LNode();
L->next=NULL;
int x;
while(scanf("%d",&x))
{
if(x==0) break;
LNode *s=new LNode();
s->next=L->next;
L->next=s;
s->data=x;
}
return L;
}
尾插法
LinkList List_TailInsert(LinkList &L)
{
L=new LNode();
L->next=NULL;
LNode *r=L;
int x;
while(scanf("%d",&x))
{
if(x==0) break;
LNode *s=new LNode();
r->next=s;
s->data=x;
s->next=NULL;
r=s;
}
return L;
}
遍歷
void LocateElem(LinkList L)
{
LNode *p=L->next;
while(p!=NULL)
{
printf("%d\n",p->data);
p=p->next;
}
}
插入
void ListInsert(LinkList &L,int i,int e)
{
LNode *p=L;
int cnt=0;
while(cnt<i-1)
{
p=p->next;
cnt++;
}
LNode *s=new LNode();
s->data=e;
s->next=p->next;
p->next=s;
}
刪除
void ListDelete(LinkList &L,int i)
{
LNode *p=L;
LNode *q=new LNode();
int cnt=0;
while(cnt<i-1)
{
p=p->next;
cnt++;
}
q=p->next;
p->next=q->next;
delete(q);
}
靜態鏈表定義
# define MaxSize 50
typedef struct
{
ElemType data;
int next;
}Sqlist[MaxSize]
棧與隊列
考試內容
- 棧、 隊列、 字符串、 數組的基本概念、 特點
- 棧和隊列基於順序存儲的定義與實現
- 棧和隊列基於鏈式存儲的定義與實現
- 稀疏矩陣的壓縮存儲及轉置算法實現
棧
定義
typedef struct
{
Elemtype data[MaxSize];
int top;
}SqStack;
typedef struct Linknode
{
Elemtype data;
struct Linknode *next;
}*LiStack;
初始化
void InitStack(SqStack &S)
{
S.top=-1;
}
進棧
bool Push(SqStack &S,int e)
{
if(S.top==MaxSize-1) return false;
S.data[++S.top]=e;
return true;
}
出棧
bool Pop(SqStack &S,int &e)
{
if(S.top==-1) return false;
e=S.data[S.top--];
return true;
}
查看
bool GetTop(SqStack &S,int &x)
{
if(S.top==-1) return false;
x=S.data[S.top];
return true;
}
隊列
定義
typedef struct
{
Elemtype data[MaxSize];
int front,rear;
}SqQueue;
初始化
void InitQueue(SqQueue &Q)
{
Q.front=Q.rear=0;
}
入隊
bool EnQueue(SqQueue &Q,int x)
{
if((Q.rear+1)%MaxSize==Q.front) return false;
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize;
return true;
}
出隊
bool DeQueue(SqQueue &Q)
{
if(Q.front==Q.rear) return false;
Q.front=(Q.front+1)%MaxSize;
return true;
}
查看
bool GetHead(SqQueue &Q,int &x)
{
if(Q.front==Q.rear) return false;
x=Q.data[Q.front];
return true;
}
括號匹配
問題描述http://acm.usx.edu.cn/AspNet/question.aspx?qid=9523
#include <stdio.h>
#include <string>
#include <iostream>
using namespace std;
#define MaxSize 50
typedef struct
{
char data[MaxSize];
int top;
}SqStack;
bool Push(SqStack &S,char e)
{
if(S.top==MaxSize-1) return false;
S.data[++S.top]=e;
return true;
}
bool Pop(SqStack &S,char &e)
{
if(S.top==-1) return false;
e=S.data[S.top--];
return true;
}
bool judge(SqStack &S,string s)
{
int i;
char ch;
for(i=0;i<s.size();i++)
{
if(s[i]=='('|| s[i]=='[') Push(S,s[i]);
else
{
if(S.top==-1) return false;
Pop(S,ch);
if (s[i]==')' && ch!='(' ) return false;
if (s[i]==']' && ch!='[' ) return false;
}
}
return S.top==-1;
}
int main()
{
string s;
while(cin>>s)
{
SqStack S;
S.top=-1;
if(judge(S,s)) printf("yes\n");
else printf("no\n");
}
return 0;
}
表達式
考了很多次了,其中后綴表達式為重點,那只講后綴吧
左優先原則:只要左邊的運算符能先計算,就優先計算
后綴表達式計算方法:
問題描述http://acm.usx.edu.cn/AspNet/question.aspx?qid=9518
#include <iostream>
#include <stack>
#include <string>
#include <iomanip>
using namespace std;
double soul(string s)
{
int sum=0,i;
for(i=0;i<s.size();i++)
sum=sum*10+(s[i]-'0');
return sum;
}
int main()
{
int n,i;
double a,b,c;
double sum;
string s;
while(1)
{
stack <double> S;
while(cin>>s)
{
if(s=="$") break;
else if(s=="+"||s=="-"||s=="*"||s=="/" )
{
a=S.top();
S.pop();
b=S.top();
S.pop();
if(s=="-") c=b-a;
else if(s=="+") c=a+b;
else if(s=="*") c=a*b;
else if(s=="/") c=b/a;
S.push(c);
}
else
S.push(soul(s));
}
sum=S.top();
S.pop();
cout<<fixed<<setprecision(2)<<sum<<endl;
}
return 0;
}
中綴轉后綴方法
- 遇到操作符,直接加入后綴表達式
- 遇到界限符,如果是“(”打入棧,如果是“)”,彈出棧中所有運算符直到“(”,當然左右括號不加入后綴表達式
- 遇到運算符,依次彈出棧中優先級大於等於自己的的所有運算符,加入后綴表達式,若碰到“(”和棧空則停止。之后再把當前的運算符壓入棧
- 不會考,放心 = v =
稀疏矩陣
一個二維數組中元素十分少,所以用一個三元組(行標,列表,值)記錄所有的有效值,至於轉置,把行標和列表互換就行。
串
只需看KMP中next和nextval,會手算就行,考綱里面沒有要求
樹
考試內容
- 二叉樹
①二叉樹的定義、 主要特征
②二叉樹基於順序存儲和鏈式存儲的實現
③二叉樹重要操作的實現
④線索二叉樹的基本概念和構造 - 樹、 森林
①樹的存儲結構
②森林與二叉樹的相互轉換
③樹和森林的遍歷 - 特殊二叉樹及應用
①哈夫曼(Huffman) 樹
②二叉排序樹
③平衡二叉樹
④堆(堆的構造和調整過程)
二叉樹
二叉樹性質
- \(n_0=n_2+1\)
- 每層最多 \(2^{i-1}\)個結點(\(i \geq 1\))
完全二叉樹
- n個結點的完全二叉樹的高度為\(\lceil log_2(n+1) \rceil\) 或者為\(\lfloor log_2(n)+1 \rfloor\)
二叉樹的基本操作
滿二叉樹或者完全二叉樹適合順序存儲
struct TreeNode
{
Elemtype value;
bool isEmpty;
};
TreeNode t[MaxSize] ;
一般都是采用鏈式存儲結構
typedef struct BiTNode
{
Elemtype data;
struct BiTNode *left ,*right;
}BiTNode,*BiTree;
建立
BiTree build()
{
char ch;
scanf("%c",&ch);
if(ch=='*')
return NULL;
BiTNode *root=new BiTNode();
root->data=ch;
root->left=build();
root->right=build();
return root;
}
先序
void PreOrder(BiTNode *T)
{
if(T!=NULL)
{
printf("%c",T->data);
PreOrder(T->left);
PreOrder(T->right);
}
}
中序
void InOrder(BiTNode *T)
{
if(T!=NULL)
{
InOrder(T->left);
printf("%c",T->data);
InOrder(T->right);
}
}
后序
void PostOrder(BiTNode *T)
{
if(T!=NULL)
{
PostOrder(T->left);
PostOrder(T->right);
printf("%c",T->data);
}
}
求深度
int TreeDepth(BiTNode *T)
{
if(T==NULL) return 0;
int l,r;
l=TreeDepth(T->left);
r=TreeDepth(T->right);
return l>r ? l+1:r+1;
}
層次遍歷
void LevelOrder(BiTree T)
{
queue<BiTNode*>q;
q.push(T);
while(!q.empty())
{
BiTNode *node=q.front(); q.pop();
printf("%c",node->data);
if(node->left!=NULL) q.push(node->left) ;
if(node->right!=NULL) q.push(node->right) ;
}
}
根據前序,中序,后序三種中的兩種構造唯一確定的樹的方法,手會寫就行
線索二叉樹
考試要求為:線索二叉樹的基本概念和構造
看來沒有操作,看來懂就行,手會寫就好了。(只要看中序)
定義
typedef struct ThreadNode
{
char data;
struct BiTNode *left ,*right;
int ltag,rtag; //1表示是線索,0表示是孩子
}ThreadNode,*ThreadTree;
樹
樹的存儲方式
- 雙親表示法(純數組)
- 孩子表示法(數組鏈表,指向孩子)
- 孩子兄弟表示法(純鏈表,且最重要):森林與二叉樹可以相互轉換
孩子兄弟表示法定義
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;//第一個指向左孩子,第二個指向兄弟
}CSNode,*CSTree;
二叉樹和森林相互轉換:通過孩子兄弟表示法實現相互轉化
樹和森林的遍歷
- 樹的先根遍歷和這棵樹相應二叉樹的先序序列是相同的
- 樹的后根遍歷和這棵樹相應二叉樹的中序序列是相同的
- 當然問你一顆樹的先根(后根)遍歷直接對其進行先序(后根)遍歷就好了
- 對於森林的先根遍歷則是對其各自子樹進行先序遍歷
- 對於森林的中序遍歷則是對其各自子樹進行類似后根遍歷
- 反正就兩種遍歷 1.先序 2.不是先序
樹 | 森林 | 二叉樹 |
---|---|---|
先根遍歷 | 先序遍歷 | 先序遍歷 |
后根遍歷 | 中序遍歷 | 中序遍歷 |
二叉排序樹
又稱二叉查找樹(BST)
定義
typedef struct BSTNode
{
int data;
struct BSTNode *left,*right;
}BSTNode,*BSTree;
建立
void insert(BSTNode *&root,int x)
{
if(root==NULL)
{
root=new BSTNode();
root->left=root->right=NULL;
root->data=x;
return ;
}
if(root->data<x)
insert(root->left,x);
else if(root->data>x)
insert(root->right,x);
}
查找
BSTNode *BST_Search(BSTree T, int x)
{
while(T!=NULL&& T->data!=x)
{
if(T->data>x) T=T->left;
else T=T->right;
}
return T;
}
刪除
- 若刪除的結點為葉子結點,直接刪
- 若刪除的結點只有左子樹或者右子樹,使其替代
- 若刪除的結點既有左子樹又有右子樹。找右子樹的最小值替代或者找左子樹的最大值替代,產生的后果再用前兩種情況彌補
查找效率分析:
- 查找成功時:ASL=(\(\sum\)本層高度*本層個數)/結點個數
- 查找失敗時:ASL=(\(\sum\)本層高度*本層補上的葉子個數)/補上的葉子個數
平衡二叉樹
簡稱平衡樹(AVL)
結點的平衡因子:左子樹高-右子樹高
- 插入新節點后為保持平衡,有四種解決方式,分別為:LL,RR,LR,RL。手會變換就行
- 每次調整的都是最小不平衡子樹
- 每次調整的時候都要滿足二叉排序樹的特性
查找效率分析:查找一個關鍵詞最多需要對比h次,即\(log_2n\)
哈夫曼樹
WPL(帶權路徑長度)=\(\sum\) 路徑長度*葉子結點權重
- 哈夫曼樹構造方法:每次從集合中獲得兩個權重最小的結點,把他們從集合中刪除,並讓其成為兄弟結點,生成的新結點為兩結點權重之和,並把新節點放入集合中。
- n個葉子結點的哈夫曼樹總結點為2n-1
- 哈夫曼樹不唯一,但WPL必然相同
- 哈夫曼編碼:可變長度編碼。 先構造哈夫曼樹,向左走為0,向右走為1(當然相反也可以),葉子結點就有固定的編碼了。
堆
在完全二叉樹中,讓根結點大於孩子結點,同樣孩子結點也滿足這樣的條件我們叫做大根堆。若根結點小於孩子結點,同樣孩子結點也滿足這樣的條件我們叫做小根堆(具體操作我們在堆排序里面講)
圖
考試內容
- 基本的圖算法
- 最小生成樹
- 單源最短路徑
- 最短路徑
- 最大流
定義
- G= (V,E)V為頂點,E為邊。
- 線性表和樹可以為空,但是圖的頂點不能為空
- 若E為無向邊,則稱邊。若E為有向邊,則稱弧。
- 一條弧中沒有箭頭的為弧尾,有箭頭的為弧頭。
- 我們談論的都是簡單圖(沒有重復邊,不存在頂點到自己的邊)
- 無向邊的度(TD)為2e,有向圖的度(TD)=出度(OD)+入度(ID)=e
- 無向圖中頂點v和頂點w路徑存在,則稱連通。在有向圖中,頂點v和頂點w相互有路徑則稱強連通
- 連通圖:無向圖所有頂點連通。強連通圖:所有頂點強連通。
- 極大:邊盡可能的多,極小:邊盡可能的少
- 連通圖的生成樹是包含所有頂點的一個極小聯通子圖。
- 無向完全圖:無向圖任意兩個頂點之間都存在邊。有向完全圖:有向圖任意兩個頂點之間都存在方向相反的兩條弧。
存儲方式
- 鄰接矩陣:一個二維數組。其中\(A^n[i][j]\)表示由頂點i到頂點j長度為n的路徑的數目
- 鄰接表:一個一維數組加鏈表。
- 十字鏈表:存儲有向圖,解決了鄰接表找入邊不方便的問題
- 鄰接多重表:存儲無向圖,刪除邊刪除節點很方便
- 后兩種考的少,懂就行
基本的圖算法
- 廣度優先遍歷(BFS)
- 深度優先遍歷(DFS)
- 時間復雜度:BFS和DFS一樣,鄰接矩陣O(\(|V|^2\)),鄰接矩陣O(|V|+|E|)
最小生成樹
- 最小生成樹(MST)的樹形是不唯一的
測試數據http://acm.usx.edu.cn/aspnet/question.aspx?qid=9561
Prime
#include <iostream>
using namespace std;
int map[12][12]; //圖
int dist[12]; //距離
bool visit[12];//判斷是否被放入樹中
int inf=0x3f3f3f3f; //最大值
int n;// 頂點數
int m;// 邊數
int cost;//代價
void Prime(int u)
{
int i,v;
//初始化
for(i=1;i<=n;i++) visit[i]=false;
for(i=1;i<=n;i++) dist[i]=inf;
for(i=1;i<=n;i++) dist[i]=map[u][i];
visit[u]=true;
for(v=0;v<n-1;v++)
{
int k=-1,mmin=inf;
for(i=1;i<=n;i++)
{
if(dist[i]<mmin && visit[i]==false) k=i,mmin=dist[i];
}
visit[k]=true;
for(i=1;i<=n;i++)
{
if(dist[i]>map[k][i] && visit[i]==false) dist[i]=map[k][i];
}
}
}
int main()
{
int i,j,x,y,z;
while(scanf("%d %d",&n,&m)!=-1) //輸入數據
{
if(n==0 && m==0) break;
//初始化
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++) map[i][j]=inf;
map[i][i]=0;
}
for(i=1;i<=m;i++)
{
scanf("%d %d %d",&x,&y,&z);
map[x][y]=map[y][x]=z;
}
Prime(1);
cost=0;
for(i=1;i<=n;i++) cost+=dist[i];
printf("%d\n",cost);
}
return 0;
}
Kruskal
#include <iostream>
#include <algorithm>
using namespace std;
int inf=0x3f3f3f3f; //最大值
int n;// 頂點數
int m;// 邊數
int cost;//代價
struct node
{
int from,to,w;//出發點,目的地,權重
}e[150];
int parent[12];
bool cmp(node a,node b)
{
return a.w<b.w;
}
int find(int x)
{
while(x!=parent[x]) x=parent[x];
return x;
}
bool join(int x,int y)
{
int fx=find(x);
int fy=find(y);
if(fx==fy) return false;
else parent[fx]=fy;return true;
}
void Kruskal()
{
int i,j,cnt=0;cost=0;
for(i=1;i<=n;i++) parent[i]=i;
for(i=1;i<=m;i++)
{
if(join(e[i].from,e[i].to)==true) cost+=e[i].w,cnt++;
if(cnt==n-1) break;
}
}
int main()
{
int i,j,x,y,z;
while(scanf("%d %d",&n,&m)!=-1) //輸入數據
{
if(n==0 && m==0) break;
//初始化
for(i=1;i<=m;i++)
{
scanf("%d %d %d",&x,&y,&z);
e[i].from=x;e[i].to=y;e[i].w=z;
}
sort(e+1,e+m+1,cmp);
Kruskal();
printf("%d\n",cost);
}
return 0;
}
最短路徑
測試數據http://poj.org/problem?id=2387
Dijkstra
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
int inf=0x3f3f3f3f; //最大值
int n;// 頂點數
int m;// 邊數
int map[1005][1005]; //圖
int dist[1005];
bool visit[1005];
void Dijkstra(int u)
{
int i,v;
for(i=1;i<=n;i++) dist[i]=map[u][i];
visit[u]=true;
for(v=0;v<n-1;v++)
{
int k=-1,mmin=inf;
for(i=1;i<=n;i++) if(mmin>dist[i]&& visit[i]==false) k=i,mmin=dist[i];
visit[k]=true;
for(i=1;i<=n;i++)
{
if(dist[i]>dist[k]+map[k][i] && visit[i]==false ) dist[i]=dist[k]+map[k][i];
}
}
}
int main()
{
int i,j,x,y,z;
scanf("%d %d",&m,&n);
memset(map,inf,sizeof(map));
memset(dist,inf,sizeof(dist));
memset(visit,false,sizeof(visit));
for(i= 0;i<m;i++)
{
scanf("%d %d %d",&x,&y,&z);
if(z < map[x][y])
map[x][y] = map[y][x] = z;
}
Dijkstra(1);
printf("%d\n",dist[n]);
return 0;
}
Floyd
void Floyd()
{
int k,i,j;
for(k=1;k<=n;k++)
{
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
map[i][j]=min(map[i][j],map[i][k]+map[k][j]);
}
}
}
二分圖(Bipartite Graph)
先插個題外話,介紹下二分圖,2020考了個題,我大但的預測2021是不可能再考了,但還是記錄下。其實按考綱來准確來說二分圖的內容應該歸為最大流。(因為他求解的方式是通過最大流)
二分圖又稱作二部圖,是圖論中的一種特殊模型。 設G=(V,E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A,B),並且圖中的每條邊(i,j)所關聯的兩個頂點i和j分別屬於這兩個不同的頂點集(i in A,j in B),則稱圖G為一個二分圖。
簡而言之,就是頂點集V可分割為兩個互不相交的子集,並且圖中每條邊依附的兩個頂點都分屬於這兩個互不相交的子集,兩個子集內的頂點不相鄰。區別二分圖,關鍵是看點集是否能分成兩個獨立的點集。如下圖:
因為二分圖本質來說不是考試內容,諸多性質和概念就不介紹了
- 最大匹配:邊數最多的匹配
這是2020年寧波大學招生考試中的一題算法題
還是這張圖,幫你清楚認知題目意思,有連線的就是彼此同意,沒有連線就是彼此不同意,請注意,一旦學生1去了崗位1,學生2就不能去崗位1了,問你實習人數最大化就是求最大匹配
求二分圖的方法很多,有匈牙利算法,Hopcroft-Carp算法等等,我們這邊采用最大流(先學最大流回頭再來做這題)
最大流(Maximum Flow)
太好,b站有大佬,https://www.bilibili.com/video/BV1eQ4y1K7db?from=search&seid=12110115477561942610,視頻看起來生動形象,就很棒,因為考試不會去要求算法的優劣性,所以我們直接學一個最簡單的FF算法就行了。
測試數據http://poj.org/problem?id=1273
FF算法模板如下
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
using namespace std;
int map[300][300];
int used[300];
int n,m;
const int INF= 0x3f3f3f3f;
int DFS(int s, int t, int f)
{
if(s==t)
return f;//找到終點了,此時剩下的流量就是能獲得的流量
int i;
for(i=1;i<=n;i++)
{
if(map[s][i] >0 && used[i] ==0)//從s開始找
{
used[i]=1;
int d=DFS(i, t, min(f, map[s][i]));//問有沒有增廣路
if(d>0)
{
map[s][i] -=d;
map[i][s] +=d;
return d;
}
}
}
return 0;
}
int maxflow(int s, int t)
{
int flow=0;
while(true)
{
memset(used, 0, sizeof(used));
int f= DFS(s,t, INF);//不斷找s到t的增廣路
if(f == 0)
return flow; //找不到了就回去
flow += f;//找到一個流量f的就賺了
}
}
void init()
{
memset(map, 0, sizeof(map));
return ;
}
int main()
{
while(scanf("%d %d", &m, &n) != EOF)
{
init();
int k1,k2, cap;
int i;
for(i=1;i<=m;i++)
{
scanf("%d %d %d", &k1, &k2, &cap);
map[k1][k2] += cap; //可能有多條路 ,所以要加,考試的時候直接寫等於沒什么關系
}
int ans=maxflow(1,n);
printf("%d\n", ans);
}
return 0;
}
最大流介紹到此,那么我們來思考下上述二分圖的解決方法,其實也很簡單,就是將連線的容值全部是看成是1,並且人為造一個匯源和匯點,最后的最大流就是我們的實習人數最大化,如下圖:
測試數據http://poj.org/problem?id=1274(一摸一樣的二分圖問題)
二分圖解法:
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
using namespace std;
int map[405][405];
int used[405];
int n,m;
const int INF= 0x3f3f3f3f;
int DFS(int s, int t, int f)
{
if(s==t)
return f;//找到終點了,此時剩下的流量就是能獲得的流量
int i;
for(i=1;i<=n+m+2;i++)
{
if(map[s][i] >0 && used[i] ==0)//從s開始找
{
used[i]=1;
int d=DFS(i, t, min(f, map[s][i]));//問有沒有增廣路
if(d>0)
{
map[s][i] -=d;
map[i][s] +=d;
return d;
}
}
}
return 0;
}
int maxflow(int s, int t)
{
int flow=0;
while(true)
{
memset(used, 0, sizeof(used));
int f= DFS(s,t, INF);//不斷找s到t的增廣路
if(f == 0)
return flow; //找不到了就回去
flow += f;//找到一個流量f的就賺了
}
}
void init()
{
memset(map, 0, sizeof(map));
return ;
}
int main()
{
while(scanf("%d %d", &n, &m) != EOF)
{
init();
int num, cap;
int i,j,k;
for(i=1;i<=n;i++)
{
scanf("%d", &num);
for(j=0;j<num;j++)
{
scanf("%d", &k);
map[i+1][k+n+1]=1;
}
}
for(i=1;i<=n;i++) map[1][i+1]=1;
for(i=1;i<=m;i++) map[n+1+i][n+m+2]=1;
int ans=maxflow(1,n+m+2);
printf("%d\n", ans);
}
return 0;
}
查找
考試要求
- 順序查找法
- 折半查找法
- B 樹及其基本操作、 B+樹的基本概念
- 散列(Hash)表
順序查找法
又稱為線性查找,順式存儲和鏈式存儲都可以
typedef struct{
Elemtype *elem;
int TableLen;
}SSTable;
查找函數
int Search_Seq(SSTable ST,int key)
{
ST.elem[0]=key;
int i;
for(i=ST.TableLen;ST.elem[i]!=key;i--);
return i;
}
- 查找成功ASL=\(\sum_{i=1}^n \dfrac{1}{n}*i=\dfrac{1+n}{2}\)
- 查找失敗ASL=n+1
如果是有序的情況下,查找失敗不用遍歷完順序表,只要找到第一個比自己大(小)的元素就可以停止,把情況想象成一棵樹,失敗的情況有n+1種
查找失敗ASL=\(\dfrac{1+2+...+n+n}{n+1}=\dfrac{n}{2}+\dfrac{n}{n+1}\)
折半查找法
又稱二分查找,適用於原本有序的順序表,是不能用於鏈表儲存的。
查找函數
int Binary_Search(int SeqList[],int key)
{
int low=0,high=11,i,mid;
while(low<=high)
{
mid=(low+high)/2;
if(SeqList[mid]==key) return mid;
if(SeqList[mid]<key) low=mid+1;
else high=mid-1;
}
return -1;
}
查找成功和失敗ASL情況和二叉排序樹情況一樣
- 查找成功時:ASL=(\(\sum\)本層高度*本層個數)/結點個數
- 查找失敗時:ASL=(\(\sum\)本層高度*本層補上的葉子個數)/補上的葉子個數
- 折半查找的時間復雜度為\(O(log_2n)\)
- 折半查找的時候向上取整還是向下取整要確認好(一般都是向下取整 )
B樹
B(B-)樹又稱多路平衡查找樹
B樹的操作會手算就行,定義和性質如下
- B樹就是m叉查找樹
- B樹除了根結點以外,每個結點必須最少有\(\lceil m/2 \rceil\)-1個關鍵字,最多有m-1個的關鍵字
- B樹規定任意一個結點,其所有子樹的高度都要相同
- B樹關鍵字的值:子樹0<關鍵字1<子樹1<關鍵字2<子樹2
- n個結點B樹的高度:\(log_m(n+1) \leq h \leq log_{\lceil m/2\rceil}((n+1)/2)+1\)
操作
- 查找:查找類似二叉查找樹,不同之處B樹是m叉查找樹
- 插入:不斷插入新結點的時候,當個數等於m時,從中間位置(\(\lceil m/2 \rceil\))將其中的關鍵字分為兩部分,左部分的不動,右部分的放到新結點中,中間的結點插入到原結點的父節點中,產生的影響也也按這種方式處理
刪除
- 若刪除的是終端結點的情況下,
- 終端結點個數大於\(\lceil m/2 \rceil\)-1直接刪除。
- 若個數等於\(\lceil m/2 \rceil\)-1時,不夠刪,則找左右兄弟借,若兄弟不夠借,則將它和它的兄弟和它的父親的一個結點合並,產生的影響也按刪除的情況來解決。
- 若不是終端結點,找被刪除結點的直接前驅(或后繼)來替代,轉化為終端結點刪除的情況。
B+樹
只要概念就好
- 每個分支節點最多有m課子樹
- 每個非葉結點至少有兩顆子樹,其他至少要有\(\lceil m/2 \rceil\)課子樹
- 結點的子樹個數和關鍵字個數相等
- 所有的葉子結點包含所有的關鍵字及指向相應記錄的指針,並且從小到大
散列(Hash)表
散列(Hash Table)又叫哈希表,特點是關鍵字與其存儲地址直接相關,利用空間換時間
構造方法:
- 除留余數法(沒錯就考這個其他都不太行):H(key)=key%m,m為素數
- 直接定址法:H(key)=key 或者 H(key)=a*key+b,不會產生沖突,他適用於關鍵字的分布基本連續的情況
- 數字分析法:關鍵字為r進制數,r個數碼在各位出現的頻率不一定相同。(比如電話號碼前幾位都相同我們不做當關鍵字,但后面4位基本不相同我們就用它作為關鍵字)
- 平方取中法:關鍵字的平方值的中間幾位作為散列地址
解決處理沖突的方法:
- 拉鏈法:用鏈表
- 開放地址法:一般都是都是用線性探測法。不同開放地址法只是\(d_i\)不同,H(key)=(key+\(d_i\))%m表示第i次沖突時H(key)的取值
查找長度:需要對比關鍵字的個數
- 成功時ASL=(\(\sum\)每個關鍵詞比較個數)/關鍵詞個數
- 失敗時ASL=(\(\sum\)表中序號比較個數)/表長的有效個數
裝填因子\(\alpha\)=表中記錄數/散列表長度(裝填因子越大,則查找效率越低)
排序
插入排序
- 直接插入排序:時間復雜度O(\(n^2\)) 是穩定的
void InsertSort(int A[],int n)
{
int i,j;
for(i=2;i<=n;i++)
{
if(A[i]<A[i-1])
{
A[0]=A[i];
for(j=i-1;A[j]>A[0];j--)
A[j+1]=A[j];
}
A[j+1]=A[0];
}
}
- 折半插入排序:時間復雜度O(\(n^2\)) 是穩定的
void Binary_InsertSort(int A[],int n)
{
int i,j,low,high,mid;
for(i=2;i<=n;i++)
{
if(A[i]<A[i-1])
{
A[0]=A[i];
low=1,high=i-1;
while(high>=low)
{
mid=(low+high)/2;
if(A[mid]>A[0]) high=mid-1;
else low=mid+1;
}
for(j=i-1;j>=high+1;j--)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
}
- 希爾排序 :時間復雜度O(\(n^{1.3}\)) 是不穩定的
別上代碼了,大題目不會考的,會手算就行
交換排序
- 冒泡排序:時間復雜度O(\(n^2\)) 是穩定的
void BubbleSort(int A[],int n)
{
int i,j;
bool flag;
for(i=0;i<n-1;i++)
{
flag=false;
for(j=n-1;j>i;j--)
{
if(A[j-1]>A[j]) swap(A[j-1],A[j]),flag=true;
}
if(flag==false) return ;
}
}
- 快速排序(考試重點):時間復雜度O(\(nlog_2n\))空間復雜度O(\(log_2n\)) 是不穩定的
int Partition(int A[],int low,int high)
{
int pivot=A[low];
while(low<high)
{
while(low<high && A[high]>=pivot) high--;
A[low]=A[high];
while(low<high && A[low]<=pivot) low++;
A[high]=A[low];
}
A[low]=pivot;
return low;
}
void QucikSort(int A[],int low,int high)
{
if(low<high)
{
int pivotpos=Partition(A,low,high);
QucikSort(A,low,pivotpos-1);
QucikSort(A,pivotpos+1,high);
}
}
選擇排序
- 簡單選擇排序:時間復雜度O(\(n^2\)) 是不穩定的
void SelectSort(int A[],int n)
{
int i,j,minn;
for(i=0;i<n-1;i++)
{
minn=i;
for(j=i+1;j<n;j++)
{
if(A[j]<A[minn]) minn=j;
}
if(i!=minn) swap(A[minn],A[i]);
}
}
- 堆排序(考試重點):時間復雜度O(\(nlog_2n\)) 是不穩定的
// 堆排序
// 調整元素k為根的子樹
void HeadAdjust(int A[],int k,int len)
{
A[0]=A[k];
for(int i=2*k;i<=len;i=i*2)
{
if(i<len && A[i]<A[i+1]) i++;
if(A[0]>A[i]) break;
else
{
A[k]=A[i];
k=i;
}
}
A[k]=A[0];
}
// 先建立一個大根堆
void BuildMaxHeap(int A[],int len)
{
for(int i=len/2;i>0;i--) HeadAdjust(A,i,len);
}
void HeapSort(int A[],int len)
{
BuildMaxHeap(A,len);
for(int i=len;i>1;i--)
{
swap(A[i],A[1]);
HeadAdjust(A,1,i-1);
}
}
歸並排序
(考試重點)時間復雜度O(\(nlog_2n\)) 空間復雜度O(n)是穩定的
void Merge(int A[],int low,int mid,int high)
{
int i,j,k;
for(k=low;k<=high;k++) B[k]=A[k];
for(i=low,j=mid+1,k=i;i<=mid&& j<=high;k++)
{
if(B[i]<B[j]) A[k]=B[i++];
else A[k]=B[j++];
}
while(i<=mid) A[k++]=B[i++];
while(j<=high) A[k++]=B[j++];
}
void MergeSort(int A[],int low,int high)
{
if(low<high)
{
int mid=(low+high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high);
}
}
基數排序
沒有比較關鍵字,而是按照(個十百)位序大小排序,比如我們先拿個位從大到小排序,然后我們再按十從大到小排序,再按百位操作就可以得到從大到小的排序。(會手算就行)