算法基础-->数组
来源:互联网 发布:数控镗铣床编程代码 编辑:程序博客网 时间:2024/05/21 12:48
本篇博文将详细总结与数组相关的一些算法。
求局部最大值
问题描述
给定一个无重复 元素的数组
显然,遍历一遍可以找到全局最大值,而全局最大值显然是局部最大值,但是时间复杂度达到
问题分析
定义:若子数组
我们假定称该子数组为“高原数组”。(高原数组这个词是我们自创的,为了形象说明)
若高原数组长度为1,则该高原数组的元素为局部最大值。
算法描述
使用索引
求中点
若
时间复杂度为
代码实现
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>using namespace std;int LocalMaximum(char* a, int size){ int left = 0; int right = size - 1; int mid; while (right>left) { mid = (left+ right) / 2; if (a[mid] > a[mid + 1]) right = mid; else { left = mid+1;//注意这里是mid+1 } } return a[left];}
第一个缺失的整数
问题描述
给定一个数组
如
循环不变式
思路:将找到的元素放到正确的位置上,如果最终发现某个元素一直没有找到,则该元素即为所求。
循环不变式:如果某命题初始为真,且每次更改后仍然保持该命题为真,则若干次更改后该命题仍然为真。
为表述方便,下面的算法描述从1开始数。
利用循环不变式设计算法
假定前
若
若
若
若
若
若
整理算法:
- 若
A[i]=i ,i 加1,继续比较后面的元素。 - 若
A[i]<i 或A[i]>N 或A[A[i]]=A[i] ,丢弃A[i] - 若
A[i]>i ,则将A[A[i]]和A[i] 交换。
思考:如何快速丢弃(删除)
如果按常规的思想删除数组里的元素,那么删除某个元素后,其后面的元素需要依次的向前移动,其时间复杂度至少
O(n) 。如果将A[N] 赋值给A[i] ,然后N 减1,相当于把A[N] 丢弃了,A[N] 就是互换之前的A[i] 。则只需要O(1) 的时间复杂度就删除了元素。这里需要如果注意丢弃了一个元素,则可表示的连续序列最长的长度会减1,因为数组中剩余的元素个数减少了1。
代码实现
//对数组中a,b两个数进行互换void swap(int &a, int &b){//注意这里必须是交换变量地址,如果单纯交换数组中两元素,则该数组无任何改变。 int temp = a; a = b; b = temp;}int FirstMissNumber(int a[], int size){ a--;//数组下标均加1,从1开始计,在原始数组a上进行操作使得a[size]={1,2,3,..,size] int i = 1; while (i<=size) { if (a[i] == i) i++;//只有在a[i]==i时才前进一步,如果遇到缺失的,则始终得不到a[i]==i else if (a[i]<i || a[i]>size || a[i] == a[a[i]])//丢弃a[i] { //如果a[i]!=i,若 //a[i]<i表示有重复;a[i]>size表示超出可表示有序数组;a[i] == a[a[i]]则下面的互换无意义 //丢弃一个元素,则可表示的有序数组长度减1 a[i] = a[size]; size--; } else//如果a[i]!=i且i<a[i]<=size,则进行互换,将a[i]换到数组中正确的位置上。 { swap(a[i], a[a[i]]); } } return i;}int main(){ int a[] = { 3, 5, 1, 2, -3, 6 , 7 , 4, 8 }; int m = FirstMissNumber(a, 9); cout << m << endl;}
查找旋转数组的最小值
问题描述
假定一个排序数组(已经有序) 以某个未知元素为支点做了旋转,如:原数组
显然把数组遍历一遍就能找到最小值,但是时间复杂度达到
问题分析
旋转之后的数组实际上可以划分成两个有序的子数组:前面子数组的大小都大于后面子数组中的元素;
用索引
若子数组是普通升序数组,则
若子数组是循环升序数组(可以理解为两不同的递增序列),前半段子数组的元素全都大于后半段子数组中的元素:
显然,
若:
若:
代码实现
int FindMin(int *a, int size){ int low = 0; int high = size - 1; int mid; while (low<high) { mid = (low + high) / 2; if (a[mid] < a[high]) high = mid;//最小值在左半部分 else//最小值在右半部分 { low = mid+1; } } return a[low];}
零子数组
问题描述
求对于长度为
如:数组
算法流程
申请比
显然有:
算法思路:
- 对
sum[−1,0…,N−1] 排序,然后计算sum 相邻元素的差的绝对值,最小值即为所求。 - 在
A 中任意取两个前缀子数组的和求差的最小值。
时间复杂度
计算前
代码实现:
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;void bubbleSort(int a[], int size, int index[]){ for (int i = -1; i < size; i++) { for (int j = size - 1; j >= i; j--) { if (a[j] < a[j - 1]) { int t_index = index[j]; index[j] = index[j - 1]; index[j - 1] = t_index; int t = a[j]; a[j] = a[j - 1]; a[j - 1] = t; } } }}int subArray(int *a, int size){ //计算sum数组 int* sum = new int[size + 1]; sum++;//使得下标从-1开始。 sum[-1] = 0; for (int i = 0; i < size; i++) { sum[i] = sum[i - 1] + a[i]; } //定义index数组,用来记录sum数组排序时交换了的index。 int* index = new int[size + 1];//index数组记录 index++; for (int j = -1; j < size; j++) index[j] = j; //对sum数组进行排序 bubbleSort(sum, size, index);//注意sum,index都是从-1开始计,故在方法遍历sum,index时也是从-1开始计 //计算排序后sum相邻元素差最小值 int k1, k2, minDis; int min = 10000; for (int i = 0; i < size; i++) { minDis = abs(sum[i] - sum[i - 1]); if (minDis < min) { min = minDis; k1 = index[i]; k2 = index[i - 1]; } } int min_index = k1 > k2 ? k2 : k1; int max_index = k1>k2 ? k1 : k2; for (int i = min_index + 1; i <= max_index; i++) { cout << a[i] << " "; } cout << endl; return min;}int main(){ int a[] = { -1, 5, -3, -1, 7, 4, 8 }; int m = subArray(a, 7); cout << m << endl;}
最大子数组和
问题描述
给定一个数组A[0,…,n-1],求A的连续子数组,使得该子数组的和最大。
例如:
数组: 1, -2, 3, 10, -4, 7, 2, -5,
最大子数组:3, 10, -4, 7, 2
算法分析
定义:前缀和
显然有:
算法过程:
求
i 前缀sum[i] :
遍历i:0≤i≤n−1 ;sum[i]=sum[i−1]+a[i] 。计算以
a[i] 结尾 的子数组的最大值
对于某个i (固定i ):遍历−1≤j≤i ,求sum[j] 的最小值m 。(注:sum 中的i 从0开始算,j 都是从−1 开始算)sum[i]−m 即为以a[i] 结尾的数组中最大的子数组的值。
我们已经定义sum[−1]=0 ,如果对于某一个i 而言,其sum[1],sum[2],...,sum[i] 都为正数,那么最小的是sum[j]=0,sum[i]−m=sum[i] 相当于没减。但是如果sum[j] 等于一个负数m ,这个时候用sum[i]−sum[j] 就相当于在前i 项和中去掉前j 项(其中前j 和为负数)的部分,那么剩下的连续部分和肯定就是以a[i] 为尾的子数组最大值。统计
sum[i]−m 的最大值,0≤i≤n−1 求出
sum[i]−m 的最大值对应的i ,再求出其对应的j ,在原数组a[i] 到a[j] 的部分即为最大子数组。1、2、3步都是线性的,因此,时间复杂度O(n)。
代码实现
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;//返回-1<=j<=i中sum[j]最小值对应的jint getMin(int* sum, int i){ int res; int min = 1e+10; int min_index; for (int j = -1; j <= i; j++) { res = sum[j]; if (res < min){ min = res; min_index = j; } } return min_index;}//返回所有res数组中最大值对应的索引iint getMax(int *res, int size){ int ress; int max = -1e+10; int index; for (int j = 0; j < size; j++) { ress = res[j]; if (ress > max) { max = ress; index = j; } } return index;}int subArray(int *a, int size){ //计算sum数组 int* sum = new int[size + 1]; sum++;//使得下标从-1开始。 sum[-1] = 0; for (int i = 0; i < size; i++) { sum[i] = sum[i - 1] + a[i]; } //计算以每一个a[i]结尾的子数组的最大值,组成数组res int * res = new int[size]; int * index = new int[size]; int min_index; int min; for (int i = 0; i < size; i++) { min_index = getMin(sum, i);//sum是从-1计,那么在调用方法中遍历sum时也是从-1计算 min = sum[min_index]; res[i] = sum[i] - min; index[i] = min_index; } int max_index = getMax(res, size);//res数组中最大值对应的索引 int index_i = index[max_index];//上面最大值对应的i对应的j,返回 - 1 <= j <= i中sum[j]最小值对应的j for (int i = index_i + 1; i <= max_index; i++){//原数组a中j到i即为所求的最大子数组。 cout << a[i] << " "; } cout << endl; //计算res数组中最大值 int ress = res[max_index]; return ress;}int main(){ int a[] = { 1, -2, -3, 4, 5, 6 }; int size = (sizeof(a) / sizeof(int)); int m = subArray(a, size); cout << m << endl;}
该题还有动态规划的解法,这里给出动态规划代码,后面总结到动态规划部分会有详细讲解。
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;int maxSubarray(int* a, int size){ int sum = a[0]; int newFrom=0; int res = 0; int from = 0, to = 0;//a[from]到a[to]就是最大子数组 for (int i = 1; i < size; i++) { if (sum > 0) sum + a[i]; else { sum = a[i]; newFrom = i; } if (res < sum) { res = sum; from = newFrom; to = i; } } return res;}
最大间隔
问题描述
给定整数数组
排序后:
可否有更好的方法?
问题分析
假定
如果
如果
解决思路
思路:将
若没有任何数落在某区间,则该区间无效,不参与统计。
显然,这是借鉴桶排序/Hash映射的思想。
桶的数目:
同时,
如:7个数,假设最值为
代码实现
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;typedef struct tagsBucket{ int nMin; int nMax; bool bvalid; tagsBucket() :bvalid(false){}//桶的初始状态都是false,也即是桶处于无效状态 void add(int n) { if (!bvalid)//只有当放入数据时变为true,变为有效状态 { nMin = nMax = n; bvalid = true; } else { if (nMax < n) nMax = n; else if (nMin > n) nMin = n; } }}SBucket;int calMaxGap(int* a, int size){ SBucket* pBucket = new SBucket[size];//数组有size个数,那么就分size个桶 //求数组a的最大最小值 int min = a[0]; int max = a[0]; for (int i = 1; i < size; i++) { if (a[i]>max) max = a[i]; else if (a[i] < min) min = a[i]; } int ndelta = max - min; int nBucket; for (int i = 0; i < size; i++) { nBucket = (a[i] - min)*(size / ndelta);//计算a[i]应该在哪个桶中 if (nBucket >= size) nBucket = size-1;//有size个桶,但是是从0开始计 pBucket[nBucket].add(a[i]); } int i = 0; int nGap = ndelta / size; int gap; for (int j = 1; j < size; j++)//i是前一个桶,j是后一个桶。 { if (pBucket[j].bvalid)//无效打的桶不参与计算 { //计算后一个桶的最小值和前一个桶的最大值的最大间隔值 gap = pBucket[j].nMin - pBucket[i].nMax; if (nGap < gap) nGap = gap; i=j; } } return nGap;}
字符串的全排列
字符串的全排列问题是一个非常重要的问题,需要把它弄清楚了。
下面用两种方法来解决它,递归法和非递归法。
问题描述
给定字符串
递归算法
以字符串1234为例:
- 1 – 234(表示把
1 拿过来,234 做个全排列,下面同理) - 2 – 134
- 3 – 214
- 4 – 231
上面的是一个递归的过程,首先固定第一位数字,剩余的做个全排列,在第一位数固定的基础上再固定第二位数,剩余的再做全排列,依次递归下去,直到全部排列完毕。
如何保证不遗漏:保证递归前1234的顺序不变
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;void Print(int* a, int size){ for (int i = 0; i < size; i++) { cout << a[i] << " "; } cout << endl;}void Permutation(int* a, int size, int n){ if (n == size - 1){ Print(a, size); return; } for (int i = n; i < size; i++){ swap(a[i], a[n]); Permutation(a, size, n + 1);//已固定前n+1个数。 swap(a[i], a[n]);//这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。 }}int main(){ int a[] = { 1, 2, 3, 4 }; Permutation(a, sizeof(a) / sizeof(int), 0);//当前已经有0个数已经固定 return 0;}
上面的代码其实就是一个深度优先搜索 的过程。以上过程是基于数组非重复的情况。
如果字符有重复:
去除重复字符 的递归算法
以字符
- 1 – 223
- 2 – 123
- 3 – 221
带重复字符的全排列就是每个字符分别与它后面非重复出现的字符交换。
即:第
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;bool IsDuplcate(int* a, int n, int t){ while (n<t) { if (a[n] == a[t]) return false; n += 1; }}void Print(int* a, int size){ for (int i = 0; i < size; i++) { cout << a[i] << " "; } cout << endl;}void Permutation(int* a, int size, int n){ if (n == size - 1){ Print(a, size); return; } for (int i = n; i < size; i++){ if (IsDuplcate(a, n, i)) continue; swap(a[i], a[n]); Permutation(a, size, n + 1); swap(a[i], a[n]);//这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。 }}int main(){ int a[] = { 1, 2, 3, 4 }; Permutation(a, sizeof(a) / sizeof(int), 0); return 0;}
重复的字符串里面,需要循环的判断某一个字符是否出现过,其时间复杂度至少是
我们可以利用空间换时间来降低时间复杂度:
- 如果是单字符,可以使用
mark[256] ; - 如果是整数,可以遍历整数得到最大值
max 和最小值min ,使用mark[max−min+1] ; - 如果是浮点数或其他结构,考虑使用
Hash 。 - 事实上,如果发现整数间变化太大,也应该考虑使用
Hash ; - 可以认为整数/字符的情况是最朴素的
Hash 。
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;void Print(int* a, int size){ for (int i = 0; i < size; i++) { cout << a[i] << " "; } cout << endl;}void Permutation(int* a, int size, int n){ if (n == size - 1){ Print(a, size); return; } int dup[256]={0};//我们把每个字符看作ascll码值 for (int i = n; i < size; i++){ if (dup[a[i]]==1)//如果出现过就跳过 continue; dup[a[i]] = 1;//只要这个字符出现过,对应的dup[a[i]]设为1 swap(a[i], a[n]); Permutation(a, size, n + 1); swap(a[i], a[n]);//这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。 }}int main(){ int a[] = { 1, 2, 3, 4 }; Permutation(a, sizeof(a) / sizeof(int), 0); return 0;}
非递归算法
起点:字典序最小的排列,例如
终点:字典序最大的排列,例如
过程:从当前排列生成字典序刚好比它大的下一个排列。
如:
21543的下一个排列的思考过程:
逐位考察哪个能增大
- 一个数右面有比它大的数存在,它就能增大。
- 从后边开始找,那么最后一个能增大的数是——
x=1
- 增大到它右面比它大的最小的数——
y=3
应该变为
寻找下一个排列的算法步骤:后找、小大、交换、翻转
- 后找:从后往前找字符串中最后一个升序的位置
i ,即:S[k]>S[k+1](k>i),S[i]<S[i+1] ; - 查找(小大):
S[i+1…N−1] 中比Ai 大的最小值Sj ; - 交换:
Si,Sj ; - 翻转:
S[i+1…N−1]
交换操作后,S[i+1…N−1] 一定是降序的(上面的后找,查找决定了)。
我们以926520为例,考察该算法的正确性:
- 后找:可以找到
i=1 时,也就是S[1]=2 是数组中最后一个升序的值。 - 查找(小大):在
S[2,..,5] 中比2 大的最小值为5 。 - 交换:交换
S[1] 和S[3] 得956220 。 - 翻转:翻转
S[2,...,5] 得950226 。
那么可得
#include<stdio.h>#include <stdlib.h>#include<stack>#include<string>#include<iostream>using namespace std;void reverse(int *a, int from,int to){ int t; while (from<to) { t = a[from]; a[from++] = a[to]; a[to--] = t; }}void Print(int* a, int size){ for (int i = 0; i < size; i++) { cout << a[i] << " "; } cout << endl;}bool GetNextPermutation(int* a, int size){ //后找 int i = size - 2; while ((i >= 0) && (a[i] >= a[i + 1])) i--; if (i < 0) return false; //查找(小大) int j = size - 1; //因为a[i],a[i+1],a[i+2],...,a[size-1]是递减的,由上面的后找决定 //所以找到的第一个a[j]>a[i],肯定是比a[i]大的中最小的。 while (a[j] <= a[i]) j--; //交换 swap(a[j], a[i]); //翻转 reverse(a, i + 1, size - 1); return true;}int main(){ int a[] = { 1, 2, 2, 3 }; int size = sizeof(a) / sizeof(int); Print(a, size); while (GetNextPermutation(a,size))//从当前排列生成字典序刚好比它大的下一个排列。如果有返回true继续生成。 //反之没有下一个比他大的了,返回false,说明已经把全排列全部输出。 { Print(a, size); } return 0;}
由上面的算法步骤也可以看出:非递归算法能够天然解决重复字符的问题!
STL在Algorithm中集成了next_permutation:
#include<stdio.h>#include <stdlib.h>#include<iostream>#include<algorithm>using namespace std;int main(){ int a[] = { 1, 4, 2, 3 }; int size = sizeof(a) / sizeof(int); Print(a, size); while (next_permutation(a,a+4)) { Print(a, size); } return 0;}
子集和数问题 N-Sum
问题描述
已知数组
布尔向量
x[i]=0 表示不取A[i],x[i]=1 表示取A[i] - 这是个NP问题!
直接递归法
#include<stdio.h>#include <stdlib.h>#include<iostream>#include<algorithm>using namespace std;int a[] = { 1, 2, 3, 4, 5 };int size = sizeof(a) / sizeof(int);int sum = 10;void Print(int* a, bool* x){ for (int i = 0; i < size; i++) { if (x[i]) { cout << a[i] << " "; } } cout << endl;}void EnumNumber(bool* x, int i, int has)//递归到第i位,has为当前求和得到的值{ if (i >= size) return; if (has + a[i] == sum) { x[i] = true;//加上了a[i]和为sum,则对应的x[i]=true Print(a,x);//此时和为sum满足条件,则打印出x[i]为true对应的a[i] x[i] = false; } x[i] = true; //设x[i] = true EnumNumber(x, i + 1, has + a[i]);//把a[i]放进求和, x[i] = false;//把x[i]重新设为false,这一步就是回溯,恢复到原始的状态再选择其他方向进行遍历。 EnumNumber(x, i + 1, has);//再尝试不把a[i]放进求和}int main(){ bool* x = new bool[size]; memset(x, 0, size); EnumNumber(x, 0, 0); delete[] x; return 0;}
上面直接递归的代码中相当于把所有的解空间都遍历了一遍,其时间复杂度为
考虑对于分支如何限界
前提:数组
假定由
has+a[i]≤sum 并且has+r≥sum:x[i] 可以为1 ;has+(r−a[i])>=sum:x[i] 可以为0 ;
注意:这里是“可以”——可以能够:可能。从前向后不一定能推出,但是从后向前一定能推出。
#include<stdio.h>#include <stdlib.h>#include<iostream>#include<algorithm>using namespace std;int a[] = { 1,2,3,4,5,6,7,8,9,10 };int size = sizeof(a) / sizeof(int);int sum = 40;void Print(int* a, bool* x){ for (int i = 0; i < size; i++) { if (x[i]) { cout << a[i] << " "; } } cout << endl;}void EnumNumber(bool* x, int i, int has,int residue){ if (i >= size) return; if (has + a[i] == sum) { x[i] = true;//加上了a[i]和为sum,则对应的x[i]=true Print(a,x);//此时和为sum满足条件,则打印出x[i]为true对应的a[i] x[i] = false; } else if ((has + residue >= sum)&&(has + a[i] <= sum)) { x[i] = true; EnumNumber(x, i + 1, has + a[i], residue - a[i]); } if (has + residue - a[i] >= sum) { x[i] = false; EnumNumber(x, i + 1, has, residue - a[i]); }}int Sum(int* a, int size){ int sum = 0; for (int i = 0; i < size; i++) { sum += a[i]; } return sum;}int main(){ int residue = Sum(a, size); bool* x = new bool[size]; memset(x, 0, size); EnumNumber(x, 0, 0,residue); delete[] x; return 0;}
分支限界的条件是充分条件吗?不是,是必要条件,只能从后往前才是确定成立。
分支限界条件越苛刻,速度越快,但是从理论上来说其时间复杂度没有发生改变,仍然是
- 算法基础-->数组
- 【Java基础】--算法与数组
- 基础算法题-数组相关
- Java基础部分-数组和简单算法
- 基础典型算法研究:合并有序数组
- 算法基础 - 树状数组(binary indexed tree)
- Java语言基础 数组的排序算法
- 算法基础篇(7)------树状数组
- Javascript数据结构算法之数组基础篇
- 算法基础之数组去重
- Java基础(五)数组与算法
- 【基础算法强化】(2)零子数组
- JavaSE基础第三部分:Java数组和算法之数组
- JavaSE基础第三部分:Java数组和算法之算法
- 【算法基础】算法导论-最大子数组问题
- JAVA基础程序设计——数组排序、算法(数组实例+自己写的算法实例)
- 排序算法,数组进阶及面向对象基础
- 基础算法之三: 合并两个有序数组
- Sql 中存储过程详细案例
- HDU 5583 Kingdom of Black and White (暴力)
- Linux直接从Server上传文件到百度云
- MFC窗口控件随主窗口大小改变而伸缩
- stm32的定时器输入捕获与输出比较
- 算法基础-->数组
- CacheCloud+Redis Cluster 3部署
- 一个在线音乐软件的故事(四、现在就可以开始编码了吗?)
- MyEclipse安装反编译插件
- 按日期备份文件夹
- Oracle学习笔记 -- day06 DDL、DML、视图索引序列、数据导入导出、数据恢复
- C++实现单例模式
- jstl 遇到的那些坑。很有名的坑,但容易忘。记录一下,便于复习
- SQLserver本地数据库开发