新年新气象!大家新年快乐!
23年计划多学习算法,多刷题,因此建立次专栏,记录自己刷题的过程,总结经验。
计划:
- 有时间更新刷过的题目,按照专题来进行刷题,为期半年;
- 每周总结一次,周内只刷题;
- 必要时候采用画图、列表等方法进行总结,加深印象;
排序算法
冒泡排序
方法说明
冒泡排序:将被排序的数组A[1…n]垂直排列,每个A[i]看作重量为A[i]的气泡,根据轻气泡不能在重气泡之下的原则,从上往下扫描数组A,违反原则的轻气泡“上浮”,如此反复进行得到最终有序气泡数组。·
算法的复杂度
冒泡排序
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
代码
#include <iostream>>
using namespace std;
void SwapInt(int *a1, int *a2)
{
int temp = *a1;
*a1 = *a2;
*a2 = temp;
}
void BubbleSort(int a[], int len)
{
int temp = 0;
for (int i = 0; i < len - 1; i++) {
for (int j = i; j < len; j++) {
if (a[j] < a[i]) {
SwapInt(&a[i], &a[j]);
}
}
}
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1};
BubbleSort(a, 10);
cout << "sorted array:" << endl;
for (int i = 0; i < 10; i++) {
cout << a[i] << " ";
}
cout << endl;
system("pause");
}
插入排序
定义
初始时,数组R[1]自成一个有序区、无序区R[2…n]。从i=2到i=n为止,依次将R[i]插入当前有序区R[1…i-1]中,生成有序数组R。(打牌时抓牌)
代码
#include <iostream>>
using namespace std;
void InsertSort(int a[], int len)
{
int temp;
int i, j;
for (i = 1; i < len; i++) {
temp = a[i];
for (j = i - 1; j >= 0 && temp < a[j]; j--) {
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1};
InsertSort(a, 10);
PrintArray(a, 10);
system
(“pause”);
}
希尔排序
定义
插入排序相隔较远的数插入,会使得数要前移多位,多次交换,耗时。
分组排序算法:一组数按照d个分成若干组,每组进行排序,再用小的增量d1进行排序,直到增量dn=1,整个要排的数分成一组,排序完成。
代码
#include <iostream>
using namespace std;
void ShellSort(int a[], int len)
{
int temp;
int i, j;
for (int increment = len / 2; increment > 0; increment = increment / 2) {
for (i = increment; i < len; i++) {
temp = a[i];
for (j = i - increment; j >= 0 && temp < a[j]; j -= increment) {
a[j + increment] = a[j];
}
a[j + increment] = temp;
}
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1};
ShellSort(a, 10);
PrintArray(a, 10);
system("pause");
}
快速排序
定义
划分交换排序:分治策略:原问题分解成若干个规模更小的但结构与原问题相似的子问题,递归解决这些子问题,然后将这些子问题的解组合为原问题的解。
A[low,…,high]
-
分解:任选一个记录为基准pivot,划分左右两个区间a[low,…,pivot - 1]和A[pivot +1, …,
high],使得左边区间数都小于等于A[pivot],右边都大于等于A[pivot]。 -
求解:递归调用快速排序对左右子区间进行类似操作
-
组合:当“求解”步骤中的两个递归调用结束时,其左右两个子区间已有序。(快排此步骤为空操作)
算法的复杂度
快速排序
时间复杂度:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n)),最差情况:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
l
o
g
(
n
)
)
O(log(n))
O(log(n))
代码
#include <iostream>
using namespace std;
void QuickSort(int a[], int low, int high)
{
int pivot;
int i, j;
if (low < high) {
pivot = a[low];
i = low;
j = high;
while (i < j) {
while (i < j && a[j] >= pivot) {
j--; // 过滤掉在右区间中比pivot大的元素
}
if (i < j) {
a[i] = a[j];
i++; // 右区间中小于pivot元素移到i索引位置,使得小于索引i的元素都比pivot小,i++
}
while (i < j && a[i] <= pivot) {
i++; // 过滤掉在左区间中比pivot小的元素
}
if (i < j) {
a[j] = a[i];
j--; // 左区间中大于pivot元素移到j索引位置,使得大于索引j的元素都比pivot大,j--
}
}
a[i] = pivot; // 出循环,i >= j,将i的就是pivot应该放置的地方,已排序一个位置
QuickSort(a, low, i - 1);
QuickSort(a, i + 1, high);
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1};
QuickSort(a, 0, 9);
PrintArray(a, 10);
system("pause");
}
选择排序
定义
初始有序空,无序A[0,…n];每次选出最小值与有序中排序,顺序插入最后。(开始时可以有序为第一个元素,比较之后与最小值交换)
算法的复杂度
选择排序
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
代码
#include <iostream>
using namespace std;
void SelectSort(int a[], int len)
{
for (int i= 0; i < len - 1; i++) {
int temp = a[i];
int tempIndex = i;
for (int j = i; j < len; j++) {
if (a[j] < temp) {
temp = a[j];
tempIndex = j;
}
}
a[tempIndex] = a[i]; // 顺序不能反,先将a[i]值给最小值原先的位置,再将最小值给a[i]
a[i] = temp;
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1};
SelectSort(a, 10);
PrintArray(a, 10);
system("pause");
}
堆排序
定义
小根堆:所有子节点都大于父节点,Ai<=A2i且Ai<=A2i+1
大根堆:所有子节点都小于父节点,Ai>=A2i且Ai>=A2i+1
选择关键字最大或最小的记录。
筛选法进行对的调整:大根堆中,A[low]的左右子树已是堆,A[2low]和A[2low+1]分别是各自子树的关键字最大值。若A[low]不小于两个孩子节点,A[low]未违反堆性质,以A[low]为根的树已是大根堆;反之,A[low]必须和两个孩子节点较大者进行交换。交换后A[large]还是违反堆性质,要继续调整,直到large位置数值排到合适的地方,把较大的关键子逐层选上来,把较小的关键子组逐层筛选下去。
算法的复杂度
基数排序
时间复杂度:
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))
空间复杂度:
O
(
1
)
O(1)
O(1) 原地排序
代码
#include <iostream>
using namespace std;
int g_HeapSize = 0;
int LeftIndex(int index)
{
return ((index << 1) + 1);
}
int RightIndex(int index)
{
return ((index << 1) + 2);
}
void Swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 将当前元素与左右子树比较取最大值,如果当前元素是最大值,结束
// 如果不是最大值,交换当前元素和最大值元素,并且递归把交换后的元素继续进行左右子树比较,放到合适位置:叶子节点或者符合堆性质(当前为最大元素)
void MaxHeapify(int a[], int index)
{
int largeIndex = 0;
int left = LeftIndex(index);
int right = RightIndex(index);
if ((left <= g_HeapSize) && (a[left] > a[index])) {
largeIndex = left;
} else {
largeIndex = index;
}
if ((right <= g_HeapSize) && (a[right] > a[largeIndex])) {
largeIndex = right;
}
if (largeIndex != index) {
Swap(&a[largeIndex], &a[index]);
MaxHeapify(a, largeIndex); // 交换结束后的这个小元素比不一定符合堆性质,需要递归遍历,直到符合堆性质
}
}
// 初始化一个堆,原先的数组上
void BuildMaxHeap(int a[], int maxSize)
{
g_HeapSize = maxSize;
for (int i = (maxSize >> 1); i >= 0; i--) { // 只需要从后往前的一半的位置开始建立大根堆就行,堆顶元素必是最大值
MaxHeapify(a, i);
}
}
void HeapSort(int a[], int len)
{
BuildMaxHeap(a, len - 1);
for (int i = len - 1; i >= 1; i--) { // 直到最后一个元素,即最小元素
Swap(&a[0], &a[i]);
g_HeapSize--;
MaxHeapify(a, 0); // 为啥是0,因为此时只有a[0]不满足大根堆要求,a[0]元素继续进行左右子树比较,放到合适位置:叶子节点或者符合堆性质(当前为最大元素)
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,777,19,791,8,717};
HeapSort(a, 10);
PrintArray(a, 10);
system("pause");
return 0;
}
归并排序
定义
将若干个已排序的子文件合并成一个有序的文件
自定向下:
分解:一分为二,求分裂点
求解:递归对子序列进行归并排序a[low,…,mid]和a[mid + 1,…,high]
组合:已排序的两个子区间归并成一个有序的区间
递归的总结条件:子区间个数为1.
自底向上:二路归并排序:第一趟:a[1,…n]看成n个长度为1的数组,两两归并,n为偶数,得到n/2个长度为2的有序数组,n为奇数,最后一个文件不参与归并。直到最后的得到一个长度为n的数组。
算法的复杂度
归并排序
时间复杂度:
O
(
n
l
o
g
(
n
)
)
O(n log(n))
O(nlog(n))
空间复杂度:
O
(
n
)
O(n)
O(n)
代码
#include <iostream>
using namespace std;
void Merge(int a[], int temp[], int low, int highStart, int high)
{
int lowEnd = highStart - 1;
int nums = high - low + 1;
int tempIndex = low;
while (low <= lowEnd && highStart <= high) {
if (a[low] <= a[highStart]) {
temp[tempIndex++] = a[low++];
} else {
temp[tempIndex++] = a[highStart++];
}
}
while (low <= lowEnd) {
temp[tempIndex++] = a[low++];
}
while (highStart <= high) {
temp[tempIndex++] = a[highStart++];
}
for (int i = 0; i < nums; i++, high--) { // 这时候需要反向填充,low值变化,只有high值是入参固定的,借助high值填充
a[high] = temp[high];
}
}
void MSort(int a[], int temp[], int low, int high)
{
if (low >= high) { // 结束条件,原子节点return
return;
}
int mid = (low + high) / 2; // 分解
MSort(a, temp, low, mid); // 子区间归并排序
MSort(a, temp, mid + 1, high); // 子区间归并排序
Merge(a, temp, low, mid + 1, high); // 组合,有序子区间合并成一个有序区间
}
void MergeSort(int a[], int len)
{
int *temp = NULL;
temp = new int[len];
if (temp != NULL) {
MSort(a, temp, 0, len - 1);
delete []temp;
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,77,19,-1,8,17};
MergeSort(a, 10);
PrintArray(a, 10);
system("pause");
return 0;
}
基数排序
定义
基数排序是箱排序的改进。
箱排序:若干个箱子,一次扫描待排序的记录R[0],R[1],…R[n-1],把关键字等于K的记录全都装进第K个箱子里(分配),然后按照序号一次将非空的箱子首位连接起来(收集)。
基数排序基于多关键字,比如扑克牌有2个关键字:点数和花色。实现多关键字排序的方法:最高位优先级和最低位优先级,基数排序基于最低为优先级:从低位到高位依次对数据进行箱排序。
举例
10-99整数排序:45,13,58,64,29,61
先0-9箱子收集原序列的个位数字,进行排序:61,13,64,45,58,29
在0-9箱子收集上述序列的十位数字,进行排序:13,29,45,58,61,64
十位比个位更关键,采用最低位优先方法
算法的复杂度
基数排序
时间复杂度:
O
(
k
n
)
O(kn)
O(kn) 关键字的个数K(K种类关键字)* 数组元素个数n
时间复杂度:
O
(
r
n
+
r
)
O(rn + r)
O(rn+r) 关键字取值个数r * 数组元素个数n(所有箱子来存储数组元素) + 关键字取值个数(存储每种关键字取值的元素个数r)
代码
#include <iostream>
#include <math.h>
#include <cstring>
using namespace std;
int FindMax(int a[], int len)
{
int max = a[0];
for (int i = 1; i < len ; i++) {
if (max < a[i]) {
max = a[i];
}
}
return max;
}
int DigitNumber(int number)
{
int digitNum = 0;
do {
number /= 10;
digitNum++;
} while (number != 0);
return digitNum;
}
int KthNumber(int number, int k)
{
number /= pow(10, k);
return number % 10;
}
void RadixSort(int a[], int len)
{
int *temp[10]; // 定义指针数组,每个元素指向一个箱子
int count[10] = {0}; // 定义每个箱子的装的元素个数
int max = FindMax(a, len);
int maxDigit = DigitNumber(max);
int i, j, k;
for (i = 0; i < 10; i++) {
temp[i] = new int[len];
memset(temp[i], 0, sizeof(int) * len); // 每一个箱子装载前清空
}
for (i = 0; i <maxDigit; i++) {
memset(count, 0, sizeof(int) * 10); // 装载前Count清空
for (j = 0; j < len; j++) {
int x = KthNumber(a[j], i); // 求出i位的数字
temp[x][count[x]] = a[j]; // 将元素按照i位数字存储到对应的箱子中,并记录箱子中元素的个数
count[x]++;
}
int index = 0;
// 元素放回到数组中,为了下次遍历i + 1位排序
for (j = 0; j < 10; j++) {
for (k = 0; k < count[j]; k++) {
a[index++] = temp[j][k];
}
}
}
for (i = 0; i < 10; i++) {
delete []temp[i];
temp[i] = NULL;
}
}
void PrintArray(int a[], int len)
{
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << a[i] << " ";
}
cout << endl;
}
int main()
{
int a[10] = {4,1,2,3,5,777,19,791,8,717};
RadixSort(a, 10);
PrintArray(a, 10);
system("pause");
return 0;
}
排序算法总结
递归与动态规划
递归与迭代
定义
递归的解是基于子问题的解构建,有f(n-1)推导出f(n)。
递归方法:自底向上、自顶向下、数据分割
递归算法极耗空间,所有递归都可以有用迭代实现。
动态规划就是使用递归算法发现的重叠子问题(重复调用),缓存成中间结果来使用。
斐波那契数列
递归实现
#include <iostream>
using namespace std;
int Fibonacci(int i)
{
if (i == 0) {
return 0;
}
if (i == 1) {
return 1;
}
return Fibonacci(i - 1) + Fibonacci(i -2);
}
int main()
{
int i = 18;
int ret = Fibonacci(i);
cout << i << "th Fibonacci number = " << ret << endl;
system("pause");
return 0;
}
运行时间:子树节点数*节点运行时间 =
O
(
2
n
)
O(2^n)
O(2n),实际 =
O
(
1.
6
n
)
O(1.6^n)
O(1.6n)
自顶向下(记忆法)
计算Fib(i),使用可能取值Fib(0)~Fib(i-1),将之前的所有数值都存储起来吗,以备后续使用,称为记忆法。
#include <iostream>
using namespace std;
int Fibonacci(int i, int memo[])
{
// 记忆法
if (i == 0 || i == 1) {
return i;
}
if (memo[i] == 0) {
memo[i] = Fibonacci(i - 1, memo) + Fibonacci(i - 2, memo);
}
return memo[i];
}
int FibonacciAlgo(int n)
{
int a[n + 1] = {0};
return Fibonacci(n, a);
}
int main()
{
int i = 18;
int ret = FibonacciAlgo(i);
cout << i << "th Fibonacci number = " << ret << endl;
system("pause");
return 0;
}
自底向上的动态规划方法
#include <iostream>
using namespace std;
int FibonacciAlgo(int n)
{
if (n == 0 || n == 1) {
return n;
}
int memo[n] = {0};
memo[0] = 0;
memo[1] = 1;
int i;
for (i = 2; i <= n; ++i) {
memo[i] = memo[i - 1] + memo[i - 2];
}
return memo[n];
}
int main()
{
int i = 18;
int ret = FibonacciAlgo(i);
cout << i << "th Fibonacci number = " << ret << endl;
system("pause");
return 0;
}
方法优化:
#include <cstring>
#include <iostream>
using namespace std;
// 自底向上方法优化,斐波那契数值的最后两次存储的数值有关,用两个变量来替换数组,O(1)复杂度
int FibonacciAlgo(int n)
{
if (n == 0 || n == 1) {
return n;
}
int a = 0;
int b = 1;
int c = 1;
int i;
for (i = 2; i <= n; ++i) {
c = b + a;
a = b;
b = c;
}
return c;
}
int main()
{
int i = 18;
int ret = FibonacciAlgo(i);
cout << i << "th Fibonacci number = " << ret << endl;
system("pause");
return 0;
}
典型动态规划题目总结
台阶问题
class Solution {
public:
// // 递归方法超出时间限制
// // f(n) = f(n-1) + f(n-2);f(1)=1;f(2)=2;
// int climbStairs(int n) {
// if (n == 1 || n == 2) {
// return n;
// }
// return climbStairs(n - 1) + climbStairs(n - 2);
// }
// // 自底向上的动态规划方法——O(n) O(n)
// int climbStairs(int n) {
// vector<int> dp(n+1);
// dp[0]=1;dp[1]=1;
// for(int i=2;i<=n;i++)
// dp[i]=dp[i-1]+dp[i-2]; //动态转移方程
// return dp[n];
// }
// 自底向上的动态规划方法——O(n),O(1)
// a=1;b=2;c=a+b;
int climbStairs(int n) {
if (n == 1 || n == 2) {
return n;
}
int status1 = 1;
int status2 = 2;
int ret;
for (int i = 3; i <= n; ++i) {
ret = status1 + status2;
status1 = status2;
status2 = ret;
}
return ret;
}
};
// #define INT_MAX 2147483647
// #define INT_MIN (-INT_MAX - 1)
class Solution {
public:
// 自顶向下的动态规划方法
int addRungs(vector<int>& rungs, int dist) {
int len = rungs.size();
int ret = 0;
int curStep = 0;
for (int i = 0; i < len; ++i) {
// 差值,不包括当前台阶数,【5, 10】,2:到达9就可以了,所以10-5-1=4,4/2=2;【3】,1:3-0-1/1=2
int diff = rungs[i] - 1 - curStep;
if (diff >= dist) { // 是否能到达第i台阶
ret = ret + (diff / dist);
}
curStep = rungs[i];
}
return ret;
}
};
746. 使用最小花费爬楼梯
说明:典型的动态规划问题:每个台阶当前的花费体力dp[n]
下一个台阶花费dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
class Solution {
public:
// int const MAX_LEN = 1000;
// dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
int minCostClimbingStairs(vector<int>& cost) {
// int len = cost.size();
// int dp[len + 1];
// dp[0] = dp[1] = 0; // 当前台阶需要的体力值
// for (int i = 2; i <= len; ++i) {
// dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
// }
// return dp[len];
int len = cost.size();
int cur = 0;
int pre = 0; // 当前台阶需要的体力值
int ret = 0;
for (int i = 2; i <= len; ++i) {
ret = min(cur + cost[i - 1], pre + cost[i - 2]);
pre = cur;
cur = ret;
}
return ret;
}
};
面试题 08.01. 三步问题
注意:放置整型溢出风险,定义long long类型,并结果取余1000000007
ACM技巧 - 为何要对 1000000007 取模?
class Solution {
public:
int waysToStep(int n) {
if (n == 1 || n == 2) {
return n;
}
if (n == 3) {
return 4;
}
int a = 1;
int b = 2;
int c = 4;
long long ret = 0;
for (int i = 4; i <= n; ++i) {
ret = (a + b) % 1000000007;
ret = (ret + c) % 1000000007;
a = b;
b = c;
c = ret;
}
return ret % 1000000007;
}
};
机器人路径
剑指 Offer 13. 机器人的运动范围
注意:求数位之和、二维数组初始化、动态规划思想——二维元素的传染性(空间)。
class Solution {
public:
// 怎么计算数位和
int DigitBitSum(int num)
{
int ret = 0;
for (; num != 0; num /= 10) {
ret += num % 10;
}
return ret;
}
// 第(m, n)位置 = 第(m-1, n)位置 + 第(m, n-1)位置移动下来,一旦超出K,停止格子计数
int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
// 怎么定义一个m*n初始化为0的二维数组
vector<vector<int>> vis(m, vector<int>(n, 0));
int ans = 1;
// 初始化二位数组的0,0位置被传染
vis[0][0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 拦截条件满足才往下面走
if ((i == 0 && j == 0) || DigitBitSum(i) + DigitBitSum(j) > k) {
continue;
}
if (i - 1 >= 0) {
vis[i][j] |= vis[i - 1][j];
}
if (j - 1 >= 0) {
vis[i][j] |= vis[i][j - 1];
}
ans += vis[i][j]; // 如果vis【i】【j】被传染了,置1,加到结果中
}
}
return ans;
}
};
还可以采用DFS、BFS方法,BFS方法如下:注意:遍历向右和向下的元素,采用队列存储其位置(技巧)。
class Solution {
// 计算 x 的数位之和
int get(int x) {
int res=0;
for (; x; x /= 10) {
res += x % 10;
}
return res;
}
public:
int movingCount(int m, int n, int k) {
if (!k) return 1;
queue<pair<int,int> > Q;
// 向右和向下的方向数组
int dx[2] = {0, 1};
int dy[2] = {1, 0};
vector<vector<int> > vis(m, vector<int>(n, 0));
Q.push(make_pair(0, 0));
vis[0][0] = 1;
int ans = 1;
while (!Q.empty()) {
auto [x, y] = Q.front();
Q.pop();
for (int i = 0; i < 2; ++i) {
int tx = dx[i] + x;
int ty = dy[i] + y;
if (tx < 0 || tx >= m || ty < 0 || ty >= n || vis[tx][ty] || get(tx) + get(ty) > k) continue;
Q.push(make_pair(tx, ty));
vis[tx][ty] = 1;
ans++;
}
}
return ans;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/solutions/191527/ji-qi-ren-de-yun-dong-fan-wei-by-leetcode-solution/
class Solution {
public:
// f(i,j)=f(i−1,j)+f(i,j−1)
// 对于边界条件要考虑仔细
int uniquePaths(int m, int n) {
vector<vector<int>> vis(m, vector<int>(n, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 && j == 0) {
vis[0][0] = 1;
} else if (i == 0 && j > 0) {
vis[i][j] = vis[i][j - 1];
} else if (j == 0 && i > 0) {
vis[i][j] = vis[i - 1][j];
} else {
vis[i][j] = vis[i - 1][j] + vis[i][j - 1];
}
}
}
return vis[m - 1][n - 1];
}
};
数学方法
class Solution {
public:
int uniquePaths(int m, int n) {
long long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return ans;
}
};
作者:力扣官方题解
链接:https://leetcode.cn/problems/2AoeFn/solutions/1398902/lu-jing-de-shu-mu-by-leetcode-solution-ozcc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
汉诺塔
注意:状态变更,递归的方法,直到A中个数为1停止移动。
状态变更很难发现,需要读者精心话题寻找其规律。
class Solution {
public:
// n=1时,直接把盘子从A移动到C
// n>1时
// 2.1 先把上面n-1个盘子从A移动到B(子问题,递归)
// 2.2 再将最大的盘子从A移动到C
// 2.3 再将B上n-1个盘子从B移动到C(子问题,递归)
void move(int n, vector<int> &A, vector<int> &B, vector<int> &C) {
if (n == 1) {
C.push_back(A.back());
A.pop_back();
return;
}
move(n - 1, A, C, B);
C.push_back(A.back());
A.pop_back();
move(n - 1, B, A, C);
}
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n = A.size();
move(n, A, B, C);
}
};
无重复字符串的排列组合
重复字符串的排列组合
颜色填充
硬币找零
八皇后
布尔运算
堆箱子
括号匹配
幂集
树
剑指 Offer 68 - II. 二叉树的最近公共祖先
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* ans;
bool dfs(TreeNode* root, TreeNode* p, TreeNode* q)
{
// 递归的终止条件
if (root == nullptr) {
return false;
}
// 树的左和右子树遍历
bool lson = dfs(root->left, p, q);
bool rson = dfs(root->right, p, q);
// 遍历到底,再核查是否存在满足要求的节点
// 当前节点要么左和右子树存在p或q节点,要么为q或p的值且左或右子树存在另一个数值,返回当前节点符合题目要求
if ((lson && rson) || ((root->val == p->val || root->val == q->val) && (lson || rson)))
{
ans = root;
}
// 当前节点要么为q或者p的值,要么它的左或者右子树存在p或者q的值
return lson || rson || root->val == p->val || root->val == q->val;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 前提:1】所有节点的值都是唯一的。2】p、q 为不同节点且均存在于给定的二叉树中
static_cast<void>(dfs(root, p, q));
return ans;
}
};
回溯
定义
案例
剑指 Offer 12. 矩阵中的路径
class Solution {
public:
bool check(vector<vector<char>>& board, vector<vector<int>>& vis,
int i, int j, string word, int k)
{
if (board[i][j] != word[k]) {
return false;
} else if (k == word.size() - 1) {
return true;
}
vector<pair<int, int>> dir{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
vis[i][j] = 1; // 先标记,没找在回退,其他方向找,回溯方法精髓
bool ret = false;
for (const auto& d : dir) {
int newi = d.first + i;
int newj = d.second + j;
if (newi >= 0 && newi < board.size() && newj >= 0 && newj < board[0].size()) {
if (!vis[newi][newj]) { // 没被访问过才处理
if (check(board, vis, newi, newj, word, k + 1)) {
return true;
}
}
}
}
vis[i][j] = false; // 回溯,从其他路径(非【i, j】)经过i, j就可能成功找到单词
return ret;
}
bool exist(vector<vector<char>>& board, string word) {
int row = board.size();
int column = board[0].size();
vector<vector<int>> vis(row, vector<int>(column, 0));
for (int i = 0; i < row; i++) {
for (int j = 0;j < column; j++) {
if (check(board, vis, i ,j, word, 0)) {
return true;
}
}
}
return false;
}
};
有限状态机
定义
确定有限状态自动机(以下简称「自动机」)是一类计算模型。它包含一系列状态,这些状态中:
有一种特殊的状态,被称作「初始状态」。
还有一系列状态被称为「接受状态」,它们组成了一个特殊的集合。其中,一个状态可能既是「初始状态」,也是「接受状态」。
起初,这个自动机处于「初始状态」。随后,它顺序地读取字符串中的每一个字符,并根据当前状态和读入的字符,按照某个事先约定好的「转移规则」,从当前状态转移到下一个状态;当状态转移完成后,它就读取下一个字符。当字符串全部读取完毕后,如果自动机处于某个「接受状态」,则判定该字符串「被接受」;否则,判定该字符串「被拒绝」。
注意:如果输入的过程中某一步转移失败了,即不存在对应的「转移规则」,此时计算将提前中止。在这种情况下我们也判定该字符串「被拒绝」。
一个自动机,总能够回答某种形式的「对于给定的输入字符串 S,判断其是否满足条件 P」的问题。在本题中,条件 P 即为「构成合法的表示有效数字的字符串」。
自动机驱动的编程,可以被看做一种暴力枚举方法的延伸:它穷尽了在任何一种情况下,对应任何的输入,需要做的事情。
自动机在计算机科学领域有着广泛的应用。在算法领域,它与大名鼎鼎的字符串查找算法「KMP」算法有着密切的关联;在工程领域,它是实现「正则表达式」的基础。
LCR 138. 有效数字
class Solution {
public:
// 定义所以的状态
enum State {
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END
};
// 行为类型,存在非法的行为
enum ActType {
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_SPACE,
CHAR_ILLEGAL
};
ActType InputToActType(char ch)
{
if (ch >= '0' && ch <= '9') {
return CHAR_NUMBER;
} else if (ch == 'e' || ch == 'E') {
return CHAR_EXP;
} else if (ch == '.') {
return CHAR_POINT;
} else if (ch == '+' || ch == '-') {
return CHAR_SIGN;
} else if (ch == ' ') {
return CHAR_SPACE;
} else {
return CHAR_ILLEGAL;
}
}
bool validNumber(string s)
{
unordered_map<State, unordered_map<ActType, State>>transfer {
// 初始化状态下,ACT为加整数数字,那么下一个状态就是整数状态
{
STATE_INITIAL, {
{CHAR_SPACE, STATE_INITIAL},
{CHAR_NUMBER, STATE_INTEGER},
{CHAR_SIGN, STATE_INT_SIGN},
{CHAR_POINT, STATE_POINT_WITHOUT_INT}
}
}, {
STATE_INT_SIGN, {
{CHAR_NUMBER, STATE_INTEGER},
{CHAR_POINT, STATE_POINT_WITHOUT_INT}
}
}, {
STATE_INTEGER, {
{CHAR_NUMBER, STATE_INTEGER},
{CHAR_EXP, STATE_EXP},
{CHAR_POINT, STATE_POINT},
{CHAR_SPACE, STATE_END}
}
}, {
STATE_POINT, {
{CHAR_NUMBER, STATE_FRACTION},
{CHAR_EXP, STATE_EXP},
{CHAR_SPACE, STATE_END}
}
}, {
STATE_POINT_WITHOUT_INT, {
{CHAR_NUMBER, STATE_FRACTION}
}
}, {
STATE_FRACTION,
{
{CHAR_NUMBER, STATE_FRACTION},
{CHAR_EXP, STATE_EXP},
{CHAR_SPACE, STATE_END}
}
}, {
STATE_EXP,
{
{CHAR_NUMBER, STATE_EXP_NUMBER},
{CHAR_SIGN, STATE_EXP_SIGN}
}
}, {
STATE_EXP_SIGN, {
{CHAR_NUMBER, STATE_EXP_NUMBER}
}
}, {
STATE_EXP_NUMBER, {
{CHAR_NUMBER, STATE_EXP_NUMBER},
{CHAR_SPACE, STATE_END}
}
}, {
STATE_END, {
{CHAR_SPACE, STATE_END}
}
}
};
int len = s.length();
State st = STATE_INITIAL;
for (int i = 0; i < len; i++) {
ActType act = InputToActType(s[i]);
if (transfer[st].find(act) == transfer[st].end()) {
return false; // 如果当前状态在act下没用下一个状态,返回失败
} else {
st = transfer[st][act]; // 当前的状态和行为找到下一个状态赋值给状态变量
}
}
// 遍历结束之后的状态要为符合条件的5中状态之一
return st == STATE_END || st == STATE_POINT || st == STATE_INTEGER || st == STATE_FRACTION || st == STATE_EXP_NUMBER;
}
};