常见算法总结
1. 排序算法
1.1 选择排序
时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:不稳定
void SelectSort(vector<int>& nums)
{
for (int i = 0; i < nums.size(); ++i)
{
int minIndex = i;
for (int j = i + 1; j < nums.size(); ++j)
minIndex = (nums[minIndex] > nums[j]) ? j : minIndex;
::swap(nums[minIndex], nums[i]);
}
}
1.2 冒泡排序
时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:稳定
void BubleSort(vector<int>& nums)
{
for (int i = nums.size() - 1; i > 0; --i)
{
for (int j = 0; j < i; ++j)
{
if (nums[j] > nums[j + 1])
::swap(nums[j], nums[j + 1]);
}
}
}
1.3 插入排序
时间复杂度:O(n^2) 空间复杂度:O(1) 稳定性:稳定
void InsertSort(vector<int>& nums)
{
if (nums.size() <= 1)
return;
for (int i = 1; i < nums.size(); ++i)
{
for (int j = i; j > 0 && nums[j] < nums[j - 1]; --j)
::swap(nums[j], nums[j - 1]);
}
}
1.4 归并排序
时间复杂度:O(nlogn) 空间复杂度:O(n) 稳定性:稳定
void MergeSort(vector<int>& nums)
{
if (nums.size() <= 1)
return;
MergeSort_Process(nums, 0, nums.size() - 1);
}
void MergeSort_Process(vector<int>& nums, int l, int r)
{
if (l >= r)
return;
int mid = l + ((r - l) >> 1);
MergeSort_Process(nums, l, mid);
MergeSort_Process(nums, mid + 1, r);
Merge(nums, l, r, mid);
}
void Merge(vector<int>& nums, int l, int r, int mid)
{
int i = l, j = mid + 1;
vector<int> temp(r - l + 1);
int k = 0;
while (i <= mid && j <= r)
{
if (nums[i] <= nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
while (i <= mid)
temp[k++] = nums[i++];
while (j <= r)
temp[k++] = nums[j++];
::copy(temp.begin(), temp.end(), nums.begin() + l);
}
1.5 快速排序
时间复杂度:O(nlogn) 空间复杂度:O(logn) 稳定性:不稳定
void QuickSort(vector<int>& nums)
{
if (nums.size() <= 1)
return;
srand((unsigned int)time(NULL));
QuickSort_Process(nums, 0, nums.size() - 1);
}
void QuickSort_Process(vector<int>& nums, int l, int r)
{
if (l >= r)
return;
int pos = l + (rand() % (r - l + 1));
int sr = 0, br = 0;
Partition(nums, l, r, sr, br, pos);
QuickSort_Process(nums, l, sr);
QuickSort_Process(nums, br, r);
}
void Partition(vector<int>& nums, int l, int r, int& sr, int& br, int pos)
{
int val = nums[pos];
sr = l - 1, br = r + 1;
int i = l;
while (i < br)
{
if (nums[i] < val)
::swap(nums[i++], nums[++sr]);
else if (nums[i] == val)
++i;
else
::swap(nums[i], nums[--br]);
}
}
1.6 堆排序
时间复杂度:O(nlogn) 空间复杂度:O(1) 稳定性:不稳定
void Heapify(vector<int>& nums, int heapSize, int i = 0)
{
int left = 2 * i + 1, right = left + 1;
while (left < heapSize)
{
int maxIndex = (right < heapSize && nums[right] > nums[left]) ? right : left;
maxIndex = (nums[maxIndex] < nums[i]) ? i : maxIndex;
if (i == maxIndex)
return;
::swap(nums[i], nums[maxIndex]);
i = maxIndex;
left = 2 * i + 1;
right = left + 1;
}
}
void HeapSort(vector<int>& nums)
{
if (nums.size() <= 1)
return;
int heapSize = nums.size();
for (int i = heapSize - 1; i >= 0; --i)
Heapify(nums, heapSize, i);
while (heapSize > 0)
{
::swap(nums[0], nums[--heapSize]);
Heapify(nums, heapSize);
}
}
1.7 基数排序
时间复杂度:O(n * k) 空间复杂度:O(n) 稳定性:稳定 (k为最大元素位数)
// 基数排序
int GetMaxDigit(const vector<int>& nums)
{
int maxNum = ::max_element(nums.begin(), nums.end());
int res = 0;
while (maxNum != 0)
{
maxNum /= 10;
++res;
}
return res;
}
int GetDigit(int num, int d)
{
while (d > 1)
{
--d;
num /= 10;
}
return num % 10;
}
void RadixSort(vector<int>& nums)
{
if (nums.size() <= 1)
return;
int d = GetMaxDigit(nums);
vector<queue<int>> buckets(10, queue<int>());
for (int i = 0; i < d; ++i)
{
int index = 0;
for (int j = 0; j < nums.size(); ++j)
buckets[GetDigit(nums[j], i + 1)].push(nums[j]);
for (int j = 0; j < 10; ++j)
{
while (!buckets[j].empty())
{
nums[index++] = buckets[j].front();
buckets[j].pop();
}
}
}
}
2. 链表
技巧一:dummyHead
创建头结点指向首元结点,以减少针对于首元节点的大量特殊情况判断
- 合并链表
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode dummyHead;
ListNode* cur = &dummyHead, *p = list1, *q = list2;
while (p != nullptr && q != nullptr)
{
if (p->val <= q->val)
{
cur->next = p;
p = p->next;
}
else
{
cur->next = q;
q = q->next;
}
cur = cur->next;
}
if (p != nullptr)
cur->next = p;
if (q != nullptr)
cur->next = q;
return dummyHead.next;
}
- 分割链表(链表partition)
ListNode* partition(ListNode* head, int x) {
if (head == nullptr)
return nullptr;
ListNode lessDummy, // 存放小于x的链表
greaterDummy; // 存放大于等于x的链表
ListNode* p1 = &lessDummy, *p2 = &greaterDummy;
ListNode* p = head;
while (p != nullptr)
{
if (p->val < x)
{
p1->next = p;
p1 = p1->next;
}
else
{
p2->next = p;
p2 = p2->next;
}
// 断开原链表的连接
ListNode* temp = p->next;
p->next = nullptr;
p = temp;
}
p1->next = greaterDummy.next;
return lessDummy.next;
}
技巧二:双指针(快慢指针)
快指针先走,慢指针后走(针对部分情况快指针一次走两步,慢指针一次走一步)
- 删除链表倒数第k个结点
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == nullptr || n <= 0)
return head;
ListNode* fast = head, *slow = head;
int count = 0;
while (count < n)
{
++count;
fast = fast->next;
}
if (fast == nullptr) // 倒数第n个结点
{
ListNode* res = head->next;
delete head;
head = nullptr;
return res;
}
while (fast->next != nullptr)
{
fast = fast->next;
slow = slow->next;
}
ListNode* pNext= slow->next;
slow->next = pNext->next;
delete pNext;
pNext = nullptr;
return head;
}
- 链表中点
ListNode* middleNode(ListNode* head) {
if (head == nullptr)
return nullptr;
if (head->next == nullptr)
return head;
ListNode* fast = head, *slow = head;
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
- 链表入环结点
ListNode *detectCycle(ListNode *head) {
if (head == nullptr || head->next == nullptr)
return nullptr;
ListNode* fast = head, *slow = head;
do
{
if (fast == nullptr || fast->next == nullptr)
return nullptr;
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
slow = head;
while (slow != fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
技巧三:哈希表
用哈希表记录结点的对应关系,但空间复杂度会提高
- 链表的相交结点
// 1. 哈希表法
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
unordered_set<ListNode*> record;
while (headA != nullptr)
{
record.insert(headA);
headA = headA->next;
}
while (headB != nullptr)
{
if (record.count(headB) != 0)
return headB;
headB = headB->next;
}
return nullptr;
}
// 2. 双指针法
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == nullptr || headB == nullptr)
return nullptr;
ListNode* p = headA, *q = headB;
int lenA = 0, lenB = 0;
while (p->next != nullptr)
{
p = p->next;
++lenA;
}
while (q->next != nullptr)
{
q = q->next;
++lenB;
}
// 无相交
if (p != q)
return nullptr;
int delta = abs(lenA - lenB);
p = (lenA > lenB) ? headA : headB;
q = (p == headA) ? headB : headA;
// 长的那根链表先走
while (delta > 0)
{
--delta;
p = p->next;
}
// 然后再一起走
while (p != q)
{
p = p->next;
q = q->next;
}
return p;
}
经典题目
- 翻转链表
// 方法一:三指针法
ListNode* reverseList(ListNode* head) {
if (head == nullptr)
return nullptr;
ListNode* pPre = head, *pCur = head->next, *pNext;
pPre->next = nullptr;
while (pCur != nullptr)
{
pNext = pCur->next;
pCur->next = pPre;
pPre = pCur;
pCur = pNext;
}
return pPre;
}
// 方法二:头插法
ListNode* reverseList(ListNode* head) {
if (head == nullptr)
return nullptr;
ListNode* tail = head;
while (tail->next != nullptr)
tail = tail->next;
ListNode* p = head, *pNext;
while (p != tail)
{
pNext = p->next;
p->next = tail->next;
tail->next = p;
p = pNext;
}
return tail;
}
// 方法三:递归
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr)
return head;
ListNode* nextList = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return nextList;
}
- 复杂链表的复制
/** 复杂链表结构定义如下:
* class Node {
* public:
* int val;
* Node* next;
* Node* random;
*
* Node(int _val) {
* val = _val;
* next = NULL;
* random = NULL;
* }
* };
*
*/
// 方法一:哈希表法 时间复杂度:O(n) 空间复杂度:O(n)
Node* copyRandomList(Node* head) {
if (head == nullptr)
return nullptr;
unordered_map<Node*, Node*> record;
Node* cur = head;
// 构建原链表结点与拷贝链表结点的对应关系
while (cur != nullptr)
{
Node* copyNode = new Node(cur->val);
record.insert(make_pair(cur, copyNode));
cur = cur->next;
}
// 连接rand和next
cur = head;
while (cur != nullptr)
{
record[cur]->random = record[cur->random];
record[cur]->next = record[cur->next];
cur = cur->next;
}
return record[head];
}
// 方法二:链表关系映射(利用链表位置关系以代替哈希表) 时间复杂度:O(n) 空间复杂度:O(1)
Node* copyRandomList(Node* head) {
if (head == nullptr)
return nullptr;
Node* cur = head;
// 创建拷贝结点并连接在被拷贝的结点后 1 -> 1' -> 2' -> 2' -> ...
while (cur != nullptr)
{
Node* pNext = cur->next;
Node* copyNode = new Node(cur->val);
copyNode->next = cur->next;
cur->next = copyNode;
cur = pNext;
}
// 构建rand关系
cur = head;
while (cur != nullptr)
{
Node* copyNode = cur->next;
copyNode->random = (cur->random == nullptr) ? nullptr : cur->random->next;
cur = cur->next->next;
}
// 构建next
cur = head;
Node* res = head->next; // 保存结果,因为原链表在循环中会被恢复,不再指向copyNode
while (cur != nullptr)
{
Node* pNext = cur->next->next;
Node* copyNode = cur->next;
copyNode->next = (pNext == nullptr) ? nullptr : pNext->next;
cur->next = pNext; // 恢复原链表
cur = pNext;
}
return res;
}
3. 数组
技巧一:快慢指针
快指针探路,慢指针记录原位置,可减少数组中元素的搬动,以降低时间复杂度至O(n)
- 删除数组中的重复项
int removeDuplicates(vector<int>& nums) {
if (nums.size() <= 1)
return nums.size();
int slow = 0, fast = 1;
int len = nums.size();
int k = 0;
while (fast < len)
{
if (nums[slow] != nums[fast])
{
nums[k++] = nums[slow];
slow = fast;
}
++fast;
}
nums[k++] = nums[slow];
return k;
}
- 移除0
void moveZeroes(vector<int>& nums) {
if (nums.size() == 0)
return;
int fast = 0, slow = 0;
int len = nums.size();
while (fast < len)
{
if (nums[fast] != 0)
nums[slow++] = nums[fast];
++fast;
}
while (slow < len)
nums[slow++] = 0;
}
技巧二:partition思想
根据条件划分数组区域,通过遍历移动边界区域
- 奇偶分区
vector<int> exchange(vector<int>& nums) {
if (nums.size() <= 1)
return nums;
int oddRange = -1, i = 0;
int len = nums.size();
while (i < len)
{
if ((nums[i] & 0x1) == 1) // 奇数
::swap(nums[i++], nums[++oddRange]);
else // 偶数
++i;
}
return nums;
}
技巧三:左右指针
左指针指向数组头,右指针指向数组尾,两指针向中间夹逼(或从中间向两端扩散)
- 有序数组的两数之和
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0, right = numbers.size() - 1;
while (left < right)
{
if (numbers[left] + numbers[right] < target)
++left;
else if (numbers[left] + numbers[right] > target)
--right;
else
return vector<int>{left + 1, right + 1};
}
return vector<int>{0, 0};
}
- 最长回文子串
string longestPalindrome(string s) {
string res;
for (int i = 0; i < s.size(); ++i)
{
// 奇数情况
string p1 = Palindrome(s, i, i);
// 偶数情况
string p2 = Palindrome(s, i, i + 1);
// 取较大者
res = res.size() > p1.size() ? res : p1;
res = res.size() > p2.size() ? res : p2;
}
return res;
}
string Palindrome(string s, int l, int r) // O(n) <=> O(1)
{
while (l >= 0 && r < s.size())
{
if (s[l] == s[r])
{
--l;
++r;
}
else
break;
}
return string(s.begin() + l + 1, s.begin() + r);
}
技巧四:从后向前
当数组空间足够大时,从后向前有时可避免数组元素的搬移,降低时间复杂度为O(n)
- 替换空格
string replaceSpace(string s) {
int originSize = s.size();
int spaceCount = 0;
for (const auto& ch : s)
{
if (ch == ' ')
++spaceCount;
}
int newSize = originSize + 2 * spaceCount;
s.resize(newSize);
int i = originSize - 1, j = newSize - 1;
while (i >= 0)
{
if (s[i] == ' ')
{
s[j--] = '0';
s[j--] = '2';
s[j--] = '%';
--i;
}
else
s[j--] = s[i--];
}
return s;
}
4. 二叉树
思想一:递归序
一个二叉树结点在遍历过程中会被访问3次,3次访问过程都可完成相应操作,以实现不同的算法
// First visit --- PreOrder
Traverse(cur->lchild);
// Second visit --- InOrder
Traverse(cur->rchild);
// Third visit --- PostOrder
思想二:分解思想
针对当前结点,可以向左子树要信息,也可以向右子树要信息,最终需要合成出当前结点需要返回的信息,以构成完整的递归函数
struct Info
{
// data...
};
Info Process(Node* root)
{
// base-case
/* if () {} */
Info lData = Process(root->lchild); // 左子树要信息
Info rData = Process(root->rchild); // 右子树要信息
// 利用左子树信息和右子树信息合成当前信息
Info curData = /* ... */;
// 返回当前信息
return curData;
}
经典题目
- 树的子结构
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) {
// base-case
if (pRoot1 == nullptr || pRoot2 == nullptr)
return false;
bool res = false;
if (pRoot1->val == pRoot2->val)
res = DoesTree1HaveTree2(pRoot1, pRoot2); // 根节点匹配,开始查看子树
if (res == false)
res = HasSubtree(pRoot1->left, pRoot2); // 匹配失败,查看左子树
if (res == false)
res = HasSubtree(pRoot1->right, pRoot2); // 又失败,查看右子树
return res;
}
bool DoesTree1HaveTree2(TreeNode* pRoot1, TreeNode* pRoot2)
{
if (pRoot2 == nullptr)
return true;
if (pRoot1 == nullptr)
return false;
if (pRoot1->val != pRoot2->val)
return false;
return DoesTree1HaveTree2(pRoot1->left, pRoot2->left)
&& DoesTree1HaveTree2(pRoot1->right, pRoot2->right);
}
- 搜索二叉树的后序遍历序列
bool verifyPostorder(vector<int>& postorder) {
if (postorder.size() == 0)
return true;
return verifyPostorder(postorder, 0, postorder.size() - 1);
}
bool verifyPostorder(vector<int>& postorder, int l, int r)
{
int i = l;
int tar = postorder[r];
for (; i < r && postorder[i] < tar; ++i)
;
for (int j = i; j < r; ++j)
{
if (postorder[j] < tar)
return false;
}
bool leftRes = true;
if (i > l)
leftRes = verifyPostorder(postorder, l, i - 1);
bool rightRes = true;
if (i < r)
rightRes = verifyPostorder(postorder, i, r - 1);
return leftRes && rightRes;
}
- 判断是否为平衡二叉树
bool isBalanced(TreeNode* root) {
return Process(root).isBT;
}
struct RType
{
int height;
bool isBT;
RType(int height, bool isBT) : height(height), isBT(isBT) {}
};
RType Process(TreeNode* root)
{
if (root == nullptr)
return RType(0, true);
// 左树信息
RType lData = Process(root->left);
// 右树信息
RType rData = Process(root->right);
// 合成当前信息
int height = 1 + ::max(lData.height, rData.height);
bool isBT = lData.isBT && rData.isBT && (abs(lData.height - rData.height) <= 1);
return RType(height, isBT);
}
- 二叉搜索树与双向链表
TreeNode* Convert(TreeNode* pRootOfTree) {
TreeNode* pre = nullptr, *head = nullptr;
Process(pRootOfTree, pre, head);
return head;
}
void Process(TreeNode* cur, TreeNode* &pre, TreeNode* &head)
{
if (cur == nullptr)
return;
Process(cur->left, pre, head);
if (head == nullptr && cur->left == nullptr) // head-case
{
head = cur;
pre = cur;
}
else // other-case
{
cur->left = pre;
pre->right = cur;
pre = cur;
}
Process(cur->right, pre, head);
}
- 二叉树的非递归遍历过程
// pre-order
vector<int> preorderTraversal(TreeNode* root) {
if (root == nullptr)
return vector<int>();
stack<TreeNode*> s;
s.push(root);
vector<int> res;
TreeNode* cur;
while (!s.empty())
{
cur = s.top();
s.pop();
res.push_back(cur->val);
if (cur->right)
s.push(cur->right);
if (cur->left)
s.push(cur->left);
}
return res;
}
// post-order
vector<int> postorderTraversal(TreeNode* root) {
if (root == nullptr)
return vector<int>();
stack<TreeNode*> s1, s2;
s1.push(root);
vector<int> res;
TreeNode* cur;
while (!s1.empty())
{
cur = s1.top();
s1.pop();
s2.push(cur);
if (cur->left)
s1.push(cur->left);
if (cur->right)
s1.push(cur->right);
}
while (!s2.empty())
{
cur = s2.top();
s2.pop();
res.push_back(cur->val);
}
return res;
}
// in-order
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> s;
vector<int> res;
while (root != nullptr || !s.empty())
{
if (root != nullptr)
{
s.push(root);
root = root->left;
}
else
{
root = s.top();
s.pop();
res.push_back(root->val);
root = root->right;
}
}
return res;
}
5. 暴力递归
暴力递归,即回溯思想,其本质思想同递归序,根据进入结点的时机不同完成相应的操作
基本框架如下:
process() {
// base-case
...
// 处理当前信息
...
// 处理后续信息
process() ...
// 回退状态至当前状态
...
}
经典题目
- 子集问题
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) {
res.push_back(vector<int>()); // 空集情况
for (int i = 0; i < nums.size(); ++i)
{
vector<int> temp;
process(nums, i, temp);
}
return res;
}
void process(vector<int>& nums, int index, vector<int>& temp)
{
// 处理当前信息
temp.push_back(nums[index]);
res.push_back(temp);
for (int i = index + 1; i < nums.size(); ++i) // base-case
{
// 处理后续信息
process(nums, i, temp);
// 恢复状态
temp.pop_back();
}
}
- 全排列问题(有重复值情况)
vector<vector<int>> res;
vector<vector<int>> permuteUnique(vector<int>& nums) {
::sort(nums.begin(), nums.end());
vector<bool> record(nums.size(), false);
vector<int> temp;
process(nums, temp, record);
return res;
}
void process(vector<int>& nums, vector<int>& temp, vector<bool>& record)
{
if (temp.size() == nums.size())
{
res.push_back(temp);
return;
}
for (int i = 0; i < nums.size(); ++i)
{
if (record[i])
continue;
if (i > 0 && nums[i] == nums[i - 1] && !record[i - 1]) // 剪枝逻辑
continue;
temp.push_back(nums[i]);
record[i] = true;
process(nums, temp, record);
record[i] = false;
temp.pop_back();
}
}
6. 动态规划
动态规划问题的特点:
- 具备最优子结构,即是否能够通过子问题的最值得到原问题的最值
- 存在重叠子问题,即存在大量重复的问题,可通过记录表(如布尔数组、哈希表等)记录返回结果并在下一次需要的时候直接使用
步骤一:自顶向下的暴力尝试
- 分析base-case
- 罗列可能性
- 得到子问题结果
- 处理返回当前问题结果
以**不同路径问题
**为例:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?(来源:Leetcode. 62)
示例:
输入:m = 3, n = 7 输出:28
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下
int process(int m, int n, int x, int y)
{
// base-case
if (x > m || y > n)
return 0;
if ((x == m) && (y == n))
return 1;
// 可能性罗列
int p1 = process(m, n, x + 1, y);
int p2 = process(m, n, x, y + 1);
// 返回当前结果
return p1 + p2;
}
步骤二:自顶向下的记忆化搜索
由于递归过程中存在大量的重复计算,这些计算结果可以用一张记录表来记录,当第二次使用数据时可直接拿到结果,以提升效率
仍以上述题目为例:
int uniquePaths(int m, int n) {
vector<vector<int>> record(m + 1, vector<int>(n + 1, -1));
return process(m, n, 1, 1, record);
}
int process(int m, int n, int x, int y, vector<vector<int>>& record)
{
// base-case
if (x > m || y > n)
return 0;
else if ((x == m) && (y == n))
record[x][y] = 1;
else
{
// 已知record的数据,直接返回
if (record[x][y] != -1)
return record[x][y];
// 可能性罗列
int p1 = process(m, n, x + 1, y, record);
int p2 = process(m, n, x, y + 1, record);
record[x][y] = p1 + p2;
}
// 返回当前结果
return record[x][y];
}
步骤三:自底向上的动态规划
根据暴力尝试的结果,将其翻译为dp的代码:
- 尝试过程的变量即为dp表的可变范围,如一个变量为一维数组,两个变量为二维数组等
- base-case直接对应dp表的位置进行赋值
- 遍历dp表以构建数据结果,构建的过程为递归过程的逆过程
- 递归函数转化为dp表赋值,即:process(x, y) <=> dpTable[x][y]
- 参照入口递归函数的参数返回最终结果,即:process(0, 0) <=> return dpTable[0][0]
仍以上述题目为例:
int uniquePaths(int m, int n) {
return dp(m, n);
}
int dp(int m, int n)
{
// 建立变量为x和y的dp表
vector<vector<int>> dpTable(m + 2, vector<int>(n + 2, 0));
// 自底向上构建结果
for (int x = m; x > 0; --x)
{
for (int y = n; y > 0; --y)
{
// base-case
if (x == m && y == n)
dpTable[m][n] = 1;
else
{
int p1 = dpTable[x + 1][y];
int p2 = dpTable[x][y + 1];
dpTable[x][y] = p1 + p2;
}
}
}
return dpTable[1][1];
}