封面

一、第一个缺失的整数

1. 题目

给定一个数组A[0…N-1],找到从1开始,第一个不在数组中的正整数。

如给定 3,5,1,2,-3,7,14,8,输出4。

2. 分析

针对这道题目,有两种思路。

第一种思路是基于bitmap思想,开辟一个新数组B[0 .. N-1],数组元素全部初始化为0,遍历一次A数组,若遍历到的元素其值为在0N-1之间的正整数i,则在B数组中将对应下标i的元素B[i]设为1,最后遍历B数组,找到第一个值为0的元素,其下标就是需要找的第一个不在数组中的正整数。

此方法时间复杂度为O(N),空间复杂度为O(N)

若允许修改原数组,便可以引出第二种思路即循环不变式原理。

循环不变式

设计算法

合并分支

此算法时间复杂度为O(N),空间复杂度为O(1)

3. 代码

int first_miss_number(int a[], int n){
    a--; // 使a从1开始数
    int i = 1;
    while(i<=n){
        if(a[i] == i) i++;
        else if(a[i]<i || a[i]>n || a[i]==a[a[i]])
            a[i] = a[n--];
        else // a[i]>i
            swap(a[a[i]], a[i]);
    }   
    return i;
}

二、查找旋转数组的最小值

1. 题目

假定一个排序数组以某个未知元素为支点做了旋转,如:原数组0 1 2 4 5 6 7旋转后得到4 5 6 7 0 1 2。请找出旋转后数组的最小值。(假定数组中没有重复数字)

2. 分析

旋转之后的数组实际上可以划分成两个有序的子数组:前面子数组的大小都大于后面子数组中的元素;

4 5 6 7 0 1 2

注意到实际上最小的元素就是两个子数组的分界线。

寻找循环数组的最小值

可以看到,算法的思想与二分搜索比较类似,时间复杂度为O(logN),比遍历一遍数组得到最小值的方法要快。

3. 代码

int find_min(int a[], int n){
    int low = 0;
    int high = n - 1;
    int mid;
    while(low<high){
        mid = (high + low) / 2; // 需注意,若数组过大,此项可能会造成溢出
        if(a[mid]<a[high]) // 最小值在左半部分
            high = mid;
        else // 最小值在右半部分
            low = mid + 1;
    }
    return a[low];
}

三、零子数组

1. 题目

求对于长度为N的数组A,求连续子数组的和最接近0的值。

如:数组A、1, -2, 3, 10, -4, 7, 2, -5,它的所有子数组中和最接近0的是哪个?
答案是 -4, 7, 2, -5 ,其和正好是0。

2. 分析

(1) 申请比A长1的空间sum[-1,0…,N-1]sum[i]是A的前i项和。(trick:定义sum[-1] = 0)

显然有:
公式

(2) 对sum[-1,0…,N-1]排序,然后计算sum相邻元素的差的绝对值,最小值即为所求。

计算前n项和数组sum和计算sum相邻元素差的时间复杂度,都是O(N),排序的时间复杂度认为是O(NlogN),因此,总时间复杂度为O(NlogN)。

思考:如果需要返回绝对值最小的子数组本身呢?

此时可以引入结构体,其中包含两个成员sum和pos,分别表示前i项和sum和该位置pos,即pos=i。然后对该结构体根据sum值进行排序,取满足相邻元素的差的绝对值的两个结构体的pos的值,然后根据两个pos值在数组中输出对应元素,即为所求零和数组。

3. 代码

typedef struct{
    int sum;
    int pos;
}SUM;

/**
 * 求零和数组
 * 返回最接近0的值, low为零和数组第一个元素的前一个位置下标, high为零和数组最后一个元素的下标
 */
int zero_subarray(int a[], int n, int &low, int &high){
    SUM b[n];
    b[0].sum = a[0];
    b[0].pos = 0;
    for(int i=1; i<n; i++){
        b[i].sum = b[i-1].sum + a[i]; 
        b[i].pos = i;
    }
    cout<<"排序前:"; print_sum(b, n);
    // 排序 (此处省略排序的实现细节)
    sum_sort(b, n);
    cout<<"排序后:"; print_sum(b, n);
    // 计算差值最小的两个sum
    int diff, result = b[1].sum - b[0].sum;
    low = min(b[0].pos, b[1].pos);
    high = max(b[0].pos, b[1].pos);
    for(int i=1; i<n; i++){
        diff = abs(b[i].sum - b[i-1].sum);
        if(diff < result){  // 更新
            result = diff;
            low = min(b[i].pos, b[i-1].pos);
            high = max(b[i].pos, b[i-1].pos);
        }
    }
    return result;
}

/**
 * 打印零和数组
 */
void print_zero_subarray(int a[], int n, int low, int high){
    int i;
    for(i=low+1; i<high; i++)
        cout<<a[i]<<",";
    cout<<a[i]<<endl;
}

/**
 * 打印SUM结构体数组
 */
void print_sum(SUM *b, int n){
    int i=0;
    for(i=0; i<n-1; i++)
        cout<<"("<<b[i].sum<<","<<b[i].pos<<"), ";
    cout<<"("<<b[i].sum<<","<<b[i].pos<<")"<<endl;
}

/**
 * 主函数
 */
int main(){
    int a[] = {1,-2,3,10,-4,7,2,-5};
    int n = 8;
    int low, high;
    cout<<"最接近0的和:"<<zero_subarray(a, n, low, high)<<endl;
    cout<<"零和数组为:";
    print_zero_subarray(a,n,low,high);
    return 0;
}

输出结果为:

排序前:(1,0), (-1,1), (2,2), (12,3), (8,4), (15,5), (17,6), (12,7)
排序后:(-1,1), (1,0), (2,2), (8,4), (12,3), (12,7), (15,5), (17,6)
最接近0的和:0
零和数组为:-4,7,2,-5

四、最大子数组和

1. 题目

给定一个数组A[0,…,n-1],求A的连续子数组,使得该子数组的和最大。

例如:
数组: 1, -2, 3, 10, -4, 7, 2, -5,
最大子数组:3, 10, -4, 7, 2
最大子数组和:18

2. 分析与代码

本题可以利用动态规划(最优子问题)的思路来求解,时间复杂度为O(N),算法步骤如下:

记S[i]为以A[i]结尾的数组中和最大的子数组,则:

  • 令result初始化为A[0],S[0] = A[0]
  • 在i的范围[1,n)中遍历i,若S[i-1] > 0,则S[i] = S[i-1] + A[i]
  • 若S[i-1] <= 0,则S[i] = A[i]
  • 此时的最大子数组和为:result = max(result, S[i])

代码如下:

/**
 * 求解最大子数组和
 */
int max_subarray(const int a[], int n){
    if(!a || n<=0) return 0;
    int sum = a[0];
    int result = sum; // 记录当前最优解
    for(int i=1; i<n; i++){
        if(sum>0) sum+=a[i];
        else sum = a[i];
        result = max(sum, result);
    }
    return result;
}

思考:若除了输出最大子数组的和,还需要输出最大子数组本身,应该怎么做?

此时可以设置两个标志from和to来表示最大子数组的起点和终点,
记sum没为以A[i]结尾的数组中的最大子数组和,则:

  • 令result初始化为A[0],sum = A[0], from = to = -1,-1表示无元素
  • 在i的范围[1,n)中遍历i,若sum > 0,则sum = sum + A[i]
  • 若sum <= 0,则sum = A[i],记录from_new = i
  • 此时进行判断,若result < sum,即当前sum的值为最优解,更新起点from和终点to,from = from_new,to = i,

输出数组A[n]从from到to的元素,即为最大子数组。

代码如下:

/**
 * 求解最大子数组和及最大子数组本身
 */
int max_subarray_pos(const int a[], int n, int &from, int &to){
    if(!a || n<=0){
        from = to = -1;
        return 0;
    }
    int sum = a[0];
    int result = sum; // 记录当前最优解
    from = to = 0;
    int from_new; // 新的子数组起点
    for(int i=1; i<n; i++){
        if(sum>0) sum+=a[i];
        else{ // 最大子数组的起点发生了改变
            sum = a[i];
            from_new = i;
        }
        if(result < sum){ 
        // 当前的最大和大于result,将最优解的起点from改变到from_new,结束点to改变到i
            result = sum;        
            from = from_new;
            to = i;
        }
    }
    return result;
}

// 输出最大子数组代码
for(i=from; i<to; i++)
        cout<<a[i]<<",";
cout<<a[i]<<endl;

五、最大间隔

1. 题目

给定整数数组A[0…N-1],求这N个数排序后的最大间隔。

如:1,7,14,9,4,13的最大间隔为4。
排序后:1,4,7,9,13,14,最大间隔是13-9=4

显然,对原数组排序,然后求后项减前项的最大值,即为解,时间复杂度为O(nlogn)。可否有更好的方法?

(若将题中条件整数数组改为浮点数组,题目会变得更简单,注意:是变得更简单,为什么?)

2. 分析

分析

思路

桶的数目

本解法的时间复杂度为O(n)。
(也由上可知,若题干条件换成浮点数组,题目会变得更好计算桶的数目和大小,以及分配数组元素)

3. 代码

/**
 * 桶
 */
typedef struct Bucket{
    bool isEmpty;
    int min;
    int max;
    Bucket() : isEmpty(true) {}
    void add(int k){
        if(isEmpty){
            min = max = k;
            isEmpty = false;
        } else{
            if(max < k) max = k;
            if(min > k) min = k;
        }
    }
}Bucket;

/**
 * 计算最大间距
 */
int calc_max_gap(const int a[], int n){
    Bucket bucket[n];
    // 计算数组a的最值
    int max = a[0], min = a[0], i;
    for(i=0; i<n; i++){
        if(max < a[i]) max = a[i];
        if(min > a[i]) min = a[i];
    }
    // 将数据依次放入桶中
    int delta = max - min;
    int num; // 标记该数应该在哪一个桶里
    for(i=0; i<n; i++){
        num = (a[i] - min) * n / delta;
        if(num >= n) num = n-1;
        bucket[num].add(a[i]);
    }
    // 计算有效桶
    i = 0;
    int gap = delta / n;  // 初始化最小间隔为桶的间距
    int t;
    for(int j=1; j<n; j++){ // i为前一个桶,j为后一个桶
        if(!bucket[j].isEmpty){ // 桶不空
            t = bucket[j].min - bucket[i].max;
            gap = gap > t ? gap : t;
            i = j;
        }
    }
    return gap;
}

kalii
452 声望29 粉丝