column 1:
bitvector operation:
#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];
void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int test(int i){ return a[i>>SHIFT] & (1<<(i & MASK)); } (a[i>>SHIFT] & (1<<(i & MASK))) != 0
P4. how to generate k numbers that are within range [1-n) and no two numbers are equal and their position is random.
my idea: allocate a array with n bit to record the generated number if the number is generated then regenerate until the number is not generated. O(n) space. too big.
still use random algorithm
for(int i = 0; i< n; ++i)
{
a[i] = i;
}
for(int i = 0; i< k; ++i)
{
swap(a[i],random(i,n-1));
}
loop invariant:
the probability of the permutation a[0]~a[i-1] is (n-i)!/n!.
a[0] ~ a[k] saved the k unique random generated numbers within range [0-n-1].
P5. sort n numbers which cannot be stored in the memory. (bucket sort.)
1. divide the numbers into k parts and each part can be stored in the memory.
2 .use k file on the disk to store the temporary sort result. the first file stores the number 0-n/k, the second file stores the number n/k+1 - n/k*2 and so on.
3. traverse each part of numbers and store the the numbers into corresponding file according to the value of number.
bucket sort
4 .After this step sort the each file and merge them together then it is done.
sort using counting sort.
O(kn) time and O(n/k) memory space, O(n) disk space.
P9. how to ensure data is not a random value without initialization.
column 2:
1. 对一个单词的各个字母改变位置形成的新单词是原来单词的变异词。这类词可以用签名来进行排序,即他们的签名是一致的。如cabd,acdb,bcad等的签名都是abcd(对字母排序)。这样这类单词都有相同的签名就可以进行排序归拢了。
2. 对等价类可以进行签名以代表等价类中所有元素特征,原则类似哈希操作(对字母排序即为哈希函数)。
3. 二分查找不仅仅可以在排序集合中查找某一个元素(查询规则为相等),同时也可以用于确定某一个数不存在(即不相等)。
二分搜索的目的是快速缩小算法搜索的范围,所以有时可以配合线性查找。
比如找出在一个范围内不存在的数,如有一组数字,每个数的范围是[1-100],求一个数在[1-100]内,但是不属于集合的。将集合分成两份,第一份数的范围是[1-50],第二份是[51-100].
如果某一个集合中的数个数小于50,则表示该区间内的数少了,从而继续在该区间内寻找。
4. rotate算法,一个是连续替换O(n)(这方法很巧妙),一个是转置,O(2n)。
在P5.中要求将abc转换成cba。代码如下:
/*
* reverse string a from left to right
* l=left site index
* r=right site index
*/
void reverse_r(char * a,int l,int r)
{
while(l<r){
swap(a[l++],a[r--]);
}
}
/*
* change ab to ba
* n=length of whole string "ab"
* bit=length of a
* */
template<typename T>
void rotate_rcur(T * a,int n,int bit)
{
reverse_r(a,0,bit-1);
reverse_r(a,bit,n-1);
reverse_r(a,0,n-1);
}
//change abc to cba
//bit1=length of a
//bit2=length of b
//n=lenght of whole string "abc"
template<typename T>
void multirotate(T* a,int n,int bit1,int bit2){
rotate_rcur(a,bit1+bit2,bit1);
rotate_rcur(a+bit2,n-bit2,bit1);
rotate_rcur(a,n-bit1,bit2);
}
column 3:
数据结构,打表法,用数组记录数据,用输入作为下标,直接访问数组,不用if,else或者switch等条件查找。
column 4:二分法问题
看到有序就可以想到二分法。
P7,线段问题,用二分查找。判断输入是在线段上面,线段下面还是正好在线段上(on the line)。
P9,求x^n可以用二分法,因为如果n是偶数,则只要计算x^(n/2),在平方下就可以,不用计算两次x^(n/2)。如果n=奇数则x^(n)=x*x^(n-1),所以需调用O(Logn)次.
double exp(double x, int n)
{
if(n == 0)return 1;
if(even(n)){
return square(exp(x,n/2));
}
else{
return x*exp(x,n-1);
}
}
chapter1:
一些基本的东西:
1. 利用空间换时间,如桶排序,打表法,
2. 缩小计算范围,二分法,减少一半的计算量,如范围搜索,x的n次方。
3. 循环不变量十分重要,需要搞清楚循环前,循环中以及循环后不同变量的意义。比如字串左移的问题。
column 8:
算法分析与设计:
1. 理解问题,如最大字串和,是连续的字串,不能分开的。数组中有负数的。当所有数都是负数时和为0.
2. 暴力法。一般复杂度为平方或者3次方,或者利用空间或时间,反正得做出来。
3. 开始优化,优化关键是找到重复计算的地方。比如8.1的算法是O(n^3)的复杂度,但是分析发觉对于给定的i,不用对于每个j都是从i计算到j,可以利用cumulative sum来减少重复计算,复杂度
减为O(n^2). 典型的空间换时间做法,新的计算在已经计算的结果基础上进行。
4. 在暴力方法的基础上,看看是否可以用分治发,排序加二分等一些方法改进时间复杂度。
8.3就用了二分法解这道题目,将数组分为两半,求的左边的最大和,求右边的最大和,求中间的最大和,三个取最大,复杂度为O(nlogn)。
5. 当然最后可以使用贪心,动态规划,线段树,快排的分割方法,堆这些比较牛逼的东东试试看。动态规划还是比较常用的。
动态规划是典型的空间换时间的方法。先分割子问题,计算子问题,保存子问题的解,然后用子问题的解计算原问题。从而达到减少时间复杂度的目的,当然空间复杂度上升。
8.4用到了动态规划方法,其实动态规划的思想和循环不变式一致,如果已经求和dp[i]表示x[0...i]的最优解,则dp[i+1]表示x[0...i+1]的最优解。
关于这道题目,需要注意的是不能仅仅保存dp[i]表示x[0...i]的最大字串和。因为如果dp[i]的最优解不一定与x[i+1]能够连接,所以必须再保存一个以x[i]结尾的最大字串和ed[i],所以计算dp[i+1]时可得ed[i+1] = max(ed[i]+x[i+1],0),dp[i+1]=max(dp[i],ed[i+1])。
用动态规划关键是找到子问题。如果找不到子问题,就不好办了,囧。
P10:求最接近0的最大字串和。好像不能用动规,因为没有最优子结构,比如-5,3,-3,5全部相加为0,但是去掉最后个5,前边3个相加为-5不是最优子解,最优子解为3+-3=0,所以不能用动规。
方法:用cumulative array,cum[i] = cum[i-1]+x[i]。如果有cum[i] == cum[j] 则表示sum(x[i...j]) = 0。所以对cum排序,找到两个差值最小的,当然要保存cum对应的下标,否则排序后就乱掉了。
P11:cumulative array
P12:这道题目比较巧妙的,x[0..n-1]全部初始化为0,n次操作,for i = [l, u] x[i]+=v,所以如果全部加一遍需要O(n)时间,n次操作则O(n^2)。书上用了cumulative数组,比较巧妙。
对于for i = [l, u],只需记录cum[l-1] = -v, cum[u] = v。 在最后求和时for(int i = n-1;i>=1;i--)x[i] = x[i+1] + cum[i],即可
P13:n*n矩阵,找到最大子矩阵和最大。O(n^3),
for(int i = 0;i < n; i++)
//n
{
for(int k = 0; k < n;++k)sum[k] = 0;
//n
for(int j = i; j< n; ++j){
//n
for(int k = 0;k<n;++k){
//n
sum[k] += a[j][k];
}
t_max = maxsum(sum,k);
if(t_max > max)max = t_max;
}
}
这节关键说了cumulative数组的重要性,即空间换时间。
column 9:
这节讲了binary search在处理有相同元素时怎么获得某个元素第一次出现的位置。关键是当遇到等于t的元素时不要退出搜索,而是认为它左边还有可能有t,所以继续在左边找(包括a[m]).
伪码:
l = -1, u = n;
while l+1 != u
//a[l]< t, a[u]>=t, l+1<u
m = (l+n)/2
if (a[m] < t)l = m;
else
u=m
p = u
if p == n || a[p] != t
return -1;
return p;
P6:怎么判断一个字符是digit,letter,upper,lower?用打表法。
column 10:
这章节主要讲了排序(插入排序以及快速排序),关键是在当数组元素很小时不用进行快排,而是直接return,然后用插入排序,因为插入排序对几乎有序的数组排序仅需O(n)时间。
1,当数组元素个数小于某个阈值时,可以直接使用插入排序。
void qsort(int* a,int l,int r)
{
while(l<r)
{
if(r-l<32) //防止分割恶化
{
insertsort(a+l,r-l+1); //后面的插入排序
return;
}
int i = partition(a,l,r);
qsort(a,l,i-1);
l = i+1;
}
}
2,可以使用循环去除递归。
void qsort(int* a,int l,int r)
{
while(l<r) //防止过多递归
{
int i = partition(a,l,r);
qsort(a,l,i-1);
l = i+1;
}
}
P4:快排的递归堆栈最坏为O(n),如果修改成partition后对小的一半的先进行partition,大的一半用loop消除递归则可以降到O(logn)大小的堆栈。见“introduction to algorithm” 7.4
P5:对二进制变长串进行排序。同样利用排序,先将长度为1的全部移到最左边,然后递归分析长度大于1的。长度大于1的字串根据第一个字符比较,0在前面,1在后面,代码:
void m_sort(int l,int u,int len){
if(l>=u)return;
int i = l-1,j=l,m = 0;
while(j<=u){
if(strlen(strs[j])<len){
char * t = strs[i+1];
strs[++i] = strs[j];
strs[j] = t;
}
++j;
}
m = i;
j = m+1;
while(j<=u){
if(strs[j][len-1] == '0'){
char * t = strs[i+1];
strs[++i] = strs[j];
strs[j] = t;
}
j++;
}
m_sort(m+1,i,len+1);
m_sort(i+1,u,len+1);
}
P11:同样的快排分割题目,将数组分成三块,<t =t >t,分两次做,先将数组分成两块,<t >=t, 再在>=t部分分成两块,=t与>t。
代码:
/*
* fat partition, there are several t in the array
*
* partition the array into three parts: <t =t >t
*
* */
void fat_partition(int * a, int l,int r){
int i = l;
int j = l+1;
int t = a[l];
while(j<=r){
if(a[j]<t){
int tmp = a[i+1];
a[i+1] = a[j];
a[j] =tmp;
++i;
}
++j;
}
a[l] = a[i];
a[i] = t;
j = i+1;
while(j<=r){
if(a[j]==t){
int tmp = a[i+1];
a[i+1] = a[j];
a[j] = tmp;
++i;
}
++j;
}
}
这节的关键是学会利用快排的partition方法,可以快速将一个集合划分成两部分,然后使用分治方法在各个部分处理,或者只处理一部分。比如select算法就是典型的只处理一部分的。
column 12:
这一节讲述了随机算法,怎么从n个数中随机取出m个数。
1. 通过概率方法,从n个数随机取m个数,每个数取得的概率是m/n,这在书上有说明,而且可以证明。
代码:
select = m
for i = [0,n)
if(bigrand()%(n-1) < select)
select--;
print i
2. 通过set-based算法,用个set保存随机得到的数字,下次随机得到的和set中的元素比较,如果存在则继续随机,否则insert进入set。
3. shuffle算法,取前m个值。
这部分可以结合算法导论的第5章一起看
P2:不是每个元素被取得的概率相同就可以产生随机序列。
P9:很牛逼的算法,set-based的算法,保证m次可以取值可以获得m个随机数。
P10: 很大的流,要取m个数:答案很怪异,不看了。网上的说法是:每次输入一个记录时,随机产生一个0到1之间的随机数,用这些随机数维护一个大小为m的堆即可。相当于从n个数求出m个最大值(最小值)。
column 13:
介绍了几个不同的数据结构的特性,包括数组,链表,BST,bins, bitvector.等。
乘除法可以用移位代替。
column 14:主要介绍了heap,用在优先队列比较多,但内存存不下所有数据时经常用heap的,可以维持一个n个数的最大(小)堆,求n小(大)的数。
column 15:字符串处理,学到了后缀表。后缀表是个指针数组char *a[n],a[i]指向&c[i](c是原数组),这样a[n]就包含了c的所有后缀。对a进行排序,只要相邻的a进行比较就可以了,不用所有都比较。O(nlogn),具体见column 15.