本篇博文将详细总结与数组相关的一些算法。
求局部最大值
问题描述
给定一个无重复 元素的数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1],求找到一个 该数组的局部最大值。规定:在数组边界外的值无穷小。即: A [ 0 ] > A [ − 1 ] , A [ N − 1 ] > A [ N ] A[0]>A[-1],A[N-1] >A[N] A[0]>A[−1],A[N−1]>A[N]。
显然,遍历一遍可以找到全局最大值,而全局最大值显然是局部最大值,但是时间复杂度达到 O ( n ) O(n) O(n),能不能找到一个时间复杂度比 O ( n ) O(n) O(n) 还要低的解法呢?
问题分析
定义:若子数组 A r r a y [ f r o m , … , t o ] Array[from,…,to] Array[from,…,to] 满足
A
r
r
a
y
[
f
r
o
m
]
>
A
r
r
a
y
[
f
r
o
m
−
1
]
Array[from]>Array[from-1]
Array[from]>Array[from−1]
A
r
r
a
y
[
t
o
]
>
A
r
r
a
y
[
t
o
+
1
]
Array[to]>Array[to+1]
Array[to]>Array[to+1]
我们假定称该子数组为高原数组。
若高原数组长度为1,则该高原数组的元素为局部最大值。
算法描述
使用索引 l e f t 、 r i g h t left、right left、right 分别指向数组首尾,根据定义(比两边都高),该数组为高原数组。
求中点 m i d = ( l e f t + r i g h t ) / 2 , 若 A [ m i d ] > A [ m i d + 1 ] mid=(left+right)/2,若A[mid]>A[mid+1] mid=(left+right)/2,若A[mid]>A[mid+1],子数组 A [ l e f t … m i d ] A[left…mid] A[left…mid] 为高原数组。丢弃后半段并且使得 r i g h t = m i d right=mid right=mid 。
若 A [ m i d + 1 ] > A [ m i d ] A[mid+1]>A[mid] A[mid+1]>A[mid],子数组 A [ m i d … r i g h t ] A[mid…right] A[mid…right] 高原数组。丢弃前半段, l e f t = m i d + 1 left=mid+1 left=mid+1,递归直至 l e f t = = r i g h t left==right left==right。
时间复杂度为 O ( l o g N ) O(logN) O(logN) 。
代码实现
C++
#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];
}
JAVA
public class LocalMinmum{
public int func(int[] arr){
int left = 0;
int right = arr.length;
while(left < right){
int mid = (left + right) / 2;
if(arr[mid] > arr[mid+1]) right = mid;
else left = mid+1;
}
return arr[left];
}
}
第一个缺失的整数
问题描述
给定一个数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1],找到从1开始,第一个不在数组中的正整数。
如 3 , 5 , 1 , 2 , − 3 , 7 , 14 , 8 输 出 4 3,5,1,2,-3,7,14,8输出4 3,5,1,2,−3,7,14,8输出4 。
循环不变式
思路:将找到的元素放到正确的位置上,如果最终发现某个元素一直没有找到,则该元素即为所求。
循环不变式:如果某命题初始为真,且每次更改后仍然保持该命题为真,则若干次更改后该命题仍然为真。
为表述方便,下面的算法描述从1开始数。
利用循环不变式设计算法
假定前 i − 1 i-1 i−1 个数已经找到,并且依次存放在 A [ 1 , 2 , … , i − 1 ] A[1,2,…,i-1] A[1,2,…,i−1] 中,继续考察 A [ i ] A[i] A[i] :
若
A
[
i
]
<
i
且
A
[
i
]
≥
1
A[i]<i且A[i]≥1
A[i]<i且A[i]≥1 ,则
A
[
i
]
A[i]
A[i] 在
A
[
1
,
2
,
…
,
i
−
1
]
A[1,2,…,i-1]
A[1,2,…,i−1] 中已经出现过,可以直接丢弃。
若
A
[
i
]
A[i]
A[i] 为负,则更应该丢弃它。
若
A
[
i
]
>
i
A[i]>i
A[i]>i 且
A
[
i
]
≤
N
A[i]≤N
A[i]≤N ,则
A
[
i
]
A[i]
A[i] 应该置于后面的位置,即将
A
[
A
[
i
]
]
A[A[i]]
A[A[i]] 和
A
[
i
]
A[i]
A[i] 交换。
若
A
[
A
[
i
]
]
=
A
[
i
]
A[A[i]]=A[i]
A[A[i]]=A[i] ,则显然不必交换,直接丢弃
A
[
i
]
A[i]
A[i] 即可。
若
A
[
i
]
>
N
A[i]>N
A[i]>N,超出范围,则
A
[
i
]
A[i]
A[i] 丢弃。
若 A [ i ] = i A[i]=i A[i]=i ,则 A [ i ] A[i] A[i] 位于正确的位置上,则 i i i 加1,循环不变式扩大,继续比较后面的元素。
整理算法:
- 若 A [ i ] = i A[i]=i A[i]=i , i i i 加1,继续比较后面的元素。
- 若 A [ i ] < i A[i]<i A[i]<i 或 A [ i ] > N A[i]>N A[i]>N 或 A [ A [ i ] ] = A [ i ] A[A[i]]=A[i] A[A[i]]=A[i] ,丢弃 A [ i ] A[i] A[i]
- 若 A [ i ] > i A[i]>i A[i]>i ,则将 A [ A [ i ] ] 和 A [ i ] A[A[i]]和A[i] A[A[i]]和A[i] 交换。
思考:如何快速丢弃(删除) A [ i ] A[i] A[i] ?(重要的思想)
-
如果按常规的思想删除数组里的元素,那么删除某个元素后,其后面的元素需要依次的向前移动,其时间复杂度至少 O ( n ) O(n) O(n) 。如果将 A [ N ] A[N] A[N] 赋值给 A [ i ] A[i] A[i] ,然后 N N N 减1,相当于把 A [ N ] A[N] A[N] 丢弃了, A [ N ] A[N] A[N] 就是互换之前的 A [ i ] A[i] A[i] 。则只需要 O ( 1 ) O(1) O(1) 的时间复杂度就删除了元素。
-
这里需要如果注意丢弃了一个元素,则可表示的连续序列最长的长度会减1,因为数组中剩余的元素个数减少了1。
代码实现
C++
//对数组中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;
}
JAVA
public class FirstMissNumber {
public void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public int func(int[] arr, int size){
int i = 0;
while(i < size){
if(arr[i] == i+1) i += 1;
else if(arr[i] < i+1 || arr[i] > size || arr[i] == arr[arr[i]-1]){
arr[i] = arr[size-1];
size -- ;
}else{
swap(arr, i, arr[i]-1);
}
}
return i+1;
}
public static void main(String[] args){
int[] arr = {3, -1, 1, 2, 4, 6, 14, 8};
int size = 8;
FirstMissNumber obj = new FirstMissNumber();
int res = obj.func(arr, size);
System.out.println(res);
}
}
查找旋转数组的最小值
问题描述
假定一个排序数组(已经有序) 以某个未知元素为支点做了旋转,如:原数组 0124567 0 1 2 4 5 6 7 0124567 旋转后得到 4567012 4 5 6 7 0 1 2 4567012 。请找出旋转后数组的最小值。假定数组中没有重复数字。
显然把数组遍历一遍就能找到最小值,但是时间复杂度达到 O ( n ) O(n) O(n) 。有没有更快的解决办法?
问题分析
旋转之后的数组实际上可以划分成两个有序的子数组:前面子数组的大小都大于后面子数组中的元素;
用索引 l e f t , r i g h t left,right left,right 分别指向首尾元素,元素不重复。
若子数组是普通升序数组,则 A [ l e f t ] < A [ r i g h t ] A[left]<A[right] A[left]<A[right]。
若子数组是循环升序数组(可以理解为两不同的递增序列),前半段子数组的元素全都大于后半段子数组中的元素: A [ l e f t ] > A [ r i g h t ] A[left]>A[right] A[left]>A[right],计算中间位置 m i d = ( l o w + h i g h ) / 2 mid = (low+high)/2 mid=(low+high)/2;
显然, A [ l o w … m i d ] A[low…mid] A[low…mid] 与 A [ m i d + 1 … h i g h ] A[mid+1…high] A[mid+1…high] 必有一个是循环升序数组,一个是普通升序数组。
若:
A
[
m
i
d
]
>
A
[
h
i
g
h
]
A[mid]>A[high]
A[mid]>A[high],说明子数组
A
[
m
i
d
+
1
,
m
i
d
+
2
,
…
h
i
g
h
]
A[mid+1,mid+2,…high]
A[mid+1,mid+2,…high] 循环升序;更新
l
o
w
=
m
i
d
+
1
low=mid+1
low=mid+1;
若:
A
[
m
i
d
]
<
A
[
h
i
g
h
]
A[mid]<A[high]
A[mid]<A[high] ,说明子数组
A
[
m
i
d
+
1
,
m
i
d
+
2
,
…
h
i
g
h
]
A[mid+1,mid+2,…high]
A[mid+1,mid+2,…high] 普通升序;更新:
h
i
g
h
=
m
i
d
high=mid
high=mid。
代码实现
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];
}
零子数组
问题描述
求对于长度为 N N N 的数组 A A A,求连续子数组 的和最接近0的值。
如:数组 A : 1 , − 2 , 3 , 10 , − 4 , 7 , 2 , − 5 A:1, -2, 3, 10, -4, 7, 2, -5 A:1,−2,3,10,−4,7,2,−5。它是所有子数组中,和最接近 0 0 0 的是哪个?
算法流程
申请比 A A A 长1的空间 s u m [ − 1 , 0 … , N − 1 ] , s u m [ i ] sum[-1,0…,N-1],sum[i] sum[−1,0…,N−1],sum[i] 是 A A A 的前 i i i 项和。定义 s u m [ − 1 ] = 0 sum[-1] = 0 sum[−1]=0。
显然有: ∑ k = i j A k = s u m ( j ) − s u m ( i − 1 ) = A i + A i + 1 + A i + 2 + , . . . , + A j \sum_{k=i}^{j}{A}_{k}=sum(j)-sum(i-1)={A}_{i}+{A}_{i+1}+{A}_{i+2}+,...,+{A}_{j} k=i∑jAk=sum(j)−sum(i−1)=Ai+Ai+1+Ai+2+,...,+Aj
算法思路:
- 对 s u m [ − 1 , 0 … , N − 1 ] sum[-1,0…,N-1] sum[−1,0…,N−1] 排序,然后计算 s u m sum sum 相邻元素的差的绝对值,最小值即为所求。
- 在 A A A 中任意取两个前缀子数组的和求差的最小值。
时间复杂度
计算前 n n n 项和数组 s u m sum sum 和计算 s u m sum sum 相邻元素差的时间复杂度,都是 O ( N ) O(N) O(N),排序的时间复杂度认为是 O ( N l o g N ) O(NlogN) O(NlogN),因此,总时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN)。
代码实现:
#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
算法分析
定义:前缀和 s u m [ i ] = a [ 0 ] + a [ 1 ] + . . . + a [ i ] sum[i] = a[0] + a[1] + ...+a[i] sum[i]=a[0]+a[1]+...+a[i]。则: a [ i , j ] = s u m [ j ] − s u m [ i − 1 ] a[i,j]=sum[j]-sum[i-1] a[i,j]=sum[j]−sum[i−1] (定义 s u m [ − 1 ] = 0 sum[-1] = 0 sum[−1]=0 )
显然有: ∑ k = i j A k = s u m ( j ) − s u m ( i − 1 ) = A i + A i + 1 + A i + 2 + , . . . , + A j \sum_{k=i}^{j}{A}_{k}=sum(j)-sum(i-1)={A}_{i}+{A}_{i+1}+{A}_{i+2}+,...,+{A}_{j} k=i∑jAk=sum(j)−sum(i−1)=Ai+Ai+1+Ai+2+,...,+Aj
算法过程:
-
求 i i i 前缀 s u m [ i ] sum[i] sum[i]:
遍历 i : 0 ≤ i ≤ n − 1 i:0≤i≤n-1 i:0≤i≤n−1;
s u m [ i ] = s u m [ i − 1 ] + a [ i ] sum[i]=sum[i-1]+a[i] sum[i]=sum[i−1]+a[i]。 -
计算以 a [ i ] a[i] a[i] 结尾 的子数组的最大值
对于某个 i i i(固定 i i i):遍历 − 1 ≤ j ≤ i -1≤j≤i −1≤j≤i,求 s u m [ j ] sum[j] sum[j] 的最小值 m m m。(注: s u m sum sum 中的 i i i从0开始算, j j j 都是从 − 1 -1 −1 开始算)
s u m [ i ] − m sum[i]-m sum[i]−m 即为以 a [ i ] a[i] a[i] 结尾的数组中最大的子数组的值。
我们已经定义 s u m [ − 1 ] = 0 sum[-1]=0 sum[−1]=0,如果对于某一个 i i i 而言,其 s u m [ 1 ] , s u m [ 2 ] , . . . , s u m [ i ] sum[1],sum[2],...,sum[i] sum[1],sum[2],...,sum[i] 都为正数,那么最小的是 s u m [ j ] = 0 , s u m [ i ] − m = s u m [ i ] sum[j]=0,sum[i]-m=sum[i] sum[j]=0,sum[i]−m=sum[i] 相当于没减。但是如果 s u m [ j ] sum[j] sum[j]等于一个负数 m m m ,这个时候用 s u m [ i ] − s u m [ j ] sum[i]-sum[j] sum[i]−sum[j] 就相当于在前 i i i 项和中去掉前 j j j 项(其中前 j j j 和为负数)的部分,那么剩下的连续部分和肯定就是以 a [ i ] a[i] a[i] 为尾的子数组最大值。 -
统计 s u m [ i ] − m sum[i]-m sum[i]−m 的最大值, 0 ≤ i ≤ n − 1 0≤i≤n-1 0≤i≤n−1
-
求出 s u m [ i ] − m sum[i]-m sum[i]−m 的最大值对应的 i i i,再求出其对应的 j j j,在原数组 a [ i ] a[i] a[i] 到 a [ j ] a[j] 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]最小值对应的j
int 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数组中最大值对应的索引i
int 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;
}
最大间隔
问题描述
给定整数数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1],求这 N N N 个数排序后最大间隔 。如: 1 , 7 , 14 , 9 , 4 , 13 1,7,14,9,4,13 1,7,14,9,4,13 的最大间隔为 4 4 4。
排序后: 1 , 4 , 7 , 9 , 13 , 14 1,4,7,9,13,14 1,4,7,9,13,14 ,最大间隔是 13 − 9 = 4 13-9=4 13−9=4,显然,对原数组排序,然后求后项减前项的最大值,即为解。
可否有更好的方法?
问题分析
假定 N N N 个数的最大最小值为 m a x , m i n max,min max,min 则这 N N N 个数形成 N − 1 N-1 N−1 个间隔,其最小间隔是 m a x − m i n N − 1 \frac{max-min}{N-1} N−1max−min。
如果
N
N
N 个数完全均匀分布,则每两个数的间距全部是
m
a
x
−
m
i
n
N
−
1
\frac{max-min}{N-1}
N−1max−min 且最小;
如果
N
N
N 个数不是均匀分布,则每两个数的间距不均衡,最大间距必然大于
m
a
x
−
m
i
n
N
−
1
\frac{max-min}{N-1}
N−1max−min ,最小间距也会小于
m
a
x
−
m
i
n
N
−
1
\frac{max-min}{N-1}
N−1max−min 。
解决思路
思路:将 N N N 个数用间距 m a x − m i n N − 1 \frac{max-min}{N-1} N−1max−min 分成 N − 1 N-1 N−1 个区间,我们把数组中的每个数按照其大小放进对应的桶中。则落在同一区间(桶)内的数不可能有最大间距。统计后一区间的最小值与前一区间的最大值的差即可。后一个桶内的数一定比前一个桶内的所有数都要大。这里可能有人会疑问,为什么不是后一个区间的最大值减去前一个区间的最小值呢?注意后一个区间的最大值和前一个区间的最小值虽然处于不同的桶中(即可能是最大间隔),但是其在排序后的数组中并不相邻,这里求的是排序后的最大间隔,显然后一个区间的最小值和前一个区间的最大值在排序后的数组中是相邻的,并且处于不同的桶中(即有可能是最大间隔)。
若没有任何数落在某区间,则该区间无效,不参与统计。
显然,这是借鉴桶排序/Hash映射的思想。
桶的数目:
同时,
N
−
1
N-1
N−1 个桶是理论值,会造成若干个桶的数目比其他桶大
1
1
1,从而造成统计误差。
如:7个数,假设最值为 10 、 80 10、80 10、80,如果适用6个桶,则桶的大小为 70 / 6 = 11.66 70/6=11.66 70/6=11.66,每个桶分别为: [ 10 , 21 ] 、 [ 22 , 33 ] 、 [ 34 , 44 ] 、 [ 45 , 56 ] 、 [ 57 , 68 ] 、 [ 69 , 80 ] [10,21]、 [22,33]、 [34,44]、 [45,56]、 [57,68]、[69,80] [10,21]、[22,33]、[34,44]、[45,56]、[57,68]、[69,80],存在大小为 12 12 12 的桶,比理论下界 11.66 11.66 11.66 大。因此,使用 N N N 个桶。
代码实现
#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;
}
字符串的全排列
字符串的全排列问题是一个非常重要的问题,需要把它弄清楚了。
下面用两种方法来解决它,递归法和非递归法。
问题描述
给定字符串 S [ 0 … N − 1 ] S[0…N-1] S[0…N−1] ,设计算法,枚举 S S S 的全排列。
递归算法
以字符串1234为例:
- 1 – 234(表示把 1 1 1 拿过来, 234 234 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;
}
上面的代码其实就是一个深度优先搜索 的过程。以上过程是基于数组非重复的情况。
如果字符有重复:
去除重复字符 的递归算法
以字符
1223
1223
1223 为例:
- 1 – 223
- 2 – 123
- 3 – 221
带重复字符的全排列就是每个字符分别与它后面非重复出现的字符交换。
即:第
i
i
i 个字符(前)与第
j
j
j 个字符(后)交换时,要求
[
i
,
j
)
[i,j)
[i,j) 中没有与第
j
j
j 个字符相等的数。
#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;
}
重复的字符串里面,需要循环的判断某一个字符是否出现过,其时间复杂度至少是 O ( n ) O(n) O(n)。
我们可以利用空间换时间来降低时间复杂度:
- 如果是单字符,可以使用 m a r k [ 256 ] mark[256] mark[256];
- 如果是整数,可以遍历整数得到最大值 m a x max max 和最小值 m i n min min,使用 m a r k [ m a x − m i n + 1 ] mark[max-min+1] mark[max−min+1];
- 如果是浮点数或其他结构,考虑使用 H a s h Hash Hash。
- 事实上,如果发现整数间变化太大,也应该考虑使用 H a s h Hash Hash;
- 可以认为整数/字符的情况是最朴素的 H a s h 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;
}
非递归算法
起点:字典序最小的排列,例如
12345
12345
12345。
终点:字典序最大的排列,例如
54321
54321
54321。
过程:从当前排列生成字典序刚好比它大的下一个排列。
如:
21543
21543
21543 的下一个排列是
23145
23145
23145 如何计算?
21543的下一个排列的思考过程:
逐位考察哪个能增大
- 一个数右面有比它大的数存在,它就能增大。
- 从后边开始找,那么最后一个能增大的数是—— x = 1 x = 1 x=1
1 1 1 应该增大到多少?
- 增大到它右面比它大的最小的数—— y = 3 y = 3 y=3
应该变为 23 x x x 23xxx 23xxx,显然,xxx应由小到大排: 145 145 145,得到 23145 23145 23145。
寻找下一个排列的算法步骤:后找、小大、交换、翻转
- 后找:从后往前找字符串中最后一个升序的位置 i i i ,即: S [ k ] > S [ k + 1 ] ( k > i ) , S [ i ] < S [ i + 1 ] S[k]>S[k+1](k>i),S[i]<S[i+1] S[k]>S[k+1](k>i),S[i]<S[i+1];
- 查找(小大): S [ i + 1 … N − 1 ] S[i+1…N-1] S[i+1…N−1] 中比 A i Ai Ai 大的最小值 S j Sj Sj;
- 交换: S i , S j Si,Sj Si,Sj;
- 翻转:
S
[
i
+
1
…
N
−
1
]
S[i+1…N-1]
S[i+1…N−1]
交换操作后, S [ i + 1 … N − 1 ] S[i+1…N-1] S[i+1…N−1] 一定是降序的(上面的后找,查找决定了)。
我们以926520为例,考察该算法的正确性:
- 后找:可以找到 i = 1 i=1 i=1 时,也就是 S [ 1 ] = 2 S[1]=2 S[1]=2 是数组中最后一个升序的值。
- 查找(小大):在 S [ 2 , . . , 5 ] S[2,..,5] S[2,..,5] 中比 2 2 2 大的最小值为 5 5 5。
- 交换:交换 S [ 1 ] S[1] S[1] 和 S [ 3 ] S[3] S[3] 得 956220 956220 956220。
- 翻转:翻转 S [ 2 , . . . , 5 ] S[2,...,5] S[2,...,5] 得 950226 950226 950226 。
那么可得 926520 926520 926520 的下一个排列为 950226 950226 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
问题描述
已知数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1],给定某数值 s u m sum sum,找出数组中的若干个数,***使得这些数的和为 s u m sum sum***。
布尔向量 x [ 0 … N − 1 ] : x[0…N-1]: x[0…N−1]:
- x [ i ] = 0 x[i]=0 x[i]=0 表示不取 A [ i ] , x [ i ] = 1 A[i],x[i]=1 A[i],x[i]=1 表示取 A [ i ] A[i] 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;
}
上面直接递归的代码中相当于把所有的解空间都遍历了一遍,其时间复杂度为 O ( 2 n ) O({2}^{n}) O(2n),这是一个 n p np np 问题。
考虑对于分支如何限界
前提:数组 A [ 0 … N − 1 ] A[0…N-1] A[0…N−1] 的元素都大于 0 0 0,考察向量 x [ 0 … N − 1 ] x[0…N-1] x[0…N−1],假定已经确定了前 i i i 个值,现在要判定第 i + 1 i+1 i+1 个值 x [ i ] x[i] x[i] 为 0 0 0 还是 1 1 1。
假定由
x
[
0
…
i
−
1
]
x[0…i-1]
x[0…i−1] 确定的
A
[
0
…
i
−
1
]
A[0…i-1]
A[0…i−1] 的和为
h
a
s
has
has;
A
[
i
,
i
+
1
,
…
N
−
1
]
A[i,i+1,…N-1]
A[i,i+1,…N−1] 的和为
r
e
s
i
d
u
e
residue
residue (简记为
r
r
r );
- h a s + a [ i ] ≤ s u m has+a[i]≤sum has+a[i]≤sum 并且 h a s + r ≥ s u m : x [ i ] has+r≥sum:x[i] has+r≥sum:x[i] 可以为 1 1 1;
-
h
a
s
+
(
r
−
a
[
i
]
)
>
=
s
u
m
:
x
[
i
]
has+(r-a[i])>= sum:x[i]
has+(r−a[i])>=sum:x[i] 可以为
0
0
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;
}
分支限界的条件是充分条件吗?不是,是必要条件,只能从后往前才是确定成立。
分支限界条件越苛刻,速度越快,但是从理论上来说其时间复杂度没有发生改变,仍然是
O
(
2
n
)
O({2}^{n})
O(2n) 。