《算法笔记》编程笔记——第四章:散列、贪心、二分、two pointers和其他技巧
一、散列
-
bool hashTable[1000] = {false}; //如果要计算出现的次数,可以将bool变为int int hashTable[1000] = {false}; hashTable[x]++;//采取把输入的数作为数组下标来处理
-
key为整数,可以用线性探查法、平方探查法等
- 线性探查法:不断将hash值加1
- 平方探查法:H(key) + k^2(k = 1, 2, 3, 4……), H(key) - k^2(k = 1, 2, 3, 4……)
-
key不为整数
-
如果为二维坐标,可以用H§ = x * range + y;
-
如果为字符串:
int hashfunc(char s[], int len){ int id = 0; //全部为大写字母,全为小写字母,写法相同 for(int i = 0; i < len; i++){ id = id * 26 + (s[i] - 'A');//26进制转为10进制 } //最后一位为数字,前面全为字母 for( int i = 0; i < len - 1; i++){ id = id * 26 + (s[i] - 'A'); } id = id * 10 + (s[len-1] - '0'); //有大小写字母的混合,大写对应0-26,小写对应27-52 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; } } return id; }
-
二、贪心
-
贪心的总结:找到最佳排序方法,对信息进行正确的主次排序,得到最优解。
-
局部最优来达到整体最优的思想。
-
区间贪心:区间不相交问题,给出N个开区间,从中选择尽可能多的开区间,使得这些开区间之间没有交集。方法是——总是选择左端点最大的区间,左端点相同时,总是选择右端点最小的区间。
-
struct Inteval{ int x, y; }I[1000]; bool cmp(Inteval a, Inteval b){ if(a.x == b.x)return a.y < b.y; else return a.x > b.x; } int main(){ int num = 0, lastX = I[0].x; for(int i = 1; i < n; i++){ if(I[i].y <= lastX){ lastX = I[i].x;//区间最大x更新 num++;//记录个数 } } printf("%d", num); }
-
三、二分
-
二分查找适用于“查找序列中是否存在第一个满足条件的元素”,如——大于等于x,等于x……如果要找最后一个元素,则可以转换反条件的第一个元素的位置减一。
-
二分法查找模板
//查找是否有等于x的元素,序列递增;注意传入的范围为[0, n-1]!因为数组下标是从0开始 int search(int a[], int left, int right, int x){ int mid; while(left <= right){//条件有等号,当相等时,刚好有唯一满足的数;如果没有,则会返回-1。这是由程序要求决定的。 mid = (left + right) / 2; if(a[mid] == x)return mid; else if(a[mid] > x){ right = mid - 1; } else left = mid + 1; } return -1; } //查找第一个大于等于x的元素的位置,x不一定为序列中的数;传入的范围为[0, n],因为欲查询的元素有可能比序列中所有元素都大,此时应当返回n,所以范围是[0, n]而非之前的[0,n-1]!!!!!!!!!! int lower_bound(int a[], int left, int right, int x){ int mid; while(left < right){//因为底下为return left,所以这条件为小于,没有等号。如果使用等号,会陷入死循环(因为底下没有条件可以直接return位置的,只有在while循环外面才可以return) mid = (left + right) / 2; if(a[mid] >= x){//条件为大于等于。如果要查找的数为大于x,则此处条件为a[mid] > x;其余都不变 right = mid; }else{ left = mid + 1; } } return left;//当left == right的时候,返回left和right都可以。 } //查找第一个大于x的元素的位置,传入的值为[0, n] int upper_bound(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; }
-
二分法拓展
-
总结:写出f的方程式;再写一个求解的函数(例如:solve),函数里面采用二分法不断逼近方程的真实解。
-
求解方程的近似解,有精度
const double eps = 1e-5;//精度为10^-5 double f(double x){//f的方程式 return x * x -4; } double solve(double L, double R){ double left = L, right = R, mid; while(right - left > eps){ mid = (left + right) / 2; if(f(mid) > 0){ right = mid; } else{ left = mid; } } return mid;//求得精度内的解 }
-
-
快速幂
-
模板如下
//递归模板 long long binaryPow(long long a, long long b, long long m){ if(b == 0)return 1; //如果b为0,那么a^0 = 1; //b为奇数,转换为b-1 if(b%2 == 1)return a * binaryPow(a, b - 1, m) % m; else{//b为偶数,转换为b/2 long long mul = binaryPow(a, b / 2, m); return mul * mul % m; } } //迭代方法 long long binaryPow(long long a, long long b, long long m){ long long ans = 1; while(b > 0){ if(b&1){//如果b的二进制末尾为1,这种写法处理起来更快 ans = ans * a % m;//令ans累积上a } a = a * a % m; //令a平方 b >>= 1; //将b的二进制右移1位,即b = b / 2; } return ans; }
-
-
two pointers
-
思想: 给定一个递增的正整数序列和一个正整数m,求序列中两个不同位置的数a和b,使得他们的和恰好为m,输出所有满足条件的方案。如果用双重for循环来做,时间复杂度为O(N^2)。如果使用two pointers的思想,就可以将时间复杂度降低到O(n).
-
代码如下:
while(i < j){ if(a[i] + a[j] == m){ printf("%d %d\n", i, j); i++; j--; } else if(a[i] + a[j] < m){ i++;//小于m就要增大 } else{ j--; //大于m就要减小,由于i本身就是从小到大增加的,所以减少的只能是j } }
-
序列合并:假设有两个递增序列A与B,要求将它们合并为一个递增序列C
- 模板如下:
int merge(int a[], int b[], int c[], int n, int m){ int i = 0, j = 0, index = 0; while(i < n && j < m){ if(a[i] <= b[j]){ c[index++] = a[i++]; }else{ c[index++] = b[j++]; } } while(i < n)c[index++] = a[i++];//a数组多出来的数字,直接放入到c数组当中去 while(j < m)c[index++] = b[j++]; return index; //返回序列c的长度 }
-
-
归并排序
-
代码如下:
const int maxn = 100; //将数组A的[L1,R1]与[L2,R2]区间合并称为有序区间,此处L2其实就是R1+1 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];//将合并后的序列赋值返回数组A } } //将arrary数组当前区间[left, right]进行归并排序 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);//将左子区间与右子区间合并 } }
-
-
快速排序
-
代码如下:
//对区间[left, right]进行划分.此代码当序列有序的时候(比如从大到小排列,但是要求是从小到大排列,这个时候时间复杂度会到达O(n^2) int Partition(int A[], int left, int right){ int temp = A[left]; while(left < right){//只要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;//将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); } } //===================================== //鉴于以上代码的缺点,引入随机数的概念 int main(){ srand((unsigned)time(NULL)); for(int i = 0; i < 10; i++){ printf("%d", rand());//此处生成10个随机数,只是该随机数范围为[0-RAND_MAX]. //若想要生成指定区间内的随机数,那么可以用以下的式子,假设区间为[a, b] printf("%d", (int)round(1.0*rand()/RAND_MAX*(b-a) + a)); } return 0; } //故以上代码重新修改后如下 int randPartition(int a[], int left, int right){ int p = (int)round(1.0*rand()/RAND_MAX*(right-left)+left); swap(a[p], a[left]);//将第一个数换成是随机数 int temp = a[left];//将这个随机数放入临时变量temp中 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;//将相遇的下标返回 }
-
-
其他高效技巧与算法
-
打表
- 思想:在程序中一次性计算出所有需要用到的结果之后的查询直接取这些结果。 在程序B中分一次或者多次计算出所有需要用到的结果,手动将结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。
-
随机选择算法
-
问题背景:在一个无序的数组中求出第k大的数。
-
分析:如果对数组进行排序,那么时间复杂度将会到达O(nlogn)。使用随机选择算法,将可以使时间复杂度达到O(n)的程度。随机选择算法主要思想用到的是快速排序。也就是先进行一趟快排,然后比较得知快排中temp数字的大小排序情况,对要求的数进行比较,如此递归下去。
-
代码如下:
//寻找第k大的数 int randSelect(int a[], int left, int right, int k){ if(left == right)return a[left]; //p为划分后主元的位置 int p = randPartition(a,left, right); int m = p - left + 1; //第m大的数 if(m == k)return a[p]; if(k < m){ //第k大数在主元左侧 return randSelect(a, left, p-1, k); }else{ return randSelect(a, p+1, right, k-m);//注意此处是k-m大的数,因为区间变化了,所以这个数的编号需要相应地减去前m个。 } }
-
-