一. 欧几里得算法(Euclidean Algorithm)
算法描述
欧几里得算法,又称辗转相除法,用于计算两个整数的最大公约数(GCD)。算法基于这样一个事实:两个整数的最大公约数与其中较小的数和两数的差的最大公约数相同。通过递归或迭代地应用这一性质,我们可以找到两个整数的最大公约数。
算法步骤
- 确定两个非负整数
a
和b
,其中a > b
。 - 使用
a
除以b
得到余数r
。 - 将
b
赋值给a
,将余数r
赋值给b
。 - 重复步骤2和3,直到
b
为0。此时a
即为所求的最大公约数。
代码模板
//模板一:
int gcd(int a, int b)
{
while(b)
{
int r = a % b;
a = b;
b = r;
}
return a;
}
//模板二:
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
拓展题目链接
二. Fibonacci 数列
递归解法(不推荐使用)
int fibonacci(int n)
{
//数列由 0 和 1 开始
if(n <= 2) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
递推解法
const int N = 100000000;
int f3(int n)
{
a[1] = a[2] = 1;
for (int i = 3; i <= n; i ++ )
{
a[i] = a[i - 1] + a[i - 2];
}
return a[n];
}
递归+滚动变量
int fibonacci(int n)
{
int a = 1, b = 1;
int sum;
for(int i = 3; i <= n; i++)
{
sum = a+b;
a = b;
b = sum;
}
return b;
}
拓展题目链接
三. 归并排序
归并排序(Merge Sort)是一种典型的分治(Divide and Conquer)算法,它将一个大问题分解成两个或更多个相同或相似的小问题,递归地解决这些小问题,然后将解决方案组合起来,以解决原始问题。
算法描述
归并排序的主要思想是将待排序的序列分割成若干个子序列,每个子序列是一个有序的序列。然后再把有序子序列合并为整体有序序列。
算法步骤
-
分解:将当前需要排序的数组按照中间索引分成两个子数组,直到子数组的大小为1。
-
递归进行排序:递归地对子数组进行排序。由于数组被分解成单个元素,所以它们被认为是有序的。递归进行,直到合并成1个完整的数组。
-
合并:将两个有序子数组合并成一个有序数组。这是归并排序算法的核心步骤。
合并步骤详解
-
有数组
q
, 左端点l
, 右端点r
; -
确定划分边界
mid
; -
递归处理子问题
q[l..mid]
,q[mid+1..r]
; -
合并子问题
- 主体合并:至少有一个小数组添加到
tmp
数组中; - 收尾:可能存在的剩下的一个小数组的尾部直接添加到
tmp
数组中; - 复制回来:
tmp
数组覆盖原数组;
- 主体合并:至少有一个小数组添加到
代码模板
void merge_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return ;
//第一步:分成子问题
int mid = r + l >> 1;
//第二步:递归处理子问题
merge_sort(q, l, mid);
merge_sort(q, mid+1, r);
//第三步:合并子问题
int k = 0, i = l, j = mid+1;
while(i <= mid && j <= r)
if(q[i] <= q[j]) tmp[k++] = q[i++];
else tmp[k++] = q[j++];
while(i <= mid) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(k = 0, i = l; i <= r; k++, i++) q[i] = tmp[k];
}
题目链接
四. 快速排序算法
算法描述
快速排序是一种采用分治策略的排序算法。它选择一个基准元素(pivot),通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比基准元素小,另一部分的所有数据都比基准元素大,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行。
算法步骤
-
选择基准:从待排序序列中选择一个元素作为基准(
x
)。基准的选择有多种方式,比如选择序列的第一个元素、最后一个元素、中间元素或随机选择。 -
分割操作:
- 初始化两个指针,一个指向序列的开始(
l
),一个指向序列的末尾(r
)。 - 当
l < r
时进行循环:- 如果
l
指向的元素小于或等于基准,则l
自增。 - 如果
r
指向的元素大于基准,则r
自减。 - 如果
l
指向的元素大于基准且r
指向的元素小于基准,则交换这两个元素,并继续移动l
和r
指针。
- 如果
- 以基准元素为界,将序列分割为两个子序列:基准左侧所有元素均比基准小,右侧的所有元素都比基准大。
- 初始化两个指针,一个指向序列的开始(
-
递归排序:
- 对基准左侧的子序列进行快速排序。
- 对基准右侧的子序列行快速排序。
-
合并结果:由于每次分割操作后基准元素都处于其最终排序后的正确位置,因此无需合并操作,递归调用结束后,整个序列就已经有序了。
代码模板
void quick_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return ;
//第一步:分成子问题
int i = l - 1, j = r + 1;
int x = q[l + r >> 1];
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
//第二步:递归处理子问题
quick_sort(q, l, j), quick_sort(q, j+1, r);
//第三步:子问题合并.快排这一步不需要操作,但归并排序的核心在这一步骤
}
题目链接
五. 整数二分查找算法
二分查找算法(Binary Search)是一种在有序数组中查找某一特定元素的搜索算法。
算法描述
二分查找算法通过不断将待查找的区间减半,从而快速定位到目标元素的位置。算法的基本思想是将数组的中间元素与目标值进行比较,如果中间元素正好是要查找的元素,则搜索过程结束;如果目标值大于或小于中间元素,则在数组大于或小于中间元素的那一半中继续查找,直到找到目标值或搜索区间为空。
算法步骤
-
初始化:设定数组的起始索引
left
为 0,终止索引right
为数组长度减 1(即len(arr) - 1
)。 -
循环条件:当
left
小于等于right
时,执行循环体内的步骤。 -
计算中间索引:计算当前搜索区间的中间索引
mid
,可以通过(left + right) / 2
来得到。 -
比较目标值与中间元素:
- 如果
arr[mid]
等于目标值target
,则找到了目标值,返回mid
。 - 如果
arr[mid]
小于目标值target
,说明目标值在mid
的右侧,因此更新left
为mid + 1
,继续在右半部分搜索。 - 如果
arr[mid]
大于目标值target
,说明目标值在mid
的左侧,因此更新right
为mid - 1
,继续在左半部分搜索。
- 如果
-
循环结束:如果循环结束都没有找到目标值,则返回
-1
或其他表示未找到的值。
代码模板
int bsearch(int l, int r)
{
l = l-1, r = r+1;
while(l + 1 != r)
{
int mid = l + r >> 1;
if(IsBlue(mid)) l = mid;
else r = m;
}
return l or r;
}
函数IsBlue(mid)
的确定:
注意事项
- 二分查找要求数组必须是有序的。
- 二分查找的时间复杂度为
O(log n)
,其中n
是数组的长度。
题目链接
六. 快速幂
快速幂算法(Binary Exponentiation)是一种用于计算幂的高效算法。它的核心思想是将指数表示为二进制形式,并利用二进制数的每一位来快速计算幂。
算法描述
快速幂算法通过不断将指数减半和取底数的平方来减少计算幂所需的乘法次数。具体来说,它将指数转换为二进制形式,然后对于二进制形式中的每一位,如果它是1,则将当前累积的结果乘以底数;如果它是0,则不做任何操作。同时,每次迭代都将底数平方,并将指数右移一位。
算法步骤
-
初始化:设置
res
为1(因为任何数的0次幂都是1),base
为给定的底数,exponent
为给定的指数。 -
当指数不为0时:
- 如果
exponent
的二进制表示的最低位是1(即exponent
是奇数),则将result
乘以base
。 - 将
base
平方(即base = base * base
)。 - 将
exponent
右移一位(即exponent = exponent >> 1
),这相当于将exponent
除以2。
- 如果
-
返回结果:返回
result
作为最终的计算结果。
代码模板
//求 m^k,时间复杂度 O(logn)
int qmi(int m, int k)
{
int res = 1, t = m;
while (k)
{
if (k&1) res = res * t;
t = t * t;
k >>= 1;
}
return res;
}
带取模的代码模板(用于大数计算)
在实际应用中,当处理大数时,为了防止溢出,通常会在每次乘法后进行取模操作。这可以通过在每一步的乘法后添加一个% mod
操作来实现。
//求 m^k mod p,时间复杂度 O(logk)
int qmi(int m, int k, int p)
{
int res = 1 % p, t = m;
while (k)
{
if (k&1) res = res * t % p;
t = t * t % p;
k >>= 1;
}
return res;
}
typedef long long LL;
LL pmi(int a, int k, int p)
{
LL res = 1 % p, t = a;
while(k)
{
if(k&1) res = res * t % p;
t = t * t % p;
k >>= 1;
}
return res;
}
这样,即使底数和指数都非常大,也可以通过快速幂算法在合理的时间内计算出结果的模值。