读书笔记
排序
排序函数
C语言
void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*)
base:待排序的数组首地址
num:数组中待排序元素数量
size:数组元素专用空间大小
compar:排序函数的指针
qsort的一个坑
qsort的坑在于比较函数返回的是int,如果使用减法, 可能会导致溢出。
C++
void sort (Iterator first, Iterator last)
void sort (Iterator first, Iterator last, Compare comp)
first 首迭代器
last 尾迭代器
comp 比较函数
建议使用C++中的sort,而不是C中的qsort,坑太多(+_+)?
坑
PAT中的排序题主要有两部分难点,第一是复杂的排序规则,这个使用写比较函数实现。
比如这题:1015 德才论 (25分)
可以看看我的solve。应该会有启发
第二是超时,这时需要用空间换时间。
排序的信息存储
一般排序题会给出很多信息,比如学生姓名,分数,排名等信息,为了方便编写代码,常常将它们存储在一个结构体里。例
struct Stu{
string name;
int score;
int rating;
}stu[10000];
比较函数的编写: 按题目意思来,通常是先比较分数,再比较成绩。所以cmp可以这样写(cmp函数是必须自己写的)
bool cmp(Stu a,Stu b){
if(a.score!=b.score) return a.score>b.score;
else return a.name<b.name;
}
上面的样例是对于时间很充裕来说:如果时间很紧张:就要换一种写法:
怎么判断时间紧不紧张呢:我暂时没有总结。
如果给出学生的信息中:id只有6位:那么可以确定时间紧张。这个时间:就要用空间换时间了
struct Stu{
int grade;
int rating;
}student[1000000];
通过把id当成数组下标:这样:通过id去寻找一个学生信息就非常快,是O(1)。
但是这样编程就有很多不同的细节。待跟新。
排名的实现:
通常甲级的排序题:都是给出要排序的序列:再通过其中id找到该学生的排名。所以排名很重要。
待跟新
分数不同的排名不同,分数相同的排名相同但占用同一个排位,例如学生成绩为
90 88 88 88 86
1 2 2 2 5
stu[0].rating = 1;
for(int i=1;i<n;i++){
if(stu[i].score == stu[i-1].score){
stu[i].r = stu[i-1].r;
}else{
stu[i].r = i+1; //这个地方我有点不懂,等做到这种题再跟新
}
}
散列
散列定义
将元素通过一个函数转换成整数,使得该整数可以尽量唯一地代表这个元素。称这个函数为散列函数
key -> hash(key)
常见的散列函数
①直接定址法
h
a
s
h
(
k
e
y
)
=
a
∗
k
e
y
+
b
hash(key) = a*key+b
hash(key)=a∗key+b
②平方取中法
h
a
s
h
(
k
e
y
)
=
(
k
e
y
∗
k
e
y
/
1000
)
%
1000000
hash(key)= (key*key /1000)\%1000000
hash(key)=(key∗key/1000)%1000000.平方后去中间位数
③除留余数法
h
a
s
h
(
k
e
y
)
=
k
e
y
%
m
o
d
hash(key) = key\%mod
hash(key)=key%mod
碰撞
如果
k
e
y
1
!
=
k
e
y
2
key1 != key2
key1!=key2但是
h
a
s
h
(
k
e
y
1
)
=
=
h
a
s
h
(
k
e
y
2
)
hash(key1) == hash(key2)
hash(key1)==hash(key2),则发生了碰撞,解决方法:
①线性探查法
②平方查找法
③链地址法
应用:
①直接定址法
给出N个正整数,再给出M个正整数。问M个数中每个数是否在N个数中出现过?
M
.
N
<
1
0
5
M.N<10^{5}
M.N<105
创建一个数组hashtable[100001],将hashtable[N]=true,其他为false。对于M个正整数,直接查找对应的下标的值是不是true即可
这种方法非常实用,用空间换时间:将查询的复杂度降到了O(1)。
在PAT里:
1
0
5
10^5
105是可以用这个方法的。
const int max = 100001;
bool hashtable[max]={false};
int main()
{
int n,m,x;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++){
scanf("%d",&x);
hashtable[x]=true;
}
for(int i=0;i<m;i++){
scanf("%d",&x);
if(hashtable[x] ==true)
printf("YAS");
else
printf("No");
}
return 0;
}
字符串hash
字符串由很多字符组成,怎么映射到整数呢?
通过把A~Z看出0~25。这样就可以把字符串映射成为26进制数,再通过26进制转换成10进制即可。转换后的数
<
2
6
l
e
n
−
1
<26^{len}-1
<26len−1,可能很大,用long long
int hash(char s[],int len){
int id=0;
for(int i=0;i<len;i++){
id=id*26+(s[i]-'A');
}
return id;
}
如果有小写,则把a~z看成26~51.转换后的数 < 5 2 l e n − 1 <52^{len}-1 <52len−1
int hash(char s[],int len){
int id=0;
for(int i=0;i<len;i++){
if(s[i]>='A' && s[i]<='Z')
id=id*52+(s[i]-'A');
else if(s[i] >='a' && s[i]<='z')
id=id*52+(s[i]-'a')+26;
}
}
递归
递归是什么:
反复调用函数,以至于把问题缩小
学习递归
看Standford CS library中这篇讲义:Binary Tree problem,独立做完这些题目。相信你能理解并设计递归了
设计法则:
1.基准情况:必须总有某些基准情形,它无序递归就能解出
2:不断推进:对于需要递归求解的情形,每一次递归调用都必须要使求解状况朝接近基准情形方向推进
3设计法则:假设所有的递归调用都能运行
4:合成效益法则:求解一个问题的同一个实例,切勿在不同的递归调用中做重复性的工作
例子
1.判断是否为回文
bool ispalindrome(char* s,int len){
if(len<2)
return true;
else
return (s[0] == s[len-1]) && ispalindrome(s+1,len-2);
}
2.反转链表递归版:
ListNode* reverseList(ListNode* head) {
ListNode* prev =NULL;
helper(NULL,head,&prev);
return prev;
}
void helper(ListNode* pre,ListNode* cur,ListNode** head){
if(cur == NULL)
*head = pre;
else{
helper(cur,cur->next,head);
cur->next = pre;
}
}
Fibonacci不是一个好递归的例子,因为如果用递归写的化,很容易写成O(
2
n
2^{n}
2n),详情见《数据结构与算法,c语言描述》第二章内容.
3:全排列Full Permutation
从递归的角度去考虑,如果把问题描述成“输出1~n这n个整数的全排列”,那么它就可以被分为若干子问题:“输出以1开头的全排列”, “输出以2开头的全排列”,…“输出以n开头的全排列”。
const int max = 11;
int n,p[max],hash[max]={false};
void generator(int index){
if(index == n+1){
for(int i-1;i<=n;i++)
printf("%d",p[i]);
printf("\n");
return;
}
for(int i=1;i<=n;i++){
if(hash[i] == false){
p[index] = i;
hash[i] = true;
generator(index+1);
hash[i] = false;
}
}
}
4.n皇后问题
考虑到每行只能放置一个皇后,每列也只能放置一个皇后。那么如果把n列皇后所在的行号依此写入,那么就会是1~n的一个排列。于是可以在全排列的代码基础上进行求解。由于当到达递归边界时表示生成一个排列。所有需要在其内部判断是否为合法方案。即遍历每个皇后,判断它们是否在同一条对角线上。(不在同一行同一列是显然的。)
int count = 0;
void generator(int index){
if( index == n+1){
bool flag = true;
for(int i=0; i<=n; i++){
for(int j=0;j<=n; j++){
if(abs(j-i) == abs(P[i]-P[j])
flag = false;
}
}
if(flag) count++;
return;
}
for(int x=1; x<=n; x++){
if(hashTable[x] == false){
P[index] = x;
hashTable[x] = true;
generator(index+1)
hashTable[x] = false;
}
}
}
回溯剪枝版:
即在到达递归边界前的某层,由于一些事实导致已经不需要再往下递归,就可以直接返回下一层。一般把这种做法称为回溯剪枝
void generator(int index){
if(index == n+1){
count++;
return;
}
for(int x=1; x<=n; x++){
if(hashTable[x] == false){
bool flag = true;
for(int pre = 1; pre<index; pre++){
if(abs(index-pre) == abs(x-P[pre]){
flag=false;
break;
}
}
if(flag){
P[index] = x;
hashTable[x] = true;
generator(index+1);
hashTable[x] = false;
}
}
}
}
贪心
贪心是求解一类最优化问题的方法,它总是考虑在当前状态下局部最优的策略,来使全局的结果达到最优
(没学过贪心>﹏<,等学了再补充一点)
二分
二分的思想其实是把问题不断拆分为一半,这样就把问题降了
O
(
l
o
g
)
O(log)
O(log).很有分而治之中分的感觉.
俗话说的好:十个二分九个错,自己写二分很容易错怎么办呢?
C++ STL中内置了四个与二分查找有管的算法
binary_search
lower_bound
upper_bound
equal_range
C 中的二分查找
void *bsearch(const void *key, const void *base, size_t nitems, size_t size,
int (*compar)(const void *, const void *))
很推荐大家在PAT里使用这几个算法。
二分查找
书上探讨了一个更进一步的问题:查找第一个大于等于X的位置L,和第一个小于X的位置R,也就是说查找X在序列中的区间
首先:大于等于X的位置L:
/*如果A[N],则传入的初值应该为[0,N]
* A[mid] ==x,证明x第一个元素位置为mid或mid左侧
* A[mid] > x 证明x在mid左侧
* A[mid] < x 证明x在mid右侧
*/
int binarysearch(int A[],int left,int right,int x){
int mid;
while(left<right){
mid = (left+right)/2;
if(A[mid] >=x)
right = mid;
else
left = mid+1;
}
return left;
}
在查询第一个大于X的位置
/*
*
* A[mid] == x 证明第一个大于X的位置在右侧 ,left = mid+1
* A[mid] > x 证明第一个大于X的位置在左侧或者mid,right = mid
* A[mid] < x 证明第一个大于X的位置在右侧,left = mid+1
*/
int binarysearch(int A[],int left,int right,int x){
int mid;
while(left<right){
mid = (left+right)/2;
if(A[mid] <= x)
left = mid+1;
else
right = mid;
}
}
二分法拓展
二分的精髓 : 其实是利用序列的有序性,去间接的求值。
而二分法的拓展:利用序列的有序性,间接的求值。可能直接求值很难,或者根本不行。
用作当遇到很难或不能求出的值时,用二分法去估值,或计算.
example 1:sqrt()
LeetCode上面有一题:mysqrt 就是用二分法求根
如图,sqrt(x)是增函数,符合二分法递增数列,用[1,x]的区间去逼近(注意0,和1),这样就能求出sqrt。
无精度版:
int sqrt(int x){
if(x<2)
return x;
int left =1,right = x,mid;
while(left < (right-1)){
mid = (left+right)/2;
if(x/mid <mid)
right = mid;
else if(x/mid >mid)
left = mid+1;
else
return mid;
}
}
要求精度版:
double f(double x){
return x*x;
}
const double eps = 1e-5;
double sqrt(int x){
double left = 1;
double right = x;
double mid;
while((right-left)>eps){
mid = (left+right)/2;
if(f(mid) >x)
right = mid-eps;
else if(f(mid) < x)
left = mid+eps;
else
return mid;
}
return left;
}
example 2 : 求解问题
对于一个多项式
f
(
x
)
=
a
n
x
n
+
a
n
−
1
x
(
n
−
1
)
.
.
.
.
.
.
a
1
f(x) = a_nx^n+a_{n-1}x^(n-1)......a_1
f(x)=anxn+an−1x(n−1)......a1,如果
f
(
x
)
=
b
f(x) = b
f(x)=b,求x此时为:?
这是我自己想出来的题目,但是其实是很纯粹化的,一个增函数(增序列)(减函数也行),一个很难或不能求出的值,用二分法去解。
最简单的例子:
f
(
x
)
=
x
2
f(x) =x^2
f(x)=x2,当
f
(
x
)
=
b
f(x)=b
f(x)=b时,求x。
这就变为了在区间[1,b],求
x
2
x^2
x2 =b,很明显的二分了
example 3:装水问题
问题:往半圆的储水装置装水,求往里面注入多高的水时,
S
1
/
S
2
=
r
S1/S2=r
S1/S2=r(S1为侧面水的面积,S2 为侧面储水装置的面积)
example 2:其实是纯粹的题,我估计题目都会披着应用题的衣服,其实都是对多项式求和.则如上面这题,如果你硬要计算(你懂的)
请告诉我,h和r的关系你怎么计算。显然,随着h的增加,r肯定增加,所以是增序列。所以,此时二分法可以现神威
example 4: 快速幂
给定三个正整数a, b, m (a<
1
0
9
{10^9}
109, b<
1
0
18
{10^{18}}
1018, 1<m<
1
0
9
{10^9}
109), 求
a
b
%
m
{a^b\%m}
ab%m
快速幂基于二分的思想
①如果b是奇数, 那么有
a
b
=
a
∗
a
b
−
1
{a^b = a*a^{b-1}}
ab=a∗ab−1
②如果b是偶数, 那么有
a
b
=
a
b
/
2
∗
a
b
/
2
{a^b = a^{b/2}*a^{b/2}}
ab=ab/2∗ab/2
所以时间复杂度为
O
(
l
o
g
b
)
{O(logb)}
O(logb)
typedef long long LL;
LL binarypow(LL a, LL b, LL m){
if(b==0) return 1;
if(b %2 == 1) return a*binarypow(a, b-1, m) % m;
else{
LL mul = binarypow(a, b/2, m);
return mul*mul %m;
}
}
快速幂迭代写法
LL binarypow(LL a, LL b, LL m){
LL ans = 1;
while ( b>0 ){
if( b&1 )
ans = ans * a % m;
a = a * a % m;
b >>= 1;
}
return ans;
}
two pointers
利用问题本身和序列的特性,使用两个下标i,j对序列进行扫描,以较低复杂度解决问题。建议写几道leetcode的题体会下
书上给了一道题:
给一个递增序列和一个正整数M,求序列中不同的两个位置的数A,B。使得A+B=M
two sum问题
while(i<l){
if(a[i]+a[j] ==M){
printf("%d %d\n",i,j);
i++;
j--;
}
else if(a[i]+a[j] > M)
j--;
else
i++
}
归并排序
这方面的题:PAT
wiki: merge sort
visual DS: merge sort
const int maxn=100;
void merge(int A[], int L1, int R1, int L2, int R2){
int i = L1, j = L2;
int temp[maxn], index = 0;
while (i <= R1 && j<= R2){
if(A[i] <= A[j])
temp[index++] = A[i++];
else
temp[index++] = A[j++];
}
while(i<=R1)
temp[index++] = A[i++];
while(j<=R2)
temp[index++] = A[j++];
for(int i=0; i<index; i++)
A[L1+i] = temp[i];
}
void mergesort(int A[], int left, int right){
if (left< right){
int mid = (left+right)/2;
mergesort(A, left, mid);
mergesort(A, mid+1, right);
merge(A, left, mid, mid+1, right);
}
}
非递归版:
void mergesort(int A[],int n){
for(int step=2;step/2 <=n;step*=2){
for(int i=0;i<n;i+=step){
int mid = i+step/2-1;
if(mid+1 <n)
merge(A[],i,mid,mid+1,min(i+step-1,n-1));
}
}
}
快速排序
同样:
wiki: quick sort
visial DS: quick sort
//[left, right]
int Partition(int A[], int left, int right){
int temp = A[left];
while(left < right){
while(left < right && A[right]>temp) right--;
A[left] = A[right];
while(left < right && A[left]<=temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
void quicksort(int A[], int left, int right){
if(left < right){
int pos = Partition(A, left, right);
quicksort(A, left, pos-1);
quicksort(A, pos+1, right);
}
}
other
打表
把所有可能需要的结果事先计算出来。再后面需要的时候,直接查表获得。(还没用过,感觉是个挺好的方法)
活用递归
很多题目需要细心考虑过程中是否可能存在递推关系,如果存在,能使时间复杂度下降不少。
例如一类涉及序列的题目来说,如果序列的每一位所需要计算的值都可以通过该位左右两侧的结果计算得到,那么就能考虑所谓‘左右两侧的结果是否能通过递推预处理来得到 ’
感觉书上没有总结的太好,等查点资料再更新
该知识点的题目有几个PAT+solution
随机选择算法
problem: 给一个乱序的序列,返回第K大数.
int randPartition(int A[],int left,int right){
srand((unsigned)time(NULL));
int p = round(1.0/rand()*(right-left))+left;
swap(A[p],A[left]);
int temp = A[left];
while(left<right){
while(left<right && A[right] >temp) right--;
A[left++] = A[right];
while(left<right && A[left] < temp) left++;
A[right--] = A[left];
}
A[left] = temp;
return left;
}
int randselect(int A[],int left,int right,int K){
if(left == right) return A[left];
int p = partition(A,left,right);
int M = p-left+1;
if(M == K)
return A[M];
else if (M < K)
return randselect(A,p+1,right,K);
else
return randselect(A,left,p-1,K);
}