《算法笔记》第4章常用技巧及排序算法
文章目录
- 二、 常用技巧
- 1. 散列
- 2. 递归
- 2.1 全排列问题
- 2.2 n皇后问题
- 2.3 回溯法优化n皇后问题
- 3. 贪心
- 3.1 简单贪心
- 3.2 区间贪心
- 4. 二分
- 4.1 二分查找
- 4.2 二分法拓展(以求根号2近似值为例)
- 4.3 快速幂
- 5. two pointers(双指针)
- 5.1 求递增数列中的A+B=M
- 5.2 序列合并问题
- 5.3 [归并排序](#4. 归并排序)
- 5.4 [快速排序](#5. 快速排序)
- 6. 打表
- 7. 活用递推
- 8. 随机选择算法
- 三、 排序算法
- 1. 冒泡排序
- 2. 选择排序
- 3. 插入排序
- 4. 归并排序
- 4.1 二路归并(递归)
- 4.2 二路归并(非递归)
- 4.3 用sort函数替代merge实现
- 5. 快速排序
- 5.1 总是以A[left]为主元
- 5.2 随机主元
二、 常用技巧
1. 散列
原理是把一个元素key通过散列函数H转换为H(key)。
最常见的散列法:把key作为数组下标。
散列函数:直接定址法,平方取中法,除留余数法。
解决冲突的方法:线性探测法,平方探测法,链地址法。
字符串hash:把26个字母看做26进制(0~25)的数,将26进制转换为10进制,如BCD看做731。
2. 递归
- 递归边界:返回最简单底层的结果。
- 递归式:减少数据规模并向下一次递归。
2.1 全排列问题
#include <cstdio>const int maxn = 11;
//输出1~n的全排列,P为当前排列,hash记录整数x是否已经在P中
int n, P[maxn], hash[maxn] = {false};void fullP(int index) { //当前处理排列的第index号位if (index == n + 1) { //递归边界,已经处理完排列的1~n位for (int i = 1; i <= n; i++) {printf("%d", P[i]); //输出当前排列}printf("\n");return;}for (int x = 1; x <= n; x++) {if (hash[x] == false) {P[index] = x;hash[x] = true;fullP(index + 1); //递归式hash[x] = false;}}
}int main() {n = 3; //输出1~3的全排列fullP(1); //从P[1]开始填return 0;
}
2.2 n皇后问题
#include <cstdio>
#include <cmath>const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,count记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, count = 0;void fullP(int index) { //当前处理排列的第index号位if (index == n + 1) { //递归边界,已经处理完排列的1~n位bool flag = true;for (int i = 1; i <= n; i++) { //遍历任意两个皇后for (int j = i + 1; j <= n; j++) {if (abs(i - j) == abs(P[i] - P[j])) { //在一条对角线上flag = false;}}}if (flag) count++;return;}for (int x = 1; x <= n; x++) {if (hash[x] == false) {P[index] = x;hash[x] = true;fullP(index + 1); //递归式hash[x] = false;}}
}int main() {n = 8; //八皇后问题fullP(1); //从P[1]开始填printf("%d", count);return 0;
}
2.3 回溯法优化n皇后问题
#include <cstdio>
#include <cmath>const int maxn = 11;
// n皇后问题,P为当前排列,hash记录整数x是否已经在P中,ans数组记录每种情况,cnt记录合法排列个数
int n, P[maxn], hash[maxn] = {false}, ans[1000] = {0}, cnt = 0;void fullP(int index) { //当前处理排列的第index号位if (index == n + 1) { //递归边界,能到达这里的一定是合法的for (int i = 1; i <= n; i++) {ans[cnt] = ans[cnt] * 10 + P[i]; //将结果数组转化为整数}cnt++;return;}for (int x = 1; x <= n; x++) {if (hash[x] == false) {bool flag = true;for (int pre = 1; pre < index; pre++) { //遍历之前的皇后//第index列皇后的行号为x,第pre列皇后的行号为P[pre]if (abs(index - pre) == abs(x - P[pre])) {flag = false;break;}}if (flag) {P[index] = x;hash[x] = true;fullP(index + 1); //递归式hash[x] = false;}}}
}int main() {n = 8; //八皇后问题fullP(1); //从P[1]开始填printf("%d\n", cnt);for (int i = 0; i < cnt; i++) {printf("%d\n", ans[i]);}return 0;
}
3. 贪心
局部最优推全局最优。
3.1 简单贪心
3.2 区间贪心
N个开区间,选择尽可能多的开区间,使这些开区间两两没有交集。
- 若开区间1被开区间2包含,则选开区间2。
- 去除开区间包含的情况,先选所有开区间左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;const int maxn = 110;
struct Inteval {int x, y; //开区间左右段点
} I[maxn];
bool cmp(Inteval a, Inteval b) {if (a.x != b.x)return a.x > b.x; //先按左端点从大到小排序elsereturn a.y < b.y; //左端点相同的按右端点从小到大
}int main() {int n;while (scanf("%d", &n), n != 0) {for (int i = 0; i < n; i++) {scanf("%d%d", &I[i].x, &I[i].y);}sort(I, I + n, cmp);// ans记录不相交区间个数,lastX记录上一个被选中区间的左端点int ans = 1, lastX = I[0].x;for (int i = 1; i < n; i++) {if (I[i].y <= lastX) { //如果该区间右端点在lastX左边lastX = I[i].x; //以I[i]作为新选中的区间ans++;}}printf("%d\n", ans);}return 0;
}
4. 二分
4.1 二分查找
严格单调递增数列中查找元素
int binarySearch(int arr[], int left, int right, int key) {while (left <= right) {//下列句子可以优化为mid = left + (right - left) / 2,防止溢出int mid = (left + right) / 2; //取中点if (arr[mid] == key){ //找到return mid;} else if (arr[mid] > key) { //中间的数大于keyright = mid - 1;} else { //中间的数小于keyleft = mid + 1;}}return -1; //查找失败,返回-1
}
查找第一个满足条件C的元素
int binarySearch(int arr[], int left, int right, int key) {while (left < right) { //对[left,right]来说,left=right意味着到唯一位置int mid = left + (right - left) / 2;//下面if的括号内填入找的条件,如下是找第一个大于等于key的元素if (arr[mid] >= key) { //条件成立,第一个满足条件的元素的位置<=midright = mid;} else { //条件不成立,则第一个满足条件的元素的位置>midleft = mid + 1;}}return left; //返回夹出的位置
}
查找最后一个满足条件C的元素
先求第一个满足**条件"!C"**的元素,然后将该位置减1。
4.2 二分法拓展(以求根号2近似值为例)
#include <cstdio>const double eps = 1e-5;
double f(double x) {return x * x;
}
double calSqrt() {double left = 1, right = 2, mid;while (right - left > eps) {mid = (left + right) / 2;if (f(mid) > 2) {right = mid;} elseleft = mid;}return mid;
}int main() {printf("%f", calSqrt());return 0;
}
4.3 快速幂
给定三个正整数 a 、 b 、 m ( a < 1 0 9 , b < 1 0 6 , 1 < m < 1 0 9 ) a、b、m(a<10^9,b<10^6,1<m<10^9) a、b、m(a<109,b<106,1<m<109),求解 a b % m a^b\%m ab%m
递归写法
- 若 b b b是奇数,则 a b = a ∗ a b − 1 a^b =a*a^{b-1} ab=a∗ab−1
- 若 b b b是偶数,则 a b = a b / 2 ∗ a b / 2 a^b=a^{b/2}*a^{b/2} ab=ab/2∗ab/2
typedef long long LL;
//求a^b%m,递归写法
//若初始时a大于m,则在进入函数前先让a对m取模
//若m=1,则直接判定为0
LL binaryPow(LL a, LL b, LL m) {if (b == 0) return 1;// b为奇数,转换为b-1if (b % 2 == 1) { //可用if (b & 1)替代return a * binaryPow(a, b - 1, m) % m;} else { // b为偶数,转换为b/2LL mul = binaryPow(a, b / 2, m);return mul * mul % m;}
}
迭代写法
可以证明,任意的 a b a^b ab都可以表示为 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2k、…a4、a2、a1中若干项的乘积。若 b b b的二进制的 i i i位为1,则 a 2 i a^{2i} a2i就选中,注意到 a 2 k 、 … a 4 、 a 2 、 a 1 a^{2k}、…a^4、a^2、a^1 a2k、…a4、a2、a1序列中的前一项总是等于后一项的平方。
typedef long long LL;
//求a^b%m,递归写法
LL binaryPow(LL a, LL b, LL m) {LL ans = 1;while (b > 0) {if (b & 1) { //如果b的二进制末尾为1,相当于b为奇数ans = ans * a % m; //令ans累积上a}a = a * a % m; //令a平方,相当于a^2i序列b >>= 1; // b二进制右移一位,相当于b/=2}return ans;
}
5. two pointers(双指针)
5.1 求递增数列中的A+B=M
void add(int arr[], int n, int m) {int i = 0, j = n - 1;while (i < j) {if (arr[i] + arr[j] == m) { //找到满足的情况printf("%d+%d=%d\n", arr[i], arr[j], m);i++;j--;} else if (arr[i] + arr[j] < m) { // i往右移动i++;} else // j往左移动j--;}
}
5.2 序列合并问题
将两个递增序列A与B合并为一个递增序列C。
int merge(int A[], int B[], int C[], int n, int m) {int i = 0, j = 0, index = 0; // i指向A[0],j指向B[0]while (i < n && j < m) {if (A[i] <= B[j]) { //选择序列A和B中最小的元素加入序列CC[index++] = A[i++];} else {C[index++] = B[j++];}}while (i < n) C[index++] = A[i++]; //将序列A中剩余元素直接加入序列Cwhile (j < m) C[index++] = B[j++]; //将序列B中剩余元素直接加入序列Creturn index; //返回序列C的长度
}
5.3 [归并排序](#4. 归并排序)
5.4 [快速排序](#5. 快速排序)
6. 打表
- 在程序中一次性计算出所有需要的结果,之后的查询直接取这些结果,如素数表。
- 在程序B中分一次或多次计算出所有需要的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果,如提前计算出n皇后问题的方案数。
- 对没有思路的题,先用暴力程序计算小范围内的结果,然后找规律。
7. 活用递推
找到F(n)与F(n-1)的递推关系,简化计算。
8. 随机选择算法
找到一个无序数组中第K大的数(数组中元素各不相同)
int randPartition(int A[], int left, int right) {//生成[left,right]内的随机数int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));swap(A[p], A[left]); //交换A[p]和A[left]int temp = A[left];while (left < right) {while (left < right && A[right] > temp) right--; //反复左移rightA[left] = A[right]; //将A[right]移到A[left]while (left < right && A[left] < temp) left++; //反复右移leftA[right] = A[left]; //将A[left]移到A[right]}A[left] = temp; //把temp放到left和right相遇的地方return left; //返回相遇的下标
}int randSlect(int A[], int left, int right, int K) {if (left == right) return A[left]; //递归边界int p = randPartition(A, left, right); //划分后主元的最终位置为pint M = p - left + 1; // A[p]是A[left,right]中的第M大if (K == M) return A[p]; //找到第K大数if (K < M) { //第K大数在主元左侧return randSlect(A, left, p - 1, K);} else //第K大数在主元右侧return randSlect(A, p + 1, right, K - M);
}
应用举例
给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原交集、交为空集,同时在两个子集合的元素个数 n 1 n_1 n1与 n 2 n_2 n2之差的绝对值 ∣ n 1 − n 2 ∣ \lvert n_1-n_2 \rvert ∣n1−n2∣尽可能小的前提下,要求它们各自的元素之和 S 1 S_1 S1与 S 2 S_2 S2之差的绝对值 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert ∣S1−S2∣尽可能大,并求 ∣ S 1 − S 2 ∣ \lvert S_1-S_2 \rvert ∣S1−S2∣的最大值。
该问题等价于求原集合中第n/2大元素,同时根据这个元素把集合划分为两部分,使得其中一个子集合中的元素都不小于这个数,而另一个子集合中的元素都大于这个数。因此使用随机选择算法较为简便。
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <cmath>
#include <algorithm>
using namespace std;const int maxn = 100010;
int A[maxn], n;int randPartition(int A[], int left, int right) {//生成[left,right]内的随机数int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));swap(A[p], A[left]); //交换A[p]和A[left]int temp = A[left];while (left < right) {while (left < right && A[right] > temp) right--; //反复左移rightA[left] = A[right]; //将A[right]移到A[left]while (left < right && A[left] < temp) left++; //反复右移leftA[right] = A[left]; //将A[left]移到A[right]}A[left] = temp; //把temp放到left和right相遇的地方return left; //返回相遇的下标
}int randSlect(int A[], int left, int right, int K) {if (left == right) return A[left]; //递归边界int p = randPartition(A, left, right); //划分后主元的最终位置为pint M = p - left + 1; // A[p]是A[left,right]中的第M大if (K == M) return A[p]; //找到第K大数if (K < M) { //第K大数在主元左侧return randSlect(A, left, p - 1, K);} else //第K大数在主元右侧return randSlect(A, p + 1, right, K - M);
}int main() {//初始化随机数种子srand((unsigned)time(NULL));// sum和sum1记录所有整数之和与切分后前n/2个元素之和int sum = 0, sum1 = 0;scanf("%d", &n); //整数个数for (int i = 0; i < n; i++) {scanf("%d", &A[i]);sum += A[i];}randSlect(A, 0, n - 1, n / 2);for (int i = 0; i < n / 2; i++) {sum1 += A[i];}printf("%d", (sum - sum1) - sum1); //求两个子集合的元素和之差return 0;
}
三、 排序算法
1. 冒泡排序
void bubbleSort(int arr[], int n) {int flag = 0; //标记本轮是否发生元素交换for (int i = 1; i <= n - 1; i++) {for (int j = 0; j < n - i; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;flag = 1;}}if (!flag) break;}
}
2. 选择排序
void selectSort(int arr[], int n) {for (int i = 0; i < n; i++) {int min = i; //标记最小元素的下标for (int j = i + 1; j < n; j++) {if (arr[j] < arr[min]) {min = j;}}int temp = arr[i];arr[i] = arr[min];arr[min] = temp;}
}
3. 插入排序
void insertSort(int arr[], int n) {for (int i = 1; i < n; i++) {int temp = arr[i], j = i;while (j > 0 && temp < arr[j - 1]) { //元素后移arr[j] = arr[j - 1];j--;}arr[j] = temp; //插入到对应的位置}
}
4. 归并排序
4.1 二路归并(递归)
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2) {int i = L1, j = L2; // i指向A[L1],j指向A[L2]int temp[maxn], index = 0; // temp临时存放合并后的序列,index为其元素个数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++]; //将[L1,R1]中剩余元素直接加入序列Cwhile (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列Cfor (i = 0; i < index; i++) {A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A}
}void mergeSort(int A[], int left, int right) {if (left < right) {int mid = (left + right) / 2; //取[left,right]中点mergeSort(A, left, mid); //递归,对左子区间[left,mid]归并排序mergeSort(A, mid + 1, right); //递归,对右子区间[mid+1,right]归并排序merge(A, left, mid, mid + 1, right); //将左子区间和右子区间合并}
}
4.2 二路归并(非递归)
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2) {int i = L1, j = L2; // i指向A[L1],j指向A[L2]int temp[maxn], index = 0; // temp临时存放合并后的序列,index为其元素个数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++]; //将[L1,R1]中剩余元素直接加入序列Cwhile (j <= R2) temp[index++] = A[j++]; //将[L2,R2]中剩余元素直接加入序列Cfor (i = 0; i < index; i++) {A[L1 + i] = temp[i]; //将合并后的序列赋值返回序列A}
}void mergeSort(int A[], int n) {// step为组内元素个数,step/2为左子区间元素个数for (int step = 2; step / 2 <= n; step *= 2) {//每step个元素一组,组内前step/2和后step/2个元素进行合并for (int i = 0; i < n; i += step) { //对每一组int mid = i + step / 2 - 1;if (mid + 1 <= n) { //右子区间存在元素则合并,注意最后一个右子区间//左子区间为[i,mid],右子区间为[mid+1,min(i+step-1,n)]merge(A, i, mid, mid + 1, min(i + step - 1, n));}}}
}
4.3 用sort函数替代merge实现
void mergeSort(int A[], int n) {// step为组内元素个数,step/2为左子区间元素个数for (int step = 2; step / 2 <= n; step *= 2) {//每step个元素一组,组内前step/2和后step/2个元素进行合并for (int i = 0; i < n; i += step) { //对每一组sort(A + i, A + min(i + step, n + 1));}}
}
5. 快速排序
5.1 总是以A[left]为主元
//区间划分
int partition(int A[], int left, int right) {int temp = A[left]; //以A[left]为主元while (left < right) {while (left < right && A[right] > temp) right--; //反复左移rightA[left] = A[right]; //将A[right]移到A[left]while (left < right && A[left] < temp) left++; //反复右移leftA[right] = A[left]; //将A[left]移到A[right]}A[left] = temp; //把temp放到left和right相遇的地方return left; //返回相遇的下标
}
//快速排序
void quickSort(int A[], int left, int right) {if (left < right) { //当前区间的长度大于1int pos = partition(A, left, right); //划分左右子区间quickSort(A, left, pos - 1); //对左子区间递归进行快速排序quickSort(A, pos + 1, right); //对右子区间递归进行快速排序}
}
5.2 随机主元
int randPartition(int A[], int left, int right) {//生成[left,right]内的随机数int p = (int)(round(1.0 * rand() / RAND_MAX * (right - left) + left));swap(A[p], A[left]); //交换A[p]和A[left]int temp = A[left];while (left < right) {while (left < right && A[right] > temp) right--; //反复左移rightA[left] = A[right]; //将A[right]移到A[left]while (left < right && A[left] < temp) left++; //反复右移leftA[right] = A[left]; //将A[left]移到A[right]}A[left] = temp; //把temp放到left和right相遇的地方return left; //返回相遇的下标
}void quickSort(int A[], int left, int right) {if (left < right) { //当前区间的长度大于1int pos = randPartition(A, left, right); //划分左右子区间quickSort(A, left, pos - 1); //对左子区间递归进行快速排序quickSort(A, pos + 1, right); //对右子区间递归进行快速排序}
}
《算法笔记》第4章常用技巧及排序算法相关推荐
- 对以下6种常用的内部排序算法进行比较:起泡排序、直接插入排序、简单选择排序、快速排序、希尔排序、堆排序。
题目要求: (1)对以下6种常用的内部排序算法进行比较:起泡排序.直接插入排序.简单选择排序.快速排序.希尔排序.堆排序. (2)待排序表的表长不小于100:其中的数据要用伪随机数产生程序产生:至少要 ...
- <<算法很美>>——(三)十大排序算法(上)
目录 前言 冒泡排序 图解冒泡 代码实现 冒泡优化 选择排序 图解选排 代码实现 插入排序 图解插入 代码实现 希尔排序 图解希尔 代码实现: 归并排序 图解归并 代码实现 快速排序 图解快排 ...
- 《算法不好玩》专题二:基础排序算法
视频链接:<算法不好玩>专题二 2-1选择排序 无序数组→有序数组 基于排序算法可以学习的话题:「时间复杂度」.「递归」.「循环不变量」 排序算法可以分为:「基于比较的排序算法」.「非比较 ...
- 《算法笔记》第4章常用技巧
文章目录 常用技巧 1. 散列 2. 递归 2.1 全排列问题 2.2 n皇后问题 2.3 回溯法优化n皇后问题 3. 贪心 3.1 简单贪心 3.2 区间贪心 4. 二分 4.1 二分查找 4.2 ...
- 【Java数据结构与算法】第七章 冒泡排序、选择排序、插入排序和希尔排序
第七章 冒泡排序.选择排序.插入排序和希尔排序 文章目录 第七章 冒泡排序.选择排序.插入排序和希尔排序 一.冒泡排序 1.基本介绍 2.代码实现 二.选择排序 1.基本介绍 2.代码实现 三.插入排 ...
- 常用的比较排序算法总结
写在前面 一直很惧怕算法,总是感觉特别伤脑子,因此至今为止,几种基本的排序算法一直都不是很清楚,更别说时间复杂度.空间复杂度什么的了. 今天抽空理了一下,其实感觉还好,并没有那么可怕,虽然代码写出来还 ...
- 强化学习经典算法笔记(十九):无监督策略学习算法Diversity Is All You Need
强化学习经典算法笔记19:无监督策略学习算法Diversity Is All You Need DIAYN核心要点 模型定义 目标函数的构造 DIAYN算法细节 目标函数的优化 SAC的训练 判别器的 ...
- 常用七大经典排序算法总结(C语言描述)
目录 一.交换排序 1.冒泡排序 2.快速排序 二.插入排序 1.直接插入排序 2.希尔(shell)排序 三.选择排序 1.直接选择排序 2.堆(Heap)排序 四.归并排序 正文 简介 其中排序算 ...
- php常用算法的时间复杂度,php的几个经典排序算法及时间复杂度和耗时
$arr = []; for ($i=0; $i 测试结果:排序用时(秒):5.2821290493011 php排序里的经典算法首先想到的就是冒泡排序法, 排序思想:两两交换小数上浮大数下沉每轮浮出 ...
最新文章
- VirtualBox uuid冲突问题
- 最强去马赛克AI来了,分分钟回到无马世界,连像素风《我的世界》人物都能复原...
- python-sendcmd被动模式访问ftp
- flink state ttl
- 【转】oracle number与java中long、int的对应
- JS异步操作新体验之 async函数
- java长连接socket【转】http://jiewo.iteye.com/blog/1562168
- Java内存模型–快速概述和注意事项
- sql怎么撤回update_零基础快速自学SQL,2天足矣!
- 关于微型计算机的原理 叙述正确的是,微型计算机原理练习附答案概念.doc
- jenkins简介及docker部署
- Pannellum:实例之为全景图添加指南针
- mysql b树深度_为什么Mysql用B+树做索引而不用B树
- 多媒体文件格式全解说
- 创建一个Scrapy爬虫项目并运行
- 3DMax制作下雨场景
- pve万兆网卡驱动_PVE+lede+DSM网卡硬盘直通+win10
- 怎样降低硕士论文重复率
- 东南大学计算机学院程茜雅,满满的黑科技,这份录取通知书火了!
- 网络DNS域名转换成IP地址
热门文章
- STM32开发笔记103: 24位模数转换芯片ADS1258使用方法(概述)
- Orin 调试GMSL camera 96712手册重点
- samba服务器的功能
- PMI-ACP(Agile Certified Practitioner)敏捷管理专业人士资格认证考试知识点汇总
- 虚拟机给openwrt添加硬盘并且配置smb
- leetcode: 70. Climbing Stairs
- C语言:到底是不是太胖了
- 归并排序(JAVA版)
- 大数据发展必备三个条件
- 3GPP TS 23501-g51 中英文对照 | 4.2.10 Architecture Reference Model for ATSSS Support