递归
在程序中递归指函数调用自己,递归只是让解决方案更清晰,并没有性能上的优势
如果使用循环,程序的性能可能更高;如果使用递归,程序可能更容易理解
每个递归函数都有两部分:
基线条件(base case):不再调用自己,从而避免形成无限循环。
比如被处理数据的最基础情况
递归条件(recursive case):函数调用自己,通常用状态转移方程
不断缩小问题规模
栈(Stack)
原理:遵循“后进先出”,你只能从顶部添加(推入)或移除(弹出)
优点:简单高效,时间复杂度低
缺点:可能导致栈溢出,访问不灵活
适用场景:
- 函数调用和递归的实现
- 回溯算法,DFS
调用栈:用于保存每个函数调用的状态信息,包括参数、局部变量、返回地址等。当函数调用时,当前的执行状态被保存到栈中,当函数执行结束时,该栈帧从栈中弹出,控制权返回到调用函数的上一个位置。
递归调用栈:每次调用自己,类似于不断压入调用栈的过程,直到达到基线条件,递归调用开始返回值,并逐步从调用栈中弹出。
而非递归形式:需要新的数据结构存储满足条件的,非栈的自动管理调用
欧几里得算法
也称为辗转相除法,是用于计算两个非负整数的最大公约数
如果 a
和 b
是两个正整数,且 a > b
,那么 a
和 b
的最大公约数等于 b
和 a % b
的最大公约数。
int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
快速排序(排序算法)
原理:通常采用分治法策略。它通过选择一个基准pivot元素,将待排序数组分成较小元素和较大元素的两个子数组,然后递归地对这两个子数组进行排序。
优点:平均性能优越,只需要常数级别的额外空间,适合处理大规模数据。
缺点:最坏情况性能较差,不稳定排序(相等元素的相对顺序可能会发生改变)
时间:基准选择的好坏直接影响排序的效率,最好 == 平均O(nlogn),最坏O(n^2)
空间:O(logn)
实例:给定数组,并将所有小于基准元素(right)移到前面,并从所有<基准的元素最后位置划分为<部分和>部分,再将子数组递归排序
void quickSort(int arr[], int left, int right) {
if (left < right) {
int pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
}
int partition(int arr[], int left, int right) {
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[right]);
return i + 1;
}
归并 | 合并 排序(排序算法)
原理:是一种经典的基于分治法的排序算法。它通过递归地将数组分成两半,分别排序,然后将两个有序的子数组合并成一个完整的有序数组
优点:是稳定的排序算法,元素在排序后仍然保持相对顺序不变,性能比较稳定。
缺点:空间复杂度高,不适合小规模数据排序
时间:O(nlogn)
空间:O(n)
实例:给定数组,选取中点分解为子数组,从调用栈顶部开始,依次合并merge
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int leftArr[n1], rightArr[n2];
for (int i = 0; i < n1; i++)
leftArr[i] = arr[left + i];
for (int j = 0; j < n2; j++)
rightArr[j] = arr[mid + 1 + j];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
快排 VS 归并
既然快排最坏时间慢于归并,为什么不用归并?
通常会忽略时间复杂度的常量,但有时候,常量的影响可能很大
快排的常量比合并要小,因此大部分情况(非最坏情况)都要优于合并的时间
快排何时慢?
比如一个有序数组,从左到右取基准点,这会导致调用栈高度为n
如果选择中间元素,调用栈高度为log^n,归并排序正是这样选择的