基本知识:合并排序(Merge Sort)
两个已经排序的序列合并成一个序列,具体过程如下:
申请空间,使其大小为两个已经排序序列之和,然后将待排序数组复制到该数组中。
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较复制数组中两个指针所指向的元素,选择相对小的元素放入到原始待排序数组中,并移动指针到下一位置
重复步骤3直到某一指针达到序列尾
将另一序列剩下的所有元素直接复制到原始数组末尾
//将有二个有序数列a[first...mid]和a[mid...last]合并。
void mergearray(int a[], int first, int mid, int last, int temp[]) {
int i = first, j = mid + 1;
int m = mid, n = last;
int k = 0;
while (i <= m && j <= n) {
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
for (i = 0; i < k; i++)
a[first + i] = temp[i];
}
void mergesort(int a[], int first, int last, int temp[])
{
if (first < last)
{
int mid = (first + last) / 2;
mergesort(a, first, mid, temp); //左边有序
mergesort(a, mid + 1, last, temp); //右边有序
mergearray(a, first, mid, last, temp); //再将二个有序数列合并
}
}
bool MergeSort(int a[], int n)
{
int *p = new int[n];
if (p == NULL)
return false;
mergesort(a, 0, n - 1, p);
delete[] p;
return true;
}
位图表示
可使用一个20位的位串,表示<20的的非负整数集合。
例如:{1, 2, 3, 5, 8, 14}可表示为
即:0x 812E
同理,可用一个1千万位的位串,表示<10,000,000的非负整数集合,内存大小为 10 000 000/8/1024/1024=1.19MB
可分两次读取,用500万位的位串,第一次对1- 5,000,000之间的数排序,之后再对5,000,001-10,000,000之间的数排序,内存大小为 5,000,000/(8*1024*1024) = 0.596MB
伪代码
位图数据
C语言:
所申请的int数组如下:
字节位置=数据/32;
(采用位运算即右移5位)
位位置=数据%32;
(采用位运算即跟0X1F进行与操作)
位图排序
#include <stdio.h>
#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)); }
int main(){ int i;
for (i = 0; i < N; i++) clr(i);
/*Replace above 2 lines with below 3 for word-parallel init
int top = 1 + N/BITSPERWORD;
for (i = 0; i < top; i++)
a[i] = 0;
*/
while (scanf("%d", &i) != EOF)
set(i);
for (i = 0; i < N; i++)
if (test(i))
printf("%d\n", i);
return 0;
}
产生初始待排序序列
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAXN 2000000
int x[MAXN];
int randint(int a, int b){return a + (RAND_MAX * rand() + rand()) % (b + 1 - a);}
int main(int argc, char *argv[]){
int i, k, n, t, p;
srand((unsigned) time(NULL));
k = atoi(argv[1]);
n = atoi(argv[2]);
for (i = 0; i < n; i++) x[i] = i;
for (i = 0; i < k; i++) {
p = randint(i, n-1);
t = x[p]; x[p] = x[i]; x[i] = t;
printf("%d\n", x[i]);
}
return 0;
}
原则
仔细分析小问题可以带来巨大的实际好处。
普遍原则
位图数据结构
多趟算法
时间和空间的权衡,两者不可偏废
简单的设计
二分查找
在编程中,二分查找最常见应用就是在排序数组中查找某个元素。如果正在查找的元素是50,那么算法将进行如下探测:
int n, k[MAX_N]; //输入
bool binary_search (int x) {
int s = 0, e = n; //x的存在范围是k[s], k[s+1], …, k[e-1].
while( e - s >= 1) {
int s = (s + e) / 2;
if (k[ i ] == x) return true; //找到x
else if (k[ i ] < x) s = i + 1 ;
else e = i;
}
}
时间复杂度分析:
若 n = 2 , 执行 1 = log2(2) 次找到
若 n = 4 , 执行 2 = log2(4)次找到
若 n = 8 , 执行 3 = log2(8)次找到
若 n, 执行 log2(n) O(log n)
分块查找
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……。
typedef struct {
int value;
int index; } INDEX;
typedef struct {
int key; } ELEMTYPE;
int BlockSearch(INDEX index[],ELEMTYPE list[], int low, int high, int key, int m, int n)
{
int mid = 0;
while (low<=high) {
mid = (low+high)/2;
if (index[mid].value < key) low = mid +1;
else high = mid-1;
}
int position = high+1;
int i = index[position].index;
for( ; i<=index[position].index+m-1&&i<n; i++)
if ( list[i].key == key) break;
if (i<=index[position].index+m-1&&i<n) return i;
else return -1;
}
int main()
{
Index index[] = {{10,0},{20,5},{30,10},{40,15}};
ElemType list[] = {{1},{5},{6},{9},{10},{11},{15},{16},{19},{20},
{21},{25},{26},{29},{30},{31},{35},{36},{39},{40}};
for(int i=0;i<20;i++)
printf(“%d”, BlockSearch(index,list,0,3,list[i].key,5,20));
return 0;
}
向量旋转
请将一个具有n个元素的一维向量向左旋转i个位置。例如,假设n=8, i=3, 那么向量abcdefgh旋转之后得到向量defghabc.
简单的代码使用一个n元的中间向量在n步内完成该工作。你能否仅使用数十个额外字节的存储空间,在正比于n的时间内完成向量的旋转?
解法1:简单编码
如果不考虑时间要求为O(n),
那么可以每次整体左移一位,一共移动i次。只使用O(1)的空间的条件下,一共要进行元素交换O(n*i)次;
void Rotate(char *a, int length, int i)
{
for (int k=0; k<I; k++) {
char t = a[0];
for (int j=0; j<length; j++)
a[j] = a[j+1];
a[j] = t;
}
}
如果不考虑空间要求为O(1),
那么可以把前i个存入临时数组,剩下的左移i位,再把临时数组里的内容放入后i个位置中。
void Rotate(char *a, int length, int i){
char tmp[10]; //缓存区
int step= i % length; //需要移动的次数
int j=0;
if (step == 0) return;
//移出
for(j=0; j<step; j++)
tmp[j] = a[j];
//前移
for(j=step; j<length; j++)
a [j-step]=a [j];
//移回
j = 0;
while((a[length-step+j]=tmp[j]) != ‘\0’)
j++;
}
解法2:杂耍算法
T ß A[0]
A[0] ß A[3] [0] ß [ i mod n]
A[3] ß A[6] [i mod n] ß [ 2i mod n]
A[6] ß A[1] [2i mod n] ß [3i mod n]
A[1] ß A[4] [3i mod n] ß [4i mod n]
A[4] ß A[7] [4i mod n] ß [5i mod n]
A[7] ß A[2] [5i mod n] ß [6i mod n]
A[2] ß A[5] [6i mod n] ß [7i mod n]
A[5] ß T [7i mod n] ß T
void rotate(char* a, int n, int i) {
int temp = a[0];
int current = 0;
while(1) {
int next = (current + i) % n;
if (next == 0) break;
a[current] = a[next];
current= next;
}
a[current] = temp;
}
}
基础
【定义1】同余
如果两个整数(a,b)除以同一个整数m,如果得到相同的余数k。则称a,b对于模m同余。记作:a ≡ b (mod m)
【定义2】同余类
指以某一特定的整数(如m)为模,按照同余的方式对全体整数进行的分类。对给定的模m,有且恰有m个不同的模m的同余类。即:0 (mod m),1 (mod m),…,m-1 (mod m)。
【定义3】完全剩余类
所有的整数以m为模可以划分为m个没有交集的集合。从每个集合中取一个整数组成一个集合,则这个集合中的m个整数就不存在同余的整数,这个集合就叫做完全剩余类。
原理
如果i和n互质,那么序列:
0 ; i mod n ; 2i mod n ; 3i mod n ; …… ;(n-1)*i mod n
就包括了集合 {0,1,2,……n-1} 的所有元素。
前提条件
对于模n来说,序列0,1,2,……,n-1本身就是一个完全剩余类(即两两互不同余)
证明步骤
1)从此序列中任取两个数字xk,xl(0 <=k, l <= n-1),则有xk≠ xl (mod n),
2)由于i和n是互质的,所以 xk * i ≠ xl * i (mod n)
=》这就说明,xi从0开始一直取值到n-1,得到的序列0 * i,1 * i,2 *i,……(n-1)*n就是一个完全剩余类。即集合0,1,2,……n-1}
T ß A[0]
A[0] ß A[3] [0] ß [ i mod n]
A[3] ß A[6] [i mod n] ß [ 2i mod n]
A[6] ß A[1] [2i mod n] ß [3i mod n]
A[1] ß A[4] [3i mod n] ß [4i mod n]
A[4] ß A[7] [4i mod n] ß [5i mod n]
A[7] ß A[2] [5i mod n] ß [6i mod n]
A[2] ß A[5] [6i mod n] ß [7i mod n]
A[5] ß T [7i mod n] ß T
以上的赋值操作,赋值操作符的两边都得到了一个完全剩余类,也就是说所有的0 ~ n-1的所有位置都被移动过了
请注意第二个操作,X[0] = X[i mod n]。
该操作决定了整体的导向,该操作将i mod n位置的值移动到了最开始的位置。
由于i,2i,……之间的偏移量是相同的,所以整个操作实际上就是讲序列向左移动i个位置(超过了开始位置的接到最右边去)
i和n不互质的情况
void rotate(char* a, int n, int i) {
int step = gcd(n, i);
for(int j = 0; j < step; j++) {
int temp = a[j];
int current = j;
while(1) {
int next = (current + i) % n;
if (next == j) break;
a[current] = a[next]; current= next;
}
a[current] = temp;
}}
求最大公约数
辗转相除法
两个整数的最大公约数等于其中较小的数和两数的差的最大公约数。
int gcd(int a, int b) {
assert(a > 0 && b > 0);
while (a != b)
if (a>b) a -= b;
else b -= a;
return a;
}
解法3:块变换– 第1步
解法3:块变换 – 第2步
解法3:块变换 – 第3步
解法3:块变换 – 第4步
void swap(char* arr, int aStart, int bStart, int len)
{
assert(arr != NULL && aStart >=0 && bStart >=0 && len>0);
char tmp;
while(len-- > 0) {
tmp = arr[aStart]; arr[aStart] = arr[bStart]; arr[bStart] = tmp;
aStart ++; bStart ++;
}}
void Rotate(char* str, int Start, int len, int bits){
int leftStart = Start; //左半部分
int leftLen = bits;
int rightStart = Start + bits; //右半部分
int rightLen = len – bits;
if (0 == len) return;
if (leftLen > rightLen) {
swap(str, leftStart, leftStart + leftLen, rightLen);
Rotate(str,leftStart+rightLen,len-rightLen,len-2*rightLen);
} else {
swap(str, leftStart, leftStart + len - leftLen,leftLen);
Rotate(str, leftStart, len - leftLen, leftLen);
}
}
void LeftRotateString(char* str,int k)
{
Rotate(str,0,strlen(str),k);
}
int main()
{
char str[81] = "abcdefghij";
LeftRotateString(str,3);
cout<<str<<endl;
return 1;
}
变位词
给定一个英语字典,找出其中的所有变位词集合。例如,“pots”“stop”和“tops”互为变位词,因为每一个单词都可以通过改变其他单词中字母的顺序来得到。
方法1:单词所有字母的排列
任何一种考虑单词中所有字母的排列的方法都注定了要失败。
cholecystoduodenostomy有22!种排列
22!约等于1.124*1021
在大约23万个单词的字典里,即使是一个简单的变位词比较也将至少1微秒的时间,因此,总时间估算起来就是:
23万(单词) * 23万(次比较/单词) * 1(微秒/次比较)
= 52 900*10^6微秒
=52 900秒,约等于14.7小时。
方法2:签名方法
为字典中的每个单词生成一个签名,并使所有变位词具有相同的签名。
根据签名收集单词,每个签名对应一个集合,这个集合包括其所有的变位词。
单词签名生成方法:
把这单词包含所有字母,按照字母顺序排列。这样,所有变位词的标签就全相等了。
例:
“pots”的签名是:opst
“stop”的签名是:opst
“tops”的签名是:opst
“deposit”的签名是:deiopst
“dopiest”的签名是:deiopst
怎样查询表?
直接访问(Direct Access Table)
索引访问
阶梯访问
阶梯访问法
阶梯方法通过确定每项命中的阶梯层次确定其归类,它命中的“台阶”确定其类属
表中的记录对于不同的数据范围有效,而不是对不同的数据点有效
例子
<=100.0 A
<90.0% B
<75.0% C
<65.0% D
<50.0% F
Table structure?
double rangeLimit[] = {50.0, 65.0, 75.0, 90.0, 100.0};
char grade[] = {“F”, “D”, “C”, “B”, “A”};
int maxGradeLevel = sizeof(grade) -1;
int gradeLevel = 0;
char studentGrade = ‘A’;
while (studentGrade==‘A’ && gradeLevel<maxGradeLevel) {
if (studentScore>rangeLimit[gradeLevel])
studentGrade = grade[gradeLevel];
gradeLevel = gradeLevel+1;
}
七段问题
编码
建显示码表
#define BIT(a) 1<<(a)
static int ssd_tbl[] = {
BIT(0)|BIT(2)|BIT(3)|BIT(4)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0x0 */
BIT(4)|BIT(6), /* 0x1*/
BIT(0)|BIT(1)|BIT(2)|BIT(4)|BIT(5)|BIT(7)|BIT(8), /* 0x2*/
BIT(0)|BIT(1)|BIT(2)|BIT(4)|BIT(6)|BIT(7)|BIT(8), /* 0x3*/
BIT(1)|BIT(3)|BIT(4)|BIT(6)|BIT(7)|BIT(8), /* 0x4*/
BIT(0)|BIT(1)|BIT(2)|BIT(3)|BIT(6)|BIT(7)|BIT(8), /* 0x5*/
BIT(0)|BIT(1)|BIT(2)|BIT(3)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0x6*/
BIT(2)|BIT(4)|BIT(6)|BIT(7)|BIT(8), /* 0x7*/
BIT(0)|BIT(1)|BIT(2)|BIT(3)|BIT(4)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0x8*/
BIT(0)|BIT(1)|BIT(2)|BIT(3)|BIT(4)|BIT(6)|BIT(7)|BIT(8), /* 0x9*/
BIT(1)|BIT(2)|BIT(3)|BIT(4)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0xA*/
BIT(0)|BIT(1)|BIT(3)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0xB*/
BIT(0)|BIT(2)|BIT(3)|BIT(5)|BIT(7)|BIT(8), /* 0xC*/
BIT(0)|BIT(1)|BIT(4)|BIT(5)|BIT(6)|BIT(7)|BIT(8), /* 0xD*/
BIT(0)|BIT(1)|BIT(2)|BIT(3)|BIT(5)|BIT(7)|BIT(8), /* 0xE*/
BIT(1)|BIT(2)|BIT(3)|BIT(5)|BIT(7)|BIT(8), /* 0xF*/
};
文本显示
/* 第一行*/
for (i=0; i<5;i++) {
printf(" %c ", output[i] & BIT(2) ? '_' : ' ');
}
printf("\n");
/* 第二行 */
for (i=0; i<5;i++) {
printf("%c%c%c ",
output[i] & BIT(3) ? '|' : ' ',
output[i] & BIT(1) ? '_' : ' ',
output[i] & BIT(4) ? '|' : ' ');
}
printf("\n");
/* 第三行 */
for (i=0; i<5;i++) {
printf("%c%c%c ",
output[i] & BIT(5) ? '|' : ' ',
output[i] & BIT(0) ? '_' : ' ',
output[i] & BIT(6) ? '|' : ' ');
}
printf("\n");
提升性能的要素
算法与数据结构 :构建模块的关键通常是结构,这些结构表示了数据以及作用于数据的算法。
算法调优 :在特定数据结构上优化算法。
数据结构重组:在特定阶段对数据结构进行重新配置。
代码调优:与系统相关的调优,把经常使用的函数进行加速,比如关键代码使用汇编代替高级语言。
使用硬件加速器
设计层面
问题定义:追求快速系统,可能在定义该系统需要解决的问题时就已经注定成败了。良好的问题定义可以避免用户对问题需求的过高估计。问题定义和程序效率之间具有复杂的相互影响。
系统结构:将大型系统分解成模块,也许是决定其性能的最重要的单个因素。在构建出整个系统的框架以后,设计者需要完成简单的“粗略估算”,以确保程序的性能在正确的范围之内。由于提高新系统效率比改进已有系统的效率要容易的多,所以系统分析在系统设计阶段至关重要。
算法和数据结构:获得快速模块的关键是表示数据的结构以及操作数据的算法。
代码调优:在特定的结构上优化代码。
系统软件:有时候改变系统所基于的软件比改变系统本身更容易。
硬件:更快的硬件可以提高系统的性能。
原理
计算机系统中最廉价、最快速且最可靠的元件是根本不存在的。
如果仅需要较小的加速,就对效果最佳的层面做改进。
如果需要较大的加速,就对多个层面做改进。
断言的艺术
int binary_search(DataType t)
{
int l, u, m;
l = 0;
u = n-1;
assert((x[l]<=t)&&(t<=x[u]));
while(l <= u) {
assert((l<=u) && (x[l]<=t)&&(t<=x[u]));
m = (l+u)/2;
if(x[m]<t) l=m+1;
else if(x[m]==t) return m;
else u=m-1;
}
assert((l>u) && (x[l]<=t)&&(t<=x[u]));
return -1;
}
自动测试
#define s binary_search
int main()
{
int i;
for(n=0; n<N; n++) {
for(i=0; i<n; i++) x[i] = 10*i;
for(i=0; i<n-1; i++) {
assert(s(10*i) == i);
assert(s(10*-5) == -1);
}
assert(s(10*n-5) == -1);
assert(s(10*n) == -1);
}
return 0;
}
完整的程序
主函数
int main()
{
timedriver();
return 0;
}
原理
脚手架:最好的脚手架通常就是那种最容易构建的脚手架。
编码:对于高难度函数,最简单的方法是先使用伪代码描述骨架,再转换为实现语言。
测试:在脚手架中对组件进行测试要比在大系统中更容易、更彻底。
调试:对隔离在其脚手架中的程序进行调试是很困难的,但是若将其嵌入真实的运行环境调试会更困难。
计时:如果不关注运行时间,可采用线性搜索确保其正确性
粗略估算
基本技巧
两个答案比一个答案好
快速检验(量纲检验)
经验法则 72法则:单位时间增长率 * 时间 =72 则该时间完成初始值翻番
题1:假设最初投资金额为100元,复息年利率9%,实现资金翻番需要多久?
解法:利用“72法则”,将72除以9(增长率),得8,即需约8年时间,投资金额滚存至200元(翻番),而准确需时为8.0432年。
题2:盘子中的菌每小时增长3%,那么其数量多久会翻番?
解法:24小时(3 * 24 =72)
性能估计
struct node
{int i;struct node *p;}
占用8个字节
使用malloc时,事实上每个节点多占用了40个字节,共48个字节。
可以通过一些小实验来获得关键参数。
安全系数
估算不能保证百分百的正确,所以在估算的时候,我们需要有余量,给自己留后路。工作时,经常碰到这样的事情。主管问你这个事情2个礼拜能搞定么?你怎么说。
一般回答:可能需要2-3个礼拜。
Little定律
little定律:系统中物体的平均数量等于物体离开系统的平均数率和每个物体在系统中停留的平均时间的乘积。
你正在排队等待进入一个非常火爆的餐厅,你可能会花时间计算一下你必须等多久?
这个地方可以容纳60桌, 每桌大约3小时,所以进入率每小时20桌。队列上已经排了20桌,所以你还要等一小时。
地窖里有150瓶酒,每年喝完25个。问每瓶保存了多少年?
150/25 = 6年
性能分析法则:
总开销=每个单元的开销×单元的个数。
例子:如果一个群体的平均寿命为75岁,则这个群体的死亡率为多少?
1/75 =1.3%
原理
在进行粗略估算的时候,要记住爱因斯坦的名言:
任何事都应尽量简单,但不宜过于简单。
粗略估算中包含了安全系数,以补偿估算参数时的错误和对问题的了解不足。
问题及简单算法
输入:一个具有n个整数数字的向量X;
输出:在输入的任何相邻子向量中找出最大和。
例如:
X[2..6]的总和最大为187
所有输入数字都是正数时,问题很简单,最大的子向量就是整个输入向量。
所有输入数字都是负数时,最大总和子向量是空向量,空向量的总和为0。
完成任务的程序迭代了所有满足下列条件:
0 <= i <= j < n
i和j整数对;对每一个整数对,都要计算x[i…j]的总和,并检查总和是否大于迄今为止的最大总和。
maxsofar = 0;
for (i=0; i<n; i++) {
for (j=0; j<n; j++) { 时间复杂度:O(n3)
sum = 0;
for(k=i; k<=j; k++) {
sum += x[k];
}
maxsofar = max(maxsofar, sum);
}
}
两个平方算法
注意到x[i…j]中的总和与前面已计算的x[i…j-1]的总和密切相关,从而快速计算了总和。
maxsofar = 0;
for(i=0; i<n; i++) {
sum = 0; 时间复杂度:O(n2)
for(j=i; j<n; j++) {
sum += x[j];
maxsofar = max(maxsofar, sum);
}
}
通过访问在外部循环执行之前就已构建的数据结构的方式在内部循环中计算总和。
cumarr[0…n-1]的第i个元素包含x[0…i]中的各个值的累加和,所以x[i…j]中各个值的总和可以通过计算cumarr[i]-cumarr[i-1]得到。
cumarr[-1] = 0;
for (i=0; i<n; i++) {
cumarr[i] = cumarr[i-1] + x[i];
}
maxsofar = 0;
for(i=0; i<n; i++) {
for(j=i; j<n; j++) {
sum = cumarr[j] – cumarr[i-1];
maxsofar = max (maxsofar, sum);
}
} 时间复杂度:O(n2)
分治算法
分治法:要解决规模为n的问题,可递归解决两个规模近似为n/2的子问题,然后将它们的答案合并以得到整个问题的答案
初始问题要处理大小为n的向量,所以将它划分为两个子问题的最自然的方法就是创建两个大小相当的子向量,称为a和b。
然后递归找出a和b中元素和最大的子向量,称为ma和mb。
在整个向量中最大总和的子向量必定是ma或mb。事实上,最大值要么整个在a中,要么整个在b中,或跨越a和b之间的边界;将跨越边界的最大值称为mc
终止条件
只有一个元素的向量的最大值就是该向量中的唯一值(如果该数是负数,就是0)
零元素向量的最大值定义为0
计算mc
其左边是从边界开始到达a的最大子向量 其右边是从边界开始到达b的最大子向量
int maxsum3(int l, int u)
{
int i, m=0;
int lmax, sum, rmax;
if(l>u) /* zero elements */
return 0;
if(l==u) /* one elements */
return max(0, x[l]);
m = (l+u)/2;
/* find max crossing to left */
lmax = sum = 0;
for(i=m; i>=l; --i) {
sum += x[i];
lmax = max(lmax, sum);
}
/* find max crossing to right */
rmax = sum = 0;
for (i=m+1; i<=u; ++i) {
sum += x[i];
rmax = max(rmax, sum);
}
return max(lmax+rmax, maxsum3(l, m), maxsum3(m+1, u));
}
时间复杂度:O(nlogn)
扫描算法
从最左端(元素x[0])开始,一直扫描到最右端(元素x[n-1]),记下所碰到过的最大总和子向量。
最大值最初是0,假设解决了x[0…i-1]的问题,如何扩展为包含x[i]?
前i个元素中,最大总和子数组要么在i-1个元素中(存储在maxsofar中),要么截止到位置i(存储在maxendinghere中)
int maxsofar=0,i;
int maxendinghere=0;
for(i=0; i<n; i++) {
/* invariant: maxendinghere and maxsofar
are accurate for x[0...i-1]*/
maxendinghere = max(maxendinghere + x[i], 0);
maxsofar = max(maxsofar, maxendinghere);
}
理解程序的关键是变量maxendinghere
在该循环的第一个赋值语句前,maxendinghere包含了截止于位置i-1的最大子向量的值
赋值语句修改它以包含截止于位置i的最大子向量的值。
只要这样做能够保持其正值,该语句将向它增加一个值x[i];
当它变成负值时,就将它重新设为0。
急救方案集锦
问题1 : 整数求余
k = (j + rotdist) % n
大多数算术运算花费约10纳秒
%运算花费接近100纳秒
改进 k = j + rotdist
if (k >= n)
k -= n;
分析
当rotdist=1时,运行时间119纳秒下降到57纳秒
当rotdist=10时,运行时间均为206纳秒
当rotdist=1时,算法顺序访问内存,模运算决定了程序的运行时间。而当rotdist=10时,代码在内存中每隔10个字才访问一次,因此大部分运行时间用于将RAM的内容读入高速缓存。
问题2:函数、宏以及内联代码
float max (float a, float b) { return a>b ? a : b; }
改进:#define max (a, b) (a>b ? a : b)
(算法4 扫描算法)运行时间由89纳秒减少至47纳秒
int maxsum3(int l, int u){
int i, m=0;
int lmax, sum, rmax;
if(l>u) return 0; /* zero elements */
if(l==u) return max(0, x[l]); /* one elements */
m = (l+u)/2;
/* find max crossing to left */
lmax = sum = 0;
for(i=m; i>=l; --i) { sum += x[i]; lmax = max(lmax, sum); }
/* find max crossing to right */
rmax = sum = 0;
for (i=m+1; i<=u; ++i) { sum += x[i]; rmax = max(rmax, sum);
}
return max2(lmax+rmax, maxsum3(l, m), maxsum3(m+1, u));
}
问题2:函数、宏以及内联代码
(算法3分治算法)当n=10000时,运行时间由10毫秒增加至100秒,由原来的O(nlogn)增加至O(n2)。
宏那种按名称调用的语义导致算法3对自身的调用超过了两次,因此增加了渐近运行时间。
C++允许对某一函数进行内联编译,因此兼得了函数的简洁语义和宏的低廉开销,
既不使用宏,也不使用函数,而是用if语句,运行时间基本上没有变化。
问题3:顺序搜索
int ssearch1(DataType t)
{
for( i=0; i<n; i++) {
if (x[i] == t) return i;
}
return -1;
}
运行时间:4.06n纳秒
int ssearch2(DataType t)
{
hold = x[n]; x[n] = t;
for( i=0; ; i++)
if (x[i] == t) break;
x[n] =hold;
if (i==n) return -1;
else return i;
}
运行时间:3.87n纳秒
int ssearch3(DataType t)
{
x[n] = t;
for (i=0; ; i+=8) {
if (x[i ] == t) break;
if (x[i+1] == t) { i+=1; break;}
if (x[i+2] == t) { i+=2; break;}
if (x[i+3] == t) { i+=3; break;}
……
if (x[i+7] == t) { i+=7; break;}
}
if (i==n) return -1;
else return i;
}
1.70n纳秒,下降56%
对老式计算机来说,降低开销可以加速10%或20%。
对于现代计算机来说,将循环展开有助于避免管道阻塞、减少分支、增加指令级的并行性
问题4:计算球面距离
输入的第1部分是集合S,包括球面上5000个点,每个点由经度和纬度表示。
输入的第2部分是由20000个点组成的序列,每个点由经度和纬度表示。
对于序列中每一个点,程序必须指出S集中哪个点最接近它,其中的距离是 以球体中心到两个点的连线间的角度来度量。
解决方案
将S表示成包含经度和纬度值的数组。与序列中某点最接近的邻居是通过计算该点到S中每一个点的距离确定的。需要使用复杂到包括10个sin和cos函数的三角公式。
对于大型地图来说,即使在大型机上运行也需要几个小时,大大超出了项目预算。
为什么要在数据结构的层次上解决这个问题呢?
为什么不使用简单的数据结构,将这些点保存在一个数组中,通过优化代码来减少点之间距离计算的成本呢?
不使用经度和维度来表示点,而使用x,y和z坐标表示球面上的点。这样数据结构就是一个数组。
当处理序列中的点时,不多的几个三角函数就将其经/纬度转换成x,y和z坐标,然后计算它到S集中每个点的距离。
其与S集合中点的距离为x,y,z坐标差值的平方和,这样的开销通常比三角函数的开销要更加便宜
大手术—二分搜索1
int binarysearch1(int t)
{
int l, u, m;
l = 0;
u = n-1;
while (l <= u) {
m = (l + u) / 2;
if (x[m] < t)
l = m+1;
else if (x[m] == t)
return m;
else /* x[m] > t */
u = m-1;
}
return -1;
}
大手术—二分搜索2
int binarysearch2(int t)
{
int l, u, m;
l = -1;
u = n;
while (l+1 != u) {
m = (l + u) / 2;
if (x[m] < t) l = m;
else u = m;
}
if (u >= n || x[u] != t) return -1;
return u;
}
循环迭代中,只对t和x中的元素比较一次,binarysearch1有时必须比较两次。
大手术—二分搜索3
int binarysearch3(int t)
{
int i, l, d;
i=512;
l=-1;
if(x[511]<t) l=1000-512;
while( i != 1) {
i = i/2;
if(x[l+i] < t) {
l = l + i;
}
}
d = l+1;
if( d>1000 || x[d] != t) d = -1;
return d;
}
大手术—二分搜索4
int binarysearch4(int t)
{ int l, p;
if (n != 1000) return binarysearch2(t);
l = -1;
if (x[511] < t) l = 1000 - 512;
if (x[l+256] < t) l += 256;
if (x[l+128] < t) l += 128;
if (x[l+64 ] < t) l += 64;
if (x[l+32 ] < t) l += 32;
if (x[l+16 ] < t) l += 16;
if (x[l+8 ] < t) l += 8;
if (x[l+4 ] < t) l += 4;
if (x[l+2 ] < t) l += 2;
if (x[l+1 ] < t) l += 1;
p = l+1;
if (p >= n || x[p] != t) return -1;
return p;
}
当n=1000时,运行时间从350纳秒减少到125纳秒,减少了64%。
数据空间技术
关注的是减少程序所需数据的存储空间
不存储,重新计算
典型的时间换空间策略,适用于需要“存储”的对象可以根据其描述重新计算得到的情况。
判断质数
安装软件时的“典型安装”(保存一些数据)和“最小化安装”(节省磁盘空间,花费更多时间读取数据)
在网络传输时候,往往时间显得很重要。采用本地缓存的方式减少需要传输的数据量。
关注的是减少程序所需数据的存储空间
稀疏数据结构。
使用指针共享大型对象,消除存储同一对象的众多副本的所需的开销。
数据压缩
用8位的char替代 32位的int 。利用函数将两个十进制数,放到一个字节里。加密:c = 10 * a + b;解密:a = c / 10; b= c % 10;
注意/ 和 %运算符开销较大,即运行时占用内存较多。如果采用该方法,相当于用较小的内存来换取存储字节的磁盘存储空间。
关注的是减少程序所需数据的存储空间
分配策略
动态分配替代静态分配。即只有在需要的时候才分配空间。
垃圾回收
对废弃的存储空间进行回收再利用,从而那些不用的位就可以重新使用了。
例如:两个三角矩阵共享某一方阵c的空间。
引用a[i,j]的方法:
c[max(i,j), min(i,j)]
代码空间技术
有时候空间的瓶颈不在于数据,而在于程序本身的规模。
通过用函数替换代码中的常见模式可以简化程序,减少空间需求。微软删除了很少使用的函数,将整个windows 系统压缩为更加紧凑的Windows CE。
解释程序。用解释程序来替换一长行的程序文本。
翻译成机器语言。这个性价比比较低,一般用于内存宝贵的系统,如DSP(数字信号处理器)。
原理
空间开销
程序使用的内存增加10%,先前浪费的内存得到利用或者内存溢出了。在着手降低空间开销之前,应先了解空间空间开销。
空间的“热点”
少数常见类型的记录经常要占用大部分内存。
空间度量
性能监视器,允许程序员观察程序运行时的内存使用情况。
折中
有时程序员必须牺牲程序的性能、功能或可维护性以获得内存,这样的工程决策应该在所有可选办法都研究过之后才能做出。
与环境协作
编程环境对于程序的空间效率具有重要影响。重要的环境因素包括编译器和运行时系统所使用的表示方式、内存分配策略以及分页策略。良好的空间开销模型有助于确保我们不会向相反的方向努力。
使用适合任务的正确工具
四种节省数据空间的技术(重新计算、稀疏结构、信息理论以及分配策略)、三种节省代码空间的技术(函数定义、解释程序以及翻译)、最重要的原则(简单性)。
当内存很关键时,请务必考虑所有可能的选项。
插入排序
算法思路
将第i个记录插入到前面i-1个已排好序的记录中,具体过程为: 将第i个记录的关键字Ki顺次与其前面记录的关键字Ki-1,Ki-2,…, K1进行比较,将所有关键字大于Ki的记录依次向后移动一个位置,直到遇见一个关键字小于或者等于Ki的记录Kj,此时Kj后面必为空位置,将第i个记录插入空位置即可。完整的直接插入排序是从i=2开始的,也就是说,将第1个记录视为已排好序的单元素子集合,然后将第2个记录插入到单元素子集合中。i从2循环到n,即可实现完整的直接插入排序。
插入排序isort1
for i = [1, n)
for(j = i; j>0 && x[j-1] > x[j]; j--)
swap(j-1 , j)
插入排序isort2
for i = [1, n)
for(j = i; j>0 && x[j-1] > x[j]; j--)
{t = x[j]; x[j] = x[j-1]; x[j-1] = t;}
插入排序isort3
for i = [1, n)
t = x[i]
for(j = i; j>0 && x[j-1] > t; j--)
x[j] = x[j-1]
x[j] = t;
一种简单的快速排序
快速排序的算法思路:基于分治法,在排序数组时,将数组分成两个小部分,然后对他们递归排序。
例如:
围绕第一个元素55进行划分,所有小于55的移至其左边,所有大于55的移至右边。
该算法的平均运行时间远远小于插入排序O(n2)的时间,因为划分操作对排序大有裨益:通常对n个元素进行划分之后,大约有一半元素的值大于划分值,一半元素小于划分值;而在相近的运行时间内,插入排序的筛选操作只能使一个元素移动到正确的位置。
下面分别用l和u表示数组待排序部分的上界和下界,递归结束的条件是待排序部分的元素个数小于2。
void qsort(l,u)
if l >=u then /*at most one element,do nothing*/
return
/*goal:partition array around a particular value,which is eventually placed in its correct position p*/
qsort(l,p-1)
qsort(p+1,u)
给定值t之后,需要重新组织x[a…b],并计算下标m(“中间元素”的下标),使得所有小于t的元素在m的一端,所有大于t的元素在m的另一端。
完整的划分代码如下:
m=a-1
for i=[a, b]
if x[i] < t
swap(++m,i)
围绕值t=x[l]划分数组x[l..u],从而a为l+1,b为u。因此,划分循环的不变式如下所示:
循环终止时有:
然后交互x[l]和x[m]:
接下来就可以使用参数(l,m-1)和(m+1,u)分两次递归调用该函数了
完整的快速排序代码qsort1
void qsort1(l,u)
if (l >= u) return
m = l
for i=[l+1,u]
/*invariant: x[l+1..m] < x[l] && x[m+1..i-1>=x[l] */
if(x[i] < x[l]) swap(++m,i)
swap(l,m)
/*x[l..m-1] < x[m] <= x[m+1..u] */
qsort1(l,m-1)
qsort1(m+1,u)
当输入数组是不同元素的随机排列时,该快速排序平均需要O(nlogn)的时间和O(logn)的栈空间。大多数算法教材都分析了快速排序的运行时间,并证明了任何基于比较的排序至少需要O(nlogn)次比较,因此快速排序接近最优算法。
qosrt1比调优过的C库函数qsort快1倍(qsort函数的通用接口开销很大)。qsort1函数可能适用于一些表现良好的应用程序,但是也有许多快速排序算法都具有的缺点:在一些输入下,它可能退化为平方时间的算法。
void qsort21(l,u)
if (l >= u) return
m = u+1
for(i = u; i >= l; i--)
if(x[i] >= t)
swap(--m,i)
qsort21(l,m-1)
qsort21(m+1,u)
void qsort22(l,u)
if (l >= u) return
m = i = u+1
do
while x[--i] < t
;
swap(--m,i)
while i!=l
qsort22(l,m-1)
qsort22(m+1,u)
更好的几种快速排序
在随机的情况下qsort1性能较佳,但对于非随机情况下却性能低下。
例如:那个相同元素组成的数组。插入排序性能非常好:每个元素需要移动的具有均为0,所以总的运行时间为O(n);但qsort1函数性能却非常糟糕。n-1次划分中每次划分都需要O(n)时间去掉一个元素,所以总的运行时间为O(n2)。当n=1000000时,运行时间共1秒变成了2个小时。
使用双向划分可以避免这个问题,循环不变式如下:
下标i和j初始化为待划分数组的两端。主循环中有两个内循环,第一个内循环将i向右移过小元素,遇到大元素时停止;第二个内循环将j向左移过大元素,遇到小元素时停止。然后主循环测试这两个下标是否交叉并交换它们的值
向右扫描元素以避免做多余的工作,但是当所有的输入都相同时,这样就会得到平方时间的算法。
解决方法:当遇到相同的元素时停止扫描,并交换i和j的值。这样做虽然使交换的次数增加了,但却将所有元素都相同的最坏情况变成了差不多需要nlog2n次比较的最好情况
void qsort4(l,u)
if u-l < cutoff return
swap(l, randit(l,u))
t = x[l]; i = l; j = u+1
loop
do i++ while i <= u && x[i] < t
do j-- while x[j] > t
if i > j break
temp = x[i];x[i] = x[j];x[j] = temp
swap(l,j)
qsort4(l,j-1)
qsort4(j+1,u)
下表对快速排序的各个版本进行了总结。最右边一列给出了排序n个随机整数的平均运行时间,以纳秒为单位。在某些输入条件下,表中许多函数都会退化为平方时间的算法。
原理
C标准库函数qsort非常简单并且相对比较快,但比我们自己写的快速排序慢,仅仅是因为其通用而灵活的接口对每次比较都使用了函数调用。
C++标准库函数sort具有最简单的接口,可以通过调用sort(x, x+n)对数组x排序,其实现也非常高效。如果系统中的排序能够满足我们的需求,就不用自己编写代码了。
插入排序代码容易编写,对于小型的排序任务速度很快。
如果n很大,快速排序的O(n logn)运行时间就非常关键了。
可利用第九章的代码调优技术提高算法的速度。
问题
程序的输入是选区名列表以及整数m,输出是随机选择的m个选区名的列表。通常选区名有几百个(每个选区名都是一个不超过12字符的字符串),m通常在20~40。
程序的输入包含两个整数 m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度说,希望得到没有重复的有序选择,并且其中每个选择出现的概率相等。
一种解决方案
算法依次考虑整数0,1,2,…,n-1,并通过一个适当的随机测试对每个整数进行选择。通过按序访问整数,保证输出结果有序。
例如:m=2,n=5。0被选择的概率是2/5;1被选择的概率取决于0有没有被选择,如果0被选择,则1被选择的概率为1/4,否则为2/4,所有1被选择的概率为(2/5)*(1/4)+(3/5)*(2/4)=2/5;同理2被选择的概率取决于前两个,如果都没被选择,则2被选择的概率为2/3,如果前两个有一个被选择,则2被选择的概率为1/3,如果前两个都被选择,则2被选择的概率为0,故2被选择的概率为(3/5)*(3/5)*(2/3)+2*(2/5)*(3/5)*(1/3)=2/5。依次类推,每个元素被选择的概率都为2/5。
从剩下的r个元素中选择s个元素,那么下一个元素被选中的概率为s/r,从整个数据集合角度来讲,每个元素被选择的概率都是相同的
伪代码
select = m
remaining = n
for i = [0, n)
if(bigrand() % remaining) < select
print i
select--
remaining--
C代码
void genknuth(int m, int n)
{
for(int i = 0;i < n;i++) {
if((bigrand() % (n-i)) < m){
cout << i << "\n";
m--;
}}
}
时间复杂度:O(n)
设计空间
在一个初始为空的集合里面插入随机整数,直到个数足够,这里利用C++ set容器
initialize set S to empty
size = 0
while size < m do
t = bigrand() % n
if t is not in S
insert t into S
size++
print the elements of S in sorted order
void gensets(int m, int n)
{
set<int> S;
while(S.size() < m)
S.insert(bigrand() % n);
set<int>::iterator i;
for(i = S.begin(); i != S.end(); ++i)
cout << *i << endl;
}
插入操作的时间复杂度:O(log m)
遍历集合的时间复杂度: O(log m)
总的时间复杂度:O(m log m)
把包含整数0~n-1的数组的前m个元素打乱,然后把前m个元素排序输出。
void genshuf(int m, int n)
{ int i,j;
int *x = new int[n];
for(i = 0; i < n; i++) x[i] = i;
for(i = 0; i < m; i++){
j = randint(i, n-1);
int t = x[i];x[i] = x[j];x[j] = t;
}
sort(x , x+m);
for(i = 0; i < m; i++) cout << x[i] << endl;
}
空间复杂度:O(n) 时间复杂度:O(n+m log m)
m和n接近的时候,基于gensets的改进,并能在m个步骤之内就可以按要求等概率地生成有序随机数
void genfloyd(int m, int n)
{ set<int> S;
set<int>::iterator i;
for(int j = n-m; j < n; j++){
int t = bigrand() % (j+1);
if(S.find(t) == S.end()) S.insert(t); //t not in S
else S.insert(j); //t in S
}
for(i = S.begin(); i != S.end(); ++i) cout << *i << endl;
}
时间复杂度:O(m log m)
二分搜索树
依次插入整数31、41、59和26后形成的二分搜索树如下:
IntSetBST类
class IntSetBST {
private:
int n, *v, vn;
struct node {
int val;
node *left, *right;
node(int v) {
val = v; left = right = 0;
}
};
node *root;
node *rinsert(node *p, int t)
{
if (p == 0) {
p = new node(t);
n++;
} else if (t < p->val) {
p->left = rinsert(p->left, t);
} else if (t > p->val) {
p->right = rinsert(p->right, t);
} // do nothing if p->val == t
return p;
}
中序遍历
void traverse(node *p){
if (p == 0)
return;
traverse(p->left);
v[vn++] = p->val;
traverse(p->right);
}
public:
IntSetBST(int maxelements, int maxval) {
root = 0; n = 0;
}
int size() { return n; }
void insert(int t) {
root = rinsert(root, t);
}
void report(int *x) {
v = x;
vn = 0;
traverse(root);
}
};
数据结构
堆是用来表示元素集合的一种数据结构,堆中的元素可以是任何有序类型。
堆:任何结点的值都小于或等于其孩子的值的完全二叉树为小根堆;任何结点的值都大于或等于其孩子的值的完全二叉树为大根堆。
为了方便使用完全二叉树的性质,数组从下标1开始。这样:
leftChild = 2*i ;
rightChild = 2*i + 1 ;
parent = i/2 ;
null(i) = (i < 1) or (i > n)
堆的性质:顺序和形状
顺序:任何结点的值均小于或等于其子结点的值。意味着最小元素位于根结点,但是并没有说明左右结点的相对顺序
形状:除最后一层之外,其余各层均是满的;最后一层从第一个元素到最后一个元素之间不存在缺失元素。
两个关键函数
向上筛选(siftup)函数:尽可能将新元素向上筛选,通过交换该结点与其父结点来实现
void siftup(n)
pre n>0 && heap(1,n-1)
post heap(1,n)
i = n
loop
/*invariant:heap(1,n) except perhaps between i and its parent*/
if i ==1
break
p = i / 2
if x[p] <=x[i]
break
swap(p,i)
i = p
向下筛选(siftup)函数:将x[1]向下筛选,直到它没有子结点或小于等于它的子结点。
void siftdown(n)
pre heap(2,n) && n >= 0
post heap(1,n)
i = 1
loop
/*invariant:heap(1,n) except perhaps between i and its (0,1 or 2) children*/
c = 2 * i
if c > n break
/*c is the left child of i */
if c+1 <= n /*c+1 is the right child of i */
if x[c+1] <=x[c]
c++ /* c is the lesser child of i*/
if x[i] <= x[c] break
swap(c,i)
i = c
单词
为文档中包含的单词生成一个列表:利用C++ STL中的sets和strings,读取单词并插入集合S(忽略重复的单词),输出排好序的单词。
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
set<string> S;
string t;
set<string>::iterator j;
while (cin >> t)
S.insert(t);
for (j = S.begin(); j != S.end(); ++j)
cout << *j << "\n";
return 0;
}
单词计数:利用C++标准模板库中的map将整数计数与每个字符串联系起来,遍历单词并统计其个数
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main(){
map<string, int> M;
map<string, int>::iterator j;
string t;
while (cin >> t)
M[t]++;
for (j = M.begin(); j != M.end(); ++j)
cout << j->first << " " << j->second << "\n";
return 0;
}
单词计数:利用散列表的方式遍历单词并统计其个数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct node *nodeptr;
typedef struct node {
char *word;int count;nodeptr next;
} node;
#define NHASH 29989 //圣经中有29131个不同的单词,用跟29131最接近的质数作为散列表的大小
#define MULT 31 //hash时用到的乘数
nodeptr bin[NHASH]; //散列表
unsigned int hash(char *p)
{
unsigned int h = 0;
for ( ; *p; p++)
h = MULT * h + *p;
return h % NHASH;
}
#define NODEGROUP 1000
int nodesleft = 0;
nodeptr freenode;
nodeptr nmalloc(){
if (nodesleft == 0) {
freenode = (nodeptr)malloc(NODEGROUP*
sizeof(node));
nodesleft = NODEGROUP;
}
nodesleft--;
return freenode++;
}
#define CHARGROUP 10000
int charsleft = 0;char *freechar;
char *smalloc(int n){
if (charsleft < n) {
freechar = (char *)malloc(n+CHARGROUP);
charsleft = n+CHARGROUP;
}
charsleft -= n;
freechar += n;
return freechar - n;
}
//增加原先存在的单词的计数值,若之前没有该单词,则对计数器初始化
void incword(char *s){
nodeptr p;int h = hash(s); //找到与单词对应的箱
for (p = bin[h]; p != NULL; p = p->next)
if (strcmp(s, p->word) == 0) {
(p->count)++;return;}
p = nmalloc();p->count = 1;
p->word = smalloc(strlen(s)+1);
strcpy(p->word, s);
p->next = bin[h];bin[h] = p; //头插法
}
int main()
{int i;nodeptr p;char buf[100];
for (i = 0; i < NHASH; i++)
bin[i] = NULL;
while (scanf("%s", buf) != EOF)
incword(buf);
for (i = 0; i < NHASH; i++)
for (p = bin[i]; p != NULL; p = p->next)
printf("%s %d\n", p->word, p->count);
return 0;
}
单词计数的两种方法
平衡搜索树将字符串看作是不可分割的对象进行操作,标准模板库的set和map中大部分实现都使用这种结构。平衡搜索树中的元素始终处于有序状态,从而很容易的指向寻找前驱结点或按顺序输出元素之类的操作。
散列则需要深入字符串的内部,计算散列函数并将关键字散列到一个较大的表中。散列方法的平均速度很快,但缺乏平衡树提供的最坏情况的保证,也不能支持其他涉及顺序的操作。
短语
查找最长的重复子字符串:双重for循环依次比较每个字符串
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
//找到两个字符串公共部分的长度
int comlen(char *p, char *q){
int i = 0;
while (*p && (*p++ == *q++)) i++;
return i;
}
int main(){
int i, j;
int maxi, maxj;
int currentlen, maxlen = -1;
char *str = "ask not what your country can do for you, but what you can do for your country";
int length = strlen(str);
for(i = 0; i < length; i++){
for(j = i + 1; j < length; j++){
currentlen = comlen(str+i, str + j);
if(currentlen > maxlen){
maxlen = currentlen;
maxi = i;
maxj = j;
}
}
}
for(i = 0; i < maxlen; i++){
printf("%c", str[maxi + i]);
}
printf("n");
return 0;
}
查找最长的重复子字符串:后缀数组
程序最多处理MAXN个字符,这些字符存储在数组c中。
#define MAXN 50000
char c[MAXN], *a[MAXN];
使用称为“后缀数组”的数据结构,这个结构是一个字符指针数组,记为a。
读取输入时,对a进行初始化,使得每个元素指向输入字符串中的相应字符:
while(ch = getchar() != EOF)
{a[n] = &c[n]; c[n++] = ch;}
c[n] = 0;
元素a[0]指向整个字符串,下一个元素指向从第二个字符开始的数组后缀。对于输入字符串“banana”,该数组能够表示如下后缀:
char *a="banana"
a[0]=banana
a[1]=anana
a[2]=nana
a[3]=ana
a[4]=na
a[5]=a
如果某个长字符串在数组c中出现了两次,那么它将出现在两个不同的后缀中,因此我们队数组进行排序以寻找相同的后缀。“banana”数组排序为:
a[0]=a
a[1]=ana
a[2]=anana
a[3]=banana
a[4]=na
a[5]=nana
然后就可以扫描数组,通过比较相邻元素来找出最长的重复字符串。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int pstrcmp(char **p, char **q)
{
return strcmp(*p, *q);
}
int comlen(char *p, char *q)
{
int i = 0;
while (*p && (*p++ == *q++))
i++;
return i;
}
#define M 1
#define MAXN 5000000
char c[MAXN], *a[MAXN];
int main()
{
int i, ch, n = 0, maxi, maxlen = -1;
while ((ch = getchar()) != EOF) {
a[n] = &c[n];
c[n++] = ch;
}
c[n] = 0;
qsort(a, n, sizeof(char *), pstrcmp);
for (i = 0; i < n-M; i++)
if (comlen(a[i], a[i+M]) > maxlen){
maxlen = comlen(a[i], a[i+M]);
maxi = i;
}
printf("%.*s\n", maxlen, a[maxi]);
return 0;
}
生成文本
基于字母:下一个字母设置为前一个字母的随机函数,或者下一个字母设置为前k个字母的随机函数。
#include <stdio.h>
#include <stdlib.h>
char x[5000000];
int main()
{
int c, i, eqsofar, max, n = 0, k = 5;
char *p, *nextp, *q;
while ((c = getchar()) != EOF)
x[n++] = c;
x[n] = 0;
p = x;
srand(1);
for (max = 2000; max > 0; max--) {
eqsofar = 0;
for (q=x;q<x+n-k+1;q++) {
for (i=0;i<k&&*(p+i)==*(q+i);i++);
if (i==k){
if (rand() % ++eqsofar == 0){
nextp = q;
}
}
}
c = *(nextp+k);
if (c == 0)break;
putchar(c);
p = nextp+1;
}
return 0;
}
基于单词
方法一:最笨的方法是随机输出字典中的单词;
方法二:稍微好点的方法是读取一个文档,对每个单词进行计数,然后根据适当的概率选择下一个输出的单词;
方法三:如果使用在生成下一个单词时考虑前面几个单词的马尔科夫链,可以得到更加令人感兴趣的文本;
香农算法:以构建[字母级别的一阶文本]为例,随机打开一本书并在该页随机选择一个字母记录下来。然后翻到另一页开始读,直到遇到该字母,此时记录下其后面的那个字母。再翻到另外一页搜索上述第二个字母并记录其后面的那个字母。依次类推。对于[字母级别的1阶、2阶文本和单词级别的0阶、1阶文本],处理过程类似。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char inputchars[4300000];
char *word[800000];
int nword = 0;int k = 2;
int wordncmp(char *p, char* q)
{
int n = k;
for ( ; *p == *q; p++, q++)
if (*p == 0 && --n == 0) return 0;
return *p - *q;
}
int sortcmp(char **p, char **q){
return wordncmp(*p, *q);
}
char *skip(char *p, int n){
for ( ; n > 0; p++)
if (*p == 0) n--;
return p;
}
int main(){
int i, wordsleft = 10000, l, m, u;
char *phrase, *p;
word[0] = inputchars;
while (scanf("%s", word[nword]) != EOF) {
word[nword+1] = word[nword] + strlen(word[nword]) + 1;
nword++;
}
for (i = 0; i < k; i++)
word[nword][i] = 0;
for (i = 0; i < k; i++)
printf("%s\n", word[i]);
qsort(word, nword, sizeof(word[0]), sortcmp);
phrase = inputchars;
for ( ; wordsleft > 0; wordsleft--) {
l = -1;
u = nword;
while (l+1 != u) {
m = (l + u) / 2;
if (wordncmp(word[m], phrase) < 0)
l = m;
else
u = m;
}
for (i = 0; wordncmp(phrase, word[u+i]) == 0; i++)
if (rand() % (i+1) == 0)
p = word[u+i];
phrase = skip(p, 1);
if (strlen(skip(phrase, k-1)) == 0)
break;
printf("%s\n", skip(phrase, k-1));
}
return 0;
}
原理
字符串问题:字符串无处不在。
字符串的数据结构:C++ STL中的sets、strings、map等。
散列:该结构的平均速度很快,易于实现。
平衡树:该结构在最坏情况下也有较好的性能,C++标准模板库的set和map的大部分实现都采用了平衡树。
后缀数组:初始化指向文本中每个字符(或每个单词)的指针数组,对其排序得到一个后缀数组。然后可以遍历该数组以查找接近的字符串,也可以使用二分搜索查找单词或短语