斯坦福 Algorithms: Design and Analysis 1 第一周作业
来自斯坦福网站的Algorithms: Design and Analysis,与目前coursera上的版本内容没有变化,不过时间安排略有不同。
1. Problem Set 1
三路归并排序的复杂度。和两路没有啥区别,依然要递归
l
o
g
n
logn
logn层,只不过log的底数从2变成3。每一层的计算复杂度依然是
O
(
n
)
O(n)
O(n)。
根据大O表示法的定义写一下即可。
这个也可以先用大O表示法的定义写一下。发现给的条件与目标并不能直接得到。于是思考其目标成立的条件。
先看选项1,如果 f ( n ) ≤ g ( n ) f(n) \leq g(n) f(n)≤g(n),因此 2 f ( n ) ≤ 2 g ( n ) 2^{f(n)} \leq 2^{g(n)} 2f(n)≤2g(n),也就是 2 f ( n ) = O ( 2 g ( n ) ) 2^{f(n)}=O(2^{g(n)}) 2f(n)=O(2g(n))。于是选项1正确。另外可以举一反例,比如 f ( n ) = 2 n , g ( n ) = n f(n) = 2n, g(n) = n f(n)=2n,g(n)=n,此时无法得到 2 f ( n ) = O ( 2 g ( n ) ) 2^{f(n)}=O(2^{g(n)}) 2f(n)=O(2g(n))。
一共需要比较k-1次,每一次比较的次数为2n,3n,…,kn,因此其总数量级为
n
k
2
nk^{2}
nk2。
比较大小。指数比较多的可以取log之后比较。
2 Optional Theory Problems
问题1,在限定比较次数内得到第二大的数。由于其比较次数只比n大一点,所以遍历两次的做法显然不行。于是考虑本周内容中的归并方式。
step1,把每次归并数组的操作改为比较两个小数组的最大值,并保留较大的一个。因此比较次数为n/2+n/4+…+2,相加得到n-1。此时得到数组最大值。
step2,回溯与最大值比较过的数字中的最大值,它就是数组的第二大的数。(显然第二大的数一定与最大值比较过,不然无法得出最大数。)回溯的比较次数与递归函数的层数相同,即 l o g n − 1 logn-1 logn−1。于是满足比较次数的要求。
C++ 实现:
int get_second(vector<int> v) {
int n = v.size();
vector<int> record(n); //记录最大值在当前位置是否经过转换
for(int gap = 1; gap < n; gap*=2) { //非递归形式的归并,每次将最大值放在当前小数组最前方
for(int i = 0; i < n/gap/2; i++){ //比如最后一层比较即每两个数相比,0,1|2,3|4,5|6,7,比较后每两个数较大值置换到0,2,4,6位置
if(v[i*gap*2] < v[i*gap*2+gap]) {//0,1,2,3|4,5,6,7 上一层得到每4个数的最大值,也放在0和4的位置
swap(v[i*gap*2],v[i*gap*2+gap]);//最终得到的全局最大值在位置0
record[i*gap*2] = 1; //record数组表示当前位置的数是否经过位置变化而来
}
}
}
int a = v[n/2];
int index = 0;
for(int gap = n/2; gap >= 1; gap/=2) { //根据record数组的记录回溯与最大比较过的数
a = max(v[index+gap],a);//可以证明这么做的正确性
if(record[index])//假如最大值所在位置的record是0,说明其一直在当前位置
index += gap;//假如最大值位置的record是1,那么一定有最大值造成的部分(其他值的变换可能事先让其为1,但既然目前其值为max,说明最大值一定是变换过来的)。
//而最大值根据record回溯回上一次比较的位置之后不会再与当前位置的record再有联系。
}
return a;
}
实现的代码虽然不复杂但貌似被我搞得过程很复杂。仔细想了一下应该能证明这么做的正确性。
问题2,相对简单。要求用 O ( l o g n ) O(logn) O(logn)的复杂度找到驼峰型数组中的最大值。用二分查找,每次判断mid位置与左右两个数之间的关系来判断边界的变换。相对简单,C++实现如下:
int get_max(vector<int> v) {
int n = v.size();
int left = 0, right = n-1;
int mid;
while(left <= right) {
mid = (left+right);
if(v[mid] > v[mid-1]) {
if(v[mid] > v[mid+1])
return v[mid];
else
left = mid+1;
}else
right = mid-1;
}
return 0;
}
问题3,在有序数组中判断是否存在一个下标i令A[i]=i。依然是用二分查找,因为是有序数组,因此边界变化也好判断。
bool get_i(vector<int> v) {
int n = v.size();
int left = 0, right = n-1;
int mid;
while(left <= right) {
mid = (left+right);
if(v[mid] == mid) {
return true;
}else if (v[mid] > mid)
right = mid-1;
else
left = mid+1;
}
return false;
}
3 Programming Question 1
实现课程中的逆序数计算。代码如下:
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <string>
using namespace std;
long long count(vector<int>& v1,vector<int>&v2, int begin, int end, bool flag) {
// cout << begin << ' '<< end << endl;
long long res = 0;
if(begin == end-1) {
if(flag)
v2[begin] = v1[begin];
else
v1[begin] = v2[begin];
return res;
}
else {
int mid = begin+(end-begin)/2;
res += count(v1,v2,begin,mid,!flag);
res += count(v1,v2,mid,end,!flag);
int i = begin, j = mid, i2 = begin;
while(i < mid && j < end) {
if(flag) {
if(v1[i] <= v1[j]) {
v2[i2++] = v1[i++];
}else {
v2[i2++] = v1[j++];
res += mid-i;
}
}else {
if(v2[i] <= v2[j]) {
v1[i2++] = v2[i++];
}else {
v1[i2++] = v2[j++];
res += mid-i;
}
}
}
if(i < mid)
while(i < mid) {
if(flag) v2[i2++] = v1[i++];
else v1[i2++] = v2[i++];
}
if(j < end)
while(j < end) {
if(flag) v2[i2++] = v1[j++];
else v1[i2++] = v2[j++];
}
return res;
}
}
vector<int> readData(string fileName) {
cout << "read data from "<< fileName <<endl;
ifstream input(fileName);
string line;
vector<int> res;
int tmp;
if(input) {
while(getline(input,line)) {
istringstream ist(line);
ist >> tmp;
// cout << tmp << endl;
res.push_back(tmp);
}
}
return res;
}
int main() {
vector<int> test = readData("IntegerArray.txt");
// vector<int> test = {8,7,6,5,4,3,2,1,1,2,3};
vector<int> v2(test);
long long res = count(test,v2,0,test.size(),true);
cout << res <<endl;
return 0;
}
归并排序的一大问题是归并数组的处理。因为不可能只用一个数组做inplace的归并,因此至少需要两个数组。比较naive的做法是每次递归调用都生成一个新的小数组返回,这样做比较废空间与开辟数组的时间,因此可以使用两个数组将其来回利用,减少损耗。具体可以看代码,使用了一个布尔值来表示当前函数是将v2中的小数组归并到v1还是将v1中的数归并到v2中。