《算法笔记》第四章:算法初步(算法思想)



Learn to learn
1.Whenever I tackle a new subject, one of my first thoughts is what kind of structure am I trying to build. What would be the input situations that should cause me to remember this knowledge? How do I need to manipulate it, discriminate between similar-seeming situations, calculate or reason with it?
2.When my end goal is to solve a practical problem, I should begin thinking about applications.


排序算法

选择排序:从无序表中选择最值元素,放到有序表的末端
插入排序:将无序表中的以一个元素插入到有序表中的相应的位置

sort 函数

C语言:qsort函数
C++:sort函数(方便,推荐使用,需要加上#include <\\algorithm\\>和using namespace std;)
//函数模板
sort(首元素地址,尾元素地址,比较函数(可选项))//比较char类型时,默认按照字典序
//之后的cmp函数(Compare)可以自定义,以达到不同的排序效果,默认情况下是从小到大
//实现从大到小输出
bool cmp(int a, int b) return a > b;
.....
sort(a, a + 4, cmp);//从大到小排序
//*原因?*

//对结构体数组的排序
struct node{
	int x,y;
}ssd[10];
//编写cmp函数
bool cmp(node a, node b){
	if(a.x != b.x) return a.x > b.x;
	esle return a.y < b.y;//如果x相等则按照y的值进行排序
}

//容器的排序(vector、string 、deque)
sort(vector.begin(),vector.end(),cmp);
//可以调用相应的方法作为排序的标准

散列

散列是典型的运用空间换时间策略的方法,就是将数据存储在一个更加容易访问的数据结构中,例如数组。可以将数组的下标和数组中存储的元素结合起来,这样可以根据下标的信息直接访问数组内的信息。根据映射的方式不同,散列分为不同的类型。

线性散列

就先上面的例子一样,直接将数组内的信息与数组的下标相结合,这样查询的时间复杂度将为O(1)

取留余数法

把关键值除以某个数之后的余数作为散列的关键值。 H ( k e y ) = k e y % m o d H_{(key)}=key \% mod H(key)=key%mod当取mod是一个素数的时候,可以尽可能地覆盖[0, mod)范围内的所有数,还有为了不越界,数组的大小应该不小于mod。当两个数与一个数的模相等的时候,这时候就产生了冲突。

线性探测法

检查 H ( k e y ) + 1 H_{(key)}+1 H(key)+1的情况。这种方法易产生扎堆,在一定程度上会降低效率

平方探查法

检查 H ( k e y ) ± 1 2 、 H ( k e y ) ± 2 2 . . . . . . H_{(key)}\pm1^2、H_{(key)}\pm2^2 ...... H(key)±12H(key)±22......(先正后负)
如果超出了边长,则把 H ( k e y ) ± k 2 H_{(key)}\pm k^2 H(key)±k2对表长进行取模。
如果出现小于零的情况 ( ( H ( k e y ) − k 2 ) % T S i z e + T S i z e ) % T S i z e ((H_{(key)}- k^2)\%TSize + TSize)\%TSize ((H(key)k2)%TSize+TSize)%TSize(其中TSize为表长)
为了避免负数出现的麻烦,可以只进行正向的平方探测。

链地址法

将哈希值相同的元素连接成一个单链表,表头元素就是数组的元素

在标准模板库中,有map可以直接使用hash表的功能(C++11以后可以用unordered_map,速度更快)。另外将一对或者是多对的整数映射成一个整数的方法可以是像十进制数那样,一个数字代表十位一个代表个位。( H ( P ) = x ∗ R a n g e + y H_{(P)} = x *Range + y H(P)=xRange+y

map的常见用法

map函数可以将任何类型映射到其他的任何类型(包括容器),使用map函数需要添加<map>头文件,需要加上using namespace std;

//定义一个map
#include <map>
using namespace std;
map<映射前的类型,映射后的类型>容器名称
//注意一点,如果是字符串到整型的的映射必须是string而不能使用char数组

map<char, int>mp;
//map容器的访问方式
//通过下标访问
mp['c'] = 20;//注意这个是唯一的,后续的赋值会覆盖原先的值
//运用迭代器
//迭代器定义
map<typename1, typename2>::iterator it;//这两个类型名称是定义map时定义的变量类型
//访问键值
it->first;it->second;
//遍历map中的所有键值的循环
for(map<char,int>::iterator it = mp.begin(); it != map.end();it++){...}
//map是使用红黑树实现的,会以键从小到大的顺序自动排序
map常用函数
函数名功能时间复杂度
find(key)返回键为key的映射的迭代器O(logN) N为映射个数
erase()删除一个元素、删除一个区间内的所有元素传入迭代器:O(1)
传入键值:O(log(N))(N为map中的元素个数)
size()获得map中映射的对数O(1)
clear()清空map中所有的元素O(N)
//运用范例
#include <stdio.h>
#include <map>
using namespace std;
int main(){
	map<char, int>mp;
	mp['a'] = 1;
	mp['b'] = 2;
	map<char, int>::iterator it = mp.find('b');
	mp.erase(it);
	mp.erase('b');//与上面的作用相同
	mp.erase(it, mp.end());//传入删除的区间
	mp.size();
	mp.clear();
}

字符串哈希

//将字母映射成十进制数
//算法思想:将26个字母看成26进制数,然后转换成相应的十进制数,转换过程为:
//个位数加上更高位数乘以进制数,这个可以类比十进制数的构成,因为传入的时候是从高位开始的(从左向右读取),
//所以可以将每一次输入的数字都是在个位数,其他的按照进制数倍增
int hashFunc(char S[], int len) {
	int id = 0;
	for (int i = 0; i < len; ++i) {
		id = id * 26 + (S[i] - 'A');//个位数+更高位数*进制数
		printf("%d\n", id);
	}
	return id;
}
//当还遇到小写字母的时候
int hashFunc(char S[], int len){
	int id = 0;
	for(int i = 0; i < len; ++i){
		if(S[i] >= 'A' && S[i] <= 'Z'){
			id = id * 52 + (S[i] - 'A');
		}
		else if(S[i] >= 'a' && S[i] <= 'z'){
			id = id * 52 + (S[i] - 'a') + 26;//注意这里加上的26,是因为在这个52进制当中,大写字母在前面
			//类比十进制, 8 = 3 + 5;
		}
	}
	return id;
}
//如果还出现了数字,则将进制数扩大到62

递归

分治

这种思想让我想到了一个成语:众人拾柴火焰高。将”火焰高“这个目的划分成“众人拾柴”,这个规模较小而与原问题相似的子问题。这包含分治思想的三个方面:首先将一个问题进行分解,然后求解子问题,最后将问题的结果合并成原问题的结果。这样的话你只需要专注于一个小问题就可以。当然,子问题应该是相互独立的、没有交叉的。

递归

这实际上是一种分治思想,这样做的结果就是只需要专注于一个小问题即可,但是与之前“拾柴”的的例子不一样,之前的像是单层的分解,但是递归应该是多层次的。
在使用递归的时候应该注意两点,递归式和递归边界。这也就是怎么划分和划分到什么地步的问题。
下面是求解全排列的问题:在全排列问题中,每一个元素只有两种状态,被选和没有被选,只需要所有的元素的这两种状态全部遍历一遍就可以了。在选和不选中做抉择,可以将所有情况画成一棵树,这棵树就叫做决策树
图源水印

//全排列,字典序
//头部应该是不断更新的,也就是子排列中的头部
/*
算法思想:先确定排列的首部,然后剩下的子序列做同样的操作,这样就可以将问题不断减小。
*/

#include <cstdio>

const int maxn = 11;

int n;//待排元素的个数
int p[maxn];//存储当前排列
int hashTable[maxn] = { false };//判断索引元素是否在排列数组中
int count = 0;//统计全排列的个数

//处理排列的idex号位
void generateP(int index){

	//这是递归边界,前面的元素全部排列完毕,现在是要将数组输出
	if (index == n + 1) {
		for (int i = 1; i <= n; ++i) {
			printf("%d", p[i]);
		}
		printf("\n");
		return;
	}

	//遍历检查所有的元素,这里引用了外部的变量n,由主函数定义(作为参数是不是好点?)
	for (int x = 1; x <= n; ++x) {
		//哈希表是来表示索引元素是否已经加入数组了
		if (hashTable[x] == false) {
			p[index] = x;//以x作为头部的时候
			hashTable[x] = true;//更新状态

			//这里是继续下一位,注意这里还没有将哈希表重置,所以原先作为首部的元素是不会再被赋值了
			generateP(index + 1);
			hashTable[x] = false;//完成递归项中的一个,重置状态
		}
	}
	count++;
	return;
}
int main() {
	n = 9;//表示输出1~3的全排列同时还表示全排列的数组元素个数
	generateP(1);//表示从P[1]开始
	printf("\n%d", count);
	return 0;
}

在一点条件下,之后的实例都无法满足要求,这时候可以直接退出该层递归,返回上一层。这种方法成为回溯

//n皇后问题,n*n的格子中,放入n个皇后,这n个皇后不能在同行、列、对角线
#include<cstdio>
#include<cmath>

const int maxn = 11;

int n;//待排元素的个数
int p[maxn];//存储当前排列
int hashTable[maxn] = { false };//判断索引元素是否在排列数组中
int count = 0;//统计全排列的个数

void generateP(int index) {
	if (index == n + 1) {
		count++;
		return;
	}
	for (int x = 1; x <= n; x++) {
		if (hashTable[x] == false) {
			bool flag = true;
			for (int pre = 1; pre < index; pre++) {
				//检查是否在同一对角线上,注意进入if语句之后该次循环就会被跳过
				if (abs(index - pre) == abs(x - p[pre])) {
					flag = false;
						break;
				}
			}
			if (flag) {
				p[index] = x;
				hashTable[x] = true;
				generateP(index + 1);
				hashTable[x] = false;
			}
		}
	}
}

贪心

用局部最优来达到全局最优的结果。分为在线算法(Online Algorithms)和离线算法(Offline Algorithms),可以证明在线算法无法得到最优解。

//B1020月饼
//忽略最后注释部分
#include<cstdio>
#include<algorithm>
using namespace std;

struct mooncake {
	double store;
	double sell;
	double price;
}cake[1010];

//定义函数的时候一定要大括号
bool cmp(mooncake a, mooncake b) {
	return a.price > b.price;
}

int main(void) {
	int n;//月饼种类
	double D;//市场需求量

	scanf("%d%lf", &n, &D);

	//初始化库存
	for (int i = 0; i < n; ++i) {
		//少些%不会报错,但会出错
		scanf("%lf", &cake[i].store);
	}
	//初始化利润
	for (int i = 0; i < n; ++i) {
		scanf("%lf", &cake[i].sell);
		cake[i].price = cake[i].sell / cake[i].store;//计算单价
	}
	//将数组由根据单价由大到小排列
	sort(cake, cake + n, cmp);

	double ans = 0;//最大利润
	//枚举各种月饼
	for (int i = 0; i <= n; ++i) {
		//检查是否满足市场需求量
		if (cake[i].store <= D) {
			D -= cake[i].store;//更新需求量
			ans += cake[i].sell;
		}
		else {
			ans += cake[i].price * D;
			break;
		}
	}
	printf("%.2f\n", ans);
	return 0;
}





/*
算法思想:先算出单价利润最大的月饼,然后将对应的全部卖出,要考虑的
需要较为进阶的语言知识,现阶段还是不要这样做为好,直接定义最大的情况下的数组即可
*/
/*
#include<cstdio>

struct MoonPie{
    int rep;//库存
    int profit;//利润
    
    MoonPie(){};
    MoonPie(int _rep){
        rep = _rep;
    }
    MoonPie(int _profit){
        profit = _profit;
    }
    
    int value = profit / (double)rep;
};

int main(void){
    int n;//月饼种类
    int re;//需求量
    int profit,rep;
    in maxValue = 0;//最大单价
    
    scanf("%d %d",&n,&re);
    
    //输入数据,定义动态数组,
    int *p = new int[n];
    //找出单个利润最大的月饼
    //初始化数据
    for(int i = 1; i <= n; ++i){
        scanf("%d",&rep);
        p[i] = MoonPie(rep);
    }
    for(int i = 1; i <= n; ++i){
        scanf("%d",&profit);
        p[i] = MoonPie(profit);
    }
    for(int i = 1; i <= n; ++i){
        if(maxValue < p[i].value) maxValue = p[i].value;
    }
    //比较市场需求量和库存,计算最终的利润
}
*/

二分

二分查找

int binarySearch(int A[],int left, int right, int x){
	int mid;//查找区间的中间的位置
	while(left <= right){
		mid = (left + right)/2;
		if(A[mid] == x) return mid;//找到

		//更新查找区间
		else if(A[mid] > x)right = mid -1;
		else left = mid + 1;
	}
	return -1;
}
//当查找范围较大时,(left+right可能会越界)这个时候可以用mid = left+(right-left)/2  代替

当查找的元素数组中包含重复的元素时,这也就是说目标元素在数组中可能不止一个,这个时候就应该返回目标元素所在的区间。算法的总体思路:对于一个有序表,找出第一个与目标元素相等的位置和第一个与目标元素不相等的位置,得到对应的区间

//找出目标数组的上界,第一个大于等于x的元素的位置
//注意在这里二分法的上界是n因为当x不存在的时候,位置范围可能在数组范围之外的那个元素的位置
int lower_bound(int A[],int left, int right, int x){
	int mid;
	//注意这个判断条件,只是确定x的范围,对x是否存在并不关心(?)
	while (left < right){
		mid = (left + right) / 2;
		if(A[mid] >= x)right = mid;
		else left = mid +1;
	}
	return left;
}
//同样的求目标元素的上界,即数组元素第一次与目标元素不相同的时候,与求下界的函数相比,主要的区别就在于
//判断语句中少了等于,这样当指向这个数组元素的时候,就会继续进行原先的步骤,因为这个函数的主要目的就是找到第一个与目标元素不相等的元素的位置

二分查找的思想本质上就是利用目标左右两端的情况,来不断调整范围,以实现向目标趋近的目的。所以,二分法不仅仅可以用于查询满足条件的目标,还可以求目标的近似。下面是一个求 2 \sqrt{2} 2 的近似值 的例子

/*
算法思想:考虑函数f(x) = x^2,想要求sqrt(2),只需要找到函数值趋向于2的数即可。先比较区间中点mid的与f(x)的
大小,若mid>f(x), 则从[left,mid]中去寻找,其他情况类似,直到达到想要的精度
*/
const double esp = 1e-5;//需要的精度

double f(double x) return x * x;//关系函数


double calSqrt(){
	double left = 1, right = 2, mid;//左、右区间范围,中间元素指针
	
	while(right - left > esp){//检查是否满足精度
		mid = (left + right)/2;
		
		//更新区间信息
		if(f(mid) > 2)right = mid;
		else left = mid;
	}
	return mid;
}
//这本质上就是求解一个方程,因为算法只能逼近与一个点,所以需要在区间内只有一个目标,或者说保证二者的关系是单调的

快速幂

题目描述:给定三个正数a、b、m(a< 1 0 9 , b < 1 0 18 , 1 < m < 1 0 9 10^9,b<10^{18},1<m<10^9 109,b<1018,1<m<109),求 a b % m a^b\% m ab%m

算法分析:因为数字的数量级太大,直接通过循环来求解需要的时间会很多(时间复杂度为O(n)),可以借用快速幂的方法:
a b = {   a ∗ a b − 1 ( b 为 奇 数 ) a b 2 ∗ a b 2 ( b 为 偶 数 ) a^b=\left\{ \begin{aligned} \ a *a^{b-1} (b为奇数)\\a^{\frac b2}*a^{\frac b2}(b为偶数) \\ \end{aligned} \right. ab={ aab1ba2ba2bb
这样可以将幂次的乘积进一步减少

//快速幂的递归形式
typedef long long LL;

//参数分别为底数、指数、模数
LL binaryPow(LL a, LL b, LL m){
	if(b == 0)return 1;//递归出口
	
	if(b % 2 == 1) return a * binaryPow(a, b - 1, m) % m;
	else {
		LL mul = binaryPow(a, b / 2, m);
		return mul * mul % m;
	}
}
//if(b % 2 == 1)还可以这样写if (b & 1)代替,后者就是检查b的二进制形式的最后
//一位是否是1,是则为奇数,否则为偶数
//还有注意返回时不要写:
return (binariPow(a, b / 2, m) * binariPow(a, b / 2, m)) % m;
//因为这样会导致额外的运算

To Be Continue…

双指针

双指针法分为两种,一种是首尾指针,一种是快慢指针,前者用于

归并排序

快速排序

其他高效技巧与算法

回溯(动态规划)

动态规划的关键就是将一个问题划分成几个相互之间有交集的子问题。典型的问题就是求最优解的问题,可以从前到后,不断将最优的解放到数组中,这样要求解该轮下的最优解,只需要将所有情况都试一遍,然后比较出最优的那一个即可。这个过程就像是遍历一棵树,找到最优子节点,然后以子节点作为头节点继续遍历,直到满足条件。

在这里插入图片描述
动态规划问题的分析模式:

1.分析最优解的结构
2.递归地定义最优解的值
3.计算最优解(从低向上)
4.展示结果

#找出数组中两两不相邻的数字的最大值
def de_opt(arr):
	opt = np.zeros(len(arr))
	opt[0] = arr[0]
	opt[1] = max(arr[0], arr[1])
	for i in range(2, len(arr)):
		A = opt[i-2] + arr[i]
		B = opt[i-1]
		opt[i] = max(A,B)
	return opt[len(arr) - 1]

#递归写法
def rec_opt(arr, i)
	if i == 0:
		return arr[0]
	elif i == 1:
		return max(arr[0], arr[1])
	else:
		A = rec_opt(arr, i - 2) + arr[i]
		B = rec_opt(arr, i - 1)
		return max(A, B)

回溯(sù):将选择一种情况作为一个结果,然后将不选择该情况作为另一个结果,找出两种情况的最优解,得到局部的最优解。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值