916數據結構與算法


前言

主要是記錄下代碼,根據考綱把一些簡單算法記錄下,方便學習和回憶。代碼方面我盡量寫的規范點,如果大家覺的不妥請自動腦部 = v =(算了,我還是為所欲為吧,畢竟自己爽的東西

參考書是2021年王道數據結構,我寫的是書上的補充,看時請配合書一起使用

請注意,我介紹的東西全部簡化處理過了,就是有很多的定義,概念和高級算法直接舍去。原因很簡單,因為我覺的它不會考,看了歷年真題,考試只會考一些基礎的東西,他不會讓你去改進算法,不會太難。

為了確保我寫的代碼的准確性,我都會在oj上找題目做練習,算法前我都附上oj鏈接並測試,考試的時候只要把思想說清楚,關鍵的函數寫出來問題就應該不大了。(為了應用題分數真的不容易)

我自己寫的代碼在這里https://github.com/taoyeh/DataStructure

簡易版本請看這

考試重點

就兩年真題,還能怎樣。考試側重點為:棧,二叉樹,排序>圖,隊列,查找>復雜度,額外考點

#include<bits/stdc++.h> //萬能頭文件

請注意,考試重點考的是算法,是思想,不仔細看你每一步語法錯還是對,請確保大抵方向對,還有C++它寫起來方便多了。

基礎

考試內容

  1. 計算機中算法的角色
  2. 算法復雜度分析
  3. 遞歸

計算機中算法的角色

算法的定義: 算法是解決特定問題求解決步驟的描述,再計算機中表現為指令的有限序列,並且每條指令表示一個或多個操作。其擁有五個重要特性:

1.有窮性2.確定性3.可行性4.輸入5.輸出

它的目標為:

1.正確性2.可讀性3.健壯性4.效率和低儲存量需求

算法復雜度分析

  1. 時間復雜度(考試重點)
  2. 空間復雜度

線性表

考試內容

  1. 基於順序存儲的定義和實現
  2. 基於鏈式存儲的定義和實現
  3. 線性表的應用

基於順序存儲的定義和實現

線性表的順序存儲又稱順序表,特點為邏輯地址和物理地址相同

定義

靜態分配

# 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]

棧與隊列

考試內容

  1. 棧、 隊列、 字符串、 數組的基本概念、 特點
  2. 棧和隊列基於順序存儲的定義與實現
  3. 棧和隊列基於鏈式存儲的定義與實現
  4. 稀疏矩陣的壓縮存儲及轉置算法實現

定義

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;
}

中綴轉后綴方法

  1. 遇到操作符,直接加入后綴表達式
  2. 遇到界限符,如果是“(”打入棧,如果是“)”,彈出棧中所有運算符直到“(”,當然左右括號不加入后綴表達式
  3. 遇到運算符,依次彈出棧中優先級大於等於自己的的所有運算符,加入后綴表達式,若碰到“(”和棧空則停止。之后再把當前的運算符壓入棧
  4. 不會考,放心 = v =

稀疏矩陣

一個二維數組中元素十分少,所以用一個三元組(行標,列表,值)記錄所有的有效值,至於轉置,把行標和列表互換就行。

只需看KMP中next和nextval,會手算就行,考綱里面沒有要求

考試內容

  1. 二叉樹
    ①二叉樹的定義、 主要特征
    ②二叉樹基於順序存儲和鏈式存儲的實現
    ③二叉樹重要操作的實現
    ④線索二叉樹的基本概念和構造
  2. 樹、 森林
    ①樹的存儲結構
    ②森林與二叉樹的相互轉換
    ③樹和森林的遍歷
  3. 特殊二叉樹及應用
    ①哈夫曼(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(當然相反也可以),葉子結點就有固定的編碼了。

在完全二叉樹中,讓根結點大於孩子結點,同樣孩子結點也滿足這樣的條件我們叫做大根堆。若根結點小於孩子結點,同樣孩子結點也滿足這樣的條件我們叫做小根堆(具體操作我們在堆排序里面講)

考試內容

  1. 基本的圖算法
  2. 最小生成樹
  3. 單源最短路徑
  4. 最短路徑
  5. 最大流

定義

  • G= (V,E)V為頂點,E為邊。
  • 線性表和樹可以為空,但是圖的頂點不能為空
  • 若E為無向邊,則稱邊。若E為有向邊,則稱弧。
  • 一條弧中沒有箭頭的為弧尾,有箭頭的為弧頭。
  • 我們談論的都是簡單圖(沒有重復邊,不存在頂點到自己的邊)
  • 無向邊的度(TD)為2e,有向圖的度(TD)=出度(OD)+入度(ID)=e
  • 無向圖中頂點v和頂點w路徑存在,則稱連通。在有向圖中,頂點v和頂點w相互有路徑則稱強連通
  • 連通圖:無向圖所有頂點連通。強連通圖:所有頂點強連通。
  • 極大:邊盡可能的多,極小:邊盡可能的少
  • 連通圖的生成樹是包含所有頂點的一個極小聯通子圖。
  • 無向完全圖:無向圖任意兩個頂點之間都存在邊。有向完全圖:有向圖任意兩個頂點之間都存在方向相反的兩條弧。

存儲方式

  • 鄰接矩陣:一個二維數組。其中\(A^n[i][j]\)表示由頂點i到頂點j長度為n的路徑的數目
  • 鄰接表:一個一維數組加鏈表。
  • 十字鏈表:存儲有向圖,解決了鄰接表找入邊不方便的問題
  • 鄰接多重表:存儲無向圖,刪除邊刪除節點很方便
  • 后兩種考的少,懂就行

基本的圖算法

  1. 廣度優先遍歷(BFS)
  2. 深度優先遍歷(DFS)
  3. 時間復雜度: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可分割為兩個互不相交的子集,並且圖中每條邊依附的兩個頂點都分屬於這兩個互不相交的子集,兩個子集內的頂點不相鄰。區別二分圖,關鍵是看點集是否能分成兩個獨立的點集。如下圖:

3c6d55fbb2fb43169079761121a4462309f7d373.png

因為二分圖本質來說不是考試內容,諸多性質和概念就不介紹了

  • 最大匹配:邊數最多的匹配

這是2020年寧波大學招生考試中的一題算法題

image.png

還是這張圖,幫你清楚認知題目意思,有連線的就是彼此同意,沒有連線就是彼此不同意,請注意,一旦學生1去了崗位1,學生2就不能去崗位1了,問你實習人數最大化就是求最大匹配

image.png

求二分圖的方法很多,有匈牙利算法,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;
}

image.png

最大流介紹到此,那么我們來思考下上述二分圖的解決方法,其實也很簡單,就是將連線的容值全部是看成是1,並且人為造一個匯源和匯點,最后的最大流就是我們的實習人數最大化,如下圖:

image.png

測試數據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;
}

image.png

查找

考試要求

  1. 順序查找法
  2. 折半查找法
  3. B 樹及其基本操作、 B+樹的基本概念
  4. 散列(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\))將其中的關鍵字分為兩部分,左部分的不動,右部分的放到新結點中,中間的結點插入到原結點的父節點中,產生的影響也也按這種方式處理

刪除

  • 若刪除的是終端結點的情況下,
  1. 終端結點個數大於\(\lceil m/2 \rceil\)-1直接刪除。
  2. 若個數等於\(\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);
	}
}

基數排序

沒有比較關鍵字,而是按照(個十百)位序大小排序,比如我們先拿個位從大到小排序,然后我們再按十從大到小排序,再按百位操作就可以得到從大到小的排序。(會手算就行)

額外算法

額外算法請看這里https://www.cnblogs.com/xxhao/p/13698715.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM