算法设计与分析

算法设计与分析概论:递归、贪心、分治与搜索策略

算法设计与分析

一、算法级基础知识

1.算法的基本概念

解决问题的确定方法和有限步骤称为算法,对于计算机科学来说,算法指的是对特定问题的求解步骤的一种描述,是若干条指令的有穷序列。并有以下特性:输入、输出、确定性、有限性、可行性

算法与程序的区别:程序是算法用某种程序设计语言的具体实现,程序设计的实质就是构造解决问题的算法。算法+数据结构=程序,算法的结构和选择依赖于数据结构,所以数据结构是算法设计的基础。

2.算法设计的一般过程(8步骤)

1.充分理解要解决的问题

在设计算法的时候,一定要先搞清楚算法要求处理什么问题、实现哪些功能、预期获得的结果等。

2.数学模型拟制

简单地说,数学模型就是对实际问题的一种数学表达,是数学理论与实际相结合的一门学科。

3.算法详细设计

算法详细设计指的是把算法具体化,即设计出算法的详细规格说明。

4.算法描述

根据前三部分的工作,采用描述工具将算法的具体过程描述下来。

5.算法思路的正确性验证

通过公式定理来验证算法所选择的设计策略及设计思路是否正确。

6.算法分析

算法分析是对算法的效率进行分析,主要是时间效率和空间效率。

7.算法的计算机实现和测试

采用某种程序设计语言来实现算法,并在计算机上进行运行和测试。

8.文档资料的编制

撰写算法的整个设计过程。

3.算法分析(时间、空间复杂度)

算法分析:分析的是性能、效率

1.时间复杂性

时间复杂性是对算法运行时间长短的度量

2.空间复杂性

空间复杂性是对一个算法在运行过程中所占用存储空间大小的度量。

递归算法要分析空间复杂性

4.递归(必考)

子程序(或函数)直接或间接调用自己称为递归。可能分析空间复杂度。

递归算法运行的过程分为两个阶段:递推和回归

(1)递归原理:

1.当子程序(函数/方法)被调用的时候,在数据栈的栈顶 分配 它的 局部变量和形式参数的 存储空间。

2.当子程序返回时,才从栈顶 释放 它的 局部变量和形式参数的 存储空间。

3.子程序运行使用的是栈顶的那一份变量和形式参数。

4.全局变量、静态局部变量和动态分配的变量(new 分配的),分配在"堆"里,不随程序的调用返回而变化。

(2)递归算法的复杂性分析:

1.递归算法的时间复杂性分析

最常用的就是后向代入法

以全排列问题的为例:

全排列算法的时间=复杂性为:O(n!)

2.递归算法的空间复杂性分析

是指算法的递归深度,即算法在执行过程中所需要的用于存储“调用记录”的递归栈的空间大小。

5.基本的数据结构

所谓数据结构,可以定义为:组织一系列相关数据元素的某种方式(组织数据的方式)。

无关系:集合结构

一对一:线性结构,例如:顺序表与链表,栈与队列

一对多:树型结构

多对多:图型结构

(1)顺序表与链表

线性表是最简单、最常用的一种数据结构,一个线性表是n个数据元素的有限序列。

顺序表:把线性表的元素按逻辑次序依次存放在一组地址连续的存储单元,用这种方法存储的线性表简称为顺序表。

链表:链表是线性表的另一种存储方式,其特点是用一组任意的存储单元存储线性表的数据元素,它由一系列的节点组成,节点可以在运行时动态生成。主要有单链表、循环链表、双向链表。

(2)栈与队列

栈:栈可以看成是一种“特殊”的线性表,该线性表限定仅在表尾进行插入和删除操作(先进后出),栈主要应用于表达式的计算、子程序嵌套调用、递归调用等。栈的性质:通常称插入、删除的这一端称为栈顶(Top),另一端称为栈底(Button)。

队列:和栈相反,队列是一种先进先出的线性表,它只允许在表的一端插入 元素,而在另一端删除元素。队列的性质:允许删除的一端称为队头(Front),允许插入的一端称为队尾(Rear)。

(3)树与图

树:数型结构是以分支关系定义的层次结构,它是一种重要的非线性结构。

图:图是一种比线性表和树更为复杂的数据结构,可以用二元组表示,图中的数据元素通常被称为顶点(或节点),数据元素之间的关系称为边,其形式定义为:G=(V,E),集合V是顶点集,集合E是边集。

在线性表中,数据元素之间仅存在线性关系,即每个元素只有一个直接前驱和一个直接后继

在树型结构中,元素之间具有明显的层次关系,每个元素只能和上一层的一个元素相关,但也可以和下一层的多个元素相关。

在图型结构中元素之间的关系可以是任意的,一个图中任意两个元素都可以相关,即每个元素可以有多个前驱和多个后继。

(4)集合

从逻辑结构上看,集合的元素之间没有任何确定的依赖关系,主要考虑集合之间的并、交、和差操作。

二、贪心法

概念:逐步的局部最优,最终达到全局最优。

1.会场安排问题

选择最早结束时间,且不与已安排的会议重叠的会议来安排。

当前会议的开始时间 >=(大于等于) 上一个会议结束的时间

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int N;
struct Act{
	int start;
	int end;
}act[1000];
bool cmp(Act a, Act b){
	return a.end < b.end;
}
int selector(){
	int num = 1, i = 1;
	for (int j = 2; j <= N;j++) {
		if (act[j].start >= act[i].end)
		i = j;
		num ++;
	}
	return num;
}
int main() {
	cin >> N;
	for (int i = 0; i <N; i++){
		cin >> act[i].start >> act[i].end;
		act[0].start = -1;
		act[0].end = -1;
		sort(act + 1, act + N + 1, cmp);
		int res = selector();
		cout << res << endl;
	}
}

void GreedySelector(int n, struct time B[],struct time E[],bool A[]){
	int i, j;
	A[1] = true;
	j = 1;
	i = 2;
	while (i<=n){
		if (B[i] >= E[j]) {
			A[i] = true;
			j = i;
		}
	}
}
贪心算法的正确性证明

贪心算法的正确性证明需要从贪心选择性质和最优子结构性质两方面进行。

(1)贪心选择性质:

贪心选择性质的证明即证明会场安排问题存在一个以贪心选择开始的最优解。

(2)最优子结构性质

进一步,在做了贪心选择买,即选择了会议1后,原问题就简化为对C中所有与会议1相容的会议进行会议安排的子问题。即若A是原问题的一个最优解,则A’=A-{1}是会议安排问题C1={i属于C|bi>=ei}的一个最优解。

证明(反证法):假设A’不是会场安排问题C1的一个最优解。设A1是会场安排问题C1的一个最优解,那么|A1|>|A’|。令A2=A1U{1},由于A1中的会议的开始时间均大于等于e1,故A2是会议安排问题C的一个解。又因为|A2=A1U{1}|>|A’U{1}=A|,所以A不是会场安排问题C的最优解。这与A是原问题的最优解矛盾,所以A’是会场安排问题C的一个最优解。

2.哈夫曼编码

哈夫曼编码是一种编码方式,属于可变字长编码的一种。

哈夫曼树

哈夫曼编码

3.最小生成树(Prim算法、Kruskal算法)

最小包含图的所有节点而只用最少的边和最小的权值距离。

最小生成树其实是一种可以看作是树的结构,而最小生成树的结构来源于图,通过这个图我们使用某种算法形成最小生成树的算法就可以叫做最小生成树算法,具体实现上有两种算法,分别为Prim算法和Kruskal算法

(1)Prim算法(普里姆算法)

从任意一个顶点开始,让生成树长大,直到所有节点都长进来。

按Prim算法对无向连通带权图构造一棵最小生成树

struct edge { 
	double weight; 
	int u, v; 
	bool K; };
void Prim(edge Topology[], int m, int n) {
	int Point[n], num = 0, i, j;
	for (Point[0] = 0, num = 1, m = 0; num < n; num++) {
		for (i = 0; i < m; i++) {
			if (!Topology[i].K) {
				bool U, V; 
                U = V = false;
				for (int j = 0; j < num; j++) {
					if (Topology[i].u == Point[j]) U = true;
					if (Topology[i].v == Point[j]) V = true;
				}
			if (U && !V) Point[num] = Topology[i].v;
			if (!U && V) Point[num] = Topology[i].u;
			if ((U && !V) || (!U && V)) {
				Topology[i].K = true;
				break;
				}	
			}			
		}		
	}	
}

(2)Kruskal算法()

它是从边的角度出发,每一次将图中的权值最小的边取出来,在不构成环的情况下,将该边加入最小生成树。重复这个过程,直到图中所有的顶点都加入到最小生成树中,算法结束。

struct edge { 
	double weight; 
	int u, v; 
	bool K; }; 
struct  TypeTree { 
	int* A; 
	int len; };
void Merg(TypeTree& P, TypeTree& Q){
	int* M;  
    M = new int[P.len + Q.len];
	for (int i = 0; i < P.len; i++) M[i] = P.A[i];
	for (int i = 0; i < Q.len; i++) M[i + P.len] = Q.A[i];
	delete[]Q.A; 
    Q.A = null; Q.len = 0;
	delete[]P.A;  P.A = M;  
    P.len += Q.len;
}
void Kruskal(edge Topology[], int m, int n) {
	TypeTree Tree[n];
	int t, e, q;
	for (int i = 0; i < n; i++) {
		Tree[i].A = new int[1];
		Tree.len = 1;
		Tree.A[0] = i;
	}
	for (int nTree = n; nTree > 1; nTree--) {
		for (e = 0; e < m; e++) {
			if (Topology[e].K) continue;
			int T1 = -1, T2 = -1;
			for (t = 0; t < n; t++) {
				for (q = 0; q < Tree[t].len; q++) {
					if (Topology[e].u == Tree[t].A[q]) T1 = t;
					if (Topology[e].v == Tree[t].A[q]) T2 = t;
				}
			}
			if (T1 != T2) {
				Topology[e].K = true;
				Merg(Tree[T1], Tree[T2]);
				break;
			}
		}
	}
}

三、分治法

概念:分治法,字面上解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同子问题,再把子问题分成更小的子问题,直到最后各个子问题可以简单地直接求解,对各个子问题的解进行合并即得原问题的解。

通常分治法的求解过程都要遵循两大步骤:分解和治理

1.二分查找

二分查找又称为折半查找,它要求待查找的数据元素必须是按关键字大小有序排列的。

(1)二分查找算法实现的非递归形式

int binSearch1(int arr[], int n, int key) {
	int left = 0;
	int right = n - 1;
	int mid;
	while (left <= right) {
		mid = (left + right) / 2;
		if (arr[mid] == key) {
			return mid;}
		else if (arr[mid] > key) {
			right = mid - 1;}
		else if (arr[mid] < key) {
			left = mid + 1;}
	}
	return -1;
}

(2)二分查找算法实现的递归形式

int binSearch2(int arr[], int key, int left, int right) {
	int mid = (left + right) / 2;
	if (left > right) {
		return -1;}
	if (arr[mid] == key) {
		return mid;}
	else if (arr[mid] > key) {
		return binSearch(arr, key, left, mid - 1);}
	else if (arr[mid] < key) {
		return binSearch(arr, key, mid + 1, right);}
}

2.快速排序

快速排序是一种划分交换排序。

int Partition(int arr[], int left, int right) {
		int temp = arr[left];
		int i = left;
		int j = right;
		while (i<j) {
			while (i<j && arr[j]>temp)
			j--;
			if(i<j){
                swap(arr[i], arr[j]);}
			while (i<j && arr[i] <= temp)
			i++;
			if (i<j) {
                swap(arr[i], arr[j--]);}
		}	
		return j;
	}
void QuickSort(int arr[], int left, int right) {
	int mid;
	if (left < right) {
		mid = Partition(arr, left, right);
		QuickSort(arr, left, mid - 1);
		QuickSort(arr, mid + 1, right);
	}
}

四、动态规划

动态规划算法的思想比较简单,其实是分治思想和解决冗余,因此它与分治法和和贪心法类似,它们都是将待求解问题分解为更小的、相同的子问题,然后对子问题进行求解,最终产生一个最优解。

0—1背包问题


五、搜索法

概念:搜索法是利用计算机的高性能来有目的地枚举一个问题的部分或所有可能情况,从而找到解决问题的一种方法。

1.深度优先搜索DFS(先序、中序、后序)

通常用队列(先进先出,FIFO)实现
	初始化队列Q.
	Q={起点s}; 
     标记s为己访问;
	while (Q非空) {
		取Q队首元素u; u出队;
		if (u == 目标状态) {…}
		所有与u相邻且未被访问的点进入队列;
		标记与u相邻的点为已访问;
	}
基本步骤:

1.从图中某个顶点v0出发,首先访问v0。

2.访问结点v0的第一个邻接点,以这个邻接点vt作为一个新节点,访问vt所有邻接点。直到以vt出发的所有节点都被访问到,回溯到v0的下一个未被访问过的邻接点,以这个邻结点为新节点,重复上述步骤。直到图中所有与v0相通的所有节点都被访问到。

3.若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复深度优先搜索过程,直到图中的所有节点均被访问过。

给定一个无向图G(V,E),给出深度优先搜索该图的一个搜索序列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YymXWxGn-1623163738383)(C:%5CUsers%5C%E6%88%B4%E5%B0%94%5CDesktop%5C%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%5C%E6%B7%B1%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2.png)]

bool Visited[n+1];
for (int i = 1; i <= n; i++){
    Visited[i] = 0;}
void Dfsk(int k) {
	Visited[k] = 1;
	for (int j = 1; j <= n; j++){
		if (c[k][j]==1&&Visited[j]==0){
			Dfsk(j);}
	}
}
void Dfs() {
	for (int i = 1; i <= n; i++){
		if (Visited[i] == 0) {
            Dfsk(i);}
	}
}

2.回溯法

回溯法从初始状态出发,在隐式图中以深度优先的方式搜索问题的解,当发现·不满足求解条件时,就回溯,尝试其他路径。通俗地讲,回溯法是一种“能进则进,进不了换,换不了退”的基本搜索方法。

3.广(宽)度优先搜索BFS

基本步骤:

1.从图中某个顶点v0出发,首先访问v0;

2.依次访问v0的各个未被访问的邻接点;

3.依次从上述邻接点出发,访问它们的各个未被访问的邻接点。

4.若此时图中仍有未被访问的结点,则另选图中的一个未被访问的顶点作为起始点。重复广度优先搜索过程,直到图中的所有节点均被访问过。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O66adgfV-1623163738385)(C:%5CUsers%5C%E6%88%B4%E5%B0%94%5CDesktop%5C%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%5C%E5%B9%BF%E5%BA%A6%E4%BC%98%E5%85%88%E6%90%9C%E7%B4%A2.png)]

bool Visited[n + 1];
for (int i = 1; i <= n; i++){
Visited[i] = 0;}
void Bfsv0(int v0) {
	int w;
	visit(v0);
	Visited[v0] = 1;
	InitQueue(&Q);
	InsertQueue(&Q, v0);
	while (!Empty(Q){
		DeleteQueue(&Q, &v);
		for (int i = 0; i <= n; i++){
			if (g[v][i] != 0) w = i;
			if (!Visited(w)) {
				visit(w);
				Visited[w] = 1;
				InsertQueue(&Q, w);}
		}
	}
}
BFS() {
	for (int i = 0; i <= n; i++){
		if (Visited[i] == 0) {
			Bfsv0(i);
		}
	}
}

4.分支界限法

分支界限法类似于回溯法,也是一种在问题的解空间树中搜索问题解的算法,它常以宽度优先搜索或以最小耗费(最大效益)优先的方式搜索问题的解空间树

(1)0-1背包问题

分别用队列式分支界限法和优先队列式分支界限法解0-1背包问题


(2)布线问题

布线问题就是在M*N的方格阵列中,指定一个方格的中点为a,另一个方格的中点为b,问题要求找出a到b的最短布线方案(即最短路径)。布线时只能沿直线或直角,不能走斜线。黑色的单元格代表不可以通过的封锁方格。

do {
	for (int i = 0; i < Numofnbrs; i++) {
		nbr.row = here.row + offset[i].row;
		nbr.col = here.col + offset[i].col;
		if (grid[nbr.row][nbr.col] == -1) {
			Q.Add(nbr);
			grid[nbr.row][nbr.col] = grid[here.row][here.col] + 1;
		}
		if ((nbr.row == finish.row) && (nbr.col == finish.col)) {
			break;}
	}
	if ((nbr.row == finish.row) && (nbr.col == finish.col)) {
		break;}
	if (Q.Isempty()) {
		return;
		Q.Delete;
	}
} while (true)

六、随机化算法

随机数发生器

数值随机化算法

七、NP完全理论

1.易解问题和难解问题

2.P类和NP类问题

P(Polynomial,多项式),NP(Nondeterministic Polynomial),NPC(NP Complete Problem)

P类问题是NP问题的子集,NPC(NP完全)问题是NP问题的子集

1.P类问题,可以这样记:Polynomial time solve,可以在确定性图灵机计算模型上以多项式时间解决的问题类,叫做P类问题。
2.NP类问题,NonDeterministic Polynomial time verify,在非确定图灵机计算模型上能以多项式时间验证的问题类,叫做NP类问题。

如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P问题。

NP问题是指可以在多项式的时间里验证一个解的问题

八、其他

1.二叉树的三种遍历方式(先序、中序、后序)


2.求二叉树节点的个数

贪心法(4):会场安排问题、哈夫曼算法、最小生成树(Prim算法、Kruskal算法)

分治法(2):二分查找、快速排序

搜索法(4):深度优先搜索、广度优先搜索、分支界限法(0-1背包问题、布线问题)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hello一車一乗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值