乐趣区

算法第三课学习笔记

一、第一个缺失的整数

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;
}
退出移动版